mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
fix: 修复 AI 检索 Evidence 覆盖与加载更多问题
修复问一问微信查询遗漏群聊的问题 区分会话覆盖与发送者覆盖策略 新增问一问微信加载更多功能
This commit is contained in:
@@ -242,6 +242,17 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
||||
timestamp: 1_785_900_000_000 + index,
|
||||
text: `证据 ${index + 1}`
|
||||
})),
|
||||
evidenceCollection: Array.from({ length: 16 }, (_, index) => ({
|
||||
id: `E${index + 1}`,
|
||||
conversationId: 'fixture-contact',
|
||||
conversationName: '测试会话',
|
||||
conversationType: 'user',
|
||||
messageId: `collection-message-${index + 1}`,
|
||||
sender: `发送者 ${index + 1}`,
|
||||
senderId: `collection-sender-${index + 1}`,
|
||||
timestamp: 1_785_900_000_000 + index,
|
||||
text: `扩展证据 ${index + 1}`
|
||||
})),
|
||||
aggregation: {
|
||||
messageCount: 8,
|
||||
peopleCount: 1,
|
||||
@@ -279,6 +290,11 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
||||
const card = screen.getByText('E7 · 发送者 7').closest('article')
|
||||
await waitFor(() => expect(card).toHaveClass('focus-flash'))
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' })
|
||||
|
||||
const loadMore = screen.getByRole('button', { name: '加载更多证据' })
|
||||
await userEvent.click(loadMore)
|
||||
expect(screen.getByText('E9 · 发送者 9')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '加载更多证据' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the submitted result title stable while drafting a new question and clears it from 新问题', async () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('Final Evidence builder', () => {
|
||||
|
||||
expect(result.candidateCount).toBe(16)
|
||||
expect(result.evidence).toHaveLength(8)
|
||||
expect(result.collection).toHaveLength(16)
|
||||
expect(result.evidence.map((item) => item.id)).toEqual([
|
||||
'E1',
|
||||
'E2',
|
||||
@@ -71,6 +72,40 @@ describe('Final Evidence builder', () => {
|
||||
expect(result.evidence[0]).toMatchObject({ id: 'E1', sourceKind: 'voice' })
|
||||
})
|
||||
|
||||
it('covers different conversations before filling remaining relevance slots', () => {
|
||||
const candidates = Array.from({ length: 24 }, (_, index) =>
|
||||
candidate(index + 1, {
|
||||
conversationId: `conversation-${Math.floor(index / 2) + 1}`,
|
||||
conversationName: `群聊 ${Math.floor(index / 2) + 1}`,
|
||||
score: -(index + 1)
|
||||
})
|
||||
)
|
||||
|
||||
const result = buildFinalEvidence(candidates, 8, { strategy: 'conversation_coverage' })
|
||||
|
||||
expect(result.evidence).toHaveLength(8)
|
||||
expect(new Set(result.evidence.map((item) => item.conversationId)).size).toBe(8)
|
||||
expect(new Set(result.evidence.map((item) => item.conversationId))).toEqual(
|
||||
new Set(Array.from({ length: 8 }, (_, index) => `conversation-${12 - index}`))
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps sender coverage separate from conversation coverage', () => {
|
||||
const candidates = Array.from({ length: 12 }, (_, index) =>
|
||||
candidate(index + 1, {
|
||||
conversationId: `conversation-${Math.floor(index / 3) + 1}`,
|
||||
senderId: `sender-${Math.floor(index / 2) + 1}`,
|
||||
sender: `成员 ${Math.floor(index / 2) + 1}`,
|
||||
score: -(index + 1)
|
||||
})
|
||||
)
|
||||
|
||||
const result = buildFinalEvidence(candidates, 8, { strategy: 'sender_coverage' })
|
||||
|
||||
expect(result.evidence).toHaveLength(8)
|
||||
expect(new Set(result.evidence.map((item) => item.senderId)).size).toBe(6)
|
||||
})
|
||||
|
||||
it('removes citations which do not resolve to Final Evidence', () => {
|
||||
const evidence = buildFinalEvidence([candidate(1), candidate(2)], 8).evidence
|
||||
const result = sanitizeAnswerCitations('杨伟提到健身。[E1] 另有无效来源。[E10][E23]', evidence)
|
||||
|
||||
@@ -277,6 +277,90 @@ describe('AiSearchPipelineService', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps single-chat matches out of a global group lookup and covers groups in Final Evidence', async () => {
|
||||
const groups = Array.from({ length: 10 }, (_, index) => ({
|
||||
md5: `group-${index + 1}`,
|
||||
m_nsUsrName: `group-${index + 1}@chatroom`,
|
||||
m_nsNickName: `测试群 ${index + 1}`,
|
||||
type: 'group' as const
|
||||
}))
|
||||
listContactsAsync.mockResolvedValue([
|
||||
...groups,
|
||||
{
|
||||
md5: 'direct-contact',
|
||||
m_nsUsrName: 'wxid_direct',
|
||||
m_nsNickName: '单聊联系人',
|
||||
type: 'user' as const
|
||||
}
|
||||
])
|
||||
knowledge.search.mockResolvedValue({
|
||||
source: 'knowledge',
|
||||
state: 'ready',
|
||||
indexedMessageCount: 2_000,
|
||||
indexedChunkCount: 300,
|
||||
totalMessages: 2_000,
|
||||
evidence: [
|
||||
{
|
||||
chunkId: 'direct-chunk',
|
||||
conversationId: 'direct-contact',
|
||||
startTime: 1785900000000,
|
||||
endTime: 1785900000000,
|
||||
messageId: 'direct-message',
|
||||
sender: '单聊联系人',
|
||||
senderId: 'direct-sender',
|
||||
timestamp: 1785900000000,
|
||||
messageIds: ['direct-message'],
|
||||
text: 'WechatExplorer',
|
||||
score: -1
|
||||
},
|
||||
...groups.map((group, index) => ({
|
||||
chunkId: `group-chunk-${index + 1}`,
|
||||
conversationId: group.md5,
|
||||
startTime: 1785899000000 - index,
|
||||
endTime: 1785899000000 - index,
|
||||
messageId: `group-message-${index + 1}`,
|
||||
sender: `群成员 ${index + 1}`,
|
||||
senderId: `group-sender-${index + 1}`,
|
||||
timestamp: 1785899000000 - index,
|
||||
messageIds: [`group-message-${index + 1}`],
|
||||
text: 'WechatExplorer',
|
||||
score: -(index + 2)
|
||||
}))
|
||||
]
|
||||
})
|
||||
aiProvider.chat.mockReset()
|
||||
aiProvider.chat
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: '{"action":"tool","tool":"search_messages","arguments":{"query":"WechatExplorer"}}'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: '{"action":"finalize","reason":"已覆盖多个群聊"}'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: '多个群聊提到过 WechatExplorer。[E1]'
|
||||
})
|
||||
|
||||
const service = new AiSearchPipelineService(knowledge as never, aiProvider as never)
|
||||
const result = await service.run(
|
||||
{
|
||||
requestId: 'global-group-coverage',
|
||||
text: '哪个群说过 WechatExplorer',
|
||||
scope: 'global',
|
||||
range: '30d'
|
||||
},
|
||||
() => undefined
|
||||
)
|
||||
|
||||
expect(result.plan.intent).toBe('global_group_topic_search')
|
||||
expect(result.evidence).toHaveLength(8)
|
||||
expect(result.evidence.every((item) => item.conversationType === 'group')).toBe(true)
|
||||
expect(new Set(result.evidence.map((item) => item.conversationId)).size).toBe(8)
|
||||
expect(result.evidence.some((item) => item.conversationId === 'direct-contact')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps real evidence when the answer model fails', async () => {
|
||||
aiProvider.chat.mockReset()
|
||||
aiProvider.chat
|
||||
@@ -530,7 +614,11 @@ describe('AiSearchPipelineService', () => {
|
||||
expect(knowledge.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
terms: [],
|
||||
conversationIds: ['zhongtian-contact'],
|
||||
conversationIds: expect.arrayContaining([
|
||||
'zhongtian-contact',
|
||||
'wxid_zhongtian',
|
||||
'Chat_zhongtian-contact'
|
||||
]),
|
||||
startTime: expect.any(Number)
|
||||
})
|
||||
)
|
||||
@@ -640,7 +728,11 @@ describe('AiSearchPipelineService', () => {
|
||||
expect(knowledge.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
terms: ['健身'],
|
||||
conversationIds: ['zhongtian-contact'],
|
||||
conversationIds: expect.arrayContaining([
|
||||
'zhongtian-contact',
|
||||
'wxid_zhongtian',
|
||||
'Chat_zhongtian-contact'
|
||||
]),
|
||||
startTime: expect.any(Number)
|
||||
})
|
||||
)
|
||||
@@ -1119,7 +1211,14 @@ describe('AiSearchPipelineService', () => {
|
||||
retrieval: { conversationId: 'selected-contact' }
|
||||
})
|
||||
expect(knowledge.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ conversationIds: ['selected-contact'], terms: [] })
|
||||
expect.objectContaining({
|
||||
conversationIds: expect.arrayContaining([
|
||||
'selected-contact',
|
||||
'wxid_selected',
|
||||
'Chat_selected-contact'
|
||||
]),
|
||||
terms: []
|
||||
})
|
||||
)
|
||||
expect(aiProvider.chat.mock.calls[0]?.[0][1].content).toContain('conversation-1')
|
||||
expect(result.agent.trace).not.toContainEqual(
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('AI search natural-language time ranges', () => {
|
||||
|
||||
it('classifies global topics and bare conversation names without turning names into FTS terms', () => {
|
||||
expect(buildLocalAiSearchPlan('最近谁聊过 MCP?')).toMatchObject({
|
||||
intent: 'global_topic_search',
|
||||
intent: 'global_sender_topic_search',
|
||||
topicQuery: 'MCP',
|
||||
keywords: ['MCP']
|
||||
})
|
||||
@@ -95,6 +95,26 @@ describe('AI search natural-language time ranges', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies group conversation questions separately from sender questions', () => {
|
||||
for (const question of [
|
||||
'哪个群聊过 WechatExplorer',
|
||||
'哪些群聊过 WechatExplorer',
|
||||
'哪些群讨论过 WechatExplorer',
|
||||
'哪个群说过 WechatExplorer'
|
||||
]) {
|
||||
expect(buildLocalAiSearchPlan(question)).toMatchObject({
|
||||
intent: 'global_group_topic_search',
|
||||
topicQuery: 'WechatExplorer',
|
||||
keywords: ['WechatExplorer']
|
||||
})
|
||||
}
|
||||
expect(buildLocalAiSearchPlan('谁聊过 WechatExplorer')).toMatchObject({
|
||||
intent: 'global_sender_topic_search',
|
||||
topicQuery: 'WechatExplorer',
|
||||
keywords: ['WechatExplorer']
|
||||
})
|
||||
})
|
||||
|
||||
it('matches an explicitly mentioned nickname when the user omits punctuation', () => {
|
||||
expect(includesExplicitAiSearchAlias('我和中田健身弘毅最近聊了什么?', '中田健身-弘毅')).toBe(
|
||||
true
|
||||
|
||||
@@ -114,6 +114,20 @@ describe('KnowledgeSearchService legacy fallback', () => {
|
||||
await service.dispose()
|
||||
})
|
||||
|
||||
it('accepts username and Chat_<md5> aliases in the legacy fallback scope', async () => {
|
||||
const service = new KnowledgeSearchService('/tmp/wxe-knowledge-fallback', '/missing-worker.js')
|
||||
const result = await service.search({
|
||||
text: 'Knowledge Worker fallback',
|
||||
terms: ['Knowledge Worker', 'fallback'],
|
||||
conversationIds: ['fixture-contact', 'Chat_fixture-conversation'],
|
||||
limit: 10
|
||||
})
|
||||
|
||||
expect(listMessagesAsync).toHaveBeenCalledWith('fixture-conversation', undefined, undefined)
|
||||
expect(result.evidence).toHaveLength(1)
|
||||
await service.dispose()
|
||||
})
|
||||
|
||||
it('hydrates a cached voice transcript and incrementally indexes only its conversation', async () => {
|
||||
chatState.ready = true
|
||||
chatState.accountId = 'C:/fixtures/account-a'
|
||||
|
||||
Reference in New Issue
Block a user