feat:重构dify前端组件以及转发逻辑
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* 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
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
*
|
||||
* @returns 包含会话列表的响应对象
|
||||
* @throws {Error} 当获取会话列表失败时抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await fetchConversations();
|
||||
* const conversations = response.data;
|
||||
* console.log('会话数量:', conversations.length);
|
||||
* ```
|
||||
*/
|
||||
export async function fetchConversations(): Promise<ConversationsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
limit: '100',
|
||||
});
|
||||
|
||||
const url = `${API_URL}/conversations?${params}`;
|
||||
console.log('📋 [Dify Client] 获取会话列表:', { url });
|
||||
|
||||
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(): Promise<AppParametersResponse> {
|
||||
const url = `${API_URL}/parameters`;
|
||||
console.log('⚙️ [Dify Client] 获取应用参数:', { url });
|
||||
|
||||
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 };
|
||||
Reference in New Issue
Block a user