用户操作绝对优先 - 任何用户行为立即打断AI

唯一ID验证 - 防止过期回复覆盖
状态互斥保护 - AI生成 ⇄ 用户输入互斥
智能防抖 - 3秒延迟合并多条消息
This commit is contained in:
超级老白兔
2025-10-29 15:21:27 +08:00
parent 9c5e253d75
commit 44a8fbe0c9
2 changed files with 191 additions and 28 deletions

View File

@@ -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}

View File

@@ -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 事件