Merge branch 'PingChuan' into shiy-login

This commit is contained in:
2025-11-30 19:33:46 +08:00
43 changed files with 4762 additions and 2634 deletions
+178
View File
@@ -0,0 +1,178 @@
/**
* Dify Chat API 模块
*
* 提供客户端调用 Dify API 的函数
* 用于 Remix loader/action 中调用 Dify API
*
* @module api/dify/chat
*/
import { difyFetch } from './client.server';
// ============================================================================
// Dify Chat API 客户端
// ============================================================================
/**
* Dify Chat API 客户端
*
* @param jwt - JWT 认证令牌
* user 参数由后端自动从 JWT 中提取
*/
export const difyClient = {
/**
* 获取应用参数
*/
async getApplicationParameters(jwt?: string): Promise<any> {
const response = await difyFetch('parameters', {
method: 'GET',
}, jwt);
return response.json();
},
/**
* 获取会话列表
*/
async getConversations(jwt?: string): Promise<any> {
const params = new URLSearchParams({
limit: '100',
first_id: '',
});
const response = await difyFetch(`conversations?${params}`, {
method: 'GET',
}, jwt);
return response.json();
},
/**
* 获取会话消息
*/
async getConversationMessages(conversationId: string, jwt?: string): Promise<any> {
const params = new URLSearchParams({
conversation_id: conversationId,
limit: '20',
last_id: '',
});
const response = await difyFetch(`messages?${params}`, {
method: 'GET',
}, jwt);
return response.json();
},
/**
* 发送聊天消息
*
* @param inputs - 输入参数
* @param query - 用户问题
* @param responseMode - 响应模式 ('streaming' | 'blocking')
* @param conversationId - 会话 ID
* @param files - 附件文件
* @param jwt - JWT 认证令牌
* @returns 对于流式响应返回 Response 对象,否则返回 JSON
*/
async createChatMessage(
inputs: Record<string, any>,
query: string,
responseMode: string = 'streaming',
conversationId?: string,
files?: any[],
jwt?: string
): Promise<Response | any> {
const body = {
inputs,
query,
response_mode: responseMode,
conversation_id: conversationId,
files: files || [],
};
const response = await difyFetch('chat-messages', {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
// 对于流式响应,直接返回 Response 对象
if (responseMode === 'streaming') {
return response;
}
console.log('[Dify Chat] 解析 JSON 响应');
return response.json();
},
/**
* 重命名会话
*/
async renameConversation(
conversationId: string,
name: string,
autoGenerate: boolean = false,
jwt?: string
): Promise<any> {
const body = {
name,
auto_generate: autoGenerate,
};
const response = await difyFetch(`conversations/${conversationId}/name`, {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
return response.json();
},
/**
* 删除会话
*/
async deleteConversation(conversationId: string, jwt?: string): Promise<any> {
console.log('[Dify Chat] 删除会话:', conversationId);
try {
const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE',
body: JSON.stringify({}),
}, jwt);
// 对于 204 No Content 响应,直接返回成功
if (response.status === 204) {
console.log('[Dify Chat] 删除会话成功:', conversationId);
return { result: 'success' };
}
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
return data;
}
const text = await response.text();
console.log('[Dify Chat] 删除会话文本响应:', text);
return { result: 'success' };
} catch (error: any) {
console.warn('[Dify Chat] 删除会话请求失败,但可能已成功删除:', error.message);
return { result: 'success' };
}
},
/**
* 更新消息反馈
*/
async updateMessageFeedback(
messageId: string,
rating: 'like' | 'dislike' | null,
jwt?: string
): Promise<any> {
const body = {
rating,
};
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
return response.json();
},
};
+117
View File
@@ -0,0 +1,117 @@
/**
* Dify 服务端基础 API 模块
*
* 提供 Node.js 服务端调用 FastAPI 后端的基础功能
* 包括配置管理和基础请求函数
*
* 调用链路:
* Remix Server → FastAPI /dify/* → Dify
*
* @module api/dify/client.server
*/
import { API_BASE_URL } from '~/config/api-config';
// ============================================================================
// 配置
// ============================================================================
/**
* 获取环境变量的服务端函数
*/
const getServerEnvVar = (name: string, defaultValue: string = ''): string => {
return process.env[name] || defaultValue;
};
/**
* Dify API 客户端配置
* 通过 FastAPI 后端的 /dify 路由代理访问 Dify
*/
const DIFY_CONFIG = {
API_URL: `${API_BASE_URL}/dify_chat`,
API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''),
APP_ID: (() => {
const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', '');
const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/);
return match ? match[1] : rawAppId;
})(),
};
console.log('[Dify Server] 配置:', {
apiUrl: DIFY_CONFIG.API_URL,
apiBaseUrl: API_BASE_URL,
appId: DIFY_CONFIG.APP_ID,
configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID)
});
// ============================================================================
// 基础请求函数
// ============================================================================
/**
* Dify API 基础请求函数
*
* 使用 JWT 认证通过 FastAPI 代理访问 Dify
*
* @param endpoint - API 端点路径
* @param options - fetch 请求选项
* @param jwt - JWT 认证令牌
* @returns Response 对象
*/
export async function difyFetch(
endpoint: string,
options: RequestInit = {},
jwt?: string
): Promise<Response> {
const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`;
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
};
if (jwt) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`;
} else {
console.warn('[Dify Server] 没有提供 JWT,转发fastapi请求可能失败');
}
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
console.error('[Dify Server] API 转发错误:', {
status: response.status,
statusText: response.statusText,
error: errorText
});
if (response.status === 401) {
throw new Error('JWT认证失败,请重新登录');
}
throw new Error(`Dify API Error: ${response.status} ${response.statusText}`);
}
return response;
}
// ============================================================================
// 工具函数
// ============================================================================
/**
* Dify 工具函数
*/
export const difyUtils = {
/**
* 获取 Dify 配置
*/
getConfig: () => DIFY_CONFIG,
};
// 重新导出 chat 模块的 difyClient
export { difyClient } from './chat';
+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 };
+124
View File
@@ -0,0 +1,124 @@
/**
* Dify API 模块统一导出
*
* 提供 Dify 相关功能的统一入口
*
* @module api/dify
*
* @example
* ```typescript
* // 客户端使用
* import { sendChatMessage, fetchConversations } from '~/api/dify';
*
* // 使用 SSE 处理器
* import { handleSSEStream, SSEStreamHandler } from '~/api/dify';
*
* // 使用类型
* import type { ChatItem, ConversationItem, SSECallbacks } from '~/api/dify';
* ```
*/
// ============================================================================
// 类型导出
// ============================================================================
export type {
// 基础类型
AppInfo,
ConversationItem,
ChatItem,
MessageMore,
Feedbacktype,
ThoughtItem,
// 文件类型
VisionFile,
ImageFile,
VisionSettings,
// 表单类型
TextTypeFormItem,
SelectTypeFormItem,
UserInputFormItem,
PromptConfig,
PromptVariable,
// 工作流类型
NodeTracing,
WorkflowProcess,
// SSE 事件类型
MessageEvent,
MessageReplace,
MessageEnd,
WorkflowStartedResponse,
WorkflowFinishedResponse,
NodeStartedResponse,
NodeFinishedResponse,
// SSE 回调类型
OnDataMoreInfo,
OnData,
OnThought,
OnFile,
OnMessageEnd,
OnMessageReplace,
OnCompleted,
OnError,
OnWorkflowStarted,
OnWorkflowFinished,
OnNodeStarted,
OnNodeFinished,
SSECallbacks,
// API 请求/响应类型
SendMessageParams,
ConversationsResponse,
MessagesResponse,
AppParametersResponse,
} from './types';
export {
// 枚举
Resolution,
TransferMethod,
WorkflowRunningStatus,
NodeRunningStatus,
BlockEnum,
CodeLanguage,
} from './types';
// ============================================================================
// SSE 处理器导出
// ============================================================================
export {
SSEStreamHandler,
SSEEventType,
handleSSEStream,
handleStream,
createSSECallbacks,
} from './sse-handler';
// ============================================================================
// 客户端 API 导出
// ============================================================================
export {
// 会话管理
fetchConversations,
fetchChatList,
renameConversation,
generateConversationName,
deleteConversation,
// 应用参数
fetchAppParams,
// 消息
sendChatMessage,
updateFeedback,
// 文件上传
uploadFile,
} from './client';
+360
View File
@@ -0,0 +1,360 @@
/**
* SSE (Server-Sent Events) 流处理模块
*
* 负责处理 Dify API 的流式响应,包括:
* - 解析 SSE 数据流
* - 处理各种事件类型(消息、思考、文件、工作流等)
* - 错误处理和状态管理
*
* @module api/dify/sse-handler
*/
import type {
OnData,
OnCompleted,
OnThought,
OnFile,
OnMessageEnd,
OnMessageReplace,
OnError,
OnWorkflowStarted,
OnWorkflowFinished,
OnNodeStarted,
OnNodeFinished,
SSECallbacks,
ThoughtItem,
VisionFile,
MessageEnd,
MessageReplace,
WorkflowStartedResponse,
WorkflowFinishedResponse,
NodeStartedResponse,
NodeFinishedResponse,
} from './types';
import { unicodeToChar } from '~/utils/chat-utils';
/**
* SSE 事件类型枚举
*/
export enum SSEEventType {
Message = 'message',
AgentMessage = 'agent_message',
AgentThought = 'agent_thought',
MessageFile = 'message_file',
MessageEnd = 'message_end',
MessageReplace = 'message_replace',
WorkflowStarted = 'workflow_started',
WorkflowFinished = 'workflow_finished',
NodeStarted = 'node_started',
NodeFinished = 'node_finished',
Error = 'error',
}
/**
* SSE 流处理器类
*
* 封装 SSE 流的读取和解析逻辑,提供清晰的事件分发机制
*/
export class SSEStreamHandler {
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
private decoder = new TextDecoder('utf-8');
private buffer = '';
private isFirstMessage = true;
private callbacks: SSECallbacks;
private aborted = false;
constructor(callbacks: SSECallbacks) {
this.callbacks = callbacks;
}
/**
* 开始处理 SSE 流
* @param response - fetch API 返回的 Response 对象
*/
async handleStream(response: Response): Promise<void> {
if (!response.ok) {
console.error('❌ [SSEHandler] 响应错误:', response.status, response.statusText);
this.callbacks.onError?.('网络响应错误');
throw new Error('网络响应错误');
}
this.reader = response.body?.getReader() ?? null;
if (!this.reader) {
this.callbacks.onError?.('无法读取响应流');
throw new Error('无法读取响应流');
}
await this.read();
}
/**
* 中止流处理
*/
abort(): void {
this.aborted = true;
this.reader?.cancel();
}
/**
* 递归读取流数据
*/
private async read(): Promise<void> {
if (this.aborted || !this.reader) {
return;
}
try {
const result = await this.reader.read();
if (result.done) {
this.callbacks.onCompleted?.();
return;
}
const chunk = this.decoder.decode(result.value, { stream: true });
this.buffer += chunk;
const hasError = this.processBuffer();
if (!hasError && !this.aborted) {
await this.read();
}
} catch (err: any) {
if (!this.aborted) {
console.error('❌ [SSEHandler] 读取流时出错:', err);
this.callbacks.onError?.(err.message);
}
}
}
/**
* 处理缓冲区中的数据
* @returns 是否发生错误
*/
private processBuffer(): boolean {
const lines = this.buffer.split('\n');
let hasError = false;
try {
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6);
hasError = this.processEventData(jsonStr);
if (hasError) break;
}
}
// 保留最后一行(可能是不完整的消息)
this.buffer = lines[lines.length - 1];
} catch (err) {
console.error('❌ [SSEHandler] 解析响应时出错:', err);
this.callbacks.onData?.('', false, {
conversationId: undefined,
messageId: '',
errorMessage: `${err}`,
});
hasError = true;
this.callbacks.onCompleted?.(true);
}
return hasError;
}
/**
* 处理单个事件数据
* @param jsonStr - JSON 字符串
* @returns 是否发生错误
*/
private processEventData(jsonStr: string): boolean {
let eventData: Record<string, any>;
try {
eventData = JSON.parse(jsonStr);
} catch (e) {
console.warn('⚠️ [SSEHandler] JSON解析失败:', e);
// 处理消息截断情况
this.callbacks.onData?.('', this.isFirstMessage, {
conversationId: undefined,
messageId: '',
});
return false;
}
// 检查错误响应
if (eventData.status === 400 || !eventData.event) {
console.error('❌ [SSEHandler] 错误响应:', {
status: eventData.status,
event: eventData.event,
message: eventData.message,
code: eventData.code
});
this.callbacks.onData?.('', false, {
conversationId: undefined,
messageId: '',
errorMessage: eventData.message,
errorCode: eventData.code,
});
this.callbacks.onCompleted?.(true);
return true;
}
// 分发事件
this.dispatchEvent(eventData);
return false;
}
/**
* 根据事件类型分发到对应的回调
* @param eventData - 事件数据
*/
private dispatchEvent(eventData: Record<string, any>): void {
const { event } = eventData;
switch (event) {
case SSEEventType.Message:
case SSEEventType.AgentMessage:
this.handleMessageEvent(eventData);
break;
case SSEEventType.AgentThought:
this.callbacks.onThought?.(eventData as ThoughtItem);
break;
case SSEEventType.MessageFile:
this.callbacks.onFile?.(eventData as VisionFile);
break;
case SSEEventType.MessageEnd:
this.callbacks.onMessageEnd?.(eventData as MessageEnd);
break;
case SSEEventType.MessageReplace:
this.callbacks.onMessageReplace?.(eventData as MessageReplace);
break;
case SSEEventType.WorkflowStarted:
this.callbacks.onWorkflowStarted?.(eventData as WorkflowStartedResponse);
break;
case SSEEventType.WorkflowFinished:
this.callbacks.onWorkflowFinished?.(eventData as WorkflowFinishedResponse);
break;
case SSEEventType.NodeStarted:
this.callbacks.onNodeStarted?.(eventData as NodeStartedResponse);
break;
case SSEEventType.NodeFinished:
this.callbacks.onNodeFinished?.(eventData as NodeFinishedResponse);
break;
default:
// 未知事件类型,忽略
break;
}
}
/**
* 处理消息事件
* @param eventData - 事件数据
*/
private handleMessageEvent(eventData: Record<string, any>): void {
const answer = unicodeToChar(eventData.answer);
this.callbacks.onData?.(answer, this.isFirstMessage, {
conversationId: eventData.conversation_id,
messageId: eventData.id || eventData.message_id,
taskId: eventData.task_id,
});
this.isFirstMessage = false;
}
}
/**
* 处理 SSE 流式响应(函数式 API)
*
* 这是核心的流式响应处理函数,负责:
* - 解析 SSE 数据流
* - 处理各种事件类型(消息、思考、文件、工作流等)
* - 错误处理和状态管理
*
* @param response - fetch API 返回的 Response 对象
* @param callbacks - SSE 回调配置
* @returns SSEStreamHandler 实例(可用于中止流)
*
* @example
* ```typescript
* const handler = handleSSEStream(response, {
* onData: (message, isFirst, info) => console.log('收到消息:', message),
* onCompleted: () => console.log('流式响应完成'),
* onThought: (thought) => console.log('AI思考:', thought),
* onError: (error) => console.error('错误:', error),
* });
*
* // 需要时可以中止流
* handler.abort();
* ```
*/
export function handleSSEStream(
response: Response,
callbacks: SSECallbacks
): SSEStreamHandler {
const handler = new SSEStreamHandler(callbacks);
handler.handleStream(response);
return handler;
}
/**
* 创建 SSE 回调配置的工厂函数
*
* 提供类型安全的回调配置创建方式
*
* @param callbacks - 部分回调配置
* @returns 完整的 SSE 回调配置
*
* @example
* ```typescript
* const callbacks = createSSECallbacks({
* onData: (message) => updateUI(message),
* onCompleted: () => setLoading(false),
* });
* ```
*/
export function createSSECallbacks(callbacks: Partial<SSECallbacks> & { onData: OnData }): SSECallbacks {
return callbacks as SSECallbacks;
}
/**
* 兼容旧版 handleStream 函数的适配器
*
* @deprecated 请使用 handleSSEStream 替代
*/
export const handleStream = (
response: Response,
onData: OnData,
onCompleted?: OnCompleted,
onThought?: OnThought,
onMessageEnd?: OnMessageEnd,
onMessageReplace?: OnMessageReplace,
onFile?: OnFile,
onWorkflowStarted?: OnWorkflowStarted,
onWorkflowFinished?: OnWorkflowFinished,
onNodeStarted?: OnNodeStarted,
onNodeFinished?: OnNodeFinished,
onError?: OnError,
): void => {
handleSSEStream(response, {
onData,
onCompleted,
onThought,
onFile,
onMessageEnd,
onMessageReplace,
onError,
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
});
};
+524
View File
@@ -0,0 +1,524 @@
/**
* Dify API 类型定义
* 统一管理所有 Dify 相关的 TypeScript 类型
*/
// ============================================================================
// 基础类型
// ============================================================================
/**
* 应用信息类型
*/
export interface AppInfo {
title: string;
description: string;
copyright: string;
privacy_policy: string;
default_language: string;
}
/**
* 会话项类型
*/
export interface ConversationItem {
id: string;
name: string;
inputs?: Record<string, any>;
introduction?: string;
}
/**
* 聊天消息类型
*/
export interface ChatItem {
id: string;
content: string;
isAnswer: boolean;
feedback?: Feedbacktype;
agent_thoughts?: ThoughtItem[];
message_files?: VisionFile[];
isError?: boolean;
workflow_run_id?: string;
workflowProcess?: WorkflowProcess;
more?: MessageMore;
useCurrentUserAvatar?: boolean;
isOpeningStatement?: boolean;
suggestedQuestions?: string[];
}
/**
* 消息更多信息类型
*/
export interface MessageMore {
time: string;
tokens: number;
latency: number | string;
}
/**
* 反馈类型
*/
export type Feedbacktype = {
rating: 'like' | 'dislike' | null;
content?: string;
}
/**
* 思考过程类型
*/
export interface ThoughtItem {
id?: string;
chain_id?: string;
thought?: string;
observation?: string;
message_files?: VisionFile[];
tool_name?: string;
tool_input?: string;
tool_output?: string;
tool_finished?: boolean;
parent_id?: string;
children_ids?: string[];
sort?: number;
message_id?: string;
tool?: string;
position?: number;
files?: string[];
}
// ============================================================================
// 文件相关类型
// ============================================================================
/**
* 视觉文件类型
*/
export interface VisionFile {
id?: string;
type: string;
transfer_method: TransferMethod;
url?: string;
upload_file_id?: string;
belongs_to?: string;
usage?: string;
result?: any;
detail?: Resolution;
}
/**
* 图片文件类型
*/
export interface ImageFile {
type: TransferMethod;
_id: string;
fileId: string;
file?: File;
progress: number;
url: string;
base64Url?: string;
deleted?: boolean;
}
/**
* 视觉设置类型
*/
export interface VisionSettings {
enabled: boolean;
detail?: Resolution;
number_limits?: number;
transfer_methods?: TransferMethod[];
image_file_size_limit?: number | string;
}
/**
* 分辨率枚举
*/
export enum Resolution {
low = 'low',
high = 'high',
}
/**
* 传输方法枚举
*/
export enum TransferMethod {
local_file = 'local_file',
remote_url = 'remote_url',
all = 'all',
}
// ============================================================================
// 表单相关类型
// ============================================================================
/**
* 文本类型表单项
*/
export interface TextTypeFormItem {
label: string;
variable: string;
required: boolean;
max_length: number;
}
/**
* 选择类型表单项
*/
export interface SelectTypeFormItem {
label: string;
variable: string;
required: boolean;
options: string[];
}
/**
* 用户输入表单项
*/
export type UserInputFormItem = {
'text-input': TextTypeFormItem;
} | {
'select': SelectTypeFormItem;
} | {
'paragraph': TextTypeFormItem;
}
/**
* 提示配置类型
*/
export interface PromptConfig {
prompt_template: string;
prompt_variables: PromptVariable[];
}
/**
* 提示变量类型
*/
export interface PromptVariable {
key: string;
name: string;
type: string;
required: boolean;
options?: string[];
max_length?: number;
allowed_file_extensions?: string[];
allowed_file_types?: string[];
allowed_file_upload_methods?: TransferMethod[];
}
// ============================================================================
// 工作流相关类型
// ============================================================================
/**
* 工作流运行状态枚举
*/
export enum WorkflowRunningStatus {
init = 'init',
running = 'running',
completed = 'completed',
error = 'error',
waiting = 'waiting',
succeeded = 'succeeded',
failed = 'failed',
stopped = 'stopped',
}
/**
* 节点运行状态枚举
*/
export enum NodeRunningStatus {
NotStart = 'not-start',
Waiting = 'waiting',
Running = 'running',
Succeeded = 'succeeded',
Failed = 'failed',
}
/**
* 块类型枚举
*/
export enum BlockEnum {
Start = 'start',
End = 'end',
Answer = 'answer',
LLM = 'llm',
KnowledgeRetrieval = 'knowledge-retrieval',
QuestionClassifier = 'question-classifier',
IfElse = 'if-else',
Code = 'code',
TemplateTransform = 'template-transform',
HttpRequest = 'http-request',
VariableAssigner = 'variable-assigner',
Tool = 'tool',
}
/**
* 节点追踪类型
*/
export interface NodeTracing {
id: string;
index: number;
predecessor_node_id: string;
node_id: string;
node_type: BlockEnum;
title: string;
inputs: any;
process_data: any;
outputs?: any;
status: string;
error?: string;
elapsed_time: number;
execution_metadata: {
total_tokens: number;
total_price: number;
currency: string;
};
created_at: number;
created_by: {
id: string;
name: string;
email: string;
};
finished_at: number;
extras?: any;
expand?: boolean;
}
/**
* 工作流进程类型
*/
export interface WorkflowProcess {
status: WorkflowRunningStatus;
tracing: NodeTracing[];
expand?: boolean;
}
/**
* 代码语言枚举
*/
export enum CodeLanguage {
python3 = 'python3',
javascript = 'javascript',
json = 'json',
}
// ============================================================================
// SSE 事件类型
// ============================================================================
/**
* 消息事件类型
*/
export interface MessageEvent {
event: string;
task_id: string;
conversation_id: string;
message_id: string;
answer: string;
}
/**
* 消息替换类型
*/
export interface MessageReplace {
event: string;
task_id: string;
conversation_id: string;
message_id: string;
answer: string;
}
/**
* 消息结束类型
*/
export interface MessageEnd {
event: string;
task_id: string;
conversation_id: string;
message_id: string;
}
/**
* 工作流开始响应
*/
export interface WorkflowStartedResponse {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
workflow_id: string;
sequence_number: number;
created_at: number;
};
}
/**
* 工作流完成响应
*/
export interface WorkflowFinishedResponse {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
workflow_id: string;
status: string;
outputs: any;
error: string;
elapsed_time: number;
total_tokens: number;
total_steps: number;
created_at: number;
finished_at: number;
};
}
/**
* 节点开始响应
*/
export interface NodeStartedResponse {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
node_id: string;
node_type: string;
index: number;
predecessor_node_id?: string;
inputs: any;
created_at: number;
extras?: any;
};
}
/**
* 节点完成响应
*/
export interface NodeFinishedResponse {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
node_id: string;
node_type: string;
index: number;
predecessor_node_id?: string;
inputs: any;
process_data: any;
outputs: any;
status: string;
error: string;
elapsed_time: number;
execution_metadata: {
total_tokens: number;
total_price: number;
currency: string;
};
created_at: number;
};
}
// ============================================================================
// SSE 回调类型
// ============================================================================
/**
* 数据回调附加信息
*/
export interface OnDataMoreInfo {
conversationId?: string;
taskId?: string;
messageId: string;
errorMessage?: string;
errorCode?: string;
}
/**
* SSE 回调函数类型
*/
export type OnData = (message: string, isFirstMessage: boolean, moreInfo: OnDataMoreInfo) => void;
export type OnThought = (thought: ThoughtItem) => void;
export type OnFile = (file: VisionFile) => void;
export type OnMessageEnd = (messageEnd: MessageEnd) => void;
export type OnMessageReplace = (messageReplace: MessageReplace) => void;
export type OnCompleted = (hasError?: boolean) => void;
export type OnError = (msg: string, code?: string) => void;
export type OnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void;
export type OnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void;
export type OnNodeStarted = (nodeStarted: NodeStartedResponse) => void;
export type OnNodeFinished = (nodeFinished: NodeFinishedResponse) => void;
/**
* SSE 回调配置
*/
export interface SSECallbacks {
onData: OnData;
onCompleted?: OnCompleted;
onThought?: OnThought;
onFile?: OnFile;
onMessageEnd?: OnMessageEnd;
onMessageReplace?: OnMessageReplace;
onError?: OnError;
onWorkflowStarted?: OnWorkflowStarted;
onWorkflowFinished?: OnWorkflowFinished;
onNodeStarted?: OnNodeStarted;
onNodeFinished?: OnNodeFinished;
getAbortController?: (controller: AbortController) => void;
}
// ============================================================================
// API 请求/响应类型
// ============================================================================
/**
* 发送消息请求参数
*/
export interface SendMessageParams {
query: string;
inputs?: Record<string, any>;
conversation_id?: string | null;
files?: VisionFile[];
response_mode?: 'streaming' | 'blocking';
}
/**
* 会话列表响应
*/
export interface ConversationsResponse {
data: ConversationItem[];
has_more: boolean;
limit: number;
}
/**
* 消息列表响应
*/
export interface MessagesResponse {
data: Array<{
id: string;
query: string;
answer: string;
agent_thoughts?: ThoughtItem[];
message_files?: VisionFile[];
feedback?: Feedbacktype;
}>;
has_more: boolean;
limit: number;
}
/**
* 应用参数响应
*/
export interface AppParametersResponse {
user_input_form?: UserInputFormItem[];
opening_statement?: string;
file_upload?: {
enabled: boolean;
allowed_file_extensions?: string[];
allowed_file_types?: string[];
number_limits?: number;
};
}
+300
View File
@@ -0,0 +1,300 @@
/**
* Dify Dataset 客户端 API 模块
*
* 提供浏览器端调用 Dify 知识库 API 的函数
* 通过 Remix API Routes 代理请求
*
* @module api/dify-dataset/client
*/
import axios from 'axios';
import type {
DatasetsResponse,
DocumentsResponse,
SegmentsResponse,
Document,
OperationResult,
} from './types';
// ============================================================================
// 基础配置
// ============================================================================
/**
* API 基础 URL
* 指向 Remix API Routes/api/dataset/*
*/
const API_URL = '/api/dataset';
// ============================================================================
// 知识库 API
// ============================================================================
/**
* 获取知识库列表
*
* @param page - 页码,默认 1
* @param limit - 每页数量,默认 20
* @returns 知识库列表响应
*/
export async function fetchDatasets(
page: number = 1,
limit: number = 20
): Promise<DatasetsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
const response = await axios.get<DatasetsResponse>(
`${API_URL}/datasets?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 获取单个知识库详情
*
* @param datasetId - 知识库 ID
* @returns 知识库详情
*/
export async function fetchDataset(datasetId: string): Promise<any> {
const response = await axios.get(
`${API_URL}/datasets/${datasetId}`,
{ withCredentials: true }
);
return response.data;
}
// ============================================================================
// 文档 API
// ============================================================================
/**
* 获取知识库文档列表
*
* @param datasetId - 知识库 ID
* @param page - 页码,默认 1
* @param limit - 每页数量,默认 20
* @param keyword - 搜索关键词
* @returns 文档列表响应
*/
export async function fetchDocuments(
datasetId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<DocumentsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (keyword) {
params.append('keyword', keyword);
}
console.log('[Dataset Client] 获取文档列表:', { datasetId, page, limit, keyword });
const response = await axios.get<DocumentsResponse>(
`${API_URL}/datasets/${datasetId}/documents?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 获取单个文档详情
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @returns 文档详情
*/
export async function fetchDocument(
datasetId: string,
documentId: string
): Promise<Document> {
const response = await axios.get<Document>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 删除文档
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @returns 操作结果
*/
export async function deleteDocument(
datasetId: string,
documentId: string
): Promise<OperationResult> {
console.log('[Dataset Client] 删除文档:', { datasetId, documentId });
const response = await axios.delete<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 启用/禁用文档
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param enabled - 是否启用
* @returns 操作结果
*/
export async function toggleDocumentStatus(
datasetId: string,
documentId: string,
enabled: boolean
): Promise<OperationResult> {
console.log('[Dataset Client] 切换文档状态:', { datasetId, documentId, enabled });
const response = await axios.patch<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/status`,
{ enabled },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
// ============================================================================
// 文档分段 API
// ============================================================================
/**
* 获取文档分段列表
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param page - 页码,默认 1
* @param limit - 每页数量,默认 20
* @param keyword - 搜索关键词
* @returns 分段列表响应
*/
export async function fetchSegments(
datasetId: string,
documentId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<SegmentsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (keyword) {
params.append('keyword', keyword);
}
console.log('[Dataset Client] 获取分段列表:', { datasetId, documentId, page, limit });
const response = await axios.get<SegmentsResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 删除分段
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @returns 操作结果
*/
export async function deleteSegment(
datasetId: string,
documentId: string,
segmentId: string
): Promise<OperationResult> {
console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId });
const response = await axios.delete<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 启用/禁用分段
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param enabled - 是否启用
* @returns 操作结果
*/
export async function toggleSegmentStatus(
datasetId: string,
documentId: string,
segmentId: string,
enabled: boolean
): Promise<OperationResult> {
const response = await axios.patch<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/status`,
{ enabled },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
// ============================================================================
// 文件上传 API
// ============================================================================
/**
* 上传文件到知识库
*
* @param datasetId - 知识库 ID
* @param file - 文件对象
* @param onProgress - 上传进度回调
* @returns 创建的文档信息
*/
export async function uploadDocument(
datasetId: string,
file: File,
onProgress?: (percent: number) => void
): Promise<any> {
const formData = new FormData();
formData.append('file', file);
formData.append('data', JSON.stringify({
indexing_technique: 'high_quality',
process_rule: {
mode: 'automatic',
},
}));
console.log('[Dataset Client] 上传文档:', { datasetId, fileName: file.name });
const response = await axios.post(
`${API_URL}/datasets/${datasetId}/documents/create-by-file`,
formData,
{
withCredentials: true,
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(percent);
}
},
}
);
return response.data;
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Dify Dataset API 模块统一导出
*
* @module api/dify-dataset
*/
// 类型导出
export type {
Dataset,
DatasetsResponse,
Document,
DocumentsResponse,
Segment,
SegmentsResponse,
IndexingStatus,
OperationResult,
CreateDocumentResponse,
UploadProgress,
} from './types';
// 客户端 API 导出
export {
// 知识库
fetchDatasets,
fetchDataset,
// 文档
fetchDocuments,
fetchDocument,
deleteDocument,
toggleDocumentStatus,
uploadDocument,
// 分段
fetchSegments,
deleteSegment,
toggleSegmentStatus,
} from './client';
+167
View File
@@ -0,0 +1,167 @@
/**
* Dify Dataset API 类型定义
*
* @module api/dify-dataset/types
*/
// ============================================================================
// 知识库类型
// ============================================================================
/**
* 知识库信息
*/
export interface Dataset {
id: string;
name: string;
description: string;
permission: 'only_me' | 'all_team_members';
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
indexing_technique: 'high_quality' | 'economy';
app_count: number;
document_count: number;
word_count: number;
created_by: string;
created_at: number;
updated_by: string;
updated_at: number;
}
/**
* 知识库列表响应
*/
export interface DatasetsResponse {
data: Dataset[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
// ============================================================================
// 文档类型
// ============================================================================
/**
* 文档索引状态
*/
export type IndexingStatus =
| 'waiting'
| 'parsing'
| 'cleaning'
| 'splitting'
| 'indexing'
| 'paused'
| 'error'
| 'completed';
/**
* 文档信息
*/
export interface Document {
id: string;
position: number;
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
data_source_info: {
upload_file_id?: string;
notion_page_id?: string;
website_url?: string;
};
dataset_process_rule_id: string;
name: string;
created_from: string;
created_by: string;
created_at: number;
tokens: number;
indexing_status: IndexingStatus;
error?: string;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
archived: boolean;
display_status: string;
word_count: number;
hit_count: number;
doc_form: string;
}
/**
* 文档列表响应
*/
export interface DocumentsResponse {
data: Document[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
// ============================================================================
// 文档分段类型
// ============================================================================
/**
* 文档分段
*/
export interface Segment {
id: string;
position: number;
document_id: string;
content: string;
answer?: string;
word_count: number;
tokens: number;
keywords: string[];
index_node_id: string;
index_node_hash: string;
hit_count: number;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
status: 'waiting' | 'completed' | 'error' | 'indexing';
created_by: string;
created_at: number;
indexing_at?: number;
completed_at?: number;
error?: string;
stopped_at?: number;
}
/**
* 分段列表响应
*/
export interface SegmentsResponse {
data: Segment[];
has_more: boolean;
limit: number;
total: number;
}
// ============================================================================
// 操作响应类型
// ============================================================================
/**
* 通用操作结果
*/
export interface OperationResult {
result: 'success' | 'error';
message?: string;
}
/**
* 创建文档响应
*/
export interface CreateDocumentResponse {
document: Document;
batch: string;
}
/**
* 文档上传进度
*/
export interface UploadProgress {
loaded: number;
total: number;
percent: number;
}