'use strict'; // 旧 bridge(crm-ai-demo)的确定性商品链接兜底:LLM 回答后面由代码追加真实商品/类目链接。 // 新 bridge 把 Markdown 转成 HTML 发给 LibreDesk,所以这里同时支持 Markdown 与 HTML 两种模式。 const NO_MATCH_RE = /(?:no exact match|no matching|don'?t carry|do not carry|not available|out of stock|nicht verfügbar|pas disponible|niet beschikbaar|未找到|没有找到|暂无|无货|缺货)/i; const LABELS = { Chinese: { title: '可直接查看:', details: '商品详情', stock: (n) => `${n} 件` }, English: { title: 'Click to view:', details: 'Product details', stock: (n) => `${n} pcs` }, German: { title: 'Hier ansehen:', details: 'Produktdetails', stock: (n) => `${n} Stk.` }, French: { title: 'À voir ici :', details: 'Détails du produit', stock: (n) => `${n} pièces` }, Dutch: { title: 'Hier bekijken:', details: 'Productdetails', stock: (n) => `${n} stuks` }, Spanish: { title: 'Ver aquí:', details: 'Detalles del producto', stock: (n) => `${n} piezas` }, Italian: { title: 'Vedi qui:', details: 'Dettagli prodotto', stock: (n) => `${n} pezzi` }, }; const LANG_ALIASES = { zh: 'Chinese', cn: 'Chinese', chinese: 'Chinese', en: 'English', english: 'English', de: 'German', german: 'German', fr: 'French', french: 'French', nl: 'Dutch', dutch: 'Dutch', es: 'Spanish', spanish: 'Spanish', it: 'Italian', italian: 'Italian', }; function labelsFor(lang) { const key = LANG_ALIASES[String(lang || '').toLowerCase()]; return LABELS[key] || LABELS.English; } function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function escapeMarkdownLabel(text) { return String(text || '').replace(/[[\]\\]/g, ' ').replace(/\s+/g, ' ').trim(); } /** 从工具结果里收集真实商品/类目链接(保持出现顺序,按 URL 去重)。 */ function collectProductLinks(toolResults) { const out = []; const seen = new Set(); const push = (item) => { if (!item || typeof item !== 'object') return; const url = String(item.url || item.product_url || '').trim(); if (!/^https?:\/\//i.test(url) || seen.has(url)) return; seen.add(url); out.push({ name: String(item.name || item.title || '').trim() || null, url, price: item.price != null && item.price !== '' ? String(item.price) : null, stock: item.stock != null && item.stock !== '' ? item.stock : null, }); }; for (const result of toolResults || []) { if (!result || typeof result !== 'object') continue; if (Array.isArray(result.products)) result.products.forEach(push); if (Array.isArray(result.category_links)) result.category_links.forEach(push); } return out; } /** * 把工具结果里有、但回复里没写的真实链接补到末尾。 * - 回复已明确说“没有/缺货”时不补(避免自相矛盾,沿用旧 bridge 行为); * - 已包含的 URL 自动去重; * - html=true 时输出 ,否则输出 Markdown 链接(会再经 markdownToHtml 转 HTML)。 */ function appendMissingProductLinks({ content, toolResults, lang = 'English', html = false, limit = 3 }) { const base = String(content || ''); if (!base.trim() || NO_MATCH_RE.test(base)) return { content: base, appended: 0 }; const missing = collectProductLinks(toolResults).filter((item) => !base.includes(item.url)); if (!missing.length) return { content: base, appended: 0 }; const labels = labelsFor(lang); // 有货的排前面(stock=0 的真实商品只在没有别的可选时才补),保持工具返回的相对顺序。 const stockRank = (item) => { if (item.stock == null) return 1; const n = Number(item.stock); if (!Number.isFinite(n)) return 1; return n > 0 ? 0 : 2; }; const ordered = missing .map((item, index) => ({ item, index })) .sort((a, b) => stockRank(a.item) - stockRank(b.item) || a.index - b.index) .map((x) => x.item); const top = ordered.slice(0, Math.max(1, Number(limit) || 3)); const lines = top.map((item) => { const text = item.name || labels.details; const meta = [item.price, item.stock != null ? labels.stock(item.stock) : null].filter(Boolean).join(' | '); if (html) { const anchor = `${escapeHtml(text)}`; return `
  • ${anchor}${meta ? ` — ${escapeHtml(meta)}` : ''}
  • `; } return `- [${escapeMarkdownLabel(text)}](${item.url})${meta ? ` — ${meta}` : ''}`; }); const block = html ? `

    ${escapeHtml(labels.title)}

    ` : `${labels.title}\n${lines.join('\n')}`; return { content: `${base.trimEnd()}\n\n${block}`, appended: top.length }; } module.exports = { appendMissingProductLinks, collectProductLinks, labelsFor, NO_MATCH_RE };