feat: 重构问问微信并完善知识库增量检索

统一问问微信与 Agent Hub 的查询链路
完善查询覆盖度、新鲜度和部分结果表达,避免索引滞后产生错误结论
基于会话实现真正的增量追新与历史补齐
支持后台同步、取消恢复、重启续传以及同步期间继续查询
优化知识库跨会话检索、同步状态、进度展示和侧栏布局
This commit is contained in:
Wxw-Gu
2026-09-11 17:44:48 +08:00
parent 0c4932d740
commit ab3b3731b7
69 changed files with 8317 additions and 898 deletions
+118
View File
@@ -0,0 +1,118 @@
import { describe, expect, it } from 'vitest'
import {
KNOWLEDGE_FRESHNESS_TOLERANCE_MS,
isKnowledgeFresh,
type KnowledgePassProgress,
type KnowledgeRuntimeStatus
} from '../../src/shared/knowledge'
import { formatIndexDate, knowledgeIsStale, knowledgeStateLabel } from '../../src/renderer/src/components/search/searchFormatters'
type StatusPatch = Partial<
Pick<KnowledgeRuntimeStatus, 'state' | 'indexLatestAt' | 'sourceLatestAt' | 'pass'>
>
const makePass = (phase: KnowledgePassProgress['phase']): KnowledgePassProgress => ({
phase,
cancellable: phase !== 'idle',
startedAt: 0,
scannedMessages: 0,
indexedMessages: 0,
processedConversations: 0,
totalConversations: 0,
skippedConversations: 0,
mainLoopLagMs: 0
})
const status = (patch: StatusPatch): KnowledgeRuntimeStatus =>
({
accountId: 'fixture',
state: 'ready',
indexedMessageCount: 1,
indexedChunkCount: 1,
sourceMessageCount: 1,
processedMessages: 1,
totalMessages: 1,
estimatedRemainingMs: null,
databaseBytes: 0,
walBytes: 0,
shmBytes: 0,
indexLatestAt: null,
sourceLatestAt: null,
...patch
})
describe('isKnowledgeFresh(READY 与 FRESH 是两个概念)', () => {
it('索引追平源数据时算 fresh', () => {
expect(isKnowledgeFresh({ indexLatestAt: 1000, sourceLatestAt: 1000 })).toBe(true)
})
it('容差内算 fresh:吸收 Session 元数据晚于消息落库的漂移', () => {
expect(
isKnowledgeFresh({ indexLatestAt: 1000, sourceLatestAt: 1000 + KNOWLEDGE_FRESHNESS_TOLERANCE_MS })
).toBe(true)
})
it('明显落后时算 stale', () => {
expect(
isKnowledgeFresh({ indexLatestAt: 1000, sourceLatestAt: 1000 + KNOWLEDGE_FRESHNESS_TOLERANCE_MS + 1 })
).toBe(false)
})
it('任一侧口径缺失时返回 null(无法判定,不冒充 fresh)', () => {
expect(isKnowledgeFresh({ indexLatestAt: null, sourceLatestAt: 1000 })).toBeNull()
expect(isKnowledgeFresh({ indexLatestAt: 1000, sourceLatestAt: null })).toBeNull()
})
})
describe('Knowledge UI 状态文案', () => {
const indexLatestAt = new Date('2026-08-26T11:37:24+08:00').getTime()
const sourceLatestAt = new Date('2026-09-11T11:57:24+08:00').getTime()
it('索引落后时显示「可用 · 待追新」,不得显示「已同步」', () => {
const label = knowledgeStateLabel(status({ indexLatestAt, sourceLatestAt }))
// READY ≠ FRESH:落后时既要说"还能用",又要如实说"还没追平"。
expect(label).toBe('可用 · 待追新')
expect(label).not.toBe('已同步')
expect(label).not.toContain('已追至最新')
expect(knowledgeIsStale(status({ indexLatestAt, sourceLatestAt }))).toBe(true)
// 卡片必须能给出真实的覆盖边界。
expect(formatIndexDate(indexLatestAt)).toBe('8/26')
})
it('索引真正追平源数据最新时才显示「可用 · 已追至最新」', () => {
expect(knowledgeStateLabel(status({ indexLatestAt: sourceLatestAt, sourceLatestAt }))).toBe(
'可用 · 已追至最新'
)
expect(knowledgeIsStale(status({ indexLatestAt: sourceLatestAt, sourceLatestAt }))).toBe(false)
})
it('无法判定 freshness 时不能把"不确定"说成已经追平', () => {
// 安全侧:无法确认就按"可能还没追平"表达,绝不说「已追至最新」。
expect(knowledgeStateLabel(status({ indexLatestAt, sourceLatestAt: null }))).toBe(
'可用 · 待追新'
)
expect(knowledgeIsStale(status({ indexLatestAt, sourceLatestAt: null }))).toBe(false)
})
it('同步中必须区分「正在追新」与「正在补齐历史」', () => {
expect(knowledgeStateLabel(status({ state: 'syncing', indexLatestAt, sourceLatestAt }))).toBe(
'可用 · 正在追新'
)
// 首轮全量建立历史时不能谎称"只是在追新"。
expect(
knowledgeStateLabel(
status({
state: 'syncing',
indexLatestAt,
sourceLatestAt,
pass: makePass('full')
})
)
).toBe('可用 · 正在补齐历史')
})
it('取消与失败都不能丢掉「可用」前缀(已建立的分片仍然能查)', () => {
expect(knowledgeStateLabel(status({ state: 'cancelled' }))).toBe('可用 · 同步已取消')
expect(knowledgeStateLabel(status({ state: 'error' }))).toBe('可用 · 更新失败')
})
})