Commit 6d7edaac authored by AI-甘富林's avatar AI-甘富林

多轮对话记忆

parent 2240e5ca
......@@ -43,6 +43,23 @@ import { searchMemory, addMemory } from "@/utils/mem0Client";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
const GUEST_ID_KEY = 'qianjiang_guest_uuid';
let _guestIdMemoryFallback: string | null = null;
let _activeConvId: string | null = null; // 模块级变量,绕过 React 闭包
function getOrCreateGuestId(): string {
try {
const existing = localStorage.getItem(GUEST_ID_KEY);
if (existing) return existing;
const id = crypto.randomUUID();
localStorage.setItem(GUEST_ID_KEY, id);
return id;
} catch {
// 无痕浏览等场景下 localStorage 不可用,用内存兜底保证同一会话内 ID 不变
if (!_guestIdMemoryFallback) _guestIdMemoryFallback = crypto.randomUUID();
return _guestIdMemoryFallback;
}
}
export default function ChatPage() {
const location = useLocation();
const { user } = useAuth();
......@@ -66,7 +83,9 @@ export default function ChatPage() {
// ✅ 用 ref 同步缓存 conversation_id 映射,消除 React state 异步竞态
const difyConvIdMapRef = useRef<Record<string, string>>({});
// ✅ 用 ref 保持最新 activeConversationId,避免 simulateAIResponse 闭包过期
const activeConvIdRef = useRef<string | null>(null);
useEffect(() => { activeConvIdRef.current = activeConversationId; }, [activeConversationId]);
// Convert conversations for sidebar (保留 dify_conversation_id)
const conversations = rawConversations.map(conv => ({
id: conv.id,
......@@ -283,9 +302,11 @@ export default function ChatPage() {
// AI响应处理 - 三明治架构:先查记忆 -> 问AI -> 存记忆
const simulateAIResponse = useCallback(async (userMessage: string) => {
setIsAiTyping(true);
console.trace('🔍 simulateAIResponse 入口 _activeConvId=', _activeConvId);
const activeConversationId = _activeConvId;
try {
// ========== Step 1: Pre-flight - 查询 Mem0 记忆 ==========
const userId = user?.id || 'anonymous';
const userId = user?.id || getOrCreateGuestId();
console.log('🧠 [三明治] Step 1: 查询 Mem0 记忆...');
const userContext = await searchMemory(userId, userMessage);
console.log('🧠 [三明治] 记忆上下文:', userContext ? `${userContext.length} chars` : '(empty)');
......@@ -322,7 +343,7 @@ export default function ChatPage() {
// ========== Step 2: 流式调用 AI ─────────
if (useStream) {
console.log('🚀 [STREAM] 使用流式输出模式');
console.log('🚀 [STREAM] 使用流式输出模式, activeConversationId=', activeConversationId, 'ref=', activeConvIdRef.current);
const streamMessageId = `ai_stream_${Date.now()}`;
setStreamingMessageId(streamMessageId);
......@@ -462,8 +483,9 @@ export default function ChatPage() {
await streamDifyChat(
{
message: userMessage,
userId: user?.id || 'anonymous',
userId: user?.id || getOrCreateGuestId(),
conversationId: conversationIdFromDb,
localConversationId: activeConversationId, // ★ 传入本地会话ID,Edge Function 用于加载历史
userContext: userContext,
catalog
},
......@@ -474,11 +496,12 @@ export default function ChatPage() {
}
// ========== 非流式模式 ─────────
console.log('📤 Step 2: 非流式调用');
console.log('📤 Step 2: 非流式调用, activeConversationId=', activeConversationId, 'ref=', activeConvIdRef.current);
const { data: intentCard, traceId, status, error: callError } = await callCozeEdge({
message: userMessage,
userId: user?.id || 'anonymous',
userId: user?.id || getOrCreateGuestId(),
conversationId: conversationIdFromDb,
localConversationId: activeConversationId,
userContext: userContext,
product: {
id: firstProduct.id,
......@@ -886,6 +909,20 @@ export default function ChatPage() {
// 处理发送消息
const handleSendMessage = useCallback(async (content: string) => {
// 兜底:如果没有活跃对话,自动创建一个(用局部变量避免闭包过期)
let currentConvId = activeConversationId;
if (!currentConvId) {
const newConv = await createNewConversation('新对话');
if (!newConv?.id) {
toast.error('创建对话失败');
return;
}
currentConvId = newConv.id;
activeConvIdRef.current = newConv.id;
_activeConvId = newConv.id; // ★ 模块级变量,彻底绕过闭包
}
_activeConvId = currentConvId; // 已有对话也同步
const userMessage: ChatMessageType = {
id: `user_${Date.now()}`,
content,
......@@ -899,12 +936,9 @@ export default function ChatPage() {
setMessages(prev => [...prev, userMessage]);
if (activeConversationId) {
await saveMessageLocal(userMessage, activeConversationId);
}
await saveMessageLocal(userMessage, currentConvId);
await simulateAIResponse(content);
}, [simulateAIResponse, activeConversationId, saveMessageLocal]);
}, [simulateAIResponse, activeConversationId, saveMessageLocal, createNewConversation]);
// 处理新建对话
const handleNewConversation = useCallback(async (e?: React.MouseEvent) => {
......
......@@ -23,6 +23,23 @@ import { TypingIndicator } from "@/components/chat/TypingIndicator";
import { useAuth } from "@/contexts/AuthContext";
import { AIAgent, AI_AGENTS } from "@/types/agents";
import { callCozeEdge } from "@/utils/cozeClient";
// ─── 访客 ID + 会话 ID 模块级变量(绕过 React 闭包)───
const GUEST_ID_KEY = 'qianjiang_guest_uuid';
let _guestIdMemoryFallback: string | null = null;
function getOrCreateGuestId(): string {
try {
const existing = localStorage.getItem(GUEST_ID_KEY);
if (existing) return existing;
const id = crypto.randomUUID();
localStorage.setItem(GUEST_ID_KEY, id);
return id;
} catch {
if (!_guestIdMemoryFallback) _guestIdMemoryFallback = crypto.randomUUID();
return _guestIdMemoryFallback;
}
}
let _activeConvId: string | null = null;
import { ProductDetailDialog } from "@/components/chat/ProductDetailDialog";
import { useCart } from "@/contexts/CartContext";
import { CartSidebar } from "@/components/cart/CartSidebar";
......@@ -180,6 +197,7 @@ export default function ShopPage() {
const handleNewConversation = async () => {
const newConv = await createNewConversation('商城对话');
if (newConv) {
_activeConvId = newConv.id;
setActiveConversationId(newConv.id);
// Add welcome message
......@@ -306,7 +324,9 @@ export default function ShopPage() {
// AI response handler
const simulateAIResponse = useCallback(async (userMessage: string) => {
setIsAiTyping(true);
console.log('🔍 [Shop AI] simulateAIResponse start — products in state:', products.length, 'user:', user?.id || 'anonymous');
const activeConvId = _activeConvId;
const currentUserId = user?.id || getOrCreateGuestId();
console.log('🔍 [Shop AI] simulateAIResponse start — convId:', activeConvId, 'userId:', currentUserId);
const streamMsgId = `ai_${Date.now()}`;
// 先插入一个占位消息,后续流式更新
......@@ -345,7 +365,8 @@ export default function ShopPage() {
console.log('🔍 [Shop AI] About to call callCozeEdge — catalog size:', catalog.length);
const { data: intentCard } = await callCozeEdge({
message: userMessage,
userId: user?.id || 'anonymous',
userId: currentUserId,
localConversationId: activeConvId,
product: currentProducts.length > 0 ? {
id: currentProducts[0].id,
title: currentProducts[0].name,
......@@ -414,13 +435,13 @@ export default function ShopPage() {
: msg
));
if (activeConversationId) {
if (activeConvId) {
await saveMessage({
content: aiContent,
sender_type: "ai",
message_type: messageType as DBMessage['message_type'],
metadata: responseMetadata
}, activeConversationId);
}, activeConvId);
}
} catch (error) {
console.error('AI回复失败:', error);
......@@ -439,6 +460,15 @@ export default function ShopPage() {
// 防止并发流式调用(如快速连点建议按钮)
if (isAiTyping) return;
// 兜底:确保有活跃对话
let currentConvId = activeConversationId;
if (!currentConvId) {
await handleNewConversation();
currentConvId = _activeConvId;
} else {
_activeConvId = currentConvId;
}
const userMessage: ChatMessageType = {
id: `user_${Date.now()}`,
content,
......@@ -452,13 +482,13 @@ export default function ShopPage() {
setMessages(prev => [...prev, userMessage]);
if (activeConversationId) {
if (currentConvId) {
await saveMessage({
content: userMessage.content,
sender_type: userMessage.sender,
message_type: userMessage.type as DBMessage['message_type'],
metadata: userMessage.metadata
}, activeConversationId);
}, currentConvId);
}
await simulateAIResponse(content);
......
......@@ -13,6 +13,7 @@ interface CozeRequestOptions {
message: string;
userId: string;
conversationId?: string;
localConversationId?: string; // 本地会话ID,Edge Function 用于加载历史
userContext?: string; // Mem0 记忆上下文
product?: {
id: string;
......@@ -105,6 +106,7 @@ export async function callCozeEdge(
const requestBody = {
message: options.message,
conversationId: options.conversationId || '',
localConversationId: options.localConversationId || '',
inputs: {
user_id: options.userId || 'guest',
trace_id: traceId,
......
......@@ -12,6 +12,7 @@ interface DifyStreamOptions {
message: string;
userId: string;
conversationId?: string;
localConversationId?: string; // ★ 本地 conversations 表的主键 UUID(用于加载历史)
userContext?: string;
catalog?: Array<{
id: string;
......@@ -41,6 +42,7 @@ export async function streamDifyChat(
const requestBody = {
message: options.message,
conversationId: options.conversationId || '',
localConversationId: options.localConversationId || '',
inputs: {
user_id: options.userId || 'guest',
trace_id: traceId,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment