Files
WechatExplorer/tests/fixtures/knowledge-realistic.ts
T
2026-08-06 20:29:25 +08:00

305 lines
10 KiB
TypeScript
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.
import type { KnowledgeConversationInput, KnowledgeSourceMessage } from '../../src/shared/knowledge'
export type RealisticBenchmarkCategory =
| 'chinese-continuous'
| 'chinese-short'
| 'person-name'
| 'mixed-language'
| 'url'
| 'file-name'
| 'technical-term'
| 'number-email-path'
| 'short-message'
| 'long-voice'
export interface RealisticBenchmarkCase {
id: string
category: RealisticBenchmarkCategory
/** The question as a user would naturally phrase it. */
question: string
/** Deterministic local query-router output, not an LLM-generated answer. */
searchTerms: string[]
expectedMessageIds: string[]
}
export const REALISTIC_FIXTURE_ACCOUNT = 'fixture-realistic-account'
function sourceMessage(
messageId: string,
conversationId: string,
createTime: number,
text: string,
extra: Partial<KnowledgeSourceMessage> = {}
): KnowledgeSourceMessage {
return {
accountId: REALISTIC_FIXTURE_ACCOUNT,
conversationId,
messageId,
createTime,
senderId: extra.senderId || 'fixture-member-a',
senderName: extra.senderName || '脱敏成员甲',
kind: extra.kind || 'text',
text,
attachment: extra.attachment,
voiceTranscript: extra.voiceTranscript
}
}
/**
* Fully artificial messages written in the style of real WeChat conversations.
* Nicknames, domains, addresses, mailboxes, file names and paths are all fixtures;
* no user chat record, wxid, account directory, or source-database value is included.
*/
export function createRealisticKnowledgeFixture(): {
conversations: KnowledgeConversationInput[]
cases: RealisticBenchmarkCase[]
} {
const base = Date.UTC(2026, 6, 12, 8, 0, 0)
const messages: KnowledgeSourceMessage[] = [
sourceMessage(
'msg-cn-continuous-1',
'conv-product-group',
base,
'刚确认:聊天档案导出失败时,先保留原图链接,再回退缩略图,避免用户以为图片丢了。',
{ senderId: 'member-lan', senderName: '蓝图同学' }
),
sourceMessage(
'msg-cn-continuous-noise',
'conv-product-group',
base + 60,
'导出完成后可以在任务中心查看文件夹。',
{ senderId: 'member-river', senderName: '河岸' }
),
sourceMessage('msg-cn-short-1', 'conv-family', base + 120, '周六见,咖啡我来带。', {
senderId: 'member-yu',
senderName: '小雨'
}),
sourceMessage(
'msg-person-1',
'conv-product-group',
base + 180,
'林澈把 Windows 安装包的签名检查补好了,今晚发测试包。',
{ senderId: 'member-lin', senderName: '林澈' }
),
sourceMessage(
'msg-person-noise',
'conv-product-group',
base + 240,
'小林晚点把截图发到群里。',
{ senderId: 'member-lin', senderName: '林澈' }
),
sourceMessage(
'msg-mixed-1',
'conv-engineering',
base + 300,
'Web 端的 dark mode 先跟随系统,Desktop 端继续保留手动切换。',
{ senderId: 'member-echo', senderName: 'Echo' }
),
sourceMessage(
'msg-url-1',
'conv-engineering',
base + 360,
'排障说明在 https://docs.example.invalid/guide/image-export?from=wechat ,不要把真实日志贴到公开 issue。',
{ senderId: 'member-echo', senderName: 'Echo' }
),
sourceMessage(
'msg-file-1',
'conv-filehelper',
base + 420,
'已上传 release-checklist-v2.1.9.xlsx,发布前把 macOS 和 Windows 两栏都勾完。',
{
senderId: 'self-fixture',
senderName: '我',
attachment: { name: 'release-checklist-v2.1.9.xlsx', kind: 'file', sizeBytes: 20480 }
}
),
sourceMessage(
'msg-tech-mcp',
'conv-engineering',
base + 480,
'MCP Reader 只暴露只读查询;写入操作必须经过本地确认,不能让 Agent 直接改微信数据。',
{ senderId: 'member-q', senderName: 'Q' }
),
sourceMessage(
'msg-tech-react',
'conv-engineering',
base + 540,
'React 列表先做虚拟滚动,Electron 主进程不要把十万条消息一次性发给 renderer。',
{ senderId: 'member-q', senderName: 'Q' }
),
sourceMessage(
'msg-tech-sqlite',
'conv-engineering',
base + 600,
'SQLite FTS5 的 trigram 对中文子串更友好,但短词仍要有精确匹配补偿。',
{ senderId: 'member-lan', senderName: '蓝图同学' }
),
sourceMessage(
'msg-number-email-path',
'conv-operations',
base + 660,
'工单 48291 请发给 fixture@example.invalid;测试附件放到 /tmp/wechat-fixture/export-preview/,不要使用个人目录。',
{ senderId: 'member-ops', senderName: '运营小组' }
),
sourceMessage('msg-short-1', 'conv-family', base + 720, '收到,明早十点。', {
senderId: 'member-yu',
senderName: '小雨'
}),
sourceMessage('msg-short-noise', 'conv-family', base + 780, '好的,晚安。', {
senderId: 'member-yu',
senderName: '小雨'
}),
sourceMessage('msg-voice-long-1', 'conv-project-sync', base + 840, '[语音消息]', {
senderId: 'member-voice',
senderName: '语音同学',
kind: 'voice',
voiceTranscript:
'刚才同步一下长语音结论:本周不做向量检索,也不新增记忆页面。先把现有问问微信的关键词检索放进独立 Knowledge Worker,索引只读取原始数据库,结果必须保留 messageId、会话、发送人和时间,异常时继续使用旧搜索。'
}),
sourceMessage(
'msg-decision-1',
'conv-project-sync',
base + 900,
'决定先上 FTS,不接 Embedding:先验证中文、文件名和技术词的召回,再考虑下一阶段。',
{ senderId: 'member-voice', senderName: '语音同学' }
),
sourceMessage(
'msg-url-noise',
'conv-engineering',
base + 960,
'本周会议链接仍然走内部日历,不要混在发布文档里。',
{ senderId: 'member-echo', senderName: 'Echo' }
),
sourceMessage(
'msg-file-noise',
'conv-filehelper',
base + 1020,
'旧版 release-note.txt 仅供历史核对,不要再上传。',
{ senderId: 'self-fixture', senderName: '我' }
),
sourceMessage(
'msg-long-text-1',
'conv-project-sync',
base + 1080,
'补充记录:当索引仍在建立或 Worker 发生异常,界面行为不能中断。主进程需要保留旧关键词检索作为临时回退,但 renderer 不应重新批量加载全部会话消息。等索引完成后,Evidence 应统一由知识库返回,并能跳回原聊天。',
{ senderId: 'member-lan', senderName: '蓝图同学' }
)
]
const cases: RealisticBenchmarkCase[] = [
{
id: 'cn-continuous',
category: 'chinese-continuous',
question: '图片导出失败时应该怎样避免用户误以为图片丢失?',
searchTerms: ['原图链接', '缩略图'],
expectedMessageIds: ['msg-cn-continuous-1']
},
{
id: 'cn-short',
category: 'chinese-short',
question: '周六谁带咖啡?',
searchTerms: ['周六见', '咖啡'],
expectedMessageIds: ['msg-cn-short-1']
},
{
id: 'person-name',
category: 'person-name',
question: '林澈最近补了什么?',
searchTerms: ['林澈', '签名检查'],
expectedMessageIds: ['msg-person-1']
},
{
id: 'mixed-language',
category: 'mixed-language',
question: 'dark mode 在 Web 和 Desktop 分别怎么处理?',
searchTerms: ['dark mode', 'Desktop'],
expectedMessageIds: ['msg-mixed-1']
},
{
id: 'url',
category: 'url',
question: '图片导出排障文档的网址是什么?',
searchTerms: ['docs.example.invalid/guide/image-export'],
expectedMessageIds: ['msg-url-1']
},
{
id: 'file-name',
category: 'file-name',
question: '发布检查表文件叫什么?',
searchTerms: ['release-checklist-v2.1.9.xlsx'],
expectedMessageIds: ['msg-file-1']
},
{
id: 'technical-mcp',
category: 'technical-term',
question: 'MCP Reader 的写入限制是什么?',
searchTerms: ['MCP Reader', '只读查询'],
expectedMessageIds: ['msg-tech-mcp']
},
{
id: 'technical-react-electron',
category: 'technical-term',
question: 'React 和 Electron 的大量消息处理原则是什么?',
searchTerms: ['React', 'Electron'],
expectedMessageIds: ['msg-tech-react']
},
{
id: 'technical-sqlite',
category: 'technical-term',
question: 'SQLite 的中文全文检索要选什么?',
searchTerms: ['SQLite FTS5', 'trigram'],
expectedMessageIds: ['msg-tech-sqlite']
},
{
id: 'number-email-path',
category: 'number-email-path',
question: '工单 48291 的邮箱和测试附件目录在哪?',
searchTerms: ['48291', 'fixture@example.invalid', '/tmp/wechat-fixture/export-preview'],
expectedMessageIds: ['msg-number-email-path']
},
{
id: 'short-message',
category: 'short-message',
question: '约的是几点?',
searchTerms: ['十点'],
expectedMessageIds: ['msg-short-1']
},
{
id: 'long-voice',
category: 'long-voice',
question: '长语音里对 Knowledge Worker 和 fallback 的要求是什么?',
searchTerms: ['Knowledge Worker', '旧搜索'],
expectedMessageIds: ['msg-voice-long-1']
},
{
id: 'decision',
category: 'long-voice',
question: '为什么暂时不接 Embedding',
searchTerms: ['不接 Embedding', 'FTS'],
expectedMessageIds: ['msg-decision-1']
},
{
id: 'fallback',
category: 'long-voice',
question: '索引未完成时搜索如何处理?',
searchTerms: ['Worker 发生异常', '旧关键词检索'],
expectedMessageIds: ['msg-long-text-1']
}
]
const grouped = new Map<string, KnowledgeSourceMessage[]>()
for (const item of messages) {
const current = grouped.get(item.conversationId) || []
current.push(item)
grouped.set(item.conversationId, current)
}
return {
conversations: Array.from(grouped.entries()).map(([conversationId, source]) => ({
conversationId,
completeSnapshot: true,
messages: source
})),
cases
}
}