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
99f78a49
Commit
99f78a49
authored
Jun 17, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
前端代码修复
parent
77a77a8f
Changes
4
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
260 additions
and
295 deletions
+260
-295
ChatPage.tsx
src/pages/ChatPage.tsx
+44
-114
ShopPage.tsx
src/pages/ShopPage.tsx
+69
-44
cozeClient.ts
src/utils/cozeClient.ts
+126
-131
difyStreamClient.ts
src/utils/difyStreamClient.ts
+21
-6
No files found.
src/pages/ChatPage.tsx
View file @
99f78a49
This diff is collapsed.
Click to expand it.
src/pages/ShopPage.tsx
View file @
99f78a49
import
{
useState
,
useEffect
,
useCallback
,
useRef
}
from
"react"
;
import
{
useState
,
useEffect
,
useCallback
,
useRef
}
from
"react"
;
import
{
flushSync
}
from
"react-dom"
;
import
{
supabase
}
from
"@/integrations/supabase/client"
;
import
{
supabase
}
from
"@/integrations/supabase/client"
;
import
{
ProductCard
}
from
"@/components/chat/ProductCard"
;
import
{
ProductCard
}
from
"@/components/chat/ProductCard"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
...
@@ -305,16 +306,32 @@ export default function ShopPage() {
...
@@ -305,16 +306,32 @@ 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
);
const
streamMsgId
=
`ai_
${
Date
.
now
()}
`
;
// 先插入一个占位消息,后续流式更新
const
placeholderMessage
:
ChatMessageType
=
{
id
:
streamMsgId
,
content
:
''
,
sender
:
"ai"
,
timestamp
:
new
Date
().
toLocaleTimeString
(
'zh-CN'
,
{
hour
:
'2-digit'
,
minute
:
'2-digit'
}),
type
:
"text"
,
agent
:
currentAgent
,
};
setMessages
(
prev
=>
[...
prev
,
placeholderMessage
]);
try
{
try
{
let
currentProducts
=
products
;
let
currentProducts
=
products
;
if
(
currentProducts
.
length
===
0
)
{
if
(
currentProducts
.
length
===
0
)
{
currentProducts
=
await
fetchProducts
();
currentProducts
=
await
fetchProducts
();
}
}
if
(
currentProducts
.
length
===
0
)
{
if
(
currentProducts
.
length
===
0
)
{
throw
new
Error
(
'产品数据加载失败'
);
throw
new
Error
(
'产品数据加载失败'
);
}
}
const
catalog
=
currentProducts
.
map
(
p
=>
({
const
catalog
=
currentProducts
.
map
(
p
=>
({
id
:
p
.
id
,
id
:
p
.
id
,
name
:
p
.
name
,
name
:
p
.
name
,
...
@@ -323,7 +340,9 @@ export default function ShopPage() {
...
@@ -323,7 +340,9 @@ export default function ShopPage() {
category
:
p
.
category
,
category
:
p
.
category
,
description
:
p
.
description
description
:
p
.
description
}));
}));
// ─── 流式接收 AI 回复文字 ───
let
fullContent
=
''
;
const
{
data
:
intentCard
}
=
await
callCozeEdge
({
const
{
data
:
intentCard
}
=
await
callCozeEdge
({
message
:
userMessage
,
message
:
userMessage
,
userId
:
user
?.
id
||
'anonymous'
,
userId
:
user
?.
id
||
'anonymous'
,
...
@@ -333,21 +352,38 @@ export default function ShopPage() {
...
@@ -333,21 +352,38 @@ export default function ShopPage() {
description
:
currentProducts
[
0
].
description
||
''
,
description
:
currentProducts
[
0
].
description
||
''
,
price
:
Number
(
currentProducts
[
0
].
price
)
price
:
Number
(
currentProducts
[
0
].
price
)
},
},
catalog
catalog
,
onText
:
(
text
:
string
)
=>
{
fullContent
+=
text
;
flushSync
(()
=>
{
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMsgId
?
{
...
msg
,
content
:
fullContent
}
:
msg
));
});
}
});
});
if
(
!
intentCard
)
throw
new
Error
(
'系统繁忙,请稍后再试'
);
if
(
!
intentCard
)
throw
new
Error
(
'系统繁忙,请稍后再试'
);
let
aiContent
=
intentCard
.
title
||
'收到您的消息'
;
// 清洗 JSON 代码块,避免裸露协议数据出现在正文
let
aiContent
=
fullContent
if
(
intentCard
.
highlights
&&
intentCard
.
highlights
.
length
>
0
)
{
.
replace
(
/```
(?:
json
)?\s
*
\{[\s\S]
*
?
"
(?:
picks|intent
)
"
[\s\S]
*
?\}\s
*```/gi
,
''
)
aiContent
+=
'
\
n
\
n'
+
intentCard
.
highlights
.
map
(
h
=>
`•
${
h
}
`
).
join
(
'
\
n'
);
.
replace
(
/
\{\s
*"
(?:
picks|intent
)
"
\s
*:
[\s\S]
*
?\}\s
*$/g
,
''
)
.
trim
();
if
(
!
aiContent
)
{
aiContent
=
intentCard
.
title
||
'收到您的消息'
;
if
(
intentCard
.
highlights
&&
intentCard
.
highlights
.
length
>
0
)
{
aiContent
+=
'
\
n
\
n'
+
intentCard
.
highlights
.
map
(
h
=>
`•
${
h
}
`
).
join
(
'
\
n'
);
}
}
}
if
(
intentCard
.
picks
&&
intentCard
.
picks
.
length
>
0
)
{
if
(
intentCard
.
picks
&&
intentCard
.
picks
.
length
>
0
)
{
aiContent
+=
'
\
n
\
n为您推荐了 '
+
intentCard
.
picks
.
length
+
' 个商品'
;
aiContent
+=
'
\
n
\
n为您推荐了 '
+
intentCard
.
picks
.
length
+
' 个商品'
;
}
}
let
messageType
:
ChatMessageType
[
'type'
]
=
"text"
;
let
messageType
:
ChatMessageType
[
'type'
]
=
"text"
;
let
responseMetadata
:
ChatMessageType
[
'metadata'
]
=
{};
let
responseMetadata
:
ChatMessageType
[
'metadata'
]
=
{};
...
@@ -370,50 +406,39 @@ export default function ShopPage() {
...
@@ -370,50 +406,39 @@ export default function ShopPage() {
}))
}))
};
};
}
}
const
aiMessage
:
ChatMessageType
=
{
// 更新占位消息为最终内容 + 类型 + 元数据
id
:
`ai_
${
Date
.
now
()}
`
,
setMessages
(
prev
=>
prev
.
map
(
msg
=>
content
:
aiContent
,
msg
.
id
===
streamMsgId
sender
:
"ai"
,
?
{
...
msg
,
content
:
aiContent
,
type
:
messageType
,
metadata
:
responseMetadata
}
timestamp
:
new
Date
().
toLocaleTimeString
(
'zh-CN'
,
{
:
msg
hour
:
'2-digit'
,
));
minute
:
'2-digit'
}),
type
:
messageType
,
agent
:
currentAgent
,
metadata
:
responseMetadata
};
setMessages
(
prev
=>
[...
prev
,
aiMessage
]);
if
(
activeConversationId
)
{
if
(
activeConversationId
)
{
await
saveMessage
({
await
saveMessage
({
content
:
ai
Message
.
c
ontent
,
content
:
ai
C
ontent
,
sender_type
:
aiMessage
.
sender
,
sender_type
:
"ai"
,
message_type
:
aiMessage
.
t
ype
as
DBMessage
[
'message_type'
],
message_type
:
messageT
ype
as
DBMessage
[
'message_type'
],
metadata
:
aiMessage
.
m
etadata
metadata
:
responseM
etadata
},
activeConversationId
);
},
activeConversationId
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'AI回复失败:'
,
error
);
console
.
error
(
'AI回复失败:'
,
error
);
const
errorMessage
:
ChatMessageType
=
{
// 把占位消息替换为错误消息
id
:
`ai_error_
${
Date
.
now
()}
`
,
setMessages
(
prev
=>
prev
.
map
(
msg
=>
content
:
`抱歉,遇到了技术问题,请稍后再试。`
,
msg
.
id
===
streamMsgId
sender
:
"ai"
,
?
{
...
msg
,
content
:
'抱歉,遇到了技术问题,请稍后再试。'
}
timestamp
:
new
Date
().
toLocaleTimeString
(
'zh-CN'
,
{
:
msg
hour
:
'2-digit'
,
));
minute
:
'2-digit'
}),
type
:
"text"
,
agent
:
currentAgent
};
setMessages
(
prev
=>
[...
prev
,
errorMessage
]);
}
finally
{
}
finally
{
setIsAiTyping
(
false
);
setIsAiTyping
(
false
);
}
}
},
[
currentAgent
,
products
,
activeConversationId
,
saveMessage
,
user
]);
},
[
currentAgent
,
products
,
activeConversationId
,
saveMessage
,
user
]);
const
handleSendMessage
=
useCallback
(
async
(
content
:
string
)
=>
{
const
handleSendMessage
=
useCallback
(
async
(
content
:
string
)
=>
{
// 防止并发流式调用(如快速连点建议按钮)
if
(
isAiTyping
)
return
;
const
userMessage
:
ChatMessageType
=
{
const
userMessage
:
ChatMessageType
=
{
id
:
`user_
${
Date
.
now
()}
`
,
id
:
`user_
${
Date
.
now
()}
`
,
content
,
content
,
...
@@ -437,7 +462,7 @@ export default function ShopPage() {
...
@@ -437,7 +462,7 @@ export default function ShopPage() {
}
}
await
simulateAIResponse
(
content
);
await
simulateAIResponse
(
content
);
},
[
simulateAIResponse
,
activeConversationId
,
saveMessage
]);
},
[
simulateAIResponse
,
activeConversationId
,
saveMessage
,
isAiTyping
]);
return
(
return
(
<
div
className=
"min-h-screen bg-background"
>
<
div
className=
"min-h-screen bg-background"
>
...
...
src/utils/cozeClient.ts
View file @
99f78a49
This diff is collapsed.
Click to expand it.
src/utils/difyStreamClient.ts
View file @
99f78a49
...
@@ -51,6 +51,9 @@ export async function streamDifyChat(
...
@@ -51,6 +51,9 @@ export async function streamDifyChat(
try
{
try
{
console
.
log
(
'🚀 [DIFY STREAM] 开始流式调用'
);
console
.
log
(
'🚀 [DIFY STREAM] 开始流式调用'
);
const
controller
=
new
AbortController
();
const
timeoutId
=
setTimeout
(()
=>
controller
.
abort
(),
65000
);
const
response
=
await
fetch
(
`
${
SUPABASE_URL
}
/functions/v1/dify-chat-stream`
,
{
const
response
=
await
fetch
(
`
${
SUPABASE_URL
}
/functions/v1/dify-chat-stream`
,
{
method
:
'POST'
,
method
:
'POST'
,
headers
:
{
headers
:
{
...
@@ -60,8 +63,11 @@ export async function streamDifyChat(
...
@@ -60,8 +63,11 @@ export async function streamDifyChat(
'X-User-Id': options.userId || 'guest',
'X-User-Id': options.userId || 'guest',
},
},
body: JSON.stringify(requestBody),
body: JSON.stringify(requestBody),
signal: controller.signal,
});
});
clearTimeout(timeoutId);
if (!response.ok) {
if (!response.ok) {
const errorText = await response.text();
const errorText = await response.text();
throw new Error(`
Dify
请求失败
(
$
{
response
.
status
}):
$
{
errorText
}
`
);
throw new Error(`
Dify
请求失败
(
$
{
response
.
status
}):
$
{
errorText
}
`
);
...
@@ -79,7 +85,7 @@ export async function streamDifyChat(
...
@@ -79,7 +85,7 @@ export async function streamDifyChat(
const
{
done
,
value
}
=
await
reader
.
read
();
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
{
if
(
done
)
{
if
(
buffer
.
trim
())
{
if
(
buffer
.
trim
())
{
processSSELine
(
buffer
,
callbacks
);
await
processSSELine
(
buffer
,
callbacks
);
}
}
break
;
break
;
}
}
...
@@ -89,18 +95,22 @@ export async function streamDifyChat(
...
@@ -89,18 +95,22 @@ export async function streamDifyChat(
buffer
=
lines
.
pop
()
||
''
;
buffer
=
lines
.
pop
()
||
''
;
for
(
const
line
of
lines
)
{
for
(
const
line
of
lines
)
{
processSSELine
(
line
,
callbacks
);
await
processSSELine
(
line
,
callbacks
);
}
}
}
}
console
.
log
(
'✅ [DIFY STREAM] 流式调用完成'
);
console
.
log
(
'✅ [DIFY STREAM] 流式调用完成'
);
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'❌ [DIFY STREAM] 错误:'
,
error
);
console
.
error
(
'❌ [DIFY STREAM] 错误:'
,
error
);
callbacks
.
onError
(
error
instanceof
Error
?
error
.
message
:
'流式请求失败'
);
if
(
error
instanceof
DOMException
&&
error
.
name
===
'AbortError'
)
{
callbacks
.
onError
(
'AI响应超时(65秒),请重新发送消息'
);
}
else
{
callbacks
.
onError
(
error
instanceof
Error
?
error
.
message
:
'流式请求失败'
);
}
}
}
}
}
function
processSSELine
(
line
:
string
,
callbacks
:
CozeStreamCallbacks
)
{
async
function
processSSELine
(
line
:
string
,
callbacks
:
CozeStreamCallbacks
)
{
const
trimmedLine
=
line
.
trim
();
const
trimmedLine
=
line
.
trim
();
if
(
!
trimmedLine
||
trimmedLine
.
startsWith
(
':'
))
return
;
if
(
!
trimmedLine
||
trimmedLine
.
startsWith
(
':'
))
return
;
if
(
trimmedLine
===
'data: [DONE]'
)
return
;
if
(
trimmedLine
===
'data: [DONE]'
)
return
;
...
@@ -114,15 +124,20 @@ function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
...
@@ -114,15 +124,20 @@ function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
callbacks
.
onText
(
event
.
content
);
callbacks
.
onText
(
event
.
content
);
break
;
break
;
case
'done'
:
case
'done'
:
callbacks
.
onDone
(
event
);
await
callbacks
.
onDone
(
event
);
break
;
break
;
case
'error'
:
case
'error'
:
callbacks
.
onError
(
event
.
message
);
callbacks
.
onError
(
event
.
message
);
break
;
break
;
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
// JSON.parse SyntaxError or callback rejection — safe fallback
if
(
jsonStr
&&
!
jsonStr
.
startsWith
(
'{'
))
{
if
(
jsonStr
&&
!
jsonStr
.
startsWith
(
'{'
))
{
callbacks
.
onText
(
jsonStr
);
try
{
callbacks
.
onText
(
jsonStr
);
}
catch
{
// onText itself failed; nothing more we can do
}
}
}
}
}
}
}
...
...
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