Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qianjiangb2b
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qianjiangb2b
Commits
6d7edaac
Commit
6d7edaac
authored
Jun 18, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
多轮对话记忆
parent
2240e5ca
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
87 additions
and
19 deletions
+87
-19
ChatPage.tsx
src/pages/ChatPage.tsx
+46
-12
ShopPage.tsx
src/pages/ShopPage.tsx
+37
-7
cozeClient.ts
src/utils/cozeClient.ts
+2
-0
difyStreamClient.ts
src/utils/difyStreamClient.ts
+2
-0
No files found.
src/pages/ChatPage.tsx
View file @
6d7edaac
...
...
@@ -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
)
=>
{
...
...
src/pages/ShopPage.tsx
View file @
6d7edaac
...
...
@@ -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
(
activeConv
ersation
Id
)
{
if
(
activeConvId
)
{
await
saveMessage
({
content
:
aiContent
,
sender_type
:
"ai"
,
message_type
:
messageType
as
DBMessage
[
'message_type'
],
metadata
:
responseMetadata
},
activeConv
ersation
Id
);
},
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
(
activeConversation
Id
)
{
if
(
currentConv
Id
)
{
await
saveMessage
({
content
:
userMessage
.
content
,
sender_type
:
userMessage
.
sender
,
message_type
:
userMessage
.
type
as
DBMessage
[
'message_type'
],
metadata
:
userMessage
.
metadata
},
activeConversation
Id
);
},
currentConv
Id
);
}
await
simulateAIResponse
(
content
);
...
...
src/utils/cozeClient.ts
View file @
6d7edaac
...
...
@@ -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
,
...
...
src/utils/difyStreamClient.ts
View file @
6d7edaac
...
...
@@ -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
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment