Files
leaudit-platform-frontend/app/api/dify-chat/client.ts
T
PingChuan 5bee9288b9 feat:替换 Dify 为自建 RAG去实现
1、修复了若干无权限时的失败提示语
2、新增了一个生成后续建议问题的功能
3、重构了知识问答部分的权限管理模块
4、修复了若干渲染不恰当的样式渲染
2026-04-10 16:20:32 +08:00

433 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };