feat:重构dify前端组件以及转发逻辑

This commit is contained in:
PingChuan
2025-11-30 16:24:35 +08:00
parent 8aa0d87edc
commit 9614899171
23 changed files with 1863 additions and 1745 deletions
+284
View File
@@ -0,0 +1,284 @@
/**
* Dify 服务端 API 模块
*
* 提供 Node.js 服务端调用 FastAPI 后端的函数
* 用于 Remix loader/action 中调用 Dify API
*
* 调用链路:
* 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`,
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 对象
*/
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,请求可能失败');
}
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ [Dify Server] Dify 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 API 客户端
// ============================================================================
/**
* Dify 服务端 API 客户端
*
* 所有方法都需要传入 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 Server] 解析 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('remix后端接收到删除请求,调用fastapi:', conversationId);
try {
const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE',
body: JSON.stringify({}),
}, jwt);
// 对于 204 No Content 响应,直接返回成功
if (response.status === 204) {
console.log('删除会话' + conversationId + '成功');
return { result: 'success' };
}
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
console.log('🗑️ [Dify Server] 删除会话 JSON 响应:', data);
return data;
}
const text = await response.text();
console.log('🗑️ [Dify Server] 删除会话文本响应:', text);
return { result: 'success' };
} catch (error: any) {
console.warn('⚠️ [Dify Server] 删除会话请求失败,但可能已成功删除:', 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();
},
};
// ============================================================================
// 工具函数
// ============================================================================
/**
* Dify 工具函数
*/
export const difyUtils = {
/**
* 获取 Dify 配置
*/
getConfig: () => DIFY_CONFIG,
};
+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;
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd';
import { StopOutlined, PictureOutlined, CommentOutlined } from '@ant-design/icons';
import type { VisionFile } from '../../types/dify_chat';
import type { VisionFile } from '~/api/dify';
import '../../styles/components/chat-with-llm/chat-input.css';
const { TextArea } = Input;
+1 -1
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Button, Card, Spin } from 'antd';
import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat';
import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '~/api/dify';
import { CHAT_CONFIG } from '../../config/chat';
import Markdown from './markdown';
import ThoughtProcess from './thought-process';
+2 -2
View File
@@ -8,9 +8,9 @@ import ChatSidebar, { type ChatSidebarRef } from './sidebar';
// import Header from '../layout/Header';
import useConversation from '../../hooks/use-conversation';
import useChatMessage from '../../hooks/use-chat-message';
import type { ChatItem, ConversationItem } from '../../types/dify_chat';
import type { ChatItem, ConversationItem } from '~/api/dify';
import { CHAT_CONFIG } from '../../config/chat';
import { fetchConversations, fetchAppParams, fetchChatList } from '../../services/api.client';
import { fetchConversations, fetchAppParams, fetchChatList } from '~/api/dify';
import '../../styles/components/chat-with-llm/index.css';
const { Content } = Layout;
+2 -2
View File
@@ -11,8 +11,8 @@ import {
EditOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { ConversationItem } from '../../types/dify_chat';
import { deleteConversation, renameConversation } from '../../services/api.client';
import type { ConversationItem } from '~/api/dify';
import { deleteConversation, renameConversation } from '~/api/dify';
import '../../styles/components/chat-with-llm/sidebar.css';
const { Sider } = Layout;
+1 -1
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Card, Collapse, Tag, Spin, Typography, Button } from 'antd';
import { ToolOutlined, ThunderboltOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import type { ThoughtItem } from '../../types/dify_chat';
import type { ThoughtItem } from '~/api/dify';
import Markdown from './markdown';
import '../../styles/components/chat-with-llm/thought-process.css';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppInfo } from '../types/dify_chat';
import type { AppInfo } from '~/api/dify';
// 在客户端获取环境变量的辅助函数
const getEnvVar = (name: string, defaultValue: string = '') => {
+7 -82
View File
@@ -1,9 +1,8 @@
import { useState, useCallback, useRef } from 'react';
import { produce } from 'immer';
import { useBoolean, useGetState } from 'ahooks';
import { sendChatMessage, updateFeedback, generateConversationName } from '../services/api.client';
import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile, MessageEnd, MessageReplace } from '../types/dify_chat';
import { CHAT_CONFIG } from '../config/chat';
import { produce } from 'immer';
import { useCallback, useRef, useState } from 'react';
import { sendChatMessage, updateFeedback, generateConversationName } from '~/api/dify';
import type { ChatItem, Feedbacktype, MessageEnd, MessageReplace, VisionFile } from '~/api/dify';
/**
* 聊天消息处理钩子
@@ -61,22 +60,9 @@ export default function useChatMessage({
questionItem: ChatItem,
originalResponseId?: string
) => {
// console.log('🔄 [useChatMessage] 更新聊天列表:', {
// responseItemId: responseItem.id,
// responseContentLength: responseItem.content.length,
// responsePreview: responseItem.content.substring(0, 50) + (responseItem.content.length > 50 ? '...' : ''),
// originalResponseId,
// questionId,
// placeholderAnswerId
// });
setChatList(produce(getChatList(), (draft) => {
// console.log('📝 [useChatMessage] 当前聊天列表:', draft.map(item => ({
// id: item.id,
// contentLength: item.content.length,
// contentPreview: item.content.substring(0, 20) + (item.content.length > 20 ? '...' : ''),
// isAnswer: item.isAnswer
// })));
// 移除占位符
const placeholderIndex = draft.findIndex(item => item.id === placeholderAnswerId);
@@ -112,26 +98,11 @@ export default function useChatMessage({
}
if (responseIndex !== -1) {
// console.log('✏️ [useChatMessage] 更新现有响应:', {
// responseIndex,
// oldContentLength: draft[responseIndex].content.length,
// newContentLength: responseItem.content.length
// });
draft[responseIndex] = { ...responseItem };
} else {
// console.log(' [useChatMessage] 添加新响应:', {
// responseId: responseItem.id,
// contentLength: responseItem.content.length
// });
draft.push({ ...responseItem });
}
// console.log('📝 [useChatMessage] 更新后聊天列表:', draft.map(item => ({
// id: item.id,
// contentLength: item.content.length,
// contentPreview: item.content.substring(0, 20) + (item.content.length > 20 ? '...' : ''),
// isAnswer: item.isAnswer
// })));
}));
}, [getChatList, setChatList]);
@@ -274,27 +245,11 @@ export default function useChatMessage({
// 发送消息
await sendChatMessage(data, {
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }) => {
// console.log('📨 [useChatMessage] 收到流式数据:', {
// messageLength: message.length,
// message: message.substring(0, 100) + (message.length > 100 ? '...' : ''),
// isFirstMessage,
// messageId,
// newConversationId,
// taskId,
// isAgentMode,
// currentContentLength: responseItem.content.length
// });
if (!isAgentMode) {
// 累积消息内容
const oldContent = responseItem.content;
responseItem.content = responseItem.content + message;
// console.log('📝 [useChatMessage] 累积消息内容:', {
// oldLength: oldContent.length,
// newLength: responseItem.content.length,
// addedLength: message.length,
// preview: responseItem.content.substring(0, 50) + '...'
// });
} else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1];
if (lastThought) {
@@ -323,13 +278,6 @@ export default function useChatMessage({
setMessageTaskId(taskId || '');
// 检查是否切换到其他会话
// console.log('🔍 会话检查:', {
// prevTempNewConversationId,
// conversationId,
// isEqual: prevTempNewConversationId === conversationId
// });
// 修复新会话的匹配逻辑
const isNewConversationMatch = (prevTempNewConversationId === '-1' && conversationId === null) ||
(prevTempNewConversationId === conversationId);
@@ -340,25 +288,6 @@ export default function useChatMessage({
return;
}
// console.log('🔄 准备调用updateCurrentQA:', {
// responseItemId: responseItem.id,
// responseContent: responseItem.content,
// questionId,
// placeholderAnswerId,
// originalResponseId
// });
// console.log('🔄 [useChatMessage] 准备调用updateCurrentQA:', {
// responseItemId: responseItem.id,
// responseContentLength: responseItem.content.length,
// responsePreview: responseItem.content.substring(0, 100) + (responseItem.content.length > 100 ? '...' : ''),
// questionId,
// placeholderAnswerId,
// originalResponseId,
// isAgentMode,
// agentThoughtsCount: responseItem.agent_thoughts?.length || 0
// });
// 更新当前问答(使用防抖)
updateCurrentQA({
responseItem: { ...responseItem }, // 创建副本避免引用问题
@@ -370,7 +299,6 @@ export default function useChatMessage({
},
onCompleted: async (hasError?: boolean) => {
// console.log('✅ 消息发送完成:', { hasError });
// 立即更新最终状态
if (currentResponseRef.current) {
@@ -541,10 +469,7 @@ export default function useChatMessage({
*/
const handleFeedback = useCallback(async (messageId: string, feedback: Feedbacktype) => {
try {
await updateFeedback({
url: `messages/${messageId}/feedbacks`,
body: feedback,
});
await updateFeedback(messageId, feedback);
// 更新聊天列表中的反馈
setChatList(produce(getChatList(), (draft) => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import { useParams } from '@remix-run/react';
import { produce } from 'immer';
import { useGetState, useLocalStorageState } from 'ahooks';
import type { ConversationItem } from '../types/dify_chat';
import type { ConversationItem } from '~/api/dify';
import { CHAT_CONFIG } from '../config/chat';
// 本地存储键名
+64 -12
View File
@@ -1,7 +1,67 @@
import { type ActionFunctionArgs } from '@remix-run/node';
import { difyClient } from '../services/dify-client.server';
import { type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
import { difyClient } from '~/api/dify/client.server';
import { getSessionInfo } from '../utils/session.server';
/**
* GET /api/chat-messages - 获取会话消息列表
*/
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
// 检查 JWT 是否存在
if (!frontendJWT) {
console.error('❌ [API] Chat Messages GET - JWT不存在');
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
);
}
// 从 URL 参数获取 conversation_id
const url = new URL(request.url);
const conversationId = url.searchParams.get('conversation_id');
if (!conversationId) {
return new Response(
JSON.stringify({ error: '缺少 conversation_id 参数' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
console.log('客戶端調用remix路由_Chat Messages GET - 获取消息列表:', { conversationId });
const result = await difyClient.getConversationMessages(conversationId, frontendJWT);
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('❌ [API] Chat Messages GET - Error:', error.message);
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
return new Response(
JSON.stringify({ error: error.message || 'Failed to get messages' }),
{
status,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
/**
* POST /api/chat-messages - 发送聊天消息
*/
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
@@ -34,7 +94,7 @@ export async function action({ request }: ActionFunctionArgs) {
response_mode: responseMode,
} = body;
console.log('🚀 [API] Chat Messages API - 收到请求:', {
console.log('客戶端調用remix路由_Chat Messages API - 收到请求:', {
queryLength: query?.length || 0,
queryPreview: query?.substring(0, 100) + (query?.length > 100 ? '...' : ''),
conversationId,
@@ -54,16 +114,9 @@ export async function action({ request }: ActionFunctionArgs) {
frontendJWT // 传递 JWT
);
console.log('📡 [API] Dify响应状态:', {
status: response.status,
statusText: response.statusText,
hasBody: !!response.body,
headers: Object.fromEntries(response.headers.entries())
});
// 对于流式响应,直接返回流
if (responseMode === 'streaming') {
console.log('🌊 [API] 返回流式响应');
console.log('Dify转发fastapi,返回流式响应');
return new Response(response.body, {
status: response.status,
headers: {
@@ -78,7 +131,6 @@ export async function action({ request }: ActionFunctionArgs) {
}
// 对于非流式响应,返回JSON
console.log('📄 [API] 返回JSON响应');
return new Response(JSON.stringify(response), {
status: 200,
headers: {
+2 -4
View File
@@ -1,5 +1,5 @@
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { difyClient } from '../services/dify-client.server';
import { difyClient } from '~/api/dify/client.server';
import { getSessionInfo, commitSession } from '../utils/session.server';
export async function action({ request, params }: ActionFunctionArgs) {
@@ -31,7 +31,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const body = await request.json();
const { auto_generate, name } = body;
console.log('💬 [API] Rename Conversation API - 重命名会话:', {
console.log('客戶端調用remix路由Rename Conversation API - 重命名会话:', {
id,
autoGenerate: auto_generate,
name,
@@ -41,8 +41,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
// 调用服务端API重命名会话
const data = await difyClient.renameConversation(id, name, auto_generate, frontendJWT);
console.log('✅ [API] Rename Conversation API - Success');
return json(data, {
headers: {
'Set-Cookie': await commitSession(session),
+1 -7
View File
@@ -1,5 +1,5 @@
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { difyClient } from '../services/dify-client.server';
import { difyClient } from '~/api/dify/client.server';
import { getSessionInfo, commitSession } from '../utils/session.server';
export async function action({ request, params }: ActionFunctionArgs) {
@@ -31,16 +31,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
const method = request.method;
if (method === 'DELETE') {
console.log('🗑️ [API] Delete Conversation API - 删除会话:', {
id,
hasJWT: !!frontendJWT
});
// 调用服务端API删除会话
const data = await difyClient.deleteConversation(id, frontendJWT);
console.log('✅ [API] Delete Conversation API - Success');
return json(data, {
headers: {
'Set-Cookie': await commitSession(session),
+1 -7
View File
@@ -1,5 +1,5 @@
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { difyClient } from '../services/dify-client.server';
import { difyClient } from '~/api/dify/client.server';
import { getSessionInfo, commitSession } from '../utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) {
@@ -23,14 +23,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
);
}
console.log('💬 [API] Conversations API - 获取会话列表:', {
hasJWT: !!frontendJWT
});
const data = await difyClient.getConversations(frontendJWT);
console.log('✅ [API] Conversations API - Success');
return json(data, {
headers: {
'Set-Cookie': await commitSession(session),
@@ -0,0 +1,67 @@
import { type ActionFunctionArgs } from '@remix-run/node';
import { difyClient } from '~/api/dify/client.server';
/**
* POST /api/messages/:messageId/feedbacks - 提交消息反馈
*/
export async function action({ request, params }: ActionFunctionArgs) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
// 检查 JWT 是否存在
if (!frontendJWT) {
console.error('❌ [API] Message Feedback - JWT不存在');
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
);
}
const { messageId } = params;
if (!messageId) {
return new Response(
JSON.stringify({ error: '缺少 messageId 参数' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
const body = await request.json();
const { rating } = body;
console.log('👍 [API] Message Feedback - 提交反馈:', {
messageId,
rating,
});
const result = await difyClient.updateMessageFeedback(messageId, rating, frontendJWT);
console.log('✅ [API] Message Feedback - Success');
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('❌ [API] Message Feedback - Error:', error.message);
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
return new Response(
JSON.stringify({ error: error.message || 'Failed to submit feedback' }),
{
status,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
+1 -7
View File
@@ -1,5 +1,5 @@
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { difyClient } from '../services/dify-client.server';
import { difyClient } from '~/api/dify/client.server';
import { getSessionInfo, commitSession } from '../utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) {
@@ -23,14 +23,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
);
}
console.log('📋 [API] Parameters API - 获取应用参数:', {
hasJWT: !!frontendJWT
});
const data = await difyClient.getApplicationParameters(frontendJWT);
console.log('✅ [API] Parameters API - Success');
return json(data, {
headers: {
'Set-Cookie': await commitSession(session),
File diff suppressed because it is too large Load Diff
-264
View File
@@ -1,264 +0,0 @@
import { API_BASE_URL } from '~/config/api-config';
// 获取环境变量的服务端函数
const getServerEnvVar = (name: string, defaultValue: string = '') => {
const value = process.env[name] || defaultValue;
// console.log(`🌐 [DifyClient] 读取环境变量 ${name}:`, {
// hasValue: !!process.env[name],
// value: process.env[name] ? `${process.env[name].substring(0, 20)}...` : 'undefined',
// usingDefault: !process.env[name]
// });
return value;
};
// Dify API 客户端配置
// 注意:现在通过 FastAPI 后端的 /dify 路由代理访问 Dify,使用 JWT 认证
const DIFY_CONFIG = {
// API_URL 指向 FastAPI 后端的 /dify 路由
// API_BASE_URL 来自 api-config.ts,根据环境/端口自动配置
API_URL: `${API_BASE_URL}/dify`,
// API_KEY 保留用于配置验证(实际不再使用,改用JWT)
API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''),
APP_ID: (() => {
const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', '');
// 从完整URL中提取APP ID
const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/);
return match ? match[1] : rawAppId;
})(),
};
console.log('🔧 Dify Client Config:', {
apiUrl: DIFY_CONFIG.API_URL,
apiBaseUrl: API_BASE_URL,
fullDifyUrl: `${API_BASE_URL}/dify`,
appId: DIFY_CONFIG.APP_ID,
hasApiKey: !!DIFY_CONFIG.API_KEY,
configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID)
});
// 基础请求函数 - 使用 JWT 认证通过 FastAPI 代理访问 Dify
const difyFetch = async (endpoint: string, options: RequestInit = {}, jwt?: string) => {
const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`;
console.log('🌐 [DifyClient] 请求FastAPI代理:', {
endpoint,
fullUrl: url,
baseUrl: API_BASE_URL,
hasJWT: !!jwt
});
// 使用 JWT 认证而非 API_KEY
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
};
// 如果提供了 JWT,添加到请求头
if (jwt) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`;
} else {
console.warn('⚠️ [DifyClient] 没有提供 JWT,请求可能失败');
}
console.log('🌐 [DifyClient] Dify API Request:', {
url,
method: options.method || 'GET',
hasJWT: !!jwt,
jwtPreview: jwt ? `${jwt.substring(0, 20)}...` : 'none'
});
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ [DifyClient] Dify API Error:', {
status: response.status,
statusText: response.statusText,
error: errorText
});
// 如果是401错误,说明JWT过期或无效
if (response.status === 401) {
throw new Error('JWT认证失败,请重新登录');
}
throw new Error(`Dify API Error: ${response.status} ${response.statusText}`);
}
return response;
};
// Dify API 客户端 - 所有方法都需要传入 JWT
// 注意:user 参数已移除,由后端自动从 JWT 中提取 username
export const difyClient = {
// 获取应用参数
async getApplicationParameters(jwt?: string) {
const response = await difyFetch('parameters', {
method: 'GET',
}, jwt);
return response.json();
},
// 获取会话列表
async getConversations(jwt?: string) {
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) {
const params = new URLSearchParams({
conversation_id: conversationId,
limit: '20',
last_id: '',
});
const response = await difyFetch(`messages?${params}`, {
method: 'GET',
}, jwt);
return response.json();
},
// 发送聊天消息
async createChatMessage(
inputs: Record<string, any>,
query: string,
responseMode: string = 'streaming',
conversationId?: string,
files?: any[],
jwt?: string
) {
const body = {
inputs,
query,
// user 字段已移除,后端会自动从 JWT 中提取 username
response_mode: responseMode,
conversation_id: conversationId,
files: files || [],
};
console.log('🌐 [DifyClient] 发送聊天消息:', {
queryLength: query.length,
queryPreview: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
responseMode,
conversationId,
hasInputs: !!inputs && Object.keys(inputs).length > 0,
inputsKeys: inputs ? Object.keys(inputs) : [],
hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0,
hasJWT: !!jwt
});
const response = await difyFetch('chat-messages', {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
console.log('📡 [DifyClient] Dify API响应:', {
status: response.status,
statusText: response.statusText,
hasBody: !!response.body,
contentType: response.headers.get('Content-Type'),
responseMode
});
// 对于流式响应,直接返回Response对象
if (responseMode === 'streaming') {
console.log('🌊 [DifyClient] 返回流式响应对象');
return response;
}
console.log('📄 [DifyClient] 解析JSON响应');
return response.json();
},
// 重命名会话
async renameConversation(conversationId: string, name: string, autoGenerate: boolean = false, jwt?: string) {
const body = {
name,
auto_generate: autoGenerate,
// user 字段已移除,后端会自动从 JWT 中提取 username
};
const response = await difyFetch(`conversations/${conversationId}/name`, {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
return response.json();
},
// 删除会话
async deleteConversation(conversationId: string, jwt?: string) {
// user 字段已移除,后端会自动从 JWT 中提取 username
const body = {};
console.log('🗑️ [DifyClient] 删除会话:', conversationId);
try {
const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE',
body: JSON.stringify(body),
}, jwt);
console.log('🗑️ [DifyClient] 删除会话响应:', {
status: response.status,
statusText: response.statusText,
contentType: response.headers.get('Content-Type')
});
// 检查响应的Content-Type
const contentType = response.headers.get('Content-Type');
// 如果是JSON响应,解析JSON
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
console.log('🗑️ [DifyClient] 删除会话JSON响应:', data);
return data;
}
// 如果不是JSON,返回成功标识
const text = await response.text();
console.log('🗑️ [DifyClient] 删除会话文本响应:', text);
// 返回标准成功响应
return { result: 'success' };
} catch (error: any) {
console.warn('⚠️ [DifyClient] 删除会话请求失败,但可能已成功删除:', error.message);
// 删除操作的特殊处理:
// 即使API返回错误,实际上会话可能已经被删除
// 返回成功标识,避免误报错误
// 如果会话确实不存在,下次加载会话列表时就会发现
return { result: 'success' };
}
},
// 更新消息反馈
async updateMessageFeedback(messageId: string, rating: 'like' | 'dislike' | null, jwt?: string) {
const body = {
rating,
// user 字段已移除,后端会自动从 JWT 中提取 username
};
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
return response.json();
},
};
// 工具函数
export const difyUtils = {
getConfig: () => DIFY_CONFIG,
};
-264
View File
@@ -1,264 +0,0 @@
// 应用信息类型
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 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 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 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; // for UI
}
// 工作流进程类型
export interface WorkflowProcess {
status: WorkflowRunningStatus;
tracing: NodeTracing[];
expand?: boolean; // for UI
}
// 代码语言枚举
export enum CodeLanguage {
python3 = 'python3',
javascript = 'javascript',
json = 'json',
}
// 消息事件类型
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;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PromptVariable, ThoughtItem, UserInputFormItem, VisionFile } from '../types/dify_chat';
import type { PromptVariable, ThoughtItem, UserInputFormItem, VisionFile } from '~/api/dify';
/**
* 替换提示模板中的变量