Commit 99f78a49 authored by AI-甘富林's avatar AI-甘富林

前端代码修复

parent 77a77a8f
......@@ -37,7 +37,6 @@ import { AIAgent, AI_AGENTS } from "@/types/agents";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/contexts/AuthContext";
import { callCozeEdge } from "@/utils/cozeClient";
import { streamCozeChat } from "@/utils/cozeStreamClient";
import { streamDifyChat } from "@/utils/difyStreamClient";
import { queryCoupons, getWelcomeCoupons, type Coupon } from "@/utils/couponClient";
import { searchMemory, addMemory } from "@/utils/mem0Client";
......@@ -95,14 +94,11 @@ export default function ChatPage() {
const [isLoading, setIsLoading] = useState(true);
const [checkoutOpen, setCheckoutOpen] = useState(false);
const [useStream, setUseStream] = useState(true); // 流式输出开关,默认开启
// 引擎策略:从后台 ai_provider_configs.consumer_chat 加载(admin 可在 /admin/ai-providers 切换)
// 可选值:'dify' | 'coze' | 'dify_with_coze_fallback' | 'coze_with_dify_fallback'
const [chatEngineStrategy, setChatEngineStrategy] = useState<string>('dify_with_coze_fallback');
// 实际派发用的主引擎(由 strategy 推导)
const aiEngine: 'dify' | 'coze' = chatEngineStrategy.startsWith('coze') ? 'coze' : 'dify';
// AI 引擎模型名(从后台 ai_provider_configs.consumer_chat 加载)
const [chatModel, setChatModel] = useState<string>('agent-default');
const [streamingMessageId, setStreamingMessageId] = useState<string | null>(null); // 当前流式消息ID
// 启动时从后台读取消费者聊天引擎策略
// 启动时从后台读取 AI 模型配置
useEffect(() => {
let cancelled = false;
(async () => {
......@@ -113,11 +109,11 @@ export default function ChatPage() {
.eq('purpose', 'consumer_chat')
.maybeSingle();
if (!cancelled && data && data.enabled !== false && data.model) {
setChatEngineStrategy(data.model);
console.log('[ChatPage] 已加载后台引擎策略:', data.model);
setChatModel(data.model);
console.log('[ChatPage] 已加载后台模型配置:', data.model);
}
} catch (e) {
console.warn('[ChatPage] 引擎策略加载失败,使用默认 dify_with_coze_fallback', e);
console.warn('[ChatPage] 模型配置加载失败,使用默认', e);
}
})();
return () => { cancelled = true; };
......@@ -324,14 +320,13 @@ export default function ChatPage() {
: '';
console.log('- 对话ID (from ref):', conversationIdFromDb || '(new conversation)');
// ========== Step 2: 根据开关选择调用方式 ==========
// ========== Step 2: 流式调用 AI ─────────
if (useStream) {
const currentEngine = aiEngine;
console.log(`🚀 [STREAM] 使用 ${currentEngine} 流式输出模式`);
console.log('🚀 [STREAM] 使用流式输出模式');
const streamMessageId = `ai_stream_${Date.now()}`;
setStreamingMessageId(streamMessageId);
// 创建初始空消息
const streamMessage: ChatMessageType = {
id: streamMessageId,
......@@ -345,50 +340,31 @@ export default function ChatPage() {
agent: currentAgent,
metadata: {}
};
setMessages(prev => [...prev, streamMessage]);
let fullContent = '';
let finalMetadata: any = {};
let streamFailed = false;
const streamCallbacks = {
onText: (text: string) => {
fullContent += text;
setMessages(prev => prev.map(msg =>
msg.id === streamMessageId
setMessages(prev => prev.map(msg =>
msg.id === streamMessageId
? { ...msg, content: fullContent }
: msg
));
},
onDone: async (data: any) => {
console.log(`✅ [STREAM/${currentEngine}] 流式响应完成:`, 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') || '为您整理了以下推荐';
}
console.log('✅ [STREAM] 流式响应完成:', data);
// 🧹 清洗:移除 AI 误把协议 JSON(picks/intent)当作正文输出的内容
fullContent = fullContent
// 移除 ```json ... ``` 代码块中包含 picks/intent 的部分
.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*"(?:picks|intent)"\s*:[\s\S]*?\}\s*$/g, '')
.trim();
// 若清洗后为空且无可推荐商品,给出友好兜底文案
if (!fullContent && !(Array.isArray(data.picks) && data.picks.length > 0)) {
fullContent = '抱歉,目前店铺暂无符合您需求的商品。我们主要经营手机及配件,您可以试试问"推荐一款手机"~';
}
......@@ -398,9 +374,8 @@ export default function ChatPage() {
highlights: data.highlights || [],
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
.map((p: any) => p.product_id)
.filter(Boolean);
......@@ -417,7 +392,7 @@ export default function ChatPage() {
category: product.category || '未分类',
stock_quantity: product.stock_quantity
}));
fullContent += `\n\n为您推荐了 ${pickedProducts.length} 个商品`;
} else {
console.warn('流式 picks 已返回,但未匹配到可渲染商品:', pickedIds);
......@@ -437,21 +412,21 @@ export default function ChatPage() {
}));
}
}
// ✅ 保存 conversation_id:先写 ref(同步、零延迟),再持久化(登录用 DB / 访客用 localStorage)
// ✅ 保存 conversation_id:先写 ref(同步、零延迟),再持久化
if (data.conversation_id && activeConversationId) {
difyConvIdMapRef.current[activeConversationId] = data.conversation_id;
await updateDifyConversationId(activeConversationId, data.conversation_id);
}
finalMetadata = responseMetadata;
setMessages(prev => prev.map(msg =>
msg.id === streamMessageId
setMessages(prev => prev.map(msg =>
msg.id === streamMessageId
? { ...msg, content: fullContent, type: messageType, metadata: responseMetadata }
: msg
));
if (activeConversationId) {
await saveMessageLocal({
id: streamMessageId,
......@@ -466,22 +441,25 @@ export default function ChatPage() {
metadata: responseMetadata
}, activeConversationId);
}
addMemory(userId, userMessage, fullContent);
setStreamingMessageId(null);
setIsAiTyping(false);
},
onError: (error: string) => {
console.error(`❌ [STREAM/${currentEngine}] 错误:`, error);
streamFailed = true;
console.error('❌ [STREAM] 错误:', error);
setMessages(prev => prev.map(msg =>
msg.id === streamMessageId
? { ...msg, content: '抱歉,AI助手暂时无法回复,请稍后再试。' }
: msg
));
setStreamingMessageId(null);
setIsAiTyping(false);
}
};
// 选择引擎:Dify 优先,失败回退 Coze
const streamFn = currentEngine === 'dify' ? streamDifyChat : streamCozeChat;
await streamFn(
await streamDifyChat(
{
message: userMessage,
userId: user?.id || 'anonymous',
......@@ -491,61 +469,13 @@ export default function ChatPage() {
},
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;
}
// ========== 非流式模式(Coze 非流式)==========
console.log('📤 [三明治] Step 2: 调用 coze-chat (非流式)');
console.log('- 用户消息:', userMessage);
console.log('- 产品数量:', currentProducts.length);
console.log('- Mem0 上下文:', userContext ? '有' : '无');
console.log('- AI引擎: Coze (非流式回退)');
const apiCall = callCozeEdge;
const { data: intentCard, traceId, status, error: callError } = await apiCall({
// ========== 非流式模式 ─────────
console.log('📤 Step 2: 非流式调用');
const { data: intentCard, traceId, status, error: callError } = await callCozeEdge({
message: userMessage,
userId: user?.id || 'anonymous',
conversationId: conversationIdFromDb,
......@@ -558,15 +488,15 @@ export default function ChatPage() {
},
catalog
});
console.log('📥 响应结果');
console.log('- AI引擎: Coze');
console.log('- AI引擎: ai-hub');
console.log('- trace_id:', traceId);
console.log('- status:', status);
console.log('- 原始 intentCard:', JSON.stringify(intentCard, null, 2));
if (callError || !intentCard) {
console.error('Coze API调用失败:', callError);
console.error('AI API调用失败:', callError);
const errorMsg = callError || `系统繁忙,请稍后再试(追踪码:${traceId})`;
toast.error("AI回复失败", {
description: errorMsg,
......@@ -952,7 +882,7 @@ export default function ChatPage() {
} finally {
setIsAiTyping(false);
}
}, [currentAgent, products, orders, fetchProducts, fetchOrders, activeConversationId, saveMessageLocal, user, chatEngineStrategy, useStream]);
}, [currentAgent, products, orders, fetchProducts, fetchOrders, activeConversationId, saveMessageLocal, user, chatModel, useStream]);
// 处理发送消息
const handleSendMessage = useCallback(async (content: string) => {
......
import { useState, useEffect, useCallback, useRef } from "react";
import { flushSync } from "react-dom";
import { supabase } from "@/integrations/supabase/client";
import { ProductCard } from "@/components/chat/ProductCard";
import { Button } from "@/components/ui/button";
......@@ -305,16 +306,32 @@ export default function ShopPage() {
// AI response handler
const simulateAIResponse = useCallback(async (userMessage: string) => {
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 {
let currentProducts = products;
if (currentProducts.length === 0) {
currentProducts = await fetchProducts();
}
if (currentProducts.length === 0) {
throw new Error('产品数据加载失败');
}
const catalog = currentProducts.map(p => ({
id: p.id,
name: p.name,
......@@ -323,7 +340,9 @@ export default function ShopPage() {
category: p.category,
description: p.description
}));
// ─── 流式接收 AI 回复文字 ───
let fullContent = '';
const { data: intentCard } = await callCozeEdge({
message: userMessage,
userId: user?.id || 'anonymous',
......@@ -333,21 +352,38 @@ export default function ShopPage() {
description: currentProducts[0].description || '',
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('系统繁忙,请稍后再试');
let aiContent = intentCard.title || '收到您的消息';
if (intentCard.highlights && intentCard.highlights.length > 0) {
aiContent += '\n\n' + intentCard.highlights.map(h => `• ${h}`).join('\n');
// 清洗 JSON 代码块,避免裸露协议数据出现在正文
let aiContent = fullContent
.replace(/```(?:json)?\s*\{[\s\S]*?"(?:picks|intent)"[\s\S]*?\}\s*```/gi, '')
.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) {
aiContent += '\n\n为您推荐了 ' + intentCard.picks.length + ' 个商品';
}
let messageType: ChatMessageType['type'] = "text";
let responseMetadata: ChatMessageType['metadata'] = {};
......@@ -370,50 +406,39 @@ export default function ShopPage() {
}))
};
}
const aiMessage: ChatMessageType = {
id: `ai_${Date.now()}`,
content: aiContent,
sender: "ai",
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
type: messageType,
agent: currentAgent,
metadata: responseMetadata
};
setMessages(prev => [...prev, aiMessage]);
// 更新占位消息为最终内容 + 类型 + 元数据
setMessages(prev => prev.map(msg =>
msg.id === streamMsgId
? { ...msg, content: aiContent, type: messageType, metadata: responseMetadata }
: msg
));
if (activeConversationId) {
await saveMessage({
content: aiMessage.content,
sender_type: aiMessage.sender,
message_type: aiMessage.type as DBMessage['message_type'],
metadata: aiMessage.metadata
content: aiContent,
sender_type: "ai",
message_type: messageType as DBMessage['message_type'],
metadata: responseMetadata
}, activeConversationId);
}
} catch (error) {
console.error('AI回复失败:', error);
const errorMessage: ChatMessageType = {
id: `ai_error_${Date.now()}`,
content: `抱歉,遇到了技术问题,请稍后再试。`,
sender: "ai",
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
type: "text",
agent: currentAgent
};
setMessages(prev => [...prev, errorMessage]);
// 把占位消息替换为错误消息
setMessages(prev => prev.map(msg =>
msg.id === streamMsgId
? { ...msg, content: '抱歉,遇到了技术问题,请稍后再试。' }
: msg
));
} finally {
setIsAiTyping(false);
}
}, [currentAgent, products, activeConversationId, saveMessage, user]);
const handleSendMessage = useCallback(async (content: string) => {
// 防止并发流式调用(如快速连点建议按钮)
if (isAiTyping) return;
const userMessage: ChatMessageType = {
id: `user_${Date.now()}`,
content,
......@@ -437,7 +462,7 @@ export default function ShopPage() {
}
await simulateAIResponse(content);
}, [simulateAIResponse, activeConversationId, saveMessage]);
}, [simulateAIResponse, activeConversationId, saveMessage, isAiTyping]);
return (
<div className="min-h-screen bg-background">
......
/**
* Consumer Chat Edge Function Client
* C端消费者聊天 - 统一调用入口
*
* 注意:callCozeEdge 是历史兼容名称;当前优先走 dify-chat-stream。
* C端消费者聊天 —— 直接大模型调用(AI Hub)
*/
import { supabase } from '@/integrations/supabase/client';
......@@ -59,7 +57,11 @@ type StreamDonePayload = Partial<CozeResponse> & {
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();
if (!trimmedLine || trimmedLine.startsWith(':') || trimmedLine === 'data: [DONE]') return;
if (!trimmedLine.startsWith('data:')) return;
......@@ -68,29 +70,37 @@ function processDifyStreamLine(line: string, state: { fullContent: string; doneD
try {
const event = JSON.parse(jsonStr);
if (event.type === 'text') {
state.fullContent += event.content || '';
const content = event.content || '';
state.fullContent += content;
onText?.(content);
} else if (event.type === 'done') {
state.doneData = event;
} else if (event.type === 'error') {
throw new Error(event.message || 'Dify 流式响应失败');
throw new Error(event.message || 'AI 流式响应失败');
}
} catch (error) {
if (error instanceof Error && !jsonStr.startsWith('{')) {
state.fullContent += jsonStr;
// JSON.parse 抛出 SyntaxError,应静默跳过该行
// 只有业务层主动抛出的 Error 才向上传播(如 type==='error' 事件)
if (error instanceof SyntaxError) {
if (jsonStr && !jsonStr.startsWith('{')) {
state.fullContent += jsonStr;
}
return;
}
throw error;
}
}
export async function callCozeEdge(options: CozeRequestOptions): Promise<{
export async function callCozeEdge(
options: CozeRequestOptions & { onText?: (text: string) => void }
): Promise<{
data: CozeResponse | null;
traceId: string;
status: number;
error?: string;
}> {
const traceId = crypto.randomUUID();
const requestBody = {
message: options.message,
conversationId: options.conversationId || '',
......@@ -102,115 +112,106 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
catalog: options.catalog || []
}
};
try {
console.log('🚀 [CHAT CLIENT] 开始调用 dify-chat-stream');
console.log('📤 请求体:', JSON.stringify(requestBody, null, 2));
console.log('🚀 [CHAT CLIENT] 调用 ai-chat-stream');
console.log('📤 消息:', options.message?.substring(0, 60));
const { data: { session } } = await supabase.auth.getSession();
console.log('🔐 用户登录状态:', session ? '已登录' : '未登录');
let lastError: any = null;
let data: any = null;
let error: any = null;
// 重试逻辑
for (let attempt = 1; attempt <= 2; attempt++) {
console.log(`🔄 尝试 ${attempt}/2...`);
try {
const invokePromise = fetch(`${SUPABASE_URL}/functions/v1/dify-chat-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session?.access_token || SUPABASE_ANON_KEY}`,
'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('响应体为空');
}
// 单次调用 + 超时保护(65秒)
const invokePromise = fetch(`${SUPABASE_URL}/functions/v1/dify-chat-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session?.access_token || SUPABASE_ANON_KEY}`,
'X-Trace-Id': traceId,
'X-User-Id': options.userId || 'guest'
},
body: JSON.stringify(requestBody)
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
const state = { fullContent: '', doneData: null as StreamDonePayload | null };
let buffer = '';
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('AI响应超时,请重新发送消息')), 65000);
});
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) processDifyStreamLine(buffer, state);
break;
}
const response = await Promise.race([invokePromise, timeoutPromise]);
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) processDifyStreamLine(line, state);
}
if (!response.ok) {
const errorText = await response.text();
console.error('❌ [CHAT CLIENT] 请求失败:', response.status, errorText);
throw new Error(`请求失败 (${response.status}): ${errorText.substring(0, 200)}`);
}
data = {
...(state.doneData || {}),
title: state.doneData?.title || options.message,
answer: state.fullContent,
};
error = null;
lastError = null;
if (!response.body) {
throw new Error('响应体为空');
}
// ─── 读取 SSE 流 ───
const reader = response.body.getReader();
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;
}
lastError = error;
if (attempt === 1) {
console.log('⏳ Edge Function 可能正在启动,2秒后重试...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (e) {
lastError = e;
console.error(`❌ 尝试 ${attempt} 失败:`, e);
if (attempt === 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
processStreamLine(line, state, batchedOnText);
}
}
}
error = error || lastError;
if (error) {
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秒),请重新发送消息';
} finally {
// 确保 rAF 回调被取消、剩余文本被刷新,即使 processStreamLine 抛出异常也不会泄漏
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
if (pendingText && onText) {
onText(pendingText);
pendingText = '';
}
}
const rawData = { ...(state.doneData || {}), answer: state.fullContent };
if (rawData.error) {
return {
data: null,
traceId,
status: 0,
error: friendlyMessage
traceId: rawData.meta?.probe_id || traceId,
status: 200,
error: `系统繁忙,请稍后再试(追踪码:${rawData.meta?.probe_id || traceId}`
};
}
if (!data) {
if (!state.doneData && !state.fullContent) {
return {
data: null,
traceId,
......@@ -218,20 +219,9 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
error: '响应数据为空'
};
}
console.log('✅ 成功获取 Dify 响应');
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}`
};
}
console.log('✅ [CHAT CLIENT] 成功获取响应, picks:', rawData.picks?.length || 0);
const cozeResponse: CozeResponse = {
title: rawData.title || options.message,
intent: rawData.intent || "GENERAL",
......@@ -241,7 +231,7 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
comparison: rawData.comparison,
next: rawData.next || [],
meta: {
platform: rawData.meta?.platform || 'dify-stream',
platform: rawData.meta?.platform || 'ai-hub',
user: rawData.meta?.user || options.userId,
catalog_count: rawData.meta?.catalog_count ?? 0,
probe_id: rawData.meta?.probe_id || traceId
......@@ -249,27 +239,32 @@ export async function callCozeEdge(options: CozeRequestOptions): Promise<{
conversation_id: rawData.conversation_id,
answer: rawData.answer
};
console.log('📊 Dify 响应:', {
platform: cozeResponse.meta.platform,
picks: cozeResponse.picks.length,
conversation_id: cozeResponse.conversation_id || '(new)'
});
return {
data: cozeResponse,
traceId: cozeResponse.meta.probe_id,
return {
data: cozeResponse,
traceId: cozeResponse.meta.probe_id,
status: 200
};
} 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 {
data: null,
traceId,
status: 0,
error: '网络连接不稳定,请重新发送消息试试'
error: friendlyMessage
};
}
}
......@@ -51,6 +51,9 @@ export async function streamDifyChat(
try {
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`, {
method: 'POST',
headers: {
......@@ -60,8 +63,11 @@ export async function streamDifyChat(
'X-User-Id': options.userId || 'guest',
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Dify 请求失败 (${response.status}): ${errorText}`);
......@@ -79,7 +85,7 @@ export async function streamDifyChat(
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
processSSELine(buffer, callbacks);
await processSSELine(buffer, callbacks);
}
break;
}
......@@ -89,18 +95,22 @@ export async function streamDifyChat(
buffer = lines.pop() || '';
for (const line of lines) {
processSSELine(line, callbacks);
await processSSELine(line, callbacks);
}
}
console.log('✅ [DIFY STREAM] 流式调用完成');
} catch (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();
if (!trimmedLine || trimmedLine.startsWith(':')) return;
if (trimmedLine === 'data: [DONE]') return;
......@@ -114,15 +124,20 @@ function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
callbacks.onText(event.content);
break;
case 'done':
callbacks.onDone(event);
await callbacks.onDone(event);
break;
case 'error':
callbacks.onError(event.message);
break;
}
} catch (e) {
// JSON.parse SyntaxError or callback rejection — safe fallback
if (jsonStr && !jsonStr.startsWith('{')) {
callbacks.onText(jsonStr);
try {
callbacks.onText(jsonStr);
} catch {
// onText itself failed; nothing more we can do
}
}
}
}
......
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