|
@@ -1,9 +1,12 @@
|
|
|
/**
|
|
/**
|
|
|
- * 验证「更多」→「政策列表」侧边栏的纯逻辑:跨消息聚合、去重、排序、关键词过滤。
|
|
|
|
|
|
|
+ * 验证「更多」→「政策列表」侧边栏的纯逻辑:去重、排序、关键词过滤。
|
|
|
*
|
|
*
|
|
|
- * 为什么要单独测:卡片区每个 PolicyMatch 实例只持有**自己那条消息**的卡片,
|
|
|
|
|
- * 侧边栏要的是**整个会话**的聚合,这段逻辑(解析历史消息 → 去重 → 排序 → 过滤)
|
|
|
|
|
- * 容易在边界上出错(坏 JSON、重复卡片、精简格式占位符、同分排序抖动)。
|
|
|
|
|
|
|
+ * 注:侧边栏列的是**本条回答里的政策**(用户 2026-09-18 调整;此前一度做成
|
|
|
|
|
+ * 「整个会话跨消息聚合」,那个采集函数已随之删除)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么要单独测:列表要经过「去重 → 排序 → 关键词过滤」三步,
|
|
|
|
|
+ * 边界容易出错(精简格式的占位符 declaration_item、同分排序抖动、空关键词、
|
|
|
|
|
+ * 空/脏输入)。这些都是纯逻辑,用断言守住比在浏览器里点便宜得多。
|
|
|
*
|
|
*
|
|
|
* 怎么跑(在项目根目录):
|
|
* 怎么跑(在项目根目录):
|
|
|
* npx esbuild harness/tools/_entry-policy-cards.ts --bundle --format=esm \
|
|
* npx esbuild harness/tools/_entry-policy-cards.ts --bundle --format=esm \
|
|
@@ -12,7 +15,6 @@
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import {
|
|
import {
|
|
|
- collectPolicyCardsFromMessages,
|
|
|
|
|
buildPolicyCardDedupKey,
|
|
buildPolicyCardDedupKey,
|
|
|
dedupePolicyItems,
|
|
dedupePolicyItems,
|
|
|
sortPolicyCardsByMatchScore,
|
|
sortPolicyCardsByMatchScore,
|
|
@@ -31,32 +33,12 @@ const check = (name, cond, extra = '') => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-/** 造一条带 POLICY_TABLE 块的 AI 消息(格式与适配层 flushContent 产出一致) */
|
|
|
|
|
-const aiMessage = (cards, extraText = '正文') =>
|
|
|
|
|
- `${extraText}\n<!-- POLICY_TABLE ${JSON.stringify({ data: cards })} POLICY_TABLE -->\n`;
|
|
|
|
|
-
|
|
|
|
|
-console.log('【1】跨消息聚合(collectPolicyCardsFromMessages)');
|
|
|
|
|
|
|
+/** 三条测试用卡片(后续各节共用) */
|
|
|
const cardA = { declaration_item: '政策甲 - 事项一', title: '政策甲', match_score: 90 };
|
|
const cardA = { declaration_item: '政策甲 - 事项一', title: '政策甲', match_score: 90 };
|
|
|
const cardB = { declaration_item: '政策乙 - 事项一', title: '政策乙', match_score: 45 };
|
|
const cardB = { declaration_item: '政策乙 - 事项一', title: '政策乙', match_score: 45 };
|
|
|
const cardC = { declaration_item: '政策丙 - 事项一', title: '政策丙', match_score: 60 };
|
|
const cardC = { declaration_item: '政策丙 - 事项一', title: '政策丙', match_score: 60 };
|
|
|
|
|
|
|
|
-const messages = [
|
|
|
|
|
- { role: 'user', content: '我有什么政策' }, // 用户消息不该被解析
|
|
|
|
|
- { role: 'ai', content: aiMessage([cardA, cardB]) },
|
|
|
|
|
- { role: 'ai', content: '这条没有卡片' },
|
|
|
|
|
- { role: 'ai', content: aiMessage([cardC]) },
|
|
|
|
|
-];
|
|
|
|
|
-const collected = collectPolicyCardsFromMessages(messages);
|
|
|
|
|
-check('收集到 3 张卡片', collected.length === 3, `实际 ${collected.length}`);
|
|
|
|
|
-check('保持消息顺序(甲→乙→丙)', collected.map((c) => c.title).join(',') === '政策甲,政策乙,政策丙', collected.map((c) => c.title).join(','));
|
|
|
|
|
-check('用户消息被跳过', !collected.some((c) => c.title === '我有什么政策'));
|
|
|
|
|
-
|
|
|
|
|
-check('空输入不抛错 → []', Array.isArray(collectPolicyCardsFromMessages([])) && collectPolicyCardsFromMessages([]).length === 0);
|
|
|
|
|
-check('null 输入不抛错 → []', collectPolicyCardsFromMessages(null).length === 0);
|
|
|
|
|
-check('坏 JSON 不抛错且不贡献卡片', collectPolicyCardsFromMessages([{ role: 'ai', content: '<!-- POLICY_TABLE {坏JSON POLICY_TABLE -->' }]).length === 0);
|
|
|
|
|
-check('无 POLICY_TABLE 块 → []', collectPolicyCardsFromMessages([{ role: 'ai', content: '纯文本回答' }]).length === 0);
|
|
|
|
|
-
|
|
|
|
|
-console.log('\n【2】去重键(buildPolicyCardDedupKey)');
|
|
|
|
|
|
|
+console.log('【1】去重键(buildPolicyCardDedupKey)');
|
|
|
check(
|
|
check(
|
|
|
'同 declaration_item → 同键',
|
|
'同 declaration_item → 同键',
|
|
|
buildPolicyCardDedupKey(cardA) === buildPolicyCardDedupKey({ ...cardA, match_score: 10 }),
|
|
buildPolicyCardDedupKey(cardA) === buildPolicyCardDedupKey({ ...cardA, match_score: 10 }),
|
|
@@ -107,9 +89,9 @@ check('大小写不敏感', matchesPolicyCardKeyword({ title: 'ABC Policy' }, 'a
|
|
|
check('不命中返回 false', matchesPolicyCardKeyword(searchable, '不存在的词') === false);
|
|
check('不命中返回 false', matchesPolicyCardKeyword(searchable, '不存在的词') === false);
|
|
|
check('item 为 null 不抛错', matchesPolicyCardKeyword(null, 'x') === false);
|
|
check('item 为 null 不抛错', matchesPolicyCardKeyword(null, 'x') === false);
|
|
|
|
|
|
|
|
-console.log('\n【6】端到端组合(聚合 → 去重 → 排序 → 过滤)');
|
|
|
|
|
|
|
+console.log('\n【6】端到端组合(去重 → 排序 → 过滤)');
|
|
|
const e2e = sortPolicyCardsByMatchScore(
|
|
const e2e = sortPolicyCardsByMatchScore(
|
|
|
- dedupePolicyItems(collectPolicyCardsFromMessages(messages), buildPolicyCardDedupKey)
|
|
|
|
|
|
|
+ dedupePolicyItems([cardA, cardB, cardC], buildPolicyCardDedupKey)
|
|
|
);
|
|
);
|
|
|
check('组合后 3 条且按分数降序', e2e.length === 3 && e2e[0].match_score === 90, e2e.map((c) => c.match_score).join(','));
|
|
check('组合后 3 条且按分数降序', e2e.length === 3 && e2e[0].match_score === 90, e2e.map((c) => c.match_score).join(','));
|
|
|
const e2eFiltered = e2e.filter((c) => matchesPolicyCardKeyword(c, '政策乙'));
|
|
const e2eFiltered = e2e.filter((c) => matchesPolicyCardKeyword(c, '政策乙'));
|