Files
crm-ai/bridge/src/reply-links.js
T

110 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 时输出 <a>,否则输出 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 = `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
return `<li>${anchor}${meta ? ` — ${escapeHtml(meta)}` : ''}</li>`;
}
return `- [${escapeMarkdownLabel(text)}](${item.url})${meta ? ` — ${meta}` : ''}`;
});
const block = html
? `<p>${escapeHtml(labels.title)}</p><ul>${lines.join('')}</ul>`
: `${labels.title}\n${lines.join('\n')}`;
return { content: `${base.trimEnd()}\n\n${block}`, appended: top.length };
}
module.exports = { appendMissingProductLinks, collectProductLinks, labelsFor, NO_MATCH_RE };