5bee9288b9
1、修复了若干无权限时的失败提示语 2、新增了一个生成后续建议问题的功能 3、重构了知识问答部分的权限管理模块 4、修复了若干渲染不恰当的样式渲染
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
/**
|
||
* Dify 客户端 API 模块
|
||
*
|
||
* 提供浏览器端调用 Dify API 的函数,通过 Remix API Routes 代理请求
|
||
*
|
||
* 调用链路:
|
||
* 客户端 → Remix /api/* → FastAPI /dify/* → Dify
|
||
*
|
||
* @module api/dify/client
|
||
*/
|
||
|
||
import axios from 'axios';
|
||
import { CHAT_CONFIG, ContentType } from '~/config/chat';
|
||
import { handleSSEStream, handleStream } from './sse-handler';
|
||
import type {
|
||
SSECallbacks,
|
||
Feedbacktype,
|
||
SendMessageParams,
|
||
ConversationsResponse,
|
||
MessagesResponse,
|
||
AppParametersResponse,
|
||
} from './types';
|
||
|
||
// ============================================================================
|
||
// 基础配置
|
||
// ============================================================================
|
||
|
||
/**
|
||
* API 基础 URL
|
||
* 指向 Remix API Routes(/api/*)
|
||
*/
|
||
const API_URL = CHAT_CONFIG.API_URL;
|
||
|
||
/**
|
||
* 基础请求选项
|
||
*/
|
||
const baseOptions: RequestInit = {
|
||
method: 'GET',
|
||
mode: 'cors',
|
||
credentials: 'include', // 携带 cookie
|
||
headers: {
|
||
'Content-Type': ContentType.json,
|
||
},
|
||
redirect: 'follow',
|
||
};
|
||
|
||
// ============================================================================
|
||
// 会话管理 API
|
||
// ============================================================================
|
||
|
||
/**
|
||
* 获取用户的会话列表
|
||
*
|
||
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
|
||
* @returns 包含会话列表的响应对象
|
||
* @throws {Error} 当获取会话列表失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const response = await fetchConversations();
|
||
* const conversations = response.data;
|
||
* console.log('会话数量:', conversations.length);
|
||
*
|
||
* // 获取指定应用的会话列表
|
||
* const appConversations = await fetchConversations('app-123');
|
||
* ```
|
||
*/
|
||
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, appId });
|
||
|
||
try {
|
||
const response = await axios.get<ConversationsResponse>(url, {
|
||
withCredentials: true,
|
||
});
|
||
|
||
console.log('📋 [Dify Client] 会话列表响应:', { status: response.status });
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
console.error('❌ [Dify Client] 获取会话列表失败:', {
|
||
status: err.response?.status,
|
||
body: err.response?.data
|
||
});
|
||
throw new Error(`获取会话列表失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取指定会话的聊天消息列表
|
||
*
|
||
* @param conversationId - 会话 ID
|
||
* @returns 包含消息列表的响应对象
|
||
* @throws {Error} 当获取消息列表失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const response = await fetchChatList('conv-123');
|
||
* const messages = response.data;
|
||
* console.log('消息数量:', messages.length);
|
||
* ```
|
||
*/
|
||
export async function fetchChatList(conversationId: string): Promise<MessagesResponse> {
|
||
const params = new URLSearchParams({
|
||
conversation_id: conversationId,
|
||
});
|
||
|
||
try {
|
||
const response = await axios.get<MessagesResponse>(
|
||
`${API_URL}/chat-messages?${params}`,
|
||
{ withCredentials: true }
|
||
);
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
throw new Error(`获取消息列表失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重命名会话
|
||
*
|
||
* @param id - 会话 ID
|
||
* @param name - 新的会话名称
|
||
* @param autoGenerate - 是否使用 AI 自动生成名称,默认为 false
|
||
* @returns 重命名结果
|
||
* @throws {Error} 当重命名失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // 手动设置名称
|
||
* await renameConversation('conv-123', '关于编程的讨论');
|
||
*
|
||
* // AI 自动生成名称
|
||
* await renameConversation('conv-123', '', true);
|
||
* ```
|
||
*/
|
||
export async function renameConversation(
|
||
id: string,
|
||
name: string,
|
||
autoGenerate: boolean = false
|
||
): Promise<{ name: string }> {
|
||
try {
|
||
const response = await axios.post(
|
||
`${API_URL}/conversations/${id}/name`,
|
||
{
|
||
name: autoGenerate ? undefined : name,
|
||
auto_generate: autoGenerate,
|
||
},
|
||
{
|
||
headers: { 'Content-Type': 'application/json' },
|
||
withCredentials: true,
|
||
}
|
||
);
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
throw new Error(`重命名会话失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成会话名称
|
||
*
|
||
* 让 AI 根据会话内容自动生成合适的会话名称
|
||
*
|
||
* @param id - 会话 ID
|
||
* @returns 包含生成名称的响应对象
|
||
* @throws {Error} 当生成名称失败时抛出错误
|
||
*/
|
||
export async function generateConversationName(id: string): Promise<{ name: string }> {
|
||
return renameConversation(id, '', true);
|
||
}
|
||
|
||
/**
|
||
* 删除会话
|
||
*
|
||
* @param id - 要删除的会话 ID
|
||
* @returns 删除操作结果
|
||
* @throws {Error} 当删除会话失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await deleteConversation('conv-123');
|
||
* console.log('会话已删除');
|
||
* ```
|
||
*/
|
||
export async function deleteConversation(id: string): Promise<{ result: string }> {
|
||
console.log('🗑️ [Dify Client] 删除会话:', id);
|
||
|
||
try {
|
||
const response = await axios.delete(`${API_URL}/conversations/${id}`, {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
withCredentials: true,
|
||
});
|
||
|
||
console.log('🗑️ [Dify Client] 删除会话响应:', {
|
||
status: response.status,
|
||
statusText: response.statusText
|
||
});
|
||
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
console.error('❌ [Dify Client] 删除会话失败:', err.response?.data);
|
||
throw new Error(`删除会话失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 应用参数 API
|
||
// ============================================================================
|
||
|
||
/**
|
||
* 获取应用参数配置
|
||
*
|
||
* @returns 包含应用参数的响应对象
|
||
* @throws {Error} 当获取应用参数失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const params = await fetchAppParams();
|
||
* const { user_input_form, opening_statement } = params;
|
||
* console.log('开场白:', opening_statement);
|
||
* ```
|
||
*/
|
||
export async function fetchAppParams(appId?: string): Promise<AppParametersResponse> {
|
||
const params = new URLSearchParams();
|
||
if (appId) {
|
||
params.append('app_id', appId);
|
||
}
|
||
|
||
const url = params.toString() ? `${API_URL}/parameters?${params}` : `${API_URL}/parameters`;
|
||
console.log('⚙️ [Dify Client] 获取应用参数:', { url, appId });
|
||
|
||
try {
|
||
const response = await axios.get<AppParametersResponse>(url, {
|
||
withCredentials: true,
|
||
});
|
||
console.log('⚙️ [Dify Client] 应用参数响应:', { status: response.status });
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
console.error('❌ [Dify Client] 获取应用参数失败:', {
|
||
status: err.response?.status,
|
||
body: err.response?.data
|
||
});
|
||
throw new Error(`获取应用参数失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 消息 API
|
||
// ============================================================================
|
||
|
||
/**
|
||
* 发送聊天消息(流式响应)
|
||
*
|
||
* @param params - 消息参数
|
||
* @param callbacks - SSE 回调配置
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await sendChatMessage(
|
||
* { query: '你好', conversation_id: 'conv-123' },
|
||
* {
|
||
* onData: (message, isFirst, info) => updateUI(message),
|
||
* onCompleted: () => setLoading(false),
|
||
* onError: (error) => showError(error),
|
||
* }
|
||
* );
|
||
* ```
|
||
*/
|
||
export async function sendChatMessage(
|
||
params: SendMessageParams,
|
||
callbacks: SSECallbacks
|
||
): Promise<void> {
|
||
const body = {
|
||
...params,
|
||
response_mode: 'streaming',
|
||
};
|
||
|
||
const url = `${API_URL}/chat-messages`;
|
||
|
||
const controller = new AbortController();
|
||
callbacks.getAbortController?.(controller);
|
||
|
||
const options: RequestInit = {
|
||
...baseOptions,
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': ContentType.stream,
|
||
},
|
||
body: JSON.stringify(body),
|
||
signal: controller.signal,
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(url, options);
|
||
|
||
if (!/^(2|3)\d{2}$/.test(response.status.toString())) {
|
||
const data = await response.json();
|
||
console.error('❌ [Dify Client] SSE 错误:', data.message || 'Server Error');
|
||
callbacks.onError?.(data.message || 'Server Error');
|
||
return;
|
||
}
|
||
|
||
handleSSEStream(response, callbacks);
|
||
} catch (err: any) {
|
||
if (err.name !== 'AbortError') {
|
||
console.error('❌ [Dify Client] SSE 请求错误:', err);
|
||
callbacks.onError?.(err.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新消息反馈
|
||
*
|
||
* @param messageId - 消息 ID
|
||
* @param feedback - 反馈内容
|
||
* @returns 反馈提交结果
|
||
* @throws {Error} 当提交反馈失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await updateFeedback('msg-123', { rating: 'like' });
|
||
* ```
|
||
*/
|
||
export async function updateFeedback(
|
||
messageId: string,
|
||
feedback: Feedbacktype
|
||
): Promise<any> {
|
||
try {
|
||
const response = await axios.post(
|
||
`${API_URL}/messages/${messageId}/feedbacks`,
|
||
feedback,
|
||
{
|
||
headers: { 'Content-Type': 'application/json' },
|
||
withCredentials: true,
|
||
}
|
||
);
|
||
return response.data;
|
||
} catch (err) {
|
||
if (axios.isAxiosError(err)) {
|
||
throw new Error(`提交反馈失败: ${err.response?.status}`);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 文件上传 API
|
||
// ============================================================================
|
||
|
||
/**
|
||
* 上传文件
|
||
*
|
||
* @param options - 上传配置选项
|
||
* @returns 包含文件 ID 的对象
|
||
* @throws {Error} 当文件上传失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const formData = new FormData();
|
||
* formData.append('file', fileBlob);
|
||
*
|
||
* const xhr = new XMLHttpRequest();
|
||
* const result = await uploadFile({
|
||
* data: formData,
|
||
* xhr: xhr,
|
||
* onProgress: (event) => {
|
||
* const progress = (event.loaded / event.total) * 100;
|
||
* console.log('上传进度:', progress + '%');
|
||
* }
|
||
* });
|
||
* console.log('文件ID:', result.id);
|
||
* ```
|
||
*/
|
||
export function uploadFile(options: {
|
||
data: FormData;
|
||
xhr: XMLHttpRequest;
|
||
onProgress?: (event: ProgressEvent) => void;
|
||
}): Promise<{ id: string }> {
|
||
const url = `${API_URL}/files/upload`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const { xhr, data, onProgress } = options;
|
||
|
||
xhr.open('POST', url);
|
||
xhr.withCredentials = true;
|
||
|
||
xhr.onreadystatechange = function () {
|
||
if (xhr.readyState === 4) {
|
||
if (xhr.status === 200) {
|
||
resolve({ id: xhr.response });
|
||
} else {
|
||
reject(new Error(xhr.responseText || '上传失败'));
|
||
}
|
||
}
|
||
};
|
||
|
||
if (onProgress) {
|
||
xhr.upload.onprogress = onProgress;
|
||
}
|
||
|
||
xhr.send(data);
|
||
});
|
||
}
|
||
|
||
// 重新导出 SSE 处理器
|
||
export { handleStream, handleSSEStream };
|