feat:重构dify前端组件以及转发逻辑
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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
@@ -1,4 +1,4 @@
|
||||
import type { AppInfo } from '../types/dify_chat';
|
||||
import type { AppInfo } from '~/api/dify';
|
||||
|
||||
// 在客户端获取环境变量的辅助函数
|
||||
const getEnvVar = (name: string, defaultValue: string = '') => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 本地存储键名
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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,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,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
@@ -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,
|
||||
};
|
||||
@@ -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,4 +1,4 @@
|
||||
import type { PromptVariable, ThoughtItem, UserInputFormItem, VisionFile } from '../types/dify_chat';
|
||||
import type { PromptVariable, ThoughtItem, UserInputFormItem, VisionFile } from '~/api/dify';
|
||||
|
||||
/**
|
||||
* 替换提示模板中的变量
|
||||
|
||||
Reference in New Issue
Block a user