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

多轮对话记忆

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