feat:前端新增初版知识库管理页面

This commit is contained in:
PingChuan
2025-11-30 19:27:01 +08:00
parent 9614899171
commit c94cc00138
40 changed files with 3034 additions and 1024 deletions
+418
View File
@@ -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 };