3f5c23123b
- 新增对话应用管理模块(dify-chat-apps),支持获取和切换对话应用 - 优化对话应用切换后自动刷新会话列表功能 - 知识库管理页面新增下拉选择器,支持切换不同知识库 - API 层支持 app_id 参数传递,实现多应用会话隔离 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
578 lines
22 KiB
TypeScript
578 lines
22 KiB
TypeScript
import { useBoolean, useGetState } from 'ahooks';
|
||
import { produce } from 'immer';
|
||
import { useCallback, useRef, useState } from 'react';
|
||
import type { ChatItem, Feedbacktype, MessageEnd, MessageReplace, VisionFile } from '~/api/dify-chat';
|
||
import { generateConversationName, sendChatMessage, updateFeedback } from '~/api/dify-chat';
|
||
|
||
/**
|
||
* 聊天消息处理钩子
|
||
*/
|
||
export default function useChatMessage({
|
||
onUpdateConversationList,
|
||
onConversationIdChange,
|
||
}: {
|
||
onUpdateConversationList?: (conversationId: string, data: { name: string }) => void;
|
||
onConversationIdChange?: (conversationId: string) => void;
|
||
}) {
|
||
// 聊天消息列表
|
||
const [chatList, setChatList, getChatList] = useGetState<ChatItem[]>([]);
|
||
// 是否正在响应中
|
||
const [isResponding, { setTrue: setResponding, setFalse: setNotResponding }] = useBoolean(false);
|
||
// 是否已停止响应
|
||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false);
|
||
// 响应的会话是否为当前会话
|
||
const [isRespondingConIsCurrCon, setIsRespondingConCurrCon, getIsRespondingConIsCurrCon] = useGetState(true);
|
||
// 消息任务ID
|
||
const [messageTaskId, setMessageTaskId] = useState('');
|
||
// 用户查询
|
||
const [userQuery, setUserQuery] = useState('');
|
||
// 中止控制器
|
||
const abortControllerRef = useRef<AbortController | null>(null);
|
||
|
||
// 当前流式响应项的引用
|
||
const currentResponseRef = useRef<ChatItem | null>(null);
|
||
|
||
/**
|
||
* 记录错误日志
|
||
*/
|
||
const logError = useCallback((message: string) => {
|
||
console.error(`[Chat Error]: ${message}`);
|
||
}, []);
|
||
|
||
/**
|
||
* 检查是否可以发送消息
|
||
*/
|
||
const checkCanSend = useCallback(() => {
|
||
if (isResponding) {
|
||
console.warn('机器人正在回复中,请稍后再试');
|
||
return false;
|
||
}
|
||
return true;
|
||
}, [isResponding]);
|
||
|
||
/**
|
||
* 立即更新聊天列表 - 移除防抖机制
|
||
*/
|
||
const updateChatList = useCallback((
|
||
responseItem: ChatItem,
|
||
questionId: string,
|
||
placeholderAnswerId: string,
|
||
questionItem: ChatItem,
|
||
originalResponseId?: string
|
||
) => {
|
||
|
||
setChatList(produce(getChatList(), (draft) => {
|
||
|
||
|
||
// 移除占位符
|
||
const placeholderIndex = draft.findIndex(item => item.id === placeholderAnswerId);
|
||
if (placeholderIndex !== -1) {
|
||
// console.log('🗑️ [useChatMessage] 移除占位符:', placeholderAnswerId, 'at index:', placeholderIndex);
|
||
draft.splice(placeholderIndex, 1);
|
||
}
|
||
|
||
// 确保问题存在
|
||
const questionIndex = draft.findIndex(item => item.id === questionId);
|
||
if (questionIndex === -1) {
|
||
console.log('➕ [useChatMessage] 添加问题:', questionId);
|
||
draft.push({ ...questionItem });
|
||
}
|
||
|
||
// 更新或添加响应 - 考虑ID可能已经改变的情况
|
||
let responseIndex = draft.findIndex(item => item.id === responseItem.id);
|
||
// console.log('🔍 [useChatMessage] 查找响应索引 (当前ID):', { responseItemId: responseItem.id, responseIndex });
|
||
|
||
// 如果找不到当前ID的响应,尝试查找原始ID
|
||
if (responseIndex === -1 && originalResponseId) {
|
||
responseIndex = draft.findIndex(item => item.id === originalResponseId);
|
||
// console.log('🔍 [useChatMessage] 查找响应索引 (原始ID):', { originalResponseId, responseIndex });
|
||
}
|
||
|
||
// 如果找不到任何匹配的响应,查找最后一个AI回答
|
||
if (responseIndex === -1) {
|
||
responseIndex = draft.findIndex((item, index) =>
|
||
item.isAnswer &&
|
||
index > draft.findIndex(q => q.id === questionId)
|
||
);
|
||
// console.log('🔍 [useChatMessage] 查找响应索引 (最后AI回答):', { responseIndex });
|
||
}
|
||
|
||
if (responseIndex !== -1) {
|
||
|
||
draft[responseIndex] = { ...responseItem };
|
||
} else {
|
||
draft.push({ ...responseItem });
|
||
}
|
||
}));
|
||
}, [getChatList, setChatList]);
|
||
|
||
/**
|
||
* 更新当前问答 - 移除防抖,立即更新
|
||
*/
|
||
const updateCurrentQA = useCallback(({
|
||
responseItem,
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
}: {
|
||
responseItem: ChatItem;
|
||
questionId: string;
|
||
placeholderAnswerId: string;
|
||
questionItem: ChatItem;
|
||
originalResponseId?: string;
|
||
}) => {
|
||
// 更新当前响应引用
|
||
currentResponseRef.current = responseItem;
|
||
|
||
// 立即更新,不使用防抖
|
||
updateChatList(responseItem, questionId, placeholderAnswerId, questionItem, originalResponseId);
|
||
}, [updateChatList]);
|
||
|
||
/**
|
||
* 转换文件格式为服务器格式
|
||
*/
|
||
const transformToServerFile = useCallback((fileItem: any) => {
|
||
return {
|
||
type: 'image',
|
||
transfer_method: fileItem.transferMethod || fileItem.transfer_method,
|
||
url: fileItem.url,
|
||
upload_file_id: fileItem.id || fileItem.upload_file_id,
|
||
};
|
||
}, []);
|
||
|
||
/**
|
||
* 发送消息
|
||
* @param message - 消息内容
|
||
* @param conversationId - 会话 ID
|
||
* @param files - 附件文件
|
||
* @param inputs - 输入参数
|
||
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
|
||
*/
|
||
const handleSend = useCallback(async (
|
||
message: string,
|
||
conversationId: string | null,
|
||
files?: VisionFile[],
|
||
inputs?: Record<string, any>,
|
||
appId?: string,
|
||
) => {
|
||
if (!checkCanSend() || !message.trim()) {
|
||
return;
|
||
}
|
||
|
||
console.log('📤 [useChatMessage] 发送消息:', {
|
||
messageLength: message.length,
|
||
messagePreview: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
||
conversationId,
|
||
isNewConversation: conversationId === null || conversationId === '-1'
|
||
});
|
||
|
||
setUserQuery(message);
|
||
setResponding();
|
||
setHasStopResponded(false);
|
||
setIsRespondingConCurrCon(true);
|
||
|
||
// 处理输入参数
|
||
const toServerInputs: Record<string, any> = {};
|
||
if (inputs) {
|
||
Object.keys(inputs).forEach((key) => {
|
||
const value = inputs[key];
|
||
if (value?.supportFileType) {
|
||
toServerInputs[key] = transformToServerFile(value);
|
||
} else if (Array.isArray(value) && value[0]?.supportFileType) {
|
||
toServerInputs[key] = value.map((item: any) => transformToServerFile(item));
|
||
} else {
|
||
toServerInputs[key] = value;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 准备请求数据
|
||
const data: Record<string, any> = {
|
||
inputs: toServerInputs,
|
||
query: message,
|
||
conversation_id: conversationId === '-1' ? null : conversationId,
|
||
app_id: appId, // 添加对话应用 ID
|
||
};
|
||
|
||
// 添加文件数据
|
||
if (files && files.length > 0) {
|
||
data.files = files.map((item) => {
|
||
if (item.transfer_method === 'local_file') {
|
||
return {
|
||
...item,
|
||
url: '',
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
}
|
||
|
||
// 生成唯一ID
|
||
const questionId = `question-${Date.now()}`;
|
||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`;
|
||
|
||
// 创建问题项
|
||
const questionItem: ChatItem = {
|
||
id: questionId,
|
||
content: message,
|
||
isAnswer: false,
|
||
message_files: files,
|
||
};
|
||
|
||
// 创建答案占位符
|
||
const placeholderAnswerItem: ChatItem = {
|
||
id: placeholderAnswerId,
|
||
content: '',
|
||
isAnswer: true,
|
||
};
|
||
|
||
// 更新聊天列表
|
||
const newList = [...getChatList(), questionItem, placeholderAnswerItem];
|
||
setChatList(newList);
|
||
|
||
let isAgentMode = false;
|
||
let hasSetResponseId = false;
|
||
const prevTempNewConversationId = conversationId || '-1';
|
||
let tempNewConversationId = '';
|
||
|
||
// 创建响应项
|
||
const responseItem: ChatItem = {
|
||
id: `response-${Date.now()}`,
|
||
content: '',
|
||
agent_thoughts: [],
|
||
message_files: [],
|
||
isAnswer: true,
|
||
};
|
||
|
||
// 保存原始响应ID
|
||
const originalResponseId = responseItem.id;
|
||
|
||
try {
|
||
// 发送消息
|
||
await sendChatMessage(data, {
|
||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }) => {
|
||
|
||
if (!isAgentMode) {
|
||
// 累积消息内容
|
||
const oldContent = responseItem.content;
|
||
responseItem.content = responseItem.content + message;
|
||
} else {
|
||
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1];
|
||
if (lastThought) {
|
||
lastThought.thought = (lastThought.thought || '') + message;
|
||
console.log('🤔 [useChatMessage] 累积思考内容:', {
|
||
thoughtLength: lastThought.thought.length,
|
||
addedLength: message.length
|
||
});
|
||
}
|
||
}
|
||
|
||
if (messageId && !hasSetResponseId) {
|
||
responseItem.id = messageId;
|
||
hasSetResponseId = true;
|
||
// console.log('🆔 设置响应ID:', { oldId: originalResponseId, newId: messageId });
|
||
}
|
||
|
||
// 重要:确保正确获取新会话ID
|
||
if (newConversationId && !tempNewConversationId) {
|
||
tempNewConversationId = newConversationId;
|
||
console.log('🆔 [useChatMessage] 首次获取到新会话ID:', {
|
||
newConversationId: tempNewConversationId,
|
||
originalConversationId: conversationId
|
||
});
|
||
}
|
||
|
||
setMessageTaskId(taskId || '');
|
||
|
||
// 修复新会话的匹配逻辑
|
||
const isNewConversationMatch = (prevTempNewConversationId === '-1' && conversationId === null) ||
|
||
(prevTempNewConversationId === conversationId);
|
||
|
||
if (!isNewConversationMatch) {
|
||
// console.log('⚠️ 会话不匹配,跳过更新');
|
||
setIsRespondingConCurrCon(false);
|
||
return;
|
||
}
|
||
|
||
// 更新当前问答(使用防抖)
|
||
updateCurrentQA({
|
||
responseItem: { ...responseItem }, // 创建副本避免引用问题
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
},
|
||
|
||
onCompleted: async (hasError?: boolean) => {
|
||
|
||
// 立即更新最终状态
|
||
if (currentResponseRef.current) {
|
||
updateCurrentQA({
|
||
responseItem: { ...currentResponseRef.current },
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
}
|
||
|
||
if (hasError) {
|
||
setNotResponding();
|
||
return;
|
||
}
|
||
|
||
// 如果是新会话,处理会话ID更新和名称生成
|
||
// 检查原始传入的conversationId是否为新会话标识
|
||
const isNewConversation = conversationId === '-1' || conversationId === null;
|
||
console.log('🔍 [useChatMessage] 检查是否需要处理新会话:', {
|
||
tempNewConversationId,
|
||
originalConversationId: conversationId,
|
||
isNewConversation,
|
||
willProcess: !!(tempNewConversationId && isNewConversation)
|
||
});
|
||
|
||
if (tempNewConversationId && isNewConversation) {
|
||
try {
|
||
console.log('🆕 [useChatMessage] 处理新会话,调用onConversationIdChange:', tempNewConversationId);
|
||
|
||
// 通知会话ID变更(这会触发localStorage更新)
|
||
onConversationIdChange?.(tempNewConversationId);
|
||
|
||
// 生成会话名称
|
||
const res = await generateConversationName(tempNewConversationId);
|
||
const { data } = res as any;
|
||
if (data?.name) {
|
||
console.log('📝 [useChatMessage] 生成会话名称:', data.name);
|
||
onUpdateConversationList?.(tempNewConversationId, { name: data.name });
|
||
}
|
||
} catch (err) {
|
||
console.error('❌ [useChatMessage] 生成会话名称失败:', err);
|
||
}
|
||
}
|
||
|
||
setNotResponding();
|
||
setUserQuery('');
|
||
},
|
||
|
||
onFile: (file) => {
|
||
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1];
|
||
if (lastThought) {
|
||
lastThought.message_files = [...(lastThought.message_files || []), { ...file }];
|
||
} else {
|
||
if (!responseItem.message_files) {
|
||
responseItem.message_files = [];
|
||
}
|
||
responseItem.message_files.push(file);
|
||
}
|
||
|
||
updateCurrentQA({
|
||
responseItem: { ...responseItem },
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
},
|
||
|
||
onThought: (thought) => {
|
||
isAgentMode = true;
|
||
|
||
if (thought.message_id && !hasSetResponseId) {
|
||
responseItem.id = thought.message_id;
|
||
hasSetResponseId = true;
|
||
}
|
||
|
||
if (!responseItem.agent_thoughts) {
|
||
responseItem.agent_thoughts = [];
|
||
}
|
||
|
||
if (responseItem.agent_thoughts.length === 0) {
|
||
responseItem.agent_thoughts.push(thought);
|
||
} else {
|
||
const lastThought = responseItem.agent_thoughts[responseItem.agent_thoughts.length - 1];
|
||
// 相同思考ID,更新现有思考
|
||
if (lastThought.id === thought.id) {
|
||
thought.thought = lastThought.thought;
|
||
thought.message_files = lastThought.message_files;
|
||
responseItem.agent_thoughts[responseItem.agent_thoughts.length - 1] = thought;
|
||
} else {
|
||
responseItem.agent_thoughts.push(thought);
|
||
}
|
||
}
|
||
|
||
// 检查是否切换到其他会话
|
||
if (prevTempNewConversationId !== conversationId) {
|
||
setIsRespondingConCurrCon(false);
|
||
return;
|
||
}
|
||
|
||
updateCurrentQA({
|
||
responseItem: { ...responseItem },
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
},
|
||
|
||
onMessageEnd: (messageEnd: MessageEnd) => {
|
||
// 处理消息结束事件 - 获取检索资源
|
||
console.log('📋 [useChatMessage] 消息结束事件:', {
|
||
messageId: messageEnd.message_id,
|
||
hasMetadata: !!messageEnd.metadata,
|
||
hasRetrieverResources: !!messageEnd.metadata?.retriever_resources,
|
||
resourceCount: messageEnd.metadata?.retriever_resources?.length || 0
|
||
});
|
||
|
||
// 如果有检索资源,更新响应项
|
||
if (messageEnd.metadata?.retriever_resources && messageEnd.metadata.retriever_resources.length > 0) {
|
||
responseItem.retriever_resources = messageEnd.metadata.retriever_resources;
|
||
|
||
// 更新聊天列表
|
||
updateCurrentQA({
|
||
responseItem: { ...responseItem },
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
}
|
||
},
|
||
|
||
onMessageReplace: (messageReplace: MessageReplace) => {
|
||
// 处理消息替换事件
|
||
responseItem.content = messageReplace.answer;
|
||
updateCurrentQA({
|
||
responseItem: { ...responseItem },
|
||
questionId,
|
||
placeholderAnswerId,
|
||
questionItem,
|
||
originalResponseId,
|
||
});
|
||
},
|
||
|
||
onError: (error) => {
|
||
logError(`聊天消息请求错误: ${error}`);
|
||
setChatList(produce(getChatList(), (draft) => {
|
||
const placeholderIndex = draft.findIndex(item => item.id === placeholderAnswerId);
|
||
if (placeholderIndex !== -1) {
|
||
draft[placeholderIndex].content = `错误: ${error}`;
|
||
draft[placeholderIndex].isError = true;
|
||
}
|
||
}));
|
||
setNotResponding();
|
||
},
|
||
|
||
getAbortController: (controller) => {
|
||
abortControllerRef.current = controller;
|
||
},
|
||
});
|
||
} catch (err: any) {
|
||
logError(`发送消息时出错: ${err.message}`);
|
||
setNotResponding();
|
||
}
|
||
}, [
|
||
checkCanSend,
|
||
getChatList,
|
||
setChatList,
|
||
logError,
|
||
onUpdateConversationList,
|
||
onConversationIdChange,
|
||
setNotResponding,
|
||
setResponding,
|
||
updateCurrentQA,
|
||
transformToServerFile,
|
||
setHasStopResponded,
|
||
setIsRespondingConCurrCon,
|
||
setMessageTaskId,
|
||
setUserQuery
|
||
]);
|
||
|
||
/**
|
||
* 处理反馈
|
||
*/
|
||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedbacktype) => {
|
||
try {
|
||
await updateFeedback(messageId, feedback);
|
||
|
||
// 更新聊天列表中的反馈
|
||
setChatList(produce(getChatList(), (draft) => {
|
||
const messageIndex = draft.findIndex(item => item.id === messageId);
|
||
if (messageIndex !== -1) {
|
||
draft[messageIndex].feedback = feedback;
|
||
}
|
||
}));
|
||
} catch (err) {
|
||
logError(`提交反馈时出错: ${err}`);
|
||
}
|
||
}, [logError, getChatList, setChatList]);
|
||
|
||
/**
|
||
* 停止响应
|
||
*/
|
||
const stopResponding = useCallback(() => {
|
||
if (abortControllerRef.current) {
|
||
abortControllerRef.current.abort();
|
||
abortControllerRef.current = null;
|
||
}
|
||
|
||
setHasStopResponded(true);
|
||
setNotResponding();
|
||
}, [setNotResponding, setHasStopResponded]);
|
||
|
||
/**
|
||
* 重新生成回答
|
||
*/
|
||
const regenerateResponse = useCallback(async (messageId: string) => {
|
||
// 找到要重新生成的消息
|
||
const messageIndex = getChatList().findIndex(item => item.id === messageId);
|
||
if (messageIndex === -1) return;
|
||
|
||
const message = getChatList()[messageIndex];
|
||
if (!message.isAnswer) return;
|
||
|
||
// 找到对应的问题
|
||
const questionIndex = messageIndex - 1;
|
||
if (questionIndex < 0) return;
|
||
|
||
const question = getChatList()[questionIndex];
|
||
if (question.isAnswer) return;
|
||
|
||
// 重新发送问题
|
||
await handleSend(question.content, null, question.message_files);
|
||
}, [getChatList, handleSend]);
|
||
|
||
/**
|
||
* 清空聊天记录
|
||
*/
|
||
const clearChatList = useCallback(() => {
|
||
setChatList([]);
|
||
currentResponseRef.current = null;
|
||
}, [setChatList]);
|
||
|
||
return {
|
||
// 基础状态
|
||
chatList,
|
||
setChatList,
|
||
getChatList,
|
||
isResponding,
|
||
hasStopResponded,
|
||
isRespondingConIsCurrCon,
|
||
messageTaskId,
|
||
userQuery,
|
||
|
||
// 核心方法
|
||
handleSend,
|
||
handleFeedback,
|
||
stopResponding,
|
||
regenerateResponse,
|
||
clearChatList,
|
||
|
||
// 辅助方法
|
||
checkCanSend,
|
||
logError,
|
||
updateCurrentQA,
|
||
};
|
||
}
|