Commit 7609888d authored by AI-甘富林's avatar AI-甘富林

多轮对话支付下单修改

parent 8ce2079e
/**
* ai-chat-stream Edge Function
* C端消费者聊天 - 直接大模型流式输出(支持多轮对话记忆 + 主动澄清)
* C端消费者聊天 - 直接大模型流式输出(支持多轮对话记忆 + 主动澄清 + 订单/售后/FAQ查询
*
* 通过 AI Hub(OpenAI 兼容 API)直接调用大模型,不再经过 Dify/Coze 中间层。
* 无 AI Hub 时回退到 Lovable Gateway。
*
* v4 更新(2026-06-18):
* - done 事件新增 action/actionPayload 字段,支持触发前端行为(弹出售后表单等)
* - done 事件新增 components 数组,支持返回 UI 组件调度指令(与 B端 GenUI 对齐)
* - System Prompt 增强:AFTERSALES 场景引导 LLM 返回 action 和 actionPayload
*
* v3 更新(2026-06-18):
* - 订单号自动提取(正则匹配多种格式)并从 orders 表查询真实订单数据
* - 售后查询:按订单ID + 用户ID 从 after_sales_requests 表查询售后记录
* - FAQ 知识库搜索:从 knowledge_base 表 ilike 全文匹配常见问题
* - 预查询+上下文注入模式:LLM 调用前并行查询数据,注入 system prompt 防幻觉
*
* v2 更新:
* - 从 messages 表加载对话历史,注入 LLM 上下文(多轮记忆)
* - 系统提示词增强:意图模糊时主动追问澄清(CLARIFY intent)
......@@ -49,7 +60,7 @@ const USE_AI_HUB = !!(AI_HUB_KEY && AI_HUB_BASE);
type HistoryMessage = { role: 'user' | 'assistant'; content: string };
// ─── 系统提示词构建 ───
function buildSystemPrompt(catalog: any[], userContext: string): string {
function buildSystemPrompt(catalog: any[], userContext: string, dataContext = ''): string {
const catalogSection = catalog.length > 0
? `\n\n## 可用产品目录(共${catalog.length}个,展示前30个)\n${
catalog.slice(0, 30).map(p =>
......@@ -62,25 +73,33 @@ function buildSystemPrompt(catalog: any[], userContext: string): string {
? `\n\n## 用户上下文\n${userContext}`
: '';
return `你是"小智",千江商城的智能购物顾问。
const dataContextSection = dataContext
? `\n\n${dataContext}\n\n**重要:以上订单/售后/FAQ数据均为真实数据,你必须据此回复,禁止编造任何订单号、状态、金额!**`
: '';
return `你是"小智",千匠商城的智能购物顾问。
## 核心规则
1. 产品ID必须从产品目录中精确复制,严禁编造或生成
1. 产品ID必须从产品目录中逐字符精确复制,严禁编造或生成——你无法凭空生成有效的UUID,编造必然导致错误
2. 所有价格、库存以目录为准,不可臆造
3. 回复使用友好、专业、简洁的中文
4. 不透露你是AI
5. 理解对话历史,记住用户之前说过的话——结合上下文做推荐和回复
6. 当系统提供了用户的订单/售后数据时,直接引用真实数据回复;当没有数据时,引导用户提供订单号查询
7. **禁止生成任何URL/链接**:回复正文中绝对不能出现任何链接地址。产品推荐统一通过下方JSON的picks字段返回,前端会自动渲染产品卡片。绝对禁止输出任何形式的网址、域名或链接
8. **禁止编造标识符**:你无法生成有效的UUID、订单号、物流单号。这些必须从系统提供的目录/数据中精确复制。如果目录中没有合适的UUID,picks留空即可,不要编造
${catalogSection}${contextSection}
${catalogSection}${contextSection}${dataContextSection}
## 澄清规则(重要)
当用户意图模糊、信息不足时,你必须**主动追问澄清**,而不是盲目推荐。需要澄清的典型场景:
1. **购物意图模糊**:用户说"想买个手机"但没提预算、品牌偏好、用途 → 追问预算范围、偏好品牌/功能、主要用途
2. **对比请求缺标准**:用户说"哪个更好"但没说看重什么 → 追问用户最在意的因素(价格/性能/外观/售后)
3. **售后请求缺信息**:用户提退货/维修但没说订单号 → 友好索要订单号
3. **售后请求缺信息**:用户提退货/维修但没说订单号 → 友好索要订单号;如果系统已经查询到订单数据则直接基于数据回复
4. **预算不明确**:用户只说"便宜的"/"好一点的" → 追问具体预算区间
5. **模糊指代**:用户说"刚才那个"、"太贵了有便宜的吗" → 结合对话历史定位之前推荐的产品
6. **序数指代**:用户说"第一个"、"第二个"、"第N个"、"最后一个"、"下单第三个" → 必须回顾上一轮对话中你推荐的 picks/商品列表,按顺序定位对应商品。例如上一轮你推荐了产品A、产品B、产品C,用户说"第二个"就是指产品B
澄清要求:
- 每次最多问2-3个关键问题,不要连珠炮式提问
......@@ -90,7 +109,7 @@ ${catalogSection}${contextSection}
## 回复末尾必须包含JSON
\`\`\`json
{"picks":[{"product_id":"从目录复制的真实UUID","note":"推荐理由"}],"intent":"SHOPPING|COMPARE|AFTERSALES|GENERAL|CLARIFY","highlights":["要点1","要点2"],"clarifies":["追问1","追问2"],"title":"简短标题"}
{"picks":[...],"intent":"SHOPPING|COMPARE|AFTERSALES|GENERAL|CLARIFY","highlights":[...],"clarifies":[...],"title":"...","extractedOrderNumber":"用户消息中提及的订单号,无则填null","action":"show_after_sales_form|...","actionPayload":{...},"components":[...]}
\`\`\`
## 规则
......@@ -99,8 +118,14 @@ ${catalogSection}${contextSection}
- clarifies: 当 intent=CLARIFY 时必填,列出2-3个具体追问问题
- highlights: 2-4个回复要点(各20-80字);澄清模式下为对话摘要
- title: 简短对话标题(≤30字)
- extractedOrderNumber: ★ 从用户消息(结合对话历史)中理解并提取订单号。即使用户说"上次那个订单"、"帮我查下订单进度"、"我之前买的那个"等模糊表达,只要能推断出订单号就填写;确实没有则填 null。LLM的理解能力可以补正则盲区
- 对比类询问使用Markdown表格
- 售后问题友好引导联系人工客服`;
- action: 触发前端行为,可选值:show_after_sales_form(弹出售后申请表单)、show_order_detail(展示订单详情)、show_faq(展示FAQ)、none(无需操作)
- actionPayload: 当 action="show_after_sales_form" 时必填,包含订单UUID、订单号和商品列表
- components: 前端展示的UI组件数组,每项含 type(组件类型) 和 props(组件属性)
· order_status_card: 订单状态卡片,props含 orderNumber, status
· after_sales_progress: 售后进度组件,props含 afterSalesId, type, status, createdAt
- **售后处理规则**:如果系统提供了订单数据且用户明确要退货/换货/维修,必须设置 action="show_after_sales_form" 并填写 actionPayload;仅当确实需要人工介入(如特殊退款审批)时才引导联系人工客服`;
}
// ─── 对话历史加载 ───
......@@ -246,6 +271,361 @@ function detectIntent(message: string, answer: string): string {
return 'GENERAL';
}
// ─── 订单号提取 ───
/** 从用户消息中提取订单号(支持多种格式) */
function extractOrderNumber(message: string): string | null {
// 格式1: ORD + 日期 + 序号,如 ORD20240618001
const ordMatch = message.match(/ORD\d{8,}/i);
if (ordMatch) return ordMatch[0].toUpperCase();
// 格式2: # 开头 + 数字/字母,如 #20240618001
const hashMatch = message.match(/#([A-Z0-9]{6,})/i);
if (hashMatch) return hashMatch[1].toUpperCase();
// 格式3: 纯数字订单号(8位以上),如 20240618001
const numMatch = message.match(/\b(\d{8,20})\b/);
if (numMatch) {
const num = numMatch[1];
// 排除明显是手机号或金额的数字
if (/^1[3-9]\d{9}$/.test(num)) return null; // 手机号
if (/^\d{1,7}$/.test(num)) return null; // 太短
return num;
}
// 格式4: 关键词 "订单号" "订单编号" "订单" 后跟的编号
const keywordMatch = message.match(/订单(?:号|编号)?\s*[::]\s*([A-Z0-9-]{6,30})/i);
if (keywordMatch) return keywordMatch[1].toUpperCase();
// 格式5: 中英文混合 UUID 风格的订单引用
const uuidLike = message.match(/\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i);
if (uuidLike) return uuidLike[1];
return null;
}
// ─── 数据查询函数 ───
/** 根据订单号查询订单详情(含商品) */
async function queryOrderByNumber(
supabase: any,
orderNumber: string
): Promise<any | null> {
try {
console.log(`🔍 [ai-chat-stream] 查询订单: order_number=${orderNumber}`);
const { data, error } = await supabase
.from('orders')
.select(`
id, order_number, status, total_amount, shipping_address,
tracking_number, notes, created_at, updated_at,
order_items (
id, quantity, unit_price, product_id,
products ( id, name, price, image_url, brand, category )
)
`)
.eq('order_number', orderNumber)
.single();
if (error) {
console.error('❌ [ai-chat-stream] 订单查询失败:', JSON.stringify(error));
return null;
}
console.log('✅ [ai-chat-stream] 订单查询成功:', {
order_number: data.order_number,
status: data.status,
items_count: data.order_items?.length || 0,
});
return data;
} catch (err) {
console.error('[ai-chat-stream] 订单查询异常:', err);
return null;
}
}
/** 根据用户ID查询最近订单列表 */
async function queryUserOrders(
supabase: any,
userId: string,
limit = 5
): Promise<any[]> {
try {
console.log(`🔍 [ai-chat-stream] 查询用户订单: user_id=${userId}`);
const { data, error } = await supabase
.from('orders')
.select(`
id, order_number, status, total_amount, created_at,
order_items (
id, quantity, unit_price, product_id,
products ( id, name, price, image_url )
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(limit);
if (error) {
console.error('❌ [ai-chat-stream] 用户订单查询失败:', JSON.stringify(error));
return [];
}
console.log('✅ [ai-chat-stream] 用户订单查询成功:', { count: data?.length || 0 });
return data || [];
} catch (err) {
console.error('[ai-chat-stream] 用户订单查询异常:', err);
return [];
}
}
/** 根据订单ID查询售后记录 */
async function queryAfterSalesByOrder(
supabase: any,
orderId: string
): Promise<any[]> {
try {
console.log(`🔍 [ai-chat-stream] 查询售后: order_id=${orderId}`);
const { data, error } = await supabase
.from('after_sales_requests')
.select(`
id, type, status, reason, description,
tracking_number, refund_amount, admin_notes,
processed_at, created_at, updated_at, order_id
`)
.eq('order_id', orderId)
.order('created_at', { ascending: false });
if (error) {
console.error('❌ [ai-chat-stream] 售后查询失败:', JSON.stringify(error));
return [];
}
console.log('✅ [ai-chat-stream] 售后查询成功:', { count: data?.length || 0 });
return data || [];
} catch (err) {
console.error('[ai-chat-stream] 售后查询异常:', err);
return [];
}
}
/** 根据用户ID查询售后记录 */
async function queryAfterSalesByUser(
supabase: any,
userId: string,
limit = 5
): Promise<any[]> {
try {
console.log(`🔍 [ai-chat-stream] 查询用户售后: user_id=${userId}`);
const { data, error } = await supabase
.from('after_sales_requests')
.select(`
id, type, status, reason, description,
tracking_number, refund_amount, admin_notes,
processed_at, created_at, updated_at, order_id
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(limit);
if (error) {
console.error('❌ [ai-chat-stream] 用户售后查询失败:', JSON.stringify(error));
return [];
}
console.log('✅ [ai-chat-stream] 用户售后查询成功:', { count: data?.length || 0 });
return data || [];
} catch (err) {
console.error('[ai-chat-stream] 用户售后查询异常:', err);
return [];
}
}
/** 从知识库搜索FAQ(ilike全文匹配) */
async function searchFAQ(
supabase: any,
query: string,
limit = 5
): Promise<Array<{ title: string; content: string; category: string }>> {
try {
console.log(`🔍 [ai-chat-stream] 搜索FAQ: query="${query.substring(0, 60)}"`);
// 提取关键词用于 ilike 搜索
const keywords = query
.replace(/[??!!,,。.、\s]+/g, ' ')
.trim()
.split(/\s+/)
.filter(w => w.length >= 2)
.slice(0, 3); // 最多3个关键词
if (keywords.length === 0) return [];
// 对每个关键词进行 ilike 搜索(OR 拼接)
let dbQuery = supabase
.from('knowledge_base')
.select('title, content, category')
.eq('is_active', true);
// 构建 OR 条件
const conditions = keywords.map(kw =>
`title.ilike.%${kw}%,content.ilike.%${kw}%`
).join(',');
dbQuery = dbQuery.or(conditions);
const { data, error } = await dbQuery.limit(limit);
if (error) {
console.error('❌ [ai-chat-stream] FAQ搜索失败:', JSON.stringify(error));
return [];
}
console.log('✅ [ai-chat-stream] FAQ搜索成功:', { keywords, results: data?.length || 0 });
return (data || []).map((row: any) => ({
title: row.title,
content: row.content?.substring(0, 500) || '', // 截断长文本
category: row.category || '',
}));
} catch (err) {
console.error('[ai-chat-stream] FAQ搜索异常:', err);
return [];
}
}
// ─── 数据上下文构建 ───
/** 将查询到的数据格式化为注入 system prompt 的 Markdown 文本 */
function buildDataContext(params: {
order: any | null;
userOrders: any[];
afterSales: any[];
faqResults: Array<{ title: string; content: string; category: string }>;
userId: string | null;
}): string {
const sections: string[] = [];
// ── 订单数据 ──
if (params.order) {
const o = params.order;
const statusMap: Record<string, string> = {
pending: '待处理',
confirmed: '已确认',
shipped: '已发货',
delivered: '已签收',
cancelled: '已取消',
};
const statusCN = statusMap[o.status] || o.status;
let orderSection = `## 用户查询的订单(真实数据,禁止编造)
| 项目 | 详情 |
|------|------|
| 订单号 | ${o.order_number} |
| 状态 | ${statusCN}${o.status}) |
| 金额 | ¥${Number(o.total_amount).toFixed(2)} |
| 下单时间 | ${o.created_at || '未知'} |`;
if (o.tracking_number) {
orderSection += `\n| 物流单号 | ${o.tracking_number} |`;
}
if (o.shipping_address) {
orderSection += `\n| 收货地址 | ${o.shipping_address} |`;
}
// 订单商品
if (o.order_items?.length > 0) {
orderSection += `\n\n**商品明细:**\n`;
orderSection += `| 商品 | 单价 | 数量 | 小计 |\n|------|------|------|------|\n`;
for (const item of o.order_items) {
const name = item.products?.name || '未知商品';
const price = Number(item.unit_price).toFixed(2);
const qty = item.quantity;
const subtotal = (Number(item.unit_price) * item.quantity).toFixed(2);
orderSection += `| ${name} | ¥${price} | ×${qty} | ¥${subtotal} |\n`;
}
}
sections.push(orderSection);
}
// ── 用户最近订单列表 ──
if (params.userOrders.length > 0 && !params.order) {
let userOrdersSection = `## 用户最近订单(真实数据,禁止编造)
| 订单号 | 状态 | 金额 | 时间 |
|------|------|------|------|
`;
const statusMap: Record<string, string> = {
pending: '待处理', confirmed: '已确认', shipped: '已发货',
delivered: '已签收', cancelled: '已取消',
};
for (const o of params.userOrders) {
const statusCN = statusMap[o.status] || o.status;
const date = o.created_at ? new Date(o.created_at).toLocaleDateString('zh-CN') : '-';
userOrdersSection += `| ${o.order_number} | ${statusCN} | ¥${Number(o.total_amount).toFixed(2)} | ${date} |\n`;
}
sections.push(userOrdersSection);
}
// ── 售后数据 ──
if (params.afterSales.length > 0) {
const typeMap: Record<string, string> = {
return: '退货', exchange: '换货', repair: '维修',
};
const statusMap: Record<string, string> = {
pending: '待审核', approved: '已通过', processing: '处理中',
completed: '已完成', rejected: '已拒绝',
};
let asSection = `## 关联售后记录(真实数据,禁止编造)
| 类型 | 状态 | 原因 | 申请时间 |
|------|------|------|------|
`;
for (const as of params.afterSales) {
const typeCN = typeMap[as.type] || as.type;
const statusCN = statusMap[as.status] || as.status;
const date = as.created_at ? new Date(as.created_at).toLocaleDateString('zh-CN') : '-';
asSection += `| ${typeCN} | ${statusCN} | ${as.reason?.substring(0, 30) || '-'} | ${date} |\n`;
}
// 附上第一条售后的详细信息
const first = params.afterSales[0];
if (first) {
asSection += `\n**最近售后详情:**\n`;
asSection += `- 类型:${typeMap[first.type] || first.type}\n`;
asSection += `- 状态:${statusMap[first.status] || first.status}\n`;
asSection += `- 原因:${first.reason || '-'}\n`;
if (first.description) asSection += `- 描述:${first.description.substring(0, 200)}\n`;
if (first.tracking_number) asSection += `- 退回物流单号:${first.tracking_number}\n`;
if (first.refund_amount) asSection += `- 退款金额:¥${Number(first.refund_amount).toFixed(2)}\n`;
if (first.admin_notes) asSection += `- 客服备注:${first.admin_notes.substring(0, 200)}\n`;
}
sections.push(asSection);
}
// ── FAQ / 知识库结果 ──
if (params.faqResults.length > 0) {
let faqSection = `## 知识库FAQ(可参考,但需结合上下文回复)
`;
for (const faq of params.faqResults) {
faqSection += `### ${faq.title}\n${faq.content}\n\n`;
}
sections.push(faqSection);
}
// ── 无数据提示 ──
if (sections.length === 0) {
sections.push(`## 数据查询结果
(本次查询未获取到订单/售后/FAQ数据。如果用户询问订单或售后问题,请友好引导用户提供正确的订单号。)`);
}
return sections.join('\n\n---\n\n');
}
// ─── 主处理 ───
Deno.serve(async (req) => {
......@@ -328,7 +708,7 @@ Deno.serve(async (req) => {
if (localConvId && userId) {
historyMessages = await loadConversationHistory(supabase, localConvId, userId, 20);
historyMessages = truncateHistory(historyMessages, 6000);
historyMessages = truncateHistory(historyMessages, 12000);
console.log('📜 [ai-chat-stream] 历史消息:', {
loaded: historyMessages.length,
totalChars: historyMessages.reduce((s, m) => s + m.content.length, 0),
......@@ -337,8 +717,67 @@ Deno.serve(async (req) => {
});
}
// ─── 数据查询:订单/售后/FAQ ───
const orderNumber = extractOrderNumber(message);
console.log('🔎 [ai-chat-stream] 订单号提取:', { orderNumber, messageLength: message.length });
let order: any = null;
let afterSales: any[] = [];
let userOrders: any[] = [];
let faqResults: Array<{ title: string; content: string; category: string }> = [];
// 并行查询:订单详情 + 售后记录 + FAQ + 用户最近订单
const [orderResult, asUserResult, faqResult, userOrdersResult] = await Promise.allSettled([
// 1. 如果提取到订单号,查询订单详情
orderNumber
? queryOrderByNumber(supabase, orderNumber)
: Promise.resolve(null),
// 2. 如果用户已登录,查询该用户的售后记录
userIdPassed
? queryAfterSalesByUser(supabase, userId, 5)
: Promise.resolve([]),
// 3. FAQ 搜索(总是执行,帮助回答常见问题)
searchFAQ(supabase, message, 5),
// 4. 用户最近订单(如果没有具体订单号但有 userId)
(userIdPassed && !orderNumber)
? queryUserOrders(supabase, userId, 5)
: Promise.resolve([]),
]);
if (orderResult.status === 'fulfilled') order = orderResult.value;
if (asUserResult.status === 'fulfilled') afterSales = asUserResult.value;
if (faqResult.status === 'fulfilled') faqResults = faqResult.value;
if (userOrdersResult.status === 'fulfilled') userOrders = userOrdersResult.value;
// 如果查到订单,再查该订单的售后记录(优先于用户级别的售后)
if (order?.id) {
try {
const orderAS = await queryAfterSalesByOrder(supabase, order.id);
if (orderAS.length > 0) afterSales = orderAS; // 订单级别售后记录覆盖用户级别
} catch {
// 保持已有售后数据
}
}
// 构建数据上下文
const dataContext = buildDataContext({
order,
userOrders,
afterSales,
faqResults,
userId: userIdPassed ? userId : null,
});
console.log('📊 [ai-chat-stream] 数据上下文构建完成:', {
hasOrder: !!order,
afterSalesCount: afterSales.length,
userOrdersCount: userOrders.length,
faqResultsCount: faqResults.length,
contextLength: dataContext.length,
});
// ─── 构建系统提示词 ───
const systemPrompt = buildSystemPrompt(catalog, inputs.user_context || '');
const systemPrompt = buildSystemPrompt(catalog, inputs.user_context || '', dataContext);
// ─── 调用大模型流式 API ───
console.log('🚀 [ai-chat-stream] Calling LLM API...');
......@@ -410,8 +849,20 @@ Deno.serve(async (req) => {
const delta = parsed?.choices?.[0]?.delta;
if (delta?.content) {
fullAnswer += delta.content;
const sseData = JSON.stringify({ type: 'text', content: delta.content });
// ★ 逐 chunk 拦截清洗——防止幻觉 URL 通过 SSE 流到达前端
let cleanContent = delta.content;
const streamUrlPatterns = [
/\[([^\]]*)\]\(https?:\/\/[^\)]+\)/gi,
/https?:\/\/[^\s]+/gi,
];
for (const p of streamUrlPatterns) {
if (p.test(cleanContent)) {
console.log('🧹 [ai-chat-stream] Stripped hallucinated URL from stream chunk');
cleanContent = cleanContent.replace(p, '');
}
}
fullAnswer += cleanContent;
const sseData = JSON.stringify({ type: 'text', content: cleanContent });
controller.enqueue(encoder.encode(`data: ${sseData}\n\n`));
}
} catch {
......@@ -428,6 +879,15 @@ Deno.serve(async (req) => {
controller.enqueue(encoder.encode(`data: ${sseData}\n\n`));
}
// ★ 第二道防线:全文本再次清洗幻觉 URL(保护 DB 写入副本)
const postUrlPatterns = [
/\[([^\]]*)\]\(https?:\/\/[^\)]+\)/gi,
/https?:\/\/[^\s]+/gi,
];
for (const p of postUrlPatterns) {
fullAnswer = fullAnswer.replace(p, '');
}
console.log('📝 [ai-chat-stream] Full answer length:', fullAnswer.length);
// 优先尝试解析 LLM 直接返回的结构化 JSON
......@@ -440,6 +900,9 @@ Deno.serve(async (req) => {
let next: string[] | undefined;
let title: string | undefined;
let clarifies: string[] | undefined;
let action: string | undefined;
let actionPayload: any | undefined;
let components: any[] | undefined;
if (structured) {
console.log('✅ [ai-chat-stream] Using structured payload from LLM');
......@@ -475,12 +938,44 @@ Deno.serve(async (req) => {
}
picks = accepted.map(a => ({ product_id: a.product_id as string, note: a.note || '' }));
// ★ 回退:结构化 JSON 存在但所有 picks 被审计拒绝 → 尝试文本解析兜底
if (picks.length === 0 && rawPicks.length > 0 && catalog.length > 0) {
console.log('⚠️ [ai-chat-stream] All structured picks rejected by audit, falling back to text parsing');
const textPicks = parsePicksFromText(fullAnswer, catalog);
if (textPicks.length > 0) {
picks = textPicks;
console.log('✅ [ai-chat-stream] Text fallback recovered picks:', picks.length);
} else {
console.log('❌ [ai-chat-stream] Text fallback also found no valid picks');
}
}
intent = structured.intent || detectIntent(message, fullAnswer);
highlights = Array.isArray(structured.highlights) ? structured.highlights : [];
comparison = typeof structured.comparison === 'string' ? structured.comparison : undefined;
next = Array.isArray(structured.next) ? structured.next : undefined;
title = typeof structured.title === 'string' ? structured.title : undefined;
clarifies = Array.isArray(structured.clarifies) ? structured.clarifies : undefined;
action = (typeof structured.action === 'string' && structured.action !== 'none') ? structured.action : undefined;
actionPayload = (action && structured.actionPayload && typeof structured.actionPayload === 'object') ? structured.actionPayload : undefined;
components = (Array.isArray(structured.components) && structured.components.length > 0) ? structured.components : undefined;
// LLM 提取的订单号(补正则盲区:如"上次那个订单"、"帮我查下进度")
const llmOrderNumber = (typeof structured.extractedOrderNumber === 'string' && structured.extractedOrderNumber.trim())
? structured.extractedOrderNumber.trim()
: null;
if (llmOrderNumber && !order) {
console.log('🧠 [ai-chat-stream] LLM 提取到正则漏掉的订单号:', llmOrderNumber);
try {
order = await queryOrderByNumber(supabase, llmOrderNumber);
if (order?.id) {
const orderAS = await queryAfterSalesByOrder(supabase, order.id);
if (orderAS.length > 0) afterSales = orderAS;
console.log('✅ [ai-chat-stream] LLM订单号查询成功,补充数据注入');
}
} catch { /* 查询失败不影响主流程 */ }
}
} else {
console.log('⚠️ [ai-chat-stream] No structured payload, falling back to text parsing');
picks = parsePicksFromText(fullAnswer, catalog);
......@@ -562,6 +1057,9 @@ Deno.serve(async (req) => {
...(next ? { next } : {}),
...(title ? { title } : {}),
...(clarifies ? { clarifies } : {}),
...(action ? { action } : {}),
...(actionPayload ? { actionPayload } : {}),
...(components ? { components } : {}),
meta: {
platform: 'ai-hub',
user: userId,
......@@ -570,6 +1068,9 @@ Deno.serve(async (req) => {
source: structured ? 'llm-structured' : 'text-fallback',
hasHistory: historyMessages.length > 0,
historyMessages: historyMessages.length,
hasOrderData: !!order,
hasAfterSalesData: afterSales.length > 0,
hasFAQData: faqResults.length > 0,
},
conversation_id: localConvId,
});
......
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