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
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
...
@@ -37,7 +37,6 @@ import { AIAgent, AI_AGENTS } from "@/types/agents";
...
@@ -37,7 +37,6 @@ import { AIAgent, AI_AGENTS } from "@/types/agents";
import
{
supabase
}
from
"@/integrations/supabase/client"
;
import
{
supabase
}
from
"@/integrations/supabase/client"
;
import
{
useAuth
}
from
"@/contexts/AuthContext"
;
import
{
useAuth
}
from
"@/contexts/AuthContext"
;
import
{
callCozeEdge
}
from
"@/utils/cozeClient"
;
import
{
callCozeEdge
}
from
"@/utils/cozeClient"
;
import
{
streamCozeChat
}
from
"@/utils/cozeStreamClient"
;
import
{
streamDifyChat
}
from
"@/utils/difyStreamClient"
;
import
{
streamDifyChat
}
from
"@/utils/difyStreamClient"
;
import
{
queryCoupons
,
getWelcomeCoupons
,
type
Coupon
}
from
"@/utils/couponClient"
;
import
{
queryCoupons
,
getWelcomeCoupons
,
type
Coupon
}
from
"@/utils/couponClient"
;
import
{
searchMemory
,
addMemory
}
from
"@/utils/mem0Client"
;
import
{
searchMemory
,
addMemory
}
from
"@/utils/mem0Client"
;
...
@@ -95,14 +94,11 @@ export default function ChatPage() {
...
@@ -95,14 +94,11 @@ export default function ChatPage() {
const
[
isLoading
,
setIsLoading
]
=
useState
(
true
);
const
[
isLoading
,
setIsLoading
]
=
useState
(
true
);
const
[
checkoutOpen
,
setCheckoutOpen
]
=
useState
(
false
);
const
[
checkoutOpen
,
setCheckoutOpen
]
=
useState
(
false
);
const
[
useStream
,
setUseStream
]
=
useState
(
true
);
// 流式输出开关,默认开启
const
[
useStream
,
setUseStream
]
=
useState
(
true
);
// 流式输出开关,默认开启
// 引擎策略:从后台 ai_provider_configs.consumer_chat 加载(admin 可在 /admin/ai-providers 切换)
// AI 引擎模型名(从后台 ai_provider_configs.consumer_chat 加载)
// 可选值:'dify' | 'coze' | 'dify_with_coze_fallback' | 'coze_with_dify_fallback'
const
[
chatModel
,
setChatModel
]
=
useState
<
string
>
(
'agent-default'
);
const
[
chatEngineStrategy
,
setChatEngineStrategy
]
=
useState
<
string
>
(
'dify_with_coze_fallback'
);
// 实际派发用的主引擎(由 strategy 推导)
const
aiEngine
:
'dify'
|
'coze'
=
chatEngineStrategy
.
startsWith
(
'coze'
)
?
'coze'
:
'dify'
;
const
[
streamingMessageId
,
setStreamingMessageId
]
=
useState
<
string
|
null
>
(
null
);
// 当前流式消息ID
const
[
streamingMessageId
,
setStreamingMessageId
]
=
useState
<
string
|
null
>
(
null
);
// 当前流式消息ID
// 启动时从后台读取
消费者聊天引擎策略
// 启动时从后台读取
AI 模型配置
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
...
@@ -113,11 +109,11 @@ export default function ChatPage() {
...
@@ -113,11 +109,11 @@ export default function ChatPage() {
.
eq
(
'purpose'
,
'consumer_chat'
)
.
eq
(
'purpose'
,
'consumer_chat'
)
.
maybeSingle
();
.
maybeSingle
();
if
(
!
cancelled
&&
data
&&
data
.
enabled
!==
false
&&
data
.
model
)
{
if
(
!
cancelled
&&
data
&&
data
.
enabled
!==
false
&&
data
.
model
)
{
setChat
EngineStrategy
(
data
.
model
);
setChat
Model
(
data
.
model
);
console
.
log
(
'[ChatPage] 已加载后台
引擎策略
:'
,
data
.
model
);
console
.
log
(
'[ChatPage] 已加载后台
模型配置
:'
,
data
.
model
);
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
console
.
warn
(
'[ChatPage]
引擎策略加载失败,使用默认 dify_with_coze_fallback
'
,
e
);
console
.
warn
(
'[ChatPage]
模型配置加载失败,使用默认
'
,
e
);
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
...
@@ -324,14 +320,13 @@ export default function ChatPage() {
...
@@ -324,14 +320,13 @@ export default function ChatPage() {
:
''
;
:
''
;
console
.
log
(
'- 对话ID (from ref):'
,
conversationIdFromDb
||
'(new conversation)'
);
console
.
log
(
'- 对话ID (from ref):'
,
conversationIdFromDb
||
'(new conversation)'
);
// ========== Step 2:
根据开关选择调用方式 ==========
// ========== Step 2:
流式调用 AI ─────────
if
(
useStream
)
{
if
(
useStream
)
{
const
currentEngine
=
aiEngine
;
console
.
log
(
'🚀 [STREAM] 使用流式输出模式'
);
console
.
log
(
`🚀 [STREAM] 使用
${
currentEngine
}
流式输出模式`
);
const
streamMessageId
=
`ai_stream_
${
Date
.
now
()}
`
;
const
streamMessageId
=
`ai_stream_
${
Date
.
now
()}
`
;
setStreamingMessageId
(
streamMessageId
);
setStreamingMessageId
(
streamMessageId
);
// 创建初始空消息
// 创建初始空消息
const
streamMessage
:
ChatMessageType
=
{
const
streamMessage
:
ChatMessageType
=
{
id
:
streamMessageId
,
id
:
streamMessageId
,
...
@@ -345,50 +340,31 @@ export default function ChatPage() {
...
@@ -345,50 +340,31 @@ export default function ChatPage() {
agent
:
currentAgent
,
agent
:
currentAgent
,
metadata
:
{}
metadata
:
{}
};
};
setMessages
(
prev
=>
[...
prev
,
streamMessage
]);
setMessages
(
prev
=>
[...
prev
,
streamMessage
]);
let
fullContent
=
''
;
let
fullContent
=
''
;
let
finalMetadata
:
any
=
{};
let
finalMetadata
:
any
=
{};
let
streamFailed
=
false
;
const
streamCallbacks
=
{
const
streamCallbacks
=
{
onText
:
(
text
:
string
)
=>
{
onText
:
(
text
:
string
)
=>
{
fullContent
+=
text
;
fullContent
+=
text
;
setMessages
(
prev
=>
prev
.
map
(
msg
=>
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
fullContent
}
?
{
...
msg
,
content
:
fullContent
}
:
msg
:
msg
));
));
},
},
onDone
:
async
(
data
:
any
)
=>
{
onDone
:
async
(
data
:
any
)
=>
{
console
.
log
(
`✅ [STREAM/
${
currentEngine
}
] 流式响应完成:`
,
data
);
console
.
log
(
'✅ [STREAM] 流式响应完成:'
,
data
);
const
isStructuredDifyPayload
=
data
?.
meta
?.
source
===
'dify-structured'
&&
typeof
fullContent
===
'string'
&&
fullContent
.
trim
().
startsWith
(
'{'
);
if
(
isStructuredDifyPayload
)
{
const
readableParts
=
[
data
.
title
?
`为您找到:
${
data
.
title
}
`
:
''
,
Array
.
isArray
(
data
.
highlights
)
&&
data
.
highlights
.
length
>
0
?
data
.
highlights
.
join
(
'
\
n'
)
:
''
,
].
filter
(
Boolean
);
fullContent
=
readableParts
.
join
(
'
\
n
\
n'
)
||
'为您整理了以下推荐'
;
}
// 🧹 清洗:移除 AI 误把协议 JSON(picks/intent)当作正文输出的内容
// 🧹 清洗:移除 AI 误把协议 JSON(picks/intent)当作正文输出的内容
fullContent
=
fullContent
fullContent
=
fullContent
// 移除 ```json ... ``` 代码块中包含 picks/intent 的部分
.
replace
(
/```
(?:
json
)?\s
*
\{[\s\S]
*
?
"
(?:
picks|intent
)
"
[\s\S]
*
?\}\s
*```/gi
,
''
)
.
replace
(
/```
(?:
json
)?\s
*
\{[\s\S]
*
?
"
(?:
picks|intent
)
"
[\s\S]
*
?\}\s
*```/gi
,
''
)
// 移除裸 JSON(行首/列表项形式) {"picks":...} 或 {"intent":...}
.
replace
(
/^
\s
*
[
•
\-
*
]?\s
*
\{\s
*"
(?:
picks|intent
)
"
[\s\S]
*
?\}\s
*$/gim
,
''
)
.
replace
(
/^
\s
*
[
•
\-
*
]?\s
*
\{\s
*"
(?:
picks|intent
)
"
[\s\S]
*
?\}\s
*$/gim
,
''
)
.
replace
(
/
\{\s
*"
(?:
picks|intent
)
"
\s
*:
[\s\S]
*
?\}\s
*$/g
,
''
)
.
replace
(
/
\{\s
*"
(?:
picks|intent
)
"
\s
*:
[\s\S]
*
?\}\s
*$/g
,
''
)
.
trim
();
.
trim
();
// 若清洗后为空且无可推荐商品,给出友好兜底文案
if
(
!
fullContent
&&
!
(
Array
.
isArray
(
data
.
picks
)
&&
data
.
picks
.
length
>
0
))
{
if
(
!
fullContent
&&
!
(
Array
.
isArray
(
data
.
picks
)
&&
data
.
picks
.
length
>
0
))
{
fullContent
=
'抱歉,目前店铺暂无符合您需求的商品。我们主要经营手机及配件,您可以试试问"推荐一款手机"~'
;
fullContent
=
'抱歉,目前店铺暂无符合您需求的商品。我们主要经营手机及配件,您可以试试问"推荐一款手机"~'
;
}
}
...
@@ -398,9 +374,8 @@ export default function ChatPage() {
...
@@ -398,9 +374,8 @@ export default function ChatPage() {
highlights
:
data
.
highlights
||
[],
highlights
:
data
.
highlights
||
[],
comparison
:
data
.
comparison
||
undefined
comparison
:
data
.
comparison
||
undefined
};
};
if
(
Array
.
isArray
(
data
.
picks
)
&&
data
.
picks
.
length
>
0
)
{
if
(
Array
.
isArray
(
data
.
picks
)
&&
data
.
picks
.
length
>
0
)
{
const
pickedIds
=
data
.
picks
const
pickedIds
=
data
.
picks
.
map
((
p
:
any
)
=>
p
.
product_id
)
.
map
((
p
:
any
)
=>
p
.
product_id
)
.
filter
(
Boolean
);
.
filter
(
Boolean
);
...
@@ -417,7 +392,7 @@ export default function ChatPage() {
...
@@ -417,7 +392,7 @@ export default function ChatPage() {
category
:
product
.
category
||
'未分类'
,
category
:
product
.
category
||
'未分类'
,
stock_quantity
:
product
.
stock_quantity
stock_quantity
:
product
.
stock_quantity
}));
}));
fullContent
+=
`\n\n为您推荐了
${
pickedProducts
.
length
}
个商品`
;
fullContent
+=
`\n\n为您推荐了
${
pickedProducts
.
length
}
个商品`
;
}
else
{
}
else
{
console
.
warn
(
'流式 picks 已返回,但未匹配到可渲染商品:'
,
pickedIds
);
console
.
warn
(
'流式 picks 已返回,但未匹配到可渲染商品:'
,
pickedIds
);
...
@@ -437,21 +412,21 @@ export default function ChatPage() {
...
@@ -437,21 +412,21 @@ export default function ChatPage() {
}));
}));
}
}
}
}
// ✅ 保存 conversation_id:先写 ref(同步、零延迟),再持久化
(登录用 DB / 访客用 localStorage)
// ✅ 保存 conversation_id:先写 ref(同步、零延迟),再持久化
if
(
data
.
conversation_id
&&
activeConversationId
)
{
if
(
data
.
conversation_id
&&
activeConversationId
)
{
difyConvIdMapRef
.
current
[
activeConversationId
]
=
data
.
conversation_id
;
difyConvIdMapRef
.
current
[
activeConversationId
]
=
data
.
conversation_id
;
await
updateDifyConversationId
(
activeConversationId
,
data
.
conversation_id
);
await
updateDifyConversationId
(
activeConversationId
,
data
.
conversation_id
);
}
}
finalMetadata
=
responseMetadata
;
finalMetadata
=
responseMetadata
;
setMessages
(
prev
=>
prev
.
map
(
msg
=>
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
fullContent
,
type
:
messageType
,
metadata
:
responseMetadata
}
?
{
...
msg
,
content
:
fullContent
,
type
:
messageType
,
metadata
:
responseMetadata
}
:
msg
:
msg
));
));
if
(
activeConversationId
)
{
if
(
activeConversationId
)
{
await
saveMessageLocal
({
await
saveMessageLocal
({
id
:
streamMessageId
,
id
:
streamMessageId
,
...
@@ -466,22 +441,25 @@ export default function ChatPage() {
...
@@ -466,22 +441,25 @@ export default function ChatPage() {
metadata
:
responseMetadata
metadata
:
responseMetadata
},
activeConversationId
);
},
activeConversationId
);
}
}
addMemory
(
userId
,
userMessage
,
fullContent
);
addMemory
(
userId
,
userMessage
,
fullContent
);
setStreamingMessageId
(
null
);
setStreamingMessageId
(
null
);
setIsAiTyping
(
false
);
setIsAiTyping
(
false
);
},
},
onError
:
(
error
:
string
)
=>
{
onError
:
(
error
:
string
)
=>
{
console
.
error
(
`❌ [STREAM/
${
currentEngine
}
] 错误:`
,
error
);
console
.
error
(
'❌ [STREAM] 错误:'
,
error
);
streamFailed
=
true
;
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
'抱歉,AI助手暂时无法回复,请稍后再试。'
}
:
msg
));
setStreamingMessageId
(
null
);
setIsAiTyping
(
false
);
}
}
};
};
// 选择引擎:Dify 优先,失败回退 Coze
await
streamDifyChat
(
const
streamFn
=
currentEngine
===
'dify'
?
streamDifyChat
:
streamCozeChat
;
await
streamFn
(
{
{
message
:
userMessage
,
message
:
userMessage
,
userId
:
user
?.
id
||
'anonymous'
,
userId
:
user
?.
id
||
'anonymous'
,
...
@@ -491,61 +469,13 @@ export default function ChatPage() {
...
@@ -491,61 +469,13 @@ export default function ChatPage() {
},
},
streamCallbacks
streamCallbacks
);
);
// Dify 失败时回退到 Coze
if
(
streamFailed
&&
currentEngine
===
'dify'
)
{
console
.
log
(
'⚠️ [FALLBACK] Dify 失败,回退到 Coze...'
);
fullContent
=
''
;
streamFailed
=
false
;
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
'正在切换到备用引擎...'
}
:
msg
));
await
streamCozeChat
(
{
message
:
userMessage
,
userId
:
user
?.
id
||
'anonymous'
,
conversationId
:
conversationIdFromDb
,
userContext
:
userContext
,
catalog
},
streamCallbacks
);
if
(
streamFailed
)
{
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
'抱歉,AI助手暂时无法回复,请稍后再试。'
}
:
msg
));
setStreamingMessageId
(
null
);
setIsAiTyping
(
false
);
}
}
else
if
(
streamFailed
)
{
// Coze 也失败了
setMessages
(
prev
=>
prev
.
map
(
msg
=>
msg
.
id
===
streamMessageId
?
{
...
msg
,
content
:
'抱歉,AI助手暂时无法回复,请稍后再试。'
}
:
msg
));
setStreamingMessageId
(
null
);
setIsAiTyping
(
false
);
}
return
;
return
;
}
}
// ========== 非流式模式(Coze 非流式)==========
// ========== 非流式模式 ─────────
console
.
log
(
'📤 [三明治] Step 2: 调用 coze-chat (非流式)'
);
console
.
log
(
'📤 Step 2: 非流式调用'
);
console
.
log
(
'- 用户消息:'
,
userMessage
);
const
{
data
:
intentCard
,
traceId
,
status
,
error
:
callError
}
=
await
callCozeEdge
({
console
.
log
(
'- 产品数量:'
,
currentProducts
.
length
);
console
.
log
(
'- Mem0 上下文:'
,
userContext
?
'有'
:
'无'
);
console
.
log
(
'- AI引擎: Coze (非流式回退)'
);
const
apiCall
=
callCozeEdge
;
const
{
data
:
intentCard
,
traceId
,
status
,
error
:
callError
}
=
await
apiCall
({
message
:
userMessage
,
message
:
userMessage
,
userId
:
user
?.
id
||
'anonymous'
,
userId
:
user
?.
id
||
'anonymous'
,
conversationId
:
conversationIdFromDb
,
conversationId
:
conversationIdFromDb
,
...
@@ -558,15 +488,15 @@ export default function ChatPage() {
...
@@ -558,15 +488,15 @@ export default function ChatPage() {
},
},
catalog
catalog
});
});
console
.
log
(
'📥 响应结果'
);
console
.
log
(
'📥 响应结果'
);
console
.
log
(
'- AI引擎:
Coze
'
);
console
.
log
(
'- AI引擎:
ai-hub
'
);
console
.
log
(
'- trace_id:'
,
traceId
);
console
.
log
(
'- trace_id:'
,
traceId
);
console
.
log
(
'- status:'
,
status
);
console
.
log
(
'- status:'
,
status
);
console
.
log
(
'- 原始 intentCard:'
,
JSON
.
stringify
(
intentCard
,
null
,
2
));
console
.
log
(
'- 原始 intentCard:'
,
JSON
.
stringify
(
intentCard
,
null
,
2
));
if
(
callError
||
!
intentCard
)
{
if
(
callError
||
!
intentCard
)
{
console
.
error
(
'
Coze
API调用失败:'
,
callError
);
console
.
error
(
'
AI
API调用失败:'
,
callError
);
const
errorMsg
=
callError
||
`系统繁忙,请稍后再试(追踪码:
${
traceId
}
)`
;
const
errorMsg
=
callError
||
`系统繁忙,请稍后再试(追踪码:
${
traceId
}
)`
;
toast
.
error
(
"AI回复失败"
,
{
toast
.
error
(
"AI回复失败"
,
{
description
:
errorMsg
,
description
:
errorMsg
,
...
@@ -952,7 +882,7 @@ export default function ChatPage() {
...
@@ -952,7 +882,7 @@ export default function ChatPage() {
}
finally
{
}
finally
{
setIsAiTyping
(
false
);
setIsAiTyping
(
false
);
}
}
},
[
currentAgent
,
products
,
orders
,
fetchProducts
,
fetchOrders
,
activeConversationId
,
saveMessageLocal
,
user
,
chat
EngineStrategy
,
useStream
]);
},
[
currentAgent
,
products
,
orders
,
fetchProducts
,
fetchOrders
,
activeConversationId
,
saveMessageLocal
,
user
,
chat
Model
,
useStream
]);
// 处理发送消息
// 处理发送消息
const
handleSendMessage
=
useCallback
(
async
(
content
:
string
)
=>
{
const
handleSendMessage
=
useCallback
(
async
(
content
:
string
)
=>
{
...
...
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
/**
/**
* Consumer Chat Edge Function Client
* Consumer Chat Edge Function Client
* C端消费者聊天 - 统一调用入口
* C端消费者聊天 —— 直接大模型调用(AI Hub)
*
* 注意:callCozeEdge 是历史兼容名称;当前优先走 dify-chat-stream。
*/
*/
import
{
supabase
}
from
'@/integrations/supabase/client'
;
import
{
supabase
}
from
'@/integrations/supabase/client'
;
...
@@ -59,7 +57,11 @@ type StreamDonePayload = Partial<CozeResponse> & {
...
@@ -59,7 +57,11 @@ type StreamDonePayload = Partial<CozeResponse> & {
type
?:
'done'
;
type
?:
'done'
;
};
};
function
processDifyStreamLine
(
line
:
string
,
state
:
{
fullContent
:
string
;
doneData
:
StreamDonePayload
|
null
})
{
function
processStreamLine
(
line
:
string
,
state
:
{
fullContent
:
string
;
doneData
:
StreamDonePayload
|
null
},
onText
?:
(
text
:
string
)
=>
void
)
{
const
trimmedLine
=
line
.
trim
();
const
trimmedLine
=
line
.
trim
();
if
(
!
trimmedLine
||
trimmedLine
.
startsWith
(
':'
)
||
trimmedLine
===
'data: [DONE]'
)
return
;
if
(
!
trimmedLine
||
trimmedLine
.
startsWith
(
':'
)
||
trimmedLine
===
'data: [DONE]'
)
return
;
if
(
!
trimmedLine
.
startsWith
(
'data:'
))
return
;
if
(
!
trimmedLine
.
startsWith
(
'data:'
))
return
;
...
@@ -68,29 +70,37 @@ function processDifyStreamLine(line: string, state: { fullContent: string; doneD
...
@@ -68,29 +70,37 @@ function processDifyStreamLine(line: string, state: { fullContent: string; doneD
try
{
try
{
const
event
=
JSON
.
parse
(
jsonStr
);
const
event
=
JSON
.
parse
(
jsonStr
);
if
(
event
.
type
===
'text'
)
{
if
(
event
.
type
===
'text'
)
{
state
.
fullContent
+=
event
.
content
||
''
;
const
content
=
event
.
content
||
''
;
state
.
fullContent
+=
content
;
onText
?.(
content
);
}
else
if
(
event
.
type
===
'done'
)
{
}
else
if
(
event
.
type
===
'done'
)
{
state
.
doneData
=
event
;
state
.
doneData
=
event
;
}
else
if
(
event
.
type
===
'error'
)
{
}
else
if
(
event
.
type
===
'error'
)
{
throw
new
Error
(
event
.
message
||
'
Dify
流式响应失败'
);
throw
new
Error
(
event
.
message
||
'
AI
流式响应失败'
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
error
instanceof
Error
&&
!
jsonStr
.
startsWith
(
'{'
))
{
// JSON.parse 抛出 SyntaxError,应静默跳过该行
state
.
fullContent
+=
jsonStr
;
// 只有业务层主动抛出的 Error 才向上传播(如 type==='error' 事件)
if
(
error
instanceof
SyntaxError
)
{
if
(
jsonStr
&&
!
jsonStr
.
startsWith
(
'{'
))
{
state
.
fullContent
+=
jsonStr
;
}
return
;
return
;
}
}
throw
error
;
throw
error
;
}
}
}
}
export
async
function
callCozeEdge
(
options
:
CozeRequestOptions
):
Promise
<
{
export
async
function
callCozeEdge
(
options
:
CozeRequestOptions
&
{
onText
?:
(
text
:
string
)
=>
void
}
):
Promise
<
{
data
:
CozeResponse
|
null
;
data
:
CozeResponse
|
null
;
traceId
:
string
;
traceId
:
string
;
status
:
number
;
status
:
number
;
error
?:
string
;
error
?:
string
;
}
>
{
}
>
{
const
traceId
=
crypto
.
randomUUID
();
const
traceId
=
crypto
.
randomUUID
();
const
requestBody
=
{
const
requestBody
=
{
message
:
options
.
message
,
message
:
options
.
message
,
conversationId
:
options
.
conversationId
||
''
,
conversationId
:
options
.
conversationId
||
''
,
...
@@ -102,115 +112,106 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
...
@@ -102,115 +112,106 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
catalog
:
options
.
catalog
||
[]
catalog
:
options
.
catalog
||
[]
}
}
};
};
try
{
try
{
console
.
log
(
'🚀 [CHAT CLIENT]
开始调用 dify
-chat-stream'
);
console
.
log
(
'🚀 [CHAT CLIENT]
调用 ai
-chat-stream'
);
console
.
log
(
'📤
请求体:'
,
JSON
.
stringify
(
requestBody
,
null
,
2
));
console
.
log
(
'📤
消息:'
,
options
.
message
?.
substring
(
0
,
60
));
const
{
data
:
{
session
}
}
=
await
supabase
.
auth
.
getSession
();
const
{
data
:
{
session
}
}
=
await
supabase
.
auth
.
getSession
();
console
.
log
(
'🔐 用户登录状态:'
,
session
?
'已登录'
:
'未登录'
);
let
lastError
:
any
=
null
;
let
data
:
any
=
null
;
let
error
:
any
=
null
;
// 重试逻辑
// 单次调用 + 超时保护(65秒)
for
(
let
attempt
=
1
;
attempt
<=
2
;
attempt
++
)
{
const
invokePromise
=
fetch
(
`
${
SUPABASE_URL
}
/functions/v1/dify-chat-stream`
,
{
console
.
log
(
`🔄 尝试
${
attempt
}
/2...`
);
method
:
'POST'
,
headers
:
{
try
{
'Content-Type'
:
'application/json'
,
const
invokePromise
=
fetch
(
`
${
SUPABASE_URL
}
/functions/v1/dify-chat-stream`
,
{
'Authorization'
:
`Bearer
${
session
?.
access_token
||
SUPABASE_ANON_KEY
}
`,
method
:
'POST'
,
'X-Trace-Id': traceId,
headers
:
{
'X-User-Id': options.userId || 'guest'
'Content-Type'
:
'application/json'
,
},
'Authorization'
:
`Bearer
${
session
?.
access_token
||
SUPABASE_ANON_KEY
}
`,
body: JSON.stringify(requestBody)
'X-Trace-Id': traceId,
});
'X-User-Id': options.userId || 'guest'
},
body: JSON.stringify(requestBody)
});
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('AI响应超时,请重新发送消息')), 65000);
});
const response = await Promise.race([invokePromise, timeoutPromise]);
if (!response.ok) {
throw new Error(`
Dify
请求失败
(
$
{
response
.
status
}):
$
{
await
response
.
text
()}
`
);
}
if
(
!
response
.
body
)
{
throw
new
Error
(
'响应体为空'
);
}
const
reader
=
response
.
body
.
getReader
();
const timeoutPromise = new Promise<never>((_, reject) => {
const
decoder
=
new
TextDecoder
();
setTimeout(() => reject(new Error('AI响应超时,请重新发送消息')), 65000);
const
state
=
{
fullContent
:
''
,
doneData
:
null
as
StreamDonePayload
|
null
};
});
let
buffer
=
''
;
while
(
true
)
{
const response = await Promise.race([invokePromise, timeoutPromise]);
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
{
if
(
buffer
.
trim
())
processDifyStreamLine
(
buffer
,
state
);
break
;
}
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
if (!response.ok) {
const
lines
=
buffer
.
split
(
'
\
n'
);
const errorText = await response.text(
);
buffer
=
lines
.
pop
()
||
''
;
console.error('❌ [CHAT CLIENT] 请求失败:', response.status, errorText)
;
for
(
const
line
of
lines
)
processDifyStreamLine
(
line
,
state
);
throw new Error(`
请求失败
(
$
{
response
.
status
}):
$
{
errorText
.
substring
(
0
,
200
)}
`
);
}
}
data
=
{
if
(
!
response
.
body
)
{
...(
state
.
doneData
||
{}),
throw
new
Error
(
'响应体为空'
);
title
:
state
.
doneData
?.
title
||
options
.
message
,
}
answer
:
state
.
fullContent
,
};
// ─── 读取 SSE 流 ───
error
=
null
;
const
reader
=
response
.
body
.
getReader
();
lastError
=
null
;
const
decoder
=
new
TextDecoder
();
const
onText
=
options
.
onText
;
const
state
=
{
fullContent
:
''
,
doneData
:
null
as
StreamDonePayload
|
null
};
let
buffer
=
''
;
// rAF 批量包装 onText:每帧最多触发一次,确保浏览器在帧之间有机会绘制
// 根因:await setTimeout(0) 从 Promise 延续中创建不触发 4ms 嵌套惩罚,
// 全部回调在 <1ms 内触发完毕,浏览器只在 VSync 边界绘制一次 → 用户看不到流式效果
let
pendingText
=
''
;
let
rafId
:
number
|
null
=
null
;
const
batchedOnText
=
onText
?
(
text
:
string
)
=>
{
pendingText
+=
text
;
if
(
rafId
===
null
)
{
rafId
=
requestAnimationFrame
(()
=>
{
if
(
pendingText
)
{
onText
(
pendingText
);
pendingText
=
''
;
}
rafId
=
null
;
});
}
}
:
undefined
;
if
(
data
)
{
try
{
while
(
true
)
{
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
{
if
(
buffer
.
trim
())
processStreamLine
(
buffer
,
state
,
batchedOnText
);
break
;
break
;
}
}
lastError
=
error
;
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
lines
=
buffer
.
split
(
'
\
n'
);
if
(
attempt
===
1
)
{
buffer
=
lines
.
pop
()
||
''
;
console
.
log
(
'⏳ Edge Function 可能正在启动,2秒后重试...'
);
for
(
const
line
of
lines
)
{
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
2000
));
processStreamLine
(
line
,
state
,
batchedOnText
);
}
}
catch
(
e
)
{
lastError
=
e
;
console
.
error
(
`❌ 尝试
${
attempt
}
失败:`
,
e
);
if
(
attempt
===
1
)
{
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
2000
));
}
}
}
}
}
}
finally
{
// 确保 rAF 回调被取消、剩余文本被刷新,即使 processStreamLine 抛出异常也不会泄漏
error
=
error
||
lastError
;
if
(
rafId
!==
null
)
{
cancelAnimationFrame
(
rafId
);
if
(
error
)
{
rafId
=
null
;
console
.
error
(
'❌ Dify Chat Functions 错误:'
,
error
);
const
errorMessage
=
error
.
message
||
String
(
error
);
let
friendlyMessage
=
'抱歉,系统暂时繁忙'
;
if
(
errorMessage
.
includes
(
'Failed to fetch'
)
||
errorMessage
.
includes
(
'NetworkError'
))
{
friendlyMessage
=
'网络连接不稳定,请重新发送消息试试'
;
}
else
if
(
errorMessage
.
includes
(
'timeout'
)
||
errorMessage
.
includes
(
'超时'
))
{
friendlyMessage
=
'AI响应超时(65秒),请重新发送消息'
;
}
}
if
(
pendingText
&&
onText
)
{
onText
(
pendingText
);
pendingText
=
''
;
}
}
const
rawData
=
{
...(
state
.
doneData
||
{}),
answer
:
state
.
fullContent
};
if
(
rawData
.
error
)
{
return
{
return
{
data
:
null
,
data
:
null
,
traceId
,
traceId
:
rawData
.
meta
?.
probe_id
||
traceId
,
status
:
0
,
status
:
20
0
,
error
:
friendlyMessage
error
:
`系统繁忙,请稍后再试(追踪码:
${
rawData
.
meta
?.
probe_id
||
traceId
}
)
`
};
};
}
}
if
(
!
data
)
{
if (!
state.doneData && !state.fullContent
) {
return {
return {
data: null,
data: null,
traceId,
traceId,
...
@@ -218,20 +219,9 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
...
@@ -218,20 +219,9 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
error: '响应数据为空'
error: '响应数据为空'
};
};
}
}
console
.
log
(
'✅ 成功获取 Dify 响应'
);
console.log('✅ [CHAT CLIENT] 成功获取响应, picks:', rawData.picks?.length || 0);
const
rawData
=
data
as
any
;
if
(
rawData
.
error
)
{
return
{
data
:
null
,
traceId
:
rawData
.
meta
?.
probe_id
||
traceId
,
status
:
200
,
error
:
`系统繁忙,请稍后再试(追踪码:
${
rawData
.
meta
?.
probe_id
||
traceId
}
)
`
};
}
const cozeResponse: CozeResponse = {
const cozeResponse: CozeResponse = {
title: rawData.title || options.message,
title: rawData.title || options.message,
intent: rawData.intent || "GENERAL",
intent: rawData.intent || "GENERAL",
...
@@ -241,7 +231,7 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
...
@@ -241,7 +231,7 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
comparison: rawData.comparison,
comparison: rawData.comparison,
next: rawData.next || [],
next: rawData.next || [],
meta: {
meta: {
platform: rawData.meta?.platform || '
dify-stream
',
platform: rawData.meta?.platform || '
ai-hub
',
user: rawData.meta?.user || options.userId,
user: rawData.meta?.user || options.userId,
catalog_count: rawData.meta?.catalog_count ?? 0,
catalog_count: rawData.meta?.catalog_count ?? 0,
probe_id: rawData.meta?.probe_id || traceId
probe_id: rawData.meta?.probe_id || traceId
...
@@ -249,27 +239,32 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
...
@@ -249,27 +239,32 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
conversation_id: rawData.conversation_id,
conversation_id: rawData.conversation_id,
answer: rawData.answer
answer: rawData.answer
};
};
console.log('📊 Dify 响应:', {
return {
platform: cozeResponse.meta.platform,
data: cozeResponse,
picks: cozeResponse.picks.length,
traceId: cozeResponse.meta.probe_id,
conversation_id: cozeResponse.conversation_id || '(new)'
});
return {
data: cozeResponse,
traceId: cozeResponse.meta.probe_id,
status: 200
status: 200
};
};
} catch (error) {
} catch (error) {
console.error('❌ 调用 Coze Edge Function 失败:', error);
console.error('❌ [CHAT CLIENT] 调用失败:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
let friendlyMessage = '抱歉,系统暂时繁忙';
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) {
friendlyMessage = '网络连接不稳定,请重新发送消息试试';
} else if (errorMessage.includes('timeout') || errorMessage.includes('超时')) {
friendlyMessage = 'AI响应超时,请重新发送消息';
} else if (errorMessage.includes('AI_HUB') || errorMessage.includes('LOVABLE')) {
friendlyMessage = 'AI服务未配置,请联系管理员';
}
return {
return {
data: null,
data: null,
traceId,
traceId,
status: 0,
status: 0,
error:
'网络连接不稳定,请重新发送消息试试'
error:
friendlyMessage
};
};
}
}
}
}
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