基于 shiy-temp分支修改

This commit is contained in:
pingchuan
2025-06-04 11:18:52 +08:00
parent 87ad3376fe
commit af33de09db
36 changed files with 6293 additions and 105 deletions
+574
View File
@@ -0,0 +1,574 @@
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';
/**
* 聊天消息处理钩子
*/
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
) => {
// console.log('🔄 更新聊天列表:', {
// responseItemId: responseItem.id,
// responseContent: responseItem.content,
// originalResponseId,
// questionId,
// placeholderAnswerId
// });
setChatList(produce(getChatList(), (draft) => {
// console.log('📝 当前聊天列表:', draft.map(item => ({ id: item.id, content: item.content.substring(0, 20), isAnswer: item.isAnswer })));
// 移除占位符
const placeholderIndex = draft.findIndex(item => item.id === placeholderAnswerId);
if (placeholderIndex !== -1) {
// console.log('🗑️ 移除占位符:', placeholderAnswerId);
draft.splice(placeholderIndex, 1);
}
// 确保问题存在
const questionIndex = draft.findIndex(item => item.id === questionId);
if (questionIndex === -1) {
// console.log(' 添加问题:', questionId);
draft.push({ ...questionItem });
}
// 更新或添加响应 - 考虑ID可能已经改变的情况
let responseIndex = draft.findIndex(item => item.id === responseItem.id);
// console.log('🔍 查找响应索引 (当前ID):', { responseItemId: responseItem.id, responseIndex });
// 如果找不到当前ID的响应,尝试查找原始ID
if (responseIndex === -1 && originalResponseId) {
responseIndex = draft.findIndex(item => item.id === originalResponseId);
// console.log('🔍 查找响应索引 (原始ID):', { originalResponseId, responseIndex });
}
// 如果找不到任何匹配的响应,查找最后一个AI回答
if (responseIndex === -1) {
responseIndex = draft.findIndex((item, index) =>
item.isAnswer &&
index > draft.findIndex(q => q.id === questionId)
);
// console.log('🔍 查找响应索引 (最后AI回答):', { responseIndex });
}
if (responseIndex !== -1) {
// console.log('✏️ 更新现有响应:', { responseIndex, newContent: responseItem.content.substring(0, 20) });
draft[responseIndex] = { ...responseItem };
} else {
// console.log(' 添加新响应:', { responseId: responseItem.id, content: responseItem.content.substring(0, 20) });
draft.push({ ...responseItem });
}
// console.log('📝 更新后聊天列表:', draft.map(item => ({ id: item.id, content: item.content.substring(0, 20), isAnswer: item.isAnswer })));
}));
}, [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,
};
}, []);
/**
* 发送消息
*/
const handleSend = useCallback(async (
message: string,
conversationId: string | null,
files?: VisionFile[],
inputs?: Record<string, any>,
) => {
if (!checkCanSend() || !message.trim()) {
return;
}
// console.log('📤 发送消息:', { message, conversationId });
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,
};
// 添加文件数据
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 }) => {
// console.log('📨 收到流式数据:', { message, isFirstMessage, messageId, newConversationId });
if (!isAgentMode) {
// 累积消息内容
responseItem.content = responseItem.content + message;
// console.log('📝 累积消息内容:', { currentContent: responseItem.content });
} else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1];
if (lastThought) {
lastThought.thought = (lastThought.thought || '') + message;
}
}
if (messageId && !hasSetResponseId) {
responseItem.id = messageId;
hasSetResponseId = true;
// console.log('🆔 设置响应ID:', { oldId: originalResponseId, newId: messageId });
}
// 重要:确保正确获取新会话ID
if (newConversationId && !tempNewConversationId) {
tempNewConversationId = newConversationId;
// console.log('🆔 获取到新会话ID:', tempNewConversationId);
}
setMessageTaskId(taskId || '');
// 检查是否切换到其他会话
// console.log('🔍 会话检查:', {
// prevTempNewConversationId,
// conversationId,
// isEqual: prevTempNewConversationId === conversationId
// });
// 修复新会话的匹配逻辑
const isNewConversationMatch = (prevTempNewConversationId === '-1' && conversationId === null) ||
(prevTempNewConversationId === conversationId);
if (!isNewConversationMatch) {
// console.log('⚠️ 会话不匹配,跳过更新');
setIsRespondingConCurrCon(false);
return;
}
// console.log('🔄 准备调用updateCurrentQA:', {
// responseItemId: responseItem.id,
// responseContent: responseItem.content,
// questionId,
// placeholderAnswerId,
// originalResponseId
// });
// 更新当前问答(使用防抖)
updateCurrentQA({
responseItem: { ...responseItem }, // 创建副本避免引用问题
questionId,
placeholderAnswerId,
questionItem,
originalResponseId,
});
},
onCompleted: async (hasError?: boolean) => {
// console.log('✅ 消息发送完成:', { hasError });
// 立即更新最终状态
if (currentResponseRef.current) {
updateCurrentQA({
responseItem: { ...currentResponseRef.current },
questionId,
placeholderAnswerId,
questionItem,
originalResponseId,
});
}
if (hasError) {
setNotResponding();
return;
}
// 如果是新会话,处理会话ID更新和名称生成
// 检查原始传入的conversationId是否为新会话标识
const isNewConversation = conversationId === '-1' || conversationId === null;
if (tempNewConversationId && isNewConversation) {
try {
// console.log('🆕 处理新会话:', {
// tempNewConversationId,
// originalConversationId: conversationId,
// isNewConversation
// });
// 通知会话ID变更(这会触发localStorage更新)
onConversationIdChange?.(tempNewConversationId);
// 生成会话名称
const res = await generateConversationName(tempNewConversationId);
const { data } = res as any;
if (data?.name) {
// console.log('📝 生成会话名称:', data.name);
onUpdateConversationList?.(tempNewConversationId, { name: data.name });
}
} catch (err) {
console.error('生成会话名称失败:', err);
}
} else {
// console.log('🔍 不是新会话,跳过处理:', {
// tempNewConversationId,
// conversationId,
// isNewConversation
// });
}
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('Message ended:', messageEnd);
},
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({
url: `messages/${messageId}/feedbacks`,
body: 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,
};
}
+246
View File
@@ -0,0 +1,246 @@
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 { CHAT_CONFIG } from '../config/chat';
// 本地存储键名
const storageConversationIdKey = 'conversationIdInfo';
// 会话信息类型(排除inputs和id)
type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>;
/**
* 获取完整的应用URL键名(与webapp-conversation保持一致)
* @param appId 应用ID
* @returns 完整的URL键名
*/
function getAppUrlKey(appId: string): string {
if (typeof window !== 'undefined') {
// 在客户端,构建完整的URL键名
const { protocol, host } = window.location;
return `${protocol}//${host}/app/${appId}`;
}
// 在服务端,使用简化的键名
return appId;
}
/**
* 会话管理钩子
* 用于管理聊天会话、当前会话状态以及会话输入
* 采用单页面应用模式,不使用URL路由导航
*/
export default function useConversation() {
const params = useParams();
// 会话列表
const [conversationList, setConversationList] = useState<ConversationItem[]>([]);
// 当前会话ID - 使用ahooks的useGetState来获得更好的状态管理
// 初始值从URL参数或localStorage获取,如果都没有则为'-1'(新会话)
const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>(() => {
// 首先尝试从URL参数获取
if (params.id) {
return params.id;
}
// 然后尝试从localStorage获取
const storedId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID);
return storedId || '-1';
});
// 新会话输入状态
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null);
// 现有会话输入状态
const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null);
// 新会话信息
const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null);
// 现有会话信息
const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null);
// 判断是否为新会话
const isNewConversation = currConversationId === '-1';
// 当前输入状态(根据是否为新会话来决定使用哪个状态)
const currInputs = isNewConversation ? newConversationInputs : existConversationInputs;
const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs;
// 当前会话信息(根据是否为新会话来决定使用哪个信息)
const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo;
/**
* 从本地存储获取会话ID
* @param appId 应用ID
* @returns 会话ID
*/
function getConversationIdFromStorage(appId: string): string {
try {
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey)
? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '{}')
: {};
// 使用完整的URL键名获取会话ID
const appUrlKey = getAppUrlKey(appId);
const conversationId = conversationIdInfo[appUrlKey];
// console.log('📖 从localStorage获取会话ID:', {
// appUrlKey,
// conversationId,
// allKeys: Object.keys(conversationIdInfo)
// });
return conversationId || '-1';
} catch (error) {
console.error('获取本地存储会话ID失败:', error);
return '-1';
}
}
/**
* 设置当前会话ID并保存到本地存储
* 纯状态管理模式,不进行URL导航
* @param id 会话ID
* @param appId 应用ID
* @param isSetToLocalStorage 是否保存到本地存储
* @param newConversationName 新会话名称(暂未使用)
*/
const setCurrConversationId = (
id: string,
appId: string,
isSetToLocalStorage = true,
newConversationName = ''
) => {
console.log('🔄 设置当前会话ID:', { id, appId, isSetToLocalStorage });
doSetCurrConversationId(id);
if (isSetToLocalStorage && id !== '-1') {
try {
// 获取现有的conversationIdInfo
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey)
? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '{}')
: {};
// 使用完整的URL键名保存会话ID(与webapp-conversation保持一致)
const appUrlKey = getAppUrlKey(appId);
conversationIdInfo[appUrlKey] = id;
globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo));
console.log('💾 会话ID已保存到localStorage:', {
appUrlKey,
conversationId: id,
fullStorage: conversationIdInfo
});
} catch (error) {
console.error('保存会话ID到本地存储失败:', error);
}
}
// 不进行URL导航,保持单页面应用模式
console.log('✅ 会话切换完成,当前会话ID:', id);
};
/**
* 重置新会话输入
* 使用immer来安全地重置状态
*/
const resetNewConversationInputs = () => {
if (!newConversationInputs) {
return;
}
setNewConversationInputs(produce(newConversationInputs, (draft) => {
Object.keys(draft).forEach((key) => {
draft[key] = '';
});
}));
};
/**
* 更新会话列表中的特定会话
* @param id 会话ID
* @param updates 要更新的字段
*/
const updateConversationInList = (id: string, updates: Partial<ConversationItem>) => {
setConversationList(produce(conversationList, (draft) => {
const index = draft.findIndex(item => item.id === id);
if (index !== -1) {
Object.assign(draft[index], updates);
}
}));
};
/**
* 添加新会话到列表
* @param conversation 新会话
*/
const addConversationToList = (conversation: ConversationItem) => {
setConversationList(produce(conversationList, (draft) => {
// 检查是否已存在,避免重复添加
const exists = draft.some(item => item.id === conversation.id);
if (!exists) {
draft.unshift(conversation);
}
}));
};
/**
* 从列表中移除会话
* @param id 会话ID
*/
const removeConversationFromList = (id: string) => {
setConversationList(produce(conversationList, (draft) => {
const index = draft.findIndex(item => item.id === id);
if (index !== -1) {
draft.splice(index, 1);
}
}));
};
/**
* 创建新会话项
* @param name 会话名称
* @param introduction 会话介绍
* @returns 新会话项
*/
const createNewConversationItem = (name: string = '新对话', introduction: string = ''): ConversationItem => {
return {
id: '-1',
name,
inputs: newConversationInputs || {},
introduction,
};
};
return {
// 基础状态
conversationList,
setConversationList,
currConversationId,
getCurrConversationId,
setCurrConversationId,
getConversationIdFromStorage,
isNewConversation,
// 输入状态
currInputs,
newConversationInputs,
existConversationInputs,
resetNewConversationInputs,
setCurrInputs,
// 会话信息
currConversationInfo,
setNewConversationInfo,
setExistConversationInfo,
// 辅助方法
updateConversationInList,
addConversationToList,
removeConversationFromList,
createNewConversationItem,
};
}