用户操作绝对优先 - 任何用户行为立即打断AI
唯一ID验证 - 防止过期回复覆盖 状态互斥保护 - AI生成 ⇄ 用户输入互斥 智能防抖 - 3秒延迟合并多条消息
This commit is contained in:
@@ -16,7 +16,10 @@ import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
|
||||
import AudioRecorder from "@/components/Upload/AudioRecorder";
|
||||
import ToContract from "./components/toContract";
|
||||
import styles from "./MessageEnter.module.scss";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
import {
|
||||
useWeChatStore,
|
||||
clearAiRequestQueue,
|
||||
} from "@/store/module/weChat/weChat";
|
||||
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||
const { Footer } = Layout;
|
||||
const { TextArea } = Input;
|
||||
@@ -63,6 +66,8 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
|
||||
// 取消AI生成
|
||||
const handleCancelAi = () => {
|
||||
// 清除AI请求定时器和队列
|
||||
clearAiRequestQueue("用户手动取消");
|
||||
// 停止AI加载状态
|
||||
updateIsLoadingAiChat(false);
|
||||
// 清空AI回复内容
|
||||
@@ -70,6 +75,22 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
message.info("已取消AI生成");
|
||||
};
|
||||
|
||||
// 监听输入框变化 - 用户开始输入时取消AI
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
|
||||
// 如果用户开始输入(且不是AI填充的内容),取消AI请求
|
||||
if (newValue && newValue !== quoteMessageContent) {
|
||||
if (isLoadingAiChat) {
|
||||
console.log("👤 用户开始输入,取消AI生成");
|
||||
clearAiRequestQueue("用户开始输入");
|
||||
updateIsLoadingAiChat(false);
|
||||
updateQuoteMessageContent("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 发送消息(支持传入内容参数,避免闭包问题)
|
||||
const handleSend = async (content?: string) => {
|
||||
const messageContent = content || inputValue; // 优先使用传入的内容
|
||||
@@ -79,6 +100,13 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户主动发送消息时,取消AI请求
|
||||
if (!content && isLoadingAiChat) {
|
||||
console.log("👤 用户主动发送消息,取消AI生成");
|
||||
clearAiRequestQueue("用户主动发送");
|
||||
updateIsLoadingAiChat(false);
|
||||
}
|
||||
|
||||
console.log("handleSend", messageContent);
|
||||
const messageId = +Date.now();
|
||||
// 构造本地消息对象
|
||||
@@ -128,17 +156,27 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
// AI 消息处理
|
||||
useEffect(() => {
|
||||
if (quoteMessageContent) {
|
||||
console.log("AI消息到达 - aiQuoteMessageContent:", aiQuoteMessageContent);
|
||||
console.log(
|
||||
"🤖 AI消息到达 - aiQuoteMessageContent:",
|
||||
aiQuoteMessageContent,
|
||||
);
|
||||
|
||||
// 检查:如果用户输入框已有内容(且不是之前的AI内容),不覆盖
|
||||
if (inputValue && inputValue !== quoteMessageContent) {
|
||||
console.log("⚠️ 用户正在输入,不覆盖输入内容");
|
||||
updateQuoteMessageContent(""); // 清空AI回复
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAiAssist) {
|
||||
// AI辅助模式:填充到输入框,等待人工确认
|
||||
console.log("AI辅助模式:填充消息到输入框");
|
||||
console.log("✨ AI辅助模式:填充消息到输入框");
|
||||
setInputValue(quoteMessageContent);
|
||||
}
|
||||
|
||||
if (isAiTakeover) {
|
||||
// AI接管模式:直接发送消息(传入内容,避免 state 闭包问题)
|
||||
console.log("AI接管模式:自动发送消息");
|
||||
console.log("🚀 AI接管模式:自动发送消息");
|
||||
handleSend(quoteMessageContent);
|
||||
}
|
||||
}
|
||||
@@ -354,7 +392,7 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextArea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="输入消息..."
|
||||
className={styles.messageInput}
|
||||
|
||||
@@ -19,6 +19,40 @@ import {
|
||||
} from "@/pages/pc/ckbox/api";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
|
||||
/**
|
||||
* AI请求防抖管理
|
||||
* 用于处理连续消息时的AI请求延迟
|
||||
*/
|
||||
let aiRequestTimer: NodeJS.Timeout | null = null;
|
||||
let pendingMessages: ChatRecord[] = []; // 待处理的消息队列
|
||||
let currentAiGenerationId: string | null = null; // 当前AI生成的唯一ID
|
||||
const AI_REQUEST_DELAY = 3000; // 3秒延迟
|
||||
|
||||
/**
|
||||
* 清除AI请求定时器和队列
|
||||
* 用于取消AI生成或切换聊天时调用
|
||||
* @param reason 清除原因(用于日志)
|
||||
*/
|
||||
export const clearAiRequestQueue = (reason: string = "手动取消") => {
|
||||
if (aiRequestTimer) {
|
||||
console.log(`🚫 清除AI请求:${reason}`);
|
||||
clearTimeout(aiRequestTimer);
|
||||
aiRequestTimer = null;
|
||||
}
|
||||
pendingMessages = [];
|
||||
currentAiGenerationId = null;
|
||||
|
||||
// 同时清除AI加载状态
|
||||
useWeChatStore.getState().updateIsLoadingAiChat(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成唯一的AI生成ID
|
||||
*/
|
||||
const generateAiId = (): string => {
|
||||
return `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 微信聊天状态管理 Store
|
||||
* 使用 Zustand 管理微信聊天相关的状态和操作
|
||||
@@ -134,6 +168,12 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
// ==================== 当前联系人管理方法 ====================
|
||||
/** 清空当前联系人和消息 */
|
||||
clearCurrentContact: () => {
|
||||
// 清除AI请求定时器和队列
|
||||
if (aiRequestTimer) {
|
||||
clearTimeout(aiRequestTimer);
|
||||
aiRequestTimer = null;
|
||||
}
|
||||
pendingMessages = [];
|
||||
set({ currentContract: null, currentMessages: [] });
|
||||
},
|
||||
/** 设置当前联系人并加载相关数据 */
|
||||
@@ -142,9 +182,18 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
"setCurrentContact - contract.aiType:",
|
||||
(contract as any).aiType,
|
||||
); // 调试:查看 aiType 字段
|
||||
|
||||
// 切换联系人时清除AI请求定时器和队列
|
||||
if (aiRequestTimer) {
|
||||
console.log("切换联系人,清除AI请求定时器和队列");
|
||||
clearTimeout(aiRequestTimer);
|
||||
aiRequestTimer = null;
|
||||
}
|
||||
pendingMessages = [];
|
||||
|
||||
const state = useWeChatStore.getState();
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [] });
|
||||
set({ currentMessages: [], isLoadingAiChat: false });
|
||||
|
||||
const params: any = {};
|
||||
|
||||
@@ -302,33 +351,109 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
message.msgType === 1 &&
|
||||
[1, 2].includes((currentContract as any).aiType || 0)
|
||||
) {
|
||||
//把数据传到存客宝
|
||||
const params: any = {
|
||||
type: "CmdNewMessage",
|
||||
wechatAccountId: currentContract.wechatAccountId,
|
||||
};
|
||||
if (isWechatGroup) {
|
||||
params.chatroomMessage = [message];
|
||||
} else {
|
||||
params.friendMessage = [message];
|
||||
console.log("📨 收到新消息,准备触发AI");
|
||||
|
||||
// 如果AI正在生成中,先取消
|
||||
if (aiRequestTimer || currentAiGenerationId) {
|
||||
console.log("⚠️ 检测到AI正在生成,取消并重新开始");
|
||||
clearAiRequestQueue("收到新消息");
|
||||
}
|
||||
const dataProcessingResult = await dataProcessing(params);
|
||||
//如果成功,就请求ai对话接口
|
||||
|
||||
// 将当前消息加入待处理队列
|
||||
pendingMessages.push(message);
|
||||
console.log(
|
||||
`📥 消息加入队列,当前队列长度: ${pendingMessages.length}`,
|
||||
);
|
||||
|
||||
// 显示AI加载状态
|
||||
set(() => ({
|
||||
isLoadingAiChat: true,
|
||||
}));
|
||||
if (!dataProcessingResult) {
|
||||
const messageContent = await aiChat({
|
||||
friendId: getMessageId,
|
||||
wechatAccountId: currentContract.wechatAccountId,
|
||||
message: message,
|
||||
});
|
||||
|
||||
set(() => ({
|
||||
quoteMessageContent: messageContent?.content || "",
|
||||
isLoadingAiChat: false,
|
||||
}));
|
||||
}
|
||||
// 设置新的定时器,延迟发送AI请求
|
||||
aiRequestTimer = setTimeout(async () => {
|
||||
console.log(
|
||||
`⏰ ${AI_REQUEST_DELAY / 1000}秒内无新消息,开始处理AI请求`,
|
||||
);
|
||||
|
||||
// 获取队列中的所有消息
|
||||
const messagesToProcess = [...pendingMessages];
|
||||
pendingMessages = []; // 清空队列
|
||||
aiRequestTimer = null;
|
||||
|
||||
// 生成唯一的AI请求ID
|
||||
const generationId = generateAiId();
|
||||
currentAiGenerationId = generationId;
|
||||
console.log(`🆔 AI生成ID: ${generationId}`);
|
||||
console.log(`📝 准备处理 ${messagesToProcess.length} 条消息`);
|
||||
|
||||
try {
|
||||
// 把所有消息数据传到存客宝
|
||||
const params: any = {
|
||||
type: "CmdNewMessage",
|
||||
wechatAccountId: currentContract.wechatAccountId,
|
||||
};
|
||||
if (isWechatGroup) {
|
||||
params.chatroomMessage = messagesToProcess;
|
||||
} else {
|
||||
params.friendMessage = messagesToProcess;
|
||||
}
|
||||
|
||||
const dataProcessingResult = await dataProcessing(params);
|
||||
|
||||
// 请求前再次检查:是否已被取消
|
||||
if (currentAiGenerationId !== generationId) {
|
||||
console.log(
|
||||
`❌ AI请求已过期 (当前ID: ${currentAiGenerationId}, 请求ID: ${generationId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果成功,就请求AI对话接口
|
||||
if (!dataProcessingResult) {
|
||||
// 使用最后一条消息作为上下文
|
||||
const lastMessage =
|
||||
messagesToProcess[messagesToProcess.length - 1];
|
||||
const messageContent = await aiChat({
|
||||
friendId: getMessageId,
|
||||
wechatAccountId: currentContract.wechatAccountId,
|
||||
message: lastMessage,
|
||||
});
|
||||
|
||||
// 回复到达前再次检查:是否已被取消
|
||||
if (currentAiGenerationId !== generationId) {
|
||||
console.log(
|
||||
`❌ AI回复已过期,丢弃 (当前ID: ${currentAiGenerationId}, 回复ID: ${generationId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 附加生成ID到回复内容
|
||||
set(() => ({
|
||||
quoteMessageContent: messageContent?.content || "",
|
||||
isLoadingAiChat: false,
|
||||
}));
|
||||
console.log(
|
||||
`✅ AI回复成功 [${generationId}]:`,
|
||||
messageContent?.content,
|
||||
);
|
||||
|
||||
// 清除当前生成ID
|
||||
currentAiGenerationId = null;
|
||||
} else {
|
||||
set(() => ({
|
||||
isLoadingAiChat: false,
|
||||
}));
|
||||
currentAiGenerationId = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ AI请求失败:", error);
|
||||
set(() => ({
|
||||
isLoadingAiChat: false,
|
||||
}));
|
||||
currentAiGenerationId = null;
|
||||
}
|
||||
}, AI_REQUEST_DELAY);
|
||||
}
|
||||
}
|
||||
// 注意:非当前聊天的会话列表更新已通过 chatMessageReceived 事件
|
||||
|
||||
Reference in New Issue
Block a user