fix(bridge): 商品链接兜底与强制检索

This commit is contained in:
wangliang
2026-10-23 13:15:37 +08:00
parent 4d7b89492b
commit 22d96edee6
9 changed files with 414 additions and 3 deletions
+119
View File
@@ -61,3 +61,122 @@ test('agent: output guard blocks fabricated order claim before send', async () =
assert.match(sent, /无法在聊天中直接下单/);
assert.doesNotMatch(sent, /已为您创建正式订单草稿/);
});
test('agent: bare Chinese product word forces search_products before replying', async () => {
const queries = [];
const tools = [{
name: 'search_products',
description: 'fake',
parameters: { type: 'object', properties: {} },
handler: async (args) => {
queries.push(args.query);
return { products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }], count: 1 };
},
}];
tools.leadStore = { get: async () => null, save: async () => null };
let sent = '';
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
llm: {
enabled: true,
model: 'fake',
chat: async () => ({ content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null }),
},
client: {
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
sendMessage: async (_uuid, content) => { sent = content; },
},
tools,
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
metrics: { record: async () => {}, enabled: false },
contextManager: { compressHistory: async (_c, h) => h },
});
const conv = { uuid: 'conv-ring-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
await agent.run(conv, { uuid: 'msg-ring-1', text_content: '戒指', content: '戒指' });
assert.deepEqual(queries, ['戒指']);
assert.match(sent, /Triple Rings Ring/);
});
test('agent: backfills real product links when the LLM omits them', async () => {
const tools = [{
name: 'search_products',
description: 'fake',
parameters: { type: 'object', properties: {} },
handler: async () => ({
products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }],
count: 1,
}),
}];
tools.leadStore = { get: async () => null, save: async () => null };
let sent = '';
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
llm: {
enabled: true,
model: 'fake',
chat: async () => ({ content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null }),
},
client: {
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
sendMessage: async (_uuid, content) => { sent = content; },
sendPrivateNote: async () => {},
},
tools,
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
metrics: { record: async () => {}, enabled: false },
contextManager: { compressHistory: async (_c, h) => h },
});
const conv = { uuid: 'conv-backfill-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
await agent.run(conv, { uuid: 'msg-backfill-1', text_content: '戒指', content: '戒指' });
assert.match(sent, /https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html/);
assert.match(sent, /可直接查看/);
});
test('agent: forces search_products when the LLM answers without tools', async () => {
const queries = [];
let calls = 0;
const tools = [{
name: 'search_products',
description: 'fake',
parameters: { type: 'object', properties: {} },
handler: async (args) => {
calls += 1;
queries.push(args.query);
if (calls === 1) throw new Error('rpc_timeout'); // 预检索失败 → 走事后强制注入
return {
products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }],
count: 1,
};
},
}];
tools.leadStore = { get: async () => null, save: async () => null };
let llmCalls = 0;
let sent = '';
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
llm: {
enabled: true,
model: 'fake',
chat: async () => {
llmCalls += 1;
if (llmCalls === 1) {
return { content: 'Stainless Steel Ring Set — $9.99 → 查看详情:https://www.yehwang.com/fake-ring', toolCalls: [], usage: null };
}
return { content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null };
},
},
client: {
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
sendMessage: async (_uuid, content) => { sent = content; },
sendPrivateNote: async () => {},
},
tools,
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
metrics: { record: async () => {}, enabled: false },
contextManager: { compressHistory: async (_c, h) => h },
});
const conv = { uuid: 'conv-force-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
await agent.run(conv, { uuid: 'msg-force-1', text_content: '戒指', content: '戒指' });
assert.equal(calls, 2, 'pre-tool fails once, forced injection retries');
assert.deepEqual(queries, ['戒指', '戒指']);
assert.doesNotMatch(sent, /Stainless Steel Ring Set/);
assert.doesNotMatch(sent, /fake-ring/);
assert.match(sent, /https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html/);
});
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { quickClassify, INTENTS } = require('../src/intent');
test('intent: bare product category word is a product search', () => {
for (const q of ['戒指', '项链', '耳环', '手链', '珠宝', 'rings', 'necklace', 'earrings', 'oorbellen', 'halskette']) {
assert.equal(quickClassify(q), INTENTS.PRODUCT_SEARCH, q);
}
});
test('intent: product noun inside a sentence is a product search', () => {
assert.equal(quickClassify('有没有不锈钢戒指'), INTENTS.PRODUCT_SEARCH);
assert.equal(quickClassify('do you sell stainless steel rings'), INTENTS.PRODUCT_SEARCH);
});
test('intent: other intents keep priority', () => {
assert.equal(quickClassify('我的订单到哪了'), INTENTS.ORDER_TRACKING);
assert.equal(quickClassify('我要转人工'), INTENTS.ESCALATE);
assert.equal(quickClassify('怎么退货'), INTENTS.POLICY_FAQ);
assert.equal(quickClassify('你好'), INTENTS.GENERAL);
});
+40
View File
@@ -140,3 +140,43 @@ test('guard: rewrites unsupported price but keeps product name', () => {
assert.doesNotMatch(r.content, /fake-a/);
assert.match(r.content, /价格以商品页为准/);
});
test('guard: drops dangling CTA text when a hallucinated product link is removed', () => {
const r = guardReply({
content: '<p>🔹 <strong>Stainless Steel Ring Set</strong> — $2.88<br>✅ 库存:充足<br>→ 查看详情:https://www.yehwang.com/stainless-steel-ring-set-12-pcs</p>',
toolResults: [],
toolCalls: [],
lang: 'zh',
});
assert.equal(r.blocked, true);
assert.ok(r.reasons.includes('stock_price_without_tool'));
assert.deepEqual(r.removed_urls, ['https://www.yehwang.com/stainless-steel-ring-set-12-pcs']);
assert.doesNotMatch(r.content, /查看详情/);
assert.doesNotMatch(r.content, /stainless-steel-ring-set-12-pcs/);
assert.match(r.content, /价格以商品页为准/);
});
test('guard: generic CTA label is removed together with the unverified URL', () => {
const r = guardReply({
content: '商品A — [查看详情](https://www.yehwang.com/fake-a)',
toolResults: [{ products: [{ name: '商品A' }] }],
toolCalls: [{ name: 'search_products', ok: true }],
lang: 'zh',
});
assert.equal(r.blocked, true);
assert.deepEqual(r.removed_urls, ['https://www.yehwang.com/fake-a']);
assert.doesNotMatch(r.content, /查看详情/);
assert.match(r.content, /商品A/);
});
test('guard: keeps meaningful anchor text when the URL is removed', () => {
const r = guardReply({
content: 'You can read [the product care guide](https://www.yehwang.com/fake-guide) for details.',
toolResults: [],
toolCalls: [],
lang: 'en',
});
assert.equal(r.blocked, true);
assert.doesNotMatch(r.content, /fake-guide/);
assert.match(r.content, /the product care guide/);
});
+69
View File
@@ -0,0 +1,69 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { appendMissingProductLinks, collectProductLinks } = require('../src/reply-links');
const TOOL_RESULTS = [{
products: [
{ name: 'Triple Rings Ring', url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html', price: '$2.88', stock: 26 },
{ name: 'Aurora Rings Ring', url: 'https://www.yehwang.com/aurora-rings-ring-silver-color-color-17549.html', price: '$3.17', stock: 63 },
],
category_links: [{ name: 'Rings', url: 'https://www.yehwang.com/jewelry/rings' }],
}];
test('reply-links: appends real product/category links as markdown', () => {
const r = appendMissingProductLinks({ content: 'Here are some rings.', toolResults: TOOL_RESULTS, lang: 'English' });
assert.equal(r.appended, 3);
assert.match(r.content, /\[Triple Rings Ring\]\(https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html\)/);
assert.match(r.content, /\[Rings\]\(https:\/\/www\.yehwang\.com\/jewelry\/rings\)/);
assert.match(r.content, /Click to view:/);
assert.match(r.content, /26 pcs/);
});
test('reply-links: skips links already written by the model', () => {
const content = 'See https://www.yehwang.com/triple-rings-ring-gold-color-17590.html for details.';
const r = appendMissingProductLinks({ content, toolResults: TOOL_RESULTS, lang: 'English' });
assert.equal(r.appended, 2);
assert.equal((r.content.match(/triple-rings-ring-gold-color-17590/g) || []).length, 1);
});
test('reply-links: respects the limit', () => {
const r = appendMissingProductLinks({ content: 'Rings:', toolResults: TOOL_RESULTS, lang: 'English', limit: 1 });
assert.equal(r.appended, 1);
assert.doesNotMatch(r.content, /Aurora Rings Ring/);
});
test('reply-links: html mode emits clickable anchors', () => {
const r = appendMissingProductLinks({ content: '<p>Rings:</p>', toolResults: TOOL_RESULTS, lang: 'Chinese', html: true });
assert.equal(r.appended, 3);
assert.match(r.content, /<a href="https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html"/);
assert.match(r.content, /可直接查看:/);
assert.doesNotMatch(r.content, /\[Triple Rings Ring\]\(/);
});
test('reply-links: does not backfill when the reply already says there is no match', () => {
const r = appendMissingProductLinks({ content: 'No exact matches found.', toolResults: TOOL_RESULTS, lang: 'English' });
assert.equal(r.appended, 0);
assert.equal(r.content, 'No exact matches found.');
});
test('reply-links: collects only http(s) urls and dedupes', () => {
const links = collectProductLinks([
{ products: [{ name: 'A', url: 'https://a.example/1' }, { name: 'B', url: 'https://a.example/1' }] },
{ category_links: [{ name: 'C', url: 'https://c.example/2' }, { name: 'bad', url: '/relative/3' }] },
]);
assert.deepEqual(links.map((l) => l.url), ['https://a.example/1', 'https://c.example/2']);
});
test('reply-links: prefers in-stock products over out-of-stock ones', () => {
const toolResults = [{ products: [
{ name: 'Out', url: 'https://a.example/out', price: '$1.00', stock: 0 },
{ name: 'In', url: 'https://a.example/in', price: '$2.00', stock: 26 },
{ name: 'Backorder', url: 'https://a.example/bo', price: '$3.00', stock: 0 },
] }];
const r = appendMissingProductLinks({ content: 'Rings:', toolResults, lang: 'English', limit: 2 });
assert.equal(r.appended, 2);
assert.match(r.content, /\[In\]\(https:\/\/a\.example\/in\)/);
assert.ok(r.content.indexOf('a.example/in') < r.content.indexOf('a.example/out'), 'in-stock link comes first');
});