cf6e9c2421
根本问题:客户端代码直接调用Dify API(12980端口),绕过了服务端代理 修改内容: 1. app/config/api-config.ts - 添加独立的 difyBaseUrl 配置(指向外网 nas.7bm.co:8000) - 导出 DIFY_BASE_URL 供服务端使用 2. app/config/chat.ts - 移除直接Dify API配置(NEXT_PUBLIC_API_URL, APP_ID, API_KEY) - 移除 generateUserId 函数 - API_URL 改为 '/api'(指向Remix API routes) 3. app/services/api.client.ts - 所有fetch调用改为相对路径 /api/* - 移除所有 Authorization 头(服务端自动处理JWT) - 移除所有 user 参数传递(服务端从JWT提取) - credentials 改为 'include' 以携带cookie 4. app/services/dify-client.server.ts - 使用 DIFY_BASE_URL 替代 API_BASE_URL 5. app/utils/dify-test.client.ts - 测试函数改为调用Remix API routes 调用链路: 客户端 → /api/* → Remix API routes → dify-client.server.ts → FastAPI /dify → Dify 解决问题: - ✅ 不再直接调用 nas.7bm.co:12980(Dify端口) - ✅ 统一通过 nas.7bm.co:8000/dify(FastAPI代理) - ✅ 所有请求都经过JWT认证 - ✅ user字段由后端自动管理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1050 lines
34 KiB
TypeScript
1050 lines
34 KiB
TypeScript
import { CHAT_CONFIG, ContentType, SSE_TIMEOUT } from '../config/chat';
|
||
import type { Feedbacktype, ThoughtItem, VisionFile, MessageEnd, MessageReplace } from '../types/dify_chat';
|
||
import { unicodeToChar } from '../utils/chat-utils';
|
||
|
||
// 基础请求选项
|
||
// 注意:客户端调用Remix API routes,不需要手动添加Authorization
|
||
// Remix会通过session自动处理JWT认证
|
||
const baseOptions = {
|
||
method: 'GET',
|
||
mode: 'cors' as RequestMode,
|
||
credentials: 'include' as RequestCredentials, // 改为include以携带cookie
|
||
headers: new Headers({
|
||
'Content-Type': ContentType.json,
|
||
// 移除Authorization头,由服务端自动处理
|
||
}),
|
||
redirect: 'follow' as RequestRedirect,
|
||
};
|
||
|
||
// 回调接口定义
|
||
export type IOnDataMoreInfo = {
|
||
conversationId?: string;
|
||
taskId?: string;
|
||
messageId: string;
|
||
errorMessage?: string;
|
||
errorCode?: string;
|
||
}
|
||
|
||
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void;
|
||
export type IOnThought = (thought: ThoughtItem) => void;
|
||
export type IOnFile = (file: VisionFile) => void;
|
||
export type IOnMessageEnd = (messageEnd: MessageEnd) => void;
|
||
export type IOnMessageReplace = (messageReplace: MessageReplace) => void;
|
||
export type IOnCompleted = (hasError?: boolean) => void;
|
||
export type IOnError = (msg: string, code?: string) => void;
|
||
|
||
// 工作流相关类型
|
||
export type WorkflowStartedResponse = {
|
||
task_id: string;
|
||
workflow_run_id: string;
|
||
event: string;
|
||
data: {
|
||
id: string;
|
||
workflow_id: string;
|
||
sequence_number: number;
|
||
created_at: number;
|
||
};
|
||
}
|
||
|
||
export type 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 type 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 type 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;
|
||
};
|
||
}
|
||
|
||
export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void;
|
||
export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void;
|
||
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void;
|
||
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void;
|
||
|
||
/**
|
||
* 处理服务器发送事件 (SSE) 流式响应
|
||
*
|
||
* 这是核心的流式响应处理函数,负责:
|
||
* - 解析 SSE 数据流
|
||
* - 处理各种事件类型(消息、思考、文件、工作流等)
|
||
* - 错误处理和状态管理
|
||
* - 实时更新 UI
|
||
*
|
||
* @param response - fetch API 返回的 Response 对象
|
||
* @param onData - 处理消息数据的回调函数
|
||
* @param onCompleted - 流式响应完成时的回调函数
|
||
* @param onThought - 处理 Agent 思考过程的回调函数
|
||
* @param onMessageEnd - 处理消息结束事件的回调函数
|
||
* @param onMessageReplace - 处理消息替换事件的回调函数
|
||
* @param onFile - 处理文件事件的回调函数
|
||
* @param onWorkflowStarted - 处理工作流开始事件的回调函数
|
||
* @param onWorkflowFinished - 处理工作流完成事件的回调函数
|
||
* @param onNodeStarted - 处理节点开始事件的回调函数
|
||
* @param onNodeFinished - 处理节点完成事件的回调函数
|
||
* @param onError - 处理错误的回调函数
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const response = await fetch('/api/chat-messages', options);
|
||
* handleStream(
|
||
* response,
|
||
* (message, isFirst, info) => console.log('收到消息:', message),
|
||
* () => console.log('流式响应完成'),
|
||
* (thought) => console.log('AI思考:', thought),
|
||
* // ... 其他回调
|
||
* );
|
||
* ```
|
||
*/
|
||
const handleStream = (
|
||
response: Response,
|
||
onData: IOnData,
|
||
onCompleted?: IOnCompleted,
|
||
onThought?: IOnThought,
|
||
onMessageEnd?: IOnMessageEnd,
|
||
onMessageReplace?: IOnMessageReplace,
|
||
onFile?: IOnFile,
|
||
onWorkflowStarted?: IOnWorkflowStarted,
|
||
onWorkflowFinished?: IOnWorkflowFinished,
|
||
onNodeStarted?: IOnNodeStarted,
|
||
onNodeFinished?: IOnNodeFinished,
|
||
onError?: IOnError,
|
||
) => {
|
||
if (!response.ok) {
|
||
console.error('❌ [handleStream] 响应错误:', response.status, response.statusText);
|
||
onError?.('网络响应错误');
|
||
throw new Error('网络响应错误');
|
||
}
|
||
|
||
const reader = response.body?.getReader();
|
||
const decoder = new TextDecoder('utf-8');
|
||
let buffer = '';
|
||
let bufferObj: Record<string, any>;
|
||
let isFirstMessage = true;
|
||
let messageCount = 0;
|
||
|
||
|
||
function read() {
|
||
let hasError = false;
|
||
reader?.read().then((result: any) => {
|
||
|
||
if (result.done) {
|
||
onCompleted && onCompleted();
|
||
return;
|
||
}
|
||
|
||
const chunk = decoder.decode(result.value, { stream: true });
|
||
buffer += chunk;
|
||
const lines = buffer.split('\n');
|
||
|
||
|
||
try {
|
||
lines.forEach((message, index) => {
|
||
if (message.startsWith('data: ')) {
|
||
const jsonStr = message.substring(6);
|
||
|
||
try {
|
||
bufferObj = JSON.parse(jsonStr) as Record<string, any>;
|
||
}
|
||
catch (e) {
|
||
console.warn('⚠️ [handleStream] JSON解析失败:', e, 'JSON:', jsonStr);
|
||
// 处理消息截断
|
||
onData('', isFirstMessage, {
|
||
conversationId: bufferObj?.conversation_id,
|
||
messageId: bufferObj?.message_id || bufferObj?.id,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (bufferObj.status === 400 || !bufferObj.event) {
|
||
console.error('❌ [handleStream] 错误响应:', {
|
||
status: bufferObj.status,
|
||
event: bufferObj.event,
|
||
message: bufferObj.message,
|
||
code: bufferObj.code
|
||
});
|
||
onData('', false, {
|
||
conversationId: undefined,
|
||
messageId: '',
|
||
errorMessage: bufferObj?.message,
|
||
errorCode: bufferObj?.code,
|
||
});
|
||
hasError = true;
|
||
onCompleted?.(true);
|
||
return;
|
||
}
|
||
|
||
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
|
||
const answer = unicodeToChar(bufferObj.answer);
|
||
|
||
|
||
onData(answer, isFirstMessage, {
|
||
conversationId: bufferObj.conversation_id,
|
||
messageId: bufferObj.id || bufferObj.message_id,
|
||
taskId: bufferObj.task_id,
|
||
});
|
||
isFirstMessage = false;
|
||
} else if (bufferObj.event === 'agent_thought' && onThought) {
|
||
// console.log('🤔 [handleStream] 处理思考事件:', bufferObj.event);
|
||
onThought(bufferObj as ThoughtItem);
|
||
} else if (bufferObj.event === 'message_file' && onFile) {
|
||
// console.log('📁 [handleStream] 处理文件事件:', bufferObj.event);
|
||
onFile(bufferObj as VisionFile);
|
||
} else if (bufferObj.event === 'message_end' && onMessageEnd) {
|
||
// console.log('🏁 [handleStream] 处理消息结束事件:', bufferObj.event);
|
||
onMessageEnd(bufferObj as MessageEnd);
|
||
} else if (bufferObj.event === 'message_replace' && onMessageReplace) {
|
||
// console.log('🔄 [handleStream] 处理消息替换事件:', bufferObj.event);
|
||
onMessageReplace(bufferObj as MessageReplace);
|
||
} else if (bufferObj.event === 'workflow_started' && onWorkflowStarted) {
|
||
// console.log('🚀 [handleStream] 处理工作流开始事件:', bufferObj.event);
|
||
onWorkflowStarted(bufferObj as WorkflowStartedResponse);
|
||
} else if (bufferObj.event === 'workflow_finished' && onWorkflowFinished) {
|
||
// console.log('🎯 [handleStream] 处理工作流完成事件:', bufferObj.event);
|
||
onWorkflowFinished(bufferObj as WorkflowFinishedResponse);
|
||
} else if (bufferObj.event === 'node_started' && onNodeStarted) {
|
||
// console.log('🔗 [handleStream] 处理节点开始事件:', bufferObj.event);
|
||
onNodeStarted(bufferObj as NodeStartedResponse);
|
||
} else if (bufferObj.event === 'node_finished' && onNodeFinished) {
|
||
// console.log('✅ [handleStream] 处理节点完成事件:', bufferObj.event);
|
||
onNodeFinished(bufferObj as NodeFinishedResponse);
|
||
} else {
|
||
// console.log('❓ [handleStream] 未知事件类型:', bufferObj.event);
|
||
}
|
||
} else if (message.trim()) {
|
||
// console.log('📝 [handleStream] 非data消息:', message.substring(0, 100));
|
||
}
|
||
});
|
||
|
||
// 保留最后一行(可能是不完整的消息)
|
||
const lastLine = lines[lines.length - 1];
|
||
buffer = lastLine;
|
||
}
|
||
catch (err) {
|
||
console.error('❌ [handleStream] 解析响应时出错:', err);
|
||
onData('', false, {
|
||
conversationId: undefined,
|
||
messageId: '',
|
||
errorMessage: `${err}`,
|
||
});
|
||
hasError = true;
|
||
onCompleted?.(true);
|
||
return;
|
||
}
|
||
|
||
if (!hasError) {
|
||
read();
|
||
} else {
|
||
}
|
||
}).catch(err => {
|
||
console.error('❌ [handleStream] 读取流时出错:', err);
|
||
onError?.(err.message);
|
||
});
|
||
}
|
||
|
||
read();
|
||
};
|
||
|
||
/**
|
||
* 基础 HTTP 请求函数
|
||
*
|
||
* 提供统一的请求配置和错误处理:
|
||
* - 自动添加认证头
|
||
* - 统一的 URL 处理
|
||
* - 错误状态码处理
|
||
* - 自动添加用户 ID
|
||
*
|
||
* @param url - 请求的 URL 路径(相对于 API 基础 URL)
|
||
* @param fetchOptions - fetch API 的配置选项
|
||
* @param needAllResponseContent - 是否需要返回完整的响应内容而不是 JSON
|
||
* @returns Promise<any> - 返回解析后的响应数据
|
||
*
|
||
* @throws {Error} 当请求失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // 发送 GET 请求
|
||
* const data = await baseFetch('conversations', { method: 'GET' });
|
||
*
|
||
* // 发送 POST 请求
|
||
* const result = await baseFetch('chat-messages', {
|
||
* method: 'POST',
|
||
* body: { query: 'Hello' }
|
||
* });
|
||
* ```
|
||
*/
|
||
const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boolean = false) => {
|
||
const options = Object.assign({}, baseOptions, fetchOptions);
|
||
|
||
// 调用Remix API routes(如 /api/conversations)
|
||
// 服务端会通过session获取JWT并调用FastAPI代理
|
||
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
|
||
|
||
const { body } = options;
|
||
if (body && typeof body === 'object') {
|
||
// 不再添加user参数,服务端会从JWT自动提取
|
||
options.body = JSON.stringify(body);
|
||
}
|
||
|
||
return fetch(urlWithPrefix, options)
|
||
.then((res: Response) => {
|
||
if (!res.ok) {
|
||
console.error('❌ Request failed:', {
|
||
status: res.status,
|
||
statusText: res.statusText,
|
||
url: urlWithPrefix
|
||
});
|
||
if (res.status === 422) {
|
||
return res.text().then(text => {
|
||
let errorMessage = text;
|
||
try {
|
||
const data = JSON.parse(text);
|
||
errorMessage = data.message || data.error || text;
|
||
} catch (e) {
|
||
// 如果不是JSON,使用原始文本
|
||
}
|
||
throw new Error(errorMessage);
|
||
});
|
||
}
|
||
throw new Error(`${res.status}: ${res.statusText}`);
|
||
}
|
||
|
||
if (needAllResponseContent) {
|
||
return res.text().then(text => {
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (e) {
|
||
return text;
|
||
}
|
||
});
|
||
}
|
||
|
||
const data = res.json();
|
||
return data;
|
||
})
|
||
.catch((err) => {
|
||
console.error('❌ Request error:', err.message);
|
||
throw err;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 发送 SSE (Server-Sent Events) POST 请求
|
||
*
|
||
* 专门用于处理流式响应的 POST 请求:
|
||
* - 配置 SSE 相关的请求头
|
||
* - 设置 AbortController 用于取消请求
|
||
* - 调用 handleStream 处理流式响应
|
||
* - 自动添加用户 ID 到请求体
|
||
*
|
||
* @param url - 请求的 URL 路径
|
||
* @param fetchOptions - fetch 配置选项
|
||
* @param callbacks - 包含各种事件回调的对象
|
||
* @param callbacks.onData - 处理消息数据的回调
|
||
* @param callbacks.onCompleted - 流式响应完成时的回调
|
||
* @param callbacks.onThought - 处理思考过程的回调
|
||
* @param callbacks.onFile - 处理文件的回调
|
||
* @param callbacks.onMessageEnd - 处理消息结束的回调
|
||
* @param callbacks.onMessageReplace - 处理消息替换的回调
|
||
* @param callbacks.onError - 处理错误的回调
|
||
* @param callbacks.getAbortController - 获取中止控制器的回调
|
||
* @param callbacks.onWorkflowStarted - 处理工作流开始的回调
|
||
* @param callbacks.onWorkflowFinished - 处理工作流完成的回调
|
||
* @param callbacks.onNodeStarted - 处理节点开始的回调
|
||
* @param callbacks.onNodeFinished - 处理节点完成的回调
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* ssePost('chat-messages', {
|
||
* body: { query: 'Hello', response_mode: 'streaming' }
|
||
* }, {
|
||
* onData: (message, isFirst, info) => updateUI(message),
|
||
* onCompleted: () => setLoading(false),
|
||
* onError: (error) => showError(error)
|
||
* });
|
||
* ```
|
||
*/
|
||
export const ssePost = (
|
||
url: string,
|
||
fetchOptions: any,
|
||
{
|
||
onData,
|
||
onCompleted,
|
||
onThought,
|
||
onFile,
|
||
onMessageEnd,
|
||
onMessageReplace,
|
||
onWorkflowStarted,
|
||
onWorkflowFinished,
|
||
onNodeStarted,
|
||
onNodeFinished,
|
||
onError,
|
||
getAbortController,
|
||
}: {
|
||
onData: IOnData;
|
||
onCompleted?: IOnCompleted;
|
||
onThought?: IOnThought;
|
||
onFile?: IOnFile;
|
||
onMessageEnd?: IOnMessageEnd;
|
||
onMessageReplace?: IOnMessageReplace;
|
||
onError?: IOnError;
|
||
getAbortController?: (abortController: AbortController) => void;
|
||
onWorkflowStarted?: IOnWorkflowStarted;
|
||
onWorkflowFinished?: IOnWorkflowFinished;
|
||
onNodeStarted?: IOnNodeStarted;
|
||
onNodeFinished?: IOnNodeFinished;
|
||
},
|
||
) => {
|
||
const options = Object.assign({}, baseOptions, {
|
||
method: 'POST',
|
||
}, fetchOptions);
|
||
|
||
// 调用Remix API routes(如 /api/chat-messages)
|
||
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
|
||
|
||
const controller = new AbortController();
|
||
if (getAbortController)
|
||
getAbortController(controller);
|
||
|
||
options.headers = {
|
||
...options.headers,
|
||
'Content-Type': 'application/json',
|
||
'Accept': ContentType.stream,
|
||
// 移除Authorization头,由服务端自动处理
|
||
};
|
||
|
||
options.signal = controller.signal;
|
||
|
||
const { body } = options;
|
||
if (body && typeof body === 'object') {
|
||
// 不再添加user参数,服务端会从JWT自动提取
|
||
options.body = JSON.stringify(body);
|
||
}
|
||
|
||
return fetch(urlWithPrefix, options)
|
||
.then((res: Response) => {
|
||
|
||
if (!/^(2|3)\d{2}$/.test(res.status.toString())) {
|
||
res.json().then((data: any) => {
|
||
console.error('❌ SSE Error:', data.message || 'Server Error');
|
||
onError?.(data.message || 'Server Error');
|
||
});
|
||
return;
|
||
}
|
||
|
||
handleStream(
|
||
res,
|
||
onData,
|
||
onCompleted,
|
||
onThought,
|
||
onMessageEnd,
|
||
onMessageReplace,
|
||
onFile,
|
||
onWorkflowStarted,
|
||
onWorkflowFinished,
|
||
onNodeStarted,
|
||
onNodeFinished,
|
||
onError
|
||
);
|
||
})
|
||
.catch((err) => {
|
||
console.error('❌ SSE Request Error:', err);
|
||
onError?.(err.message);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 获取用户的会话列表
|
||
*
|
||
* 从 Dify API 获取当前用户的所有会话:
|
||
* - 自动分页获取(最多100条)
|
||
* - 包含会话 ID、名称、输入参数等信息
|
||
* - 按时间倒序排列
|
||
*
|
||
* @returns Promise<any> - 包含会话列表的响应对象
|
||
* @returns Promise<any>.data - 会话数组
|
||
* @returns Promise<any>.has_more - 是否还有更多数据
|
||
* @returns Promise<any>.limit - 每页限制数量
|
||
*
|
||
* @throws {Error} 当获取会话列表失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const response = await fetchConversations();
|
||
* const conversations = response.data;
|
||
* console.log('会话数量:', conversations.length);
|
||
* ```
|
||
*/
|
||
export const fetchConversations = async () => {
|
||
const params = new URLSearchParams({
|
||
limit: '100',
|
||
// 不再传递user参数,服务端会从JWT自动提取
|
||
});
|
||
|
||
return fetch(`${CHAT_CONFIG.API_URL}/conversations?${params}`, {
|
||
method: 'GET',
|
||
credentials: 'include', // 携带cookie
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to fetch conversations: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 获取指定会话的聊天消息列表
|
||
*
|
||
* 从 Dify API 获取特定会话的消息历史:
|
||
* - 支持分页加载(最多20条)
|
||
* - 包含用户消息和 AI 回复
|
||
* - 按时间顺序排列
|
||
*
|
||
* @param conversationId - 会话 ID
|
||
* @returns Promise<any> - 包含消息列表的响应对象
|
||
* @returns Promise<any>.data - 消息数组
|
||
* @returns Promise<any>.has_more - 是否还有更多历史消息
|
||
* @returns Promise<any>.limit - 每页限制数量
|
||
*
|
||
* @throws {Error} 当获取消息列表失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const response = await fetchChatList('conv-123');
|
||
* const messages = response.data;
|
||
* console.log('消息数量:', messages.length);
|
||
* ```
|
||
*/
|
||
export const fetchChatList = async (conversationId: string) => {
|
||
const params = new URLSearchParams({
|
||
conversation_id: conversationId,
|
||
// 不再传递user参数,服务端会从JWT自动提取
|
||
});
|
||
|
||
return fetch(`${CHAT_CONFIG.API_URL}/messages?${params}`, {
|
||
method: 'GET',
|
||
credentials: 'include', // 携带cookie
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to fetch chat list: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 获取应用参数配置
|
||
*
|
||
* 从 Dify API 获取应用的配置信息:
|
||
* - 用户输入表单配置
|
||
* - 开场白设置
|
||
* - 文件上传配置
|
||
* - 其他应用级别设置
|
||
*
|
||
* @returns Promise<any> - 包含应用参数的响应对象
|
||
* @returns Promise<any>.user_input_form - 用户输入表单配置
|
||
* @returns Promise<any>.opening_statement - 开场白内容
|
||
* @returns Promise<any>.file_upload - 文件上传配置
|
||
*
|
||
* @throws {Error} 当获取应用参数失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const params = await fetchAppParams();
|
||
* const { user_input_form, opening_statement } = params.data;
|
||
* console.log('开场白:', opening_statement);
|
||
* ```
|
||
*/
|
||
export const fetchAppParams = async () => {
|
||
return fetch(`${CHAT_CONFIG.API_URL}/parameters`, {
|
||
method: 'GET',
|
||
credentials: 'include', // 携带cookie
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to fetch app params: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 更新消息反馈[未使用]
|
||
*
|
||
* 向 Dify API 提交用户对 AI 回复的反馈:
|
||
* - 支持点赞/点踩评价
|
||
* - 可添加文字反馈内容
|
||
* - 用于改进 AI 回复质量
|
||
*
|
||
* @param params - 反馈参数对象
|
||
* @param params.url - 包含消息 ID 的 URL
|
||
* @param params.body - 反馈内容
|
||
* @param params.body.rating - 评分:'like' | 'dislike' | null
|
||
* @param params.body.content - 文字反馈内容(可选)
|
||
* @returns Promise<any> - 反馈提交结果
|
||
*
|
||
* @throws {Error} 当提交反馈失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await updateFeedback({
|
||
* url: '/messages/msg-123/feedbacks',
|
||
* body: { rating: 'like', content: '回答很好' }
|
||
* });
|
||
* ```
|
||
*/
|
||
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
|
||
const messageId = url.split('/').pop(); // 从URL中提取messageId
|
||
|
||
return fetch(`${CHAT_CONFIG.API_URL}/messages/${messageId}/feedbacks`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include', // 携带cookie
|
||
body: JSON.stringify(body), // 不再添加user参数
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to update feedback: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 生成会话名称[未使用]
|
||
*
|
||
* 让 AI 根据会话内容自动生成合适的会话名称:
|
||
* - 基于会话中的消息内容
|
||
* - 生成简洁有意义的标题
|
||
* - 用于替换默认的"新对话"名称
|
||
*
|
||
* @param id - 会话 ID
|
||
* @returns Promise<any> - 包含生成名称的响应对象
|
||
* @returns Promise<any>.name - 生成的会话名称
|
||
*
|
||
* @throws {Error} 当生成名称失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const result = await generateConversationName('conv-123');
|
||
* console.log('生成的名称:', result.name);
|
||
* ```
|
||
*/
|
||
export const generateConversationName = async (id: string) => {
|
||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include', // 携带cookie
|
||
body: JSON.stringify({
|
||
auto_generate: true,
|
||
// 不再添加user参数
|
||
}),
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to generate conversation name: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 重命名会话
|
||
*
|
||
* 更新会话的显示名称:
|
||
* - 支持手动设置名称
|
||
* - 支持 AI 自动生成名称
|
||
* - 更新后在会话列表中显示新名称
|
||
*
|
||
* @param id - 会话 ID
|
||
* @param name - 新的会话名称(当 autoGenerate 为 false 时使用)
|
||
* @param autoGenerate - 是否使用 AI 自动生成名称,默认为 false
|
||
* @returns Promise<any> - 重命名结果
|
||
* @returns Promise<any>.name - 最终的会话名称
|
||
*
|
||
* @throws {Error} 当重命名失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* // 手动设置名称
|
||
* await renameConversation('conv-123', '关于编程的讨论');
|
||
*
|
||
* // AI 自动生成名称
|
||
* await renameConversation('conv-123', '', true);
|
||
* ```
|
||
*/
|
||
export const renameConversation = async (id: string, name: string, autoGenerate: boolean = false) => {
|
||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include', // 携带cookie
|
||
body: JSON.stringify({
|
||
name: autoGenerate ? undefined : name,
|
||
auto_generate: autoGenerate,
|
||
// 不再添加user参数
|
||
}),
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to rename conversation: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 删除会话
|
||
*
|
||
* 从用户的会话列表中永久删除指定会话:
|
||
* - 删除会话及其所有消息
|
||
* - 操作不可逆
|
||
* - 删除后从会话列表中移除
|
||
*
|
||
* @param id - 要删除的会话 ID
|
||
* @returns Promise<any> - 删除操作结果
|
||
*
|
||
* @throws {Error} 当删除会话失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await deleteConversation('conv-123');
|
||
* console.log('会话已删除');
|
||
* ```
|
||
*/
|
||
export const deleteConversation = async (id: string) => {
|
||
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include', // 携带cookie
|
||
// 不再发送body和user参数
|
||
}).then(res => {
|
||
if (!res.ok) {
|
||
throw new Error(`Failed to delete conversation: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 上传文件到 Dify API[未使用]
|
||
*
|
||
* 使用 XMLHttpRequest 上传文件:
|
||
* - 支持文件上传进度监控
|
||
* - 自动添加认证头和用户 ID
|
||
* - 返回文件 ID 用于后续引用
|
||
*
|
||
* @param fetchOptions - 上传配置选项
|
||
* @param fetchOptions.method - HTTP 方法(通常为 'POST')
|
||
* @param fetchOptions.url - 上传 URL(可选,会自动构建)
|
||
* @param fetchOptions.data - FormData 对象,包含要上传的文件
|
||
* @param fetchOptions.headers - 额外的请求头
|
||
* @param fetchOptions.xhr - XMLHttpRequest 实例
|
||
* @param fetchOptions.onprogress - 上传进度回调函数
|
||
* @returns Promise<any> - 返回包含文件 ID 的对象
|
||
* @returns Promise<any>.id - 上传后的文件 ID
|
||
*
|
||
* @throws {Error} 当文件上传失败时抛出错误
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const formData = new FormData();
|
||
* formData.append('file', fileBlob);
|
||
*
|
||
* const xhr = new XMLHttpRequest();
|
||
* const result = await upload({
|
||
* data: formData,
|
||
* xhr: xhr,
|
||
* onprogress: (event) => {
|
||
* const progress = (event.loaded / event.total) * 100;
|
||
* console.log('上传进度:', progress + '%');
|
||
* }
|
||
* });
|
||
* console.log('文件ID:', result.id);
|
||
* ```
|
||
*/
|
||
export const upload = (fetchOptions: any): Promise<any> => {
|
||
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/files/upload`;
|
||
|
||
const defaultOptions = {
|
||
method: 'POST',
|
||
url: urlWithPrefix,
|
||
data: {},
|
||
};
|
||
|
||
const options = {
|
||
...defaultOptions,
|
||
...fetchOptions,
|
||
};
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const xhr = options.xhr;
|
||
xhr.open(options.method, options.url);
|
||
|
||
for (const key in options.headers)
|
||
xhr.setRequestHeader(key, options.headers[key]);
|
||
|
||
// 不再手动添加Authorization头,由服务端处理
|
||
|
||
// 不再添加user参数到formData
|
||
// 服务端会从JWT自动提取
|
||
|
||
xhr.withCredentials = true; // 改为true以携带cookie
|
||
xhr.onreadystatechange = function () {
|
||
if (xhr.readyState === 4) {
|
||
if (xhr.status === 200)
|
||
resolve({ id: xhr.response });
|
||
else
|
||
reject(new Error(xhr.responseText || 'Upload failed'));
|
||
}
|
||
};
|
||
|
||
xhr.upload.onprogress = options.onprogress;
|
||
xhr.send(options.data);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 通用 HTTP 请求函数
|
||
*
|
||
* 基于 baseFetch 的通用请求封装:
|
||
* - 合并基础配置和自定义选项
|
||
* - 统一的错误处理
|
||
* - 支持所有 HTTP 方法
|
||
*
|
||
* @param url - 请求 URL 路径
|
||
* @param options - 请求配置选项,默认为空对象
|
||
* @param needAllResponseContent - 是否返回完整响应内容,默认为 false
|
||
* @returns Promise<any> - 请求响应数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const data = await request('conversations', { method: 'GET' });
|
||
* ```
|
||
*/
|
||
export const request = (url: string, options = {}, needAllResponseContent = false) => {
|
||
return baseFetch(url, { ...baseOptions, ...options }, needAllResponseContent);
|
||
};
|
||
|
||
/**
|
||
* 发送 GET 请求
|
||
*
|
||
* @param url - 请求 URL 路径
|
||
* @param options - 额外的请求配置选项,默认为空对象
|
||
* @returns Promise<any> - 请求响应数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const conversations = await get('conversations');
|
||
* ```
|
||
*/
|
||
export const get = (url: string, options = {}) => {
|
||
return request(url, { ...options, method: 'GET' });
|
||
};
|
||
|
||
/**
|
||
* 发送 POST 请求
|
||
*
|
||
* @param url - 请求 URL 路径
|
||
* @param options - 额外的请求配置选项,默认为空对象
|
||
* @returns Promise<any> - 请求响应数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const result = await post('chat-messages', {
|
||
* body: { query: 'Hello' }
|
||
* });
|
||
* ```
|
||
*/
|
||
export const post = (url: string, options = {}) => {
|
||
return request(url, { ...options, method: 'POST' });
|
||
};
|
||
|
||
/**
|
||
* 发送 PUT 请求
|
||
*
|
||
* @param url - 请求 URL 路径
|
||
* @param options - 额外的请求配置选项,默认为空对象
|
||
* @returns Promise<any> - 请求响应数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const result = await put('conversations/123', {
|
||
* body: { name: '新名称' }
|
||
* });
|
||
* ```
|
||
*/
|
||
export const put = (url: string, options = {}) => {
|
||
return request(url, { ...options, method: 'PUT' });
|
||
};
|
||
|
||
/**
|
||
* 发送 DELETE 请求
|
||
*
|
||
* @param url - 请求 URL 路径
|
||
* @param options - 额外的请求配置选项,默认为空对象
|
||
* @returns Promise<any> - 请求响应数据
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await del('conversations/123');
|
||
* ```
|
||
*/
|
||
export const del = (url: string, options = {}) => {
|
||
return request(url, { ...options, method: 'DELETE' });
|
||
};
|
||
|
||
/**
|
||
* 发送聊天消息
|
||
*
|
||
* 向 Dify API 发送聊天消息并处理流式响应:
|
||
* - 自动设置流式响应模式
|
||
* - 支持文件附件
|
||
* - 支持会话输入参数
|
||
* - 处理各种类型的响应事件
|
||
*
|
||
* @param body - 消息请求体
|
||
* @param body.query - 用户的问题文本
|
||
* @param body.conversation_id - 会话 ID(可选,用于继续现有会话)
|
||
* @param body.files - 附件文件列表(可选)
|
||
* @param body.inputs - 会话输入参数(可选)
|
||
* @param callbacks - 事件回调函数集合
|
||
* @param callbacks.onData - 处理消息数据的回调,必需
|
||
* @param callbacks.onCompleted - 流式响应完成时的回调,必需
|
||
* @param callbacks.onFile - 处理文件的回调(可选)
|
||
* @param callbacks.onThought - 处理思考过程的回调(可选)
|
||
* @param callbacks.onMessageEnd - 处理消息结束的回调(可选)
|
||
* @param callbacks.onMessageReplace - 处理消息替换的回调(可选)
|
||
* @param callbacks.onError - 处理错误的回调(可选)
|
||
* @param callbacks.getAbortController - 获取中止控制器的回调(可选)
|
||
* @param callbacks.onWorkflowStarted - 处理工作流开始的回调(可选)
|
||
* @param callbacks.onNodeStarted - 处理节点开始的回调(可选)
|
||
* @param callbacks.onNodeFinished - 处理节点完成的回调(可选)
|
||
* @param callbacks.onWorkflowFinished - 处理工作流完成的回调(可选)
|
||
* @returns Promise<void> - 异步操作完成
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* await sendChatMessage({
|
||
* query: '你好,请介绍一下自己',
|
||
* conversation_id: 'conv-123'
|
||
* }, {
|
||
* onData: (message, isFirst, info) => {
|
||
* console.log('收到消息:', message);
|
||
* updateChatUI(message, info.messageId);
|
||
* },
|
||
* onCompleted: (hasError) => {
|
||
* console.log('对话完成', hasError ? '有错误' : '成功');
|
||
* setLoading(false);
|
||
* },
|
||
* onThought: (thought) => {
|
||
* console.log('AI思考:', thought.thought);
|
||
* },
|
||
* onError: (error) => {
|
||
* console.error('发送失败:', error);
|
||
* showErrorMessage(error);
|
||
* }
|
||
* });
|
||
* ```
|
||
*/
|
||
export const sendChatMessage = async (
|
||
body: Record<string, any>,
|
||
{
|
||
onData,
|
||
onCompleted,
|
||
onThought,
|
||
onFile,
|
||
onError,
|
||
getAbortController,
|
||
onMessageEnd,
|
||
onMessageReplace,
|
||
onWorkflowStarted,
|
||
onNodeStarted,
|
||
onNodeFinished,
|
||
onWorkflowFinished,
|
||
}: {
|
||
onData: IOnData;
|
||
onCompleted: IOnCompleted;
|
||
onFile?: IOnFile;
|
||
onThought?: IOnThought;
|
||
onMessageEnd?: IOnMessageEnd;
|
||
onMessageReplace?: IOnMessageReplace;
|
||
onError?: IOnError;
|
||
getAbortController?: (abortController: AbortController) => void;
|
||
onWorkflowStarted?: IOnWorkflowStarted;
|
||
onNodeStarted?: IOnNodeStarted;
|
||
onNodeFinished?: IOnNodeFinished;
|
||
onWorkflowFinished?: IOnWorkflowFinished;
|
||
},
|
||
) => {
|
||
return ssePost('chat-messages', {
|
||
body: {
|
||
...body,
|
||
response_mode: 'streaming',
|
||
},
|
||
}, {
|
||
onData,
|
||
onCompleted,
|
||
onThought,
|
||
onFile,
|
||
onError,
|
||
getAbortController,
|
||
onMessageEnd,
|
||
onMessageReplace,
|
||
onNodeStarted,
|
||
onWorkflowStarted,
|
||
onWorkflowFinished,
|
||
onNodeFinished
|
||
});
|
||
};
|