From 0f270366aa82b457585adf28f283fd4fb53d9cfd Mon Sep 17 00:00:00 2001 From: Wxw-Gu <84499436+Wxw-Gu@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:39:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E6=96=87=E5=AD=97?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E6=8C=89=E6=97=B6=E9=97=B4=E5=88=86=E6=AE=B5?= =?UTF-8?q?=E4=BC=98=E5=85=88=E5=A4=84=E7=90=86=E6=9C=80=E8=BF=91=E5=9B=BE?= =?UTF-8?q?=E7=89=87=20=20-=20=E9=A6=96=E6=AC=A1=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E5=85=88=E5=A4=84=E7=90=86=E6=9C=80=E8=BF=91=207=20=E5=A4=A9?= =?UTF-8?q?=EF=BC=8C=E5=86=8D=E4=BE=9D=E6=AC=A1=E5=9B=9E=E6=BA=AF=2030=20?= =?UTF-8?q?=E5=A4=A9=20/=20=E8=BF=91=E4=B8=80=E5=B9=B4=20/=20=E6=9B=B4?= =?UTF-8?q?=E6=97=A9=E5=8E=86=E5=8F=B2=20=20-=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8E=E6=96=B0=E5=88=B0=E7=9A=84=E5=9B=BE=E7=89=87=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E8=A1=A5=E9=BD=90=EF=BC=8C=E4=B8=8D=E5=8F=97=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=9B=9E=E5=A1=AB=E5=BD=B1=E5=93=8D=20=20-=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=BA=A6=E5=A2=9E=E5=8A=A0=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=EF=BC=8C=E5=8F=AF=E5=8C=BA=E5=88=86=E3=80=8C?= =?UTF-8?q?=E6=9C=80=E8=BF=91=E5=B7=B2=E5=AE=8C=E6=95=B4=E3=80=8D=E4=B8=8E?= =?UTF-8?q?=E3=80=8C=E6=9B=B4=E6=97=A9=E4=BB=8D=E5=9C=A8=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/wcdb/win32/x64/wcdb_api.dll | Bin 357376 -> 382976 bytes src/main/index.ts | 25 +- src/main/services/chat-service.ts | 49 +- src/main/services/image-text-index-service.ts | 587 +++++++++++++++--- src/main/services/image-text-index-store.ts | 80 ++- src/main/services/local-query-api-service.ts | 16 +- src/main/wcdb4-client.ts | 82 ++- .../components/search/ImageTextIndexCard.tsx | 51 +- src/shared/image-text-index.ts | 322 ++++++++++ src/shared/local-query-api.ts | 8 + .../component/image-text-index-card.test.tsx | 102 +++ ...text-index-checkpoint-and-coverage.test.ts | 200 ++++-- .../image-text-index-image-query.test.ts | 26 +- .../image-text-index-progress-notify.test.ts | 9 +- .../image-text-index-recent-first.test.ts | 543 ++++++++++++++++ 15 files changed, 1917 insertions(+), 183 deletions(-) create mode 100644 tests/integration/image-text-index-recent-first.test.ts diff --git a/resources/wcdb/win32/x64/wcdb_api.dll b/resources/wcdb/win32/x64/wcdb_api.dll index 530f6edebaa3571d7d22e9b5e29ec5e48b17c546..f1f98150e8277838a685cb4e27ce339da78a3db0 100644 GIT binary patch delta 159532 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

Y&EP5bpZb;-XLo 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⁡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 z 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 delta 138220 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 diff --git a/src/main/index.ts b/src/main/index.ts index 8be05fb..9464d55 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -148,7 +148,10 @@ import type { AppLogEntry } from '../shared/app-log' import { appUpdateService } from './services/app-update-service' import { clearCache, getCacheSummary, openKnowledgeDirectory } from './services/cache-service' import { imageTextIndexService } from './services/image-text-index-service' -import type { ImageTextIndexStartOptions } from '../shared/image-text-index' +import { + IMAGE_TEXT_SEGMENT_MESSAGE_LIMIT, + type ImageTextIndexStartOptions +} from '../shared/image-text-index' import type { CacheClearScope } from './services/cache-service' import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-archive-service' import { VideoAssetService } from './video-asset-service' @@ -703,12 +706,20 @@ app.whenReady().then(async () => { * 全量读取一个 20 万条消息的会话实测要 15s 以上,而其中 99% 以上的行 * 图片索引根本不看 —— 那是数据边界错了,不是 OCR 慢。 */ - listImageMessages: (conversationId) => - chat.listImageMessagesAsync(conversationId, undefined, 'image-text-index'), - countConversationImages: (conversationId, sinceMs) => - chat.countImageMessagesAsync(conversationId, sinceMs), - imageWatermark: (conversationId, sinceMs) => - chat.imageConversationWatermarkAsync(conversationId, sinceMs), + listImageMessages: (conversationId, window) => + chat.listImageMessagesAsync( + conversationId, + { + ...(window?.sinceMs !== undefined ? { sinceMs: window.sinceMs } : {}), + ...(window?.beforeMs !== undefined ? { beforeMs: window.beforeMs } : {}), + limit: IMAGE_TEXT_SEGMENT_MESSAGE_LIMIT + }, + 'image-text-index' + ), + countConversationImages: (conversationId, range) => + chat.countImageMessagesAsync(conversationId, range), + imageWatermark: (conversationId, range) => + chat.imageConversationWatermarkAsync(conversationId, range), decryptService: () => ensureImageDecryptService(), capability: () => systemOcrService.getCapability(), /** diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index 427d4ec..5fa81a5 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -17,6 +17,7 @@ import { import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service' import type { ExportImageQuality } from '../../shared/image-quality' import type { ImageMessageCountProbe } from '../../shared/image-text-index' +import { imageTextWindowToSeconds } from '../../shared/image-text-index' import { wcdbDebugLog } from '../wcdb-debug' import { buildContactSearchIndex, @@ -852,23 +853,45 @@ export async function listMessagesAsync( */ export async function listImageMessagesAsync( userMd5: string, + window: { + /** 闭下界(epoch ms)。 */ + sinceMs?: number + /** 开上界(epoch ms)。 */ + beforeMs?: number + limit?: number + } = {}, requestId = '', caller: ListMessagesCaller = 'unknown' ): Promise { if (!dbRef) return [] const perf = emptyPerf(caller, requestId || nextListMessagesRequestId()) const totalStartedAt = Date.now() + // ms 半开区间 → 秒闭区间。换算只有共享契约里那一处实现。 + const { sinceSec, beforeSecInclusive } = imageTextWindowToSeconds(window) + const startTime = sinceSec ?? undefined + const endTime = beforeSecInclusive ?? undefined try { const rawReadStartedAt = Date.now() - const rawMessages = await dbRef - .getWcdb4Client() - .listImageMessagesAsync(userMd5, { requestId: perf.requestId }) + const rawMessages = await dbRef.getWcdb4Client().listImageMessagesAsync(userMd5, { + ...(window.sinceMs !== undefined ? { sinceMs: window.sinceMs } : {}), + ...(window.beforeMs !== undefined ? { beforeMs: window.beforeMs } : {}), + ...(window.limit !== undefined ? { limit: window.limit } : {}), + // recent-first:同一时间窗内**新的图片先处理**。 + order: 'desc', + requestId: perf.requestId + }) perf.rawReadMs += Date.now() - rawReadStartedAt + /** + * 时间边界必须同时交给格式化与召回归档合并。 + * + * 少了这一步,归档合并会把**窗口之外**的撤回图片补回来 —— 于是"最近 7 天" + * 这一段会混进十年前的消息,分段窗口形同虚设。 + */ const sourceMessages = listSourceMessages( userMd5, - undefined, - undefined, - undefined, + startTime, + endTime, + window.limit !== undefined ? { limit: window.limit } : undefined, rawMessages, perf.requestId, perf @@ -880,9 +903,9 @@ export async function listImageMessagesAsync( const result = mergeRecallArchiveMessages( userMd5, sourceMessages, - undefined, - undefined, - undefined + startTime, + endTime, + window.limit ) perf.sortMs += Date.now() - recallStartedAt return result @@ -933,10 +956,10 @@ export async function listMessagesForExport( */ export async function countImageMessagesAsync( userMd5: string, - sinceMs?: number + range?: number | { sinceMs?: number; beforeMs?: number } ): Promise { if (!dbRef) return { count: null, typeColumn: null, error: '微信数据库尚未就绪' } - return dbRef.getWcdb4Client().countImageMessagesAsync(userMd5, sinceMs) + return dbRef.getWcdb4Client().countImageMessagesAsync(userMd5, range) } /** @@ -947,10 +970,10 @@ export async function countImageMessagesAsync( */ export async function imageConversationWatermarkAsync( userMd5: string, - sinceMs?: number + range?: number | { sinceMs?: number; beforeMs?: number } ): Promise<{ count: number; maxLocalId: number } | null> { if (!dbRef) return null - return dbRef.getWcdb4Client().imageConversationWatermarkAsync(userMd5, sinceMs) + return dbRef.getWcdb4Client().imageConversationWatermarkAsync(userMd5, range) } export async function countVoiceMessagesAsync( diff --git a/src/main/services/image-text-index-service.ts b/src/main/services/image-text-index-service.ts index 3a4c696..9e2a8d4 100644 --- a/src/main/services/image-text-index-service.ts +++ b/src/main/services/image-text-index-service.ts @@ -17,12 +17,14 @@ import { createHash } from 'node:crypto' import { existsSync } from 'node:fs' import { DEFAULT_IMAGE_TEXT_OCR_CONCURRENCY, + IMAGE_TEXT_BACKFILL_TIER_ORDER, IMAGE_TEXT_INDEX_BATCH_SIZE, IMAGE_TEXT_INDEX_PROGRESS_INTERVAL_MS, IMAGE_TEXT_INDEX_RATE_MIN_SPAN_MS, IMAGE_TEXT_INDEX_RATE_WINDOW_MS, IMAGE_OCR_RETRIABLE_FAILURE_STATES, buildImageOcrArtifactKey, + buildImageTextBackfillSegments, imageTextProcessedPercent, isTerminalImageOcrState, resolveImageTextOcrConcurrency, @@ -30,8 +32,10 @@ import { type ImageMessageWatermark, type ImageOcrPersistedState, type ImageOcrProvenance, + type ImageTextBackfillSegment, type ImageTextIndexCountResult, type ImageTextIndexCoverage, + type ImageTextIndexPhase, type ImageTextIndexProgress, type ImageTextIndexRepairResult, type ImageTextIndexRunState, @@ -39,7 +43,8 @@ import { type ImageTextIndexStageTimings, type ImageTextIndexStartOptions, type ImageTextIndexStatus, - type ImageTextIndexStorageStats + type ImageTextIndexStorageStats, + type ImageTextTierCoverage } from '../../shared/image-text-index' import { detectSystemOcrImageFormat, @@ -86,7 +91,16 @@ export interface ImageTextIndexServiceDeps { * 由 WCDB 在 SQL 层过滤,而不是把整个会话读进来再筛。 * 缺省时回退到 `listMessages`(测试用),但生产必须接上 —— 否则大会话会拖垮一遍 pass。 */ - listImageMessages?: (conversationId: string) => Promise + listImageMessages?: ( + conversationId: string, + /** + * 时间窗(`[sinceMs, beforeMs)`,半开)。不传 = 整个会话。 + * + * recent-first 的分段计划靠它把"最近 7 天"和"更早"分开,而不是把整个会话 + * 读进来再在 JS 里筛 —— 那正是大会话跑不动的根因。 + */ + window?: { sinceMs?: number; beforeMs?: number } + ) => Promise /** * 单个会话的图片消息计数(SQL 统计,不解密)。 * @@ -94,7 +108,7 @@ export interface ImageTextIndexServiceDeps { */ countConversationImages?: ( conversationId: string, - sinceMs?: number + range?: number | { sinceMs?: number; beforeMs?: number } ) => Promise /** * 单个会话的图片消息增量水位(条数 + 最大插入序),SQL 聚合,不解密。 @@ -103,7 +117,7 @@ export interface ImageTextIndexServiceDeps { */ imageWatermark?: ( conversationId: string, - sinceMs?: number + range?: number | { sinceMs?: number; beforeMs?: number } ) => Promise decryptService?: () => ImageDecryptService | null /** 本地 OCR。 */ @@ -289,6 +303,14 @@ export class ImageTextIndexService { private counting = false private listeners = new Set<(status: ImageTextIndexStatus) => void>() private lastError: string | undefined + /** + * 当前阶段(recent-first 可见性)。 + * + * 它**不参与进度计算**:总进度永远是 `processed / totalImageMessages`。 + */ + private currentPhase: ImageTextIndexPhase = 'complete' + /** 本遍 pass 的起点,用于 `preLoop.startupMs`。 */ + private passStartedAt = 0 private startedAt: number | undefined /** 上一次清理实际重建(失效)了多少个会话的 Knowledge 索引;用于诊断与测试。 */ lastInvalidatedConversations = 0 @@ -602,6 +624,20 @@ export class ImageTextIndexService { * 它必须阻断 `complete` —— 否则 Query Agent 会拿着"覆盖完整"去回答"没有"。 */ const systemicFailure = processed > 0 && indexed === 0 && empty === 0 && missing === 0 + /** + * 覆盖完整性。 + * + * 分母取自流水线**真实走过**的集合时(`useScan`),不再要求 `counted.complete`: + * 那个标志表达的是"`countImageMessages()` 把每个会话都数上了",而进度现在已经 + * 不用那个分母了。继续要求它,会让一个**已经跑完**的索引因为"某个会话数不上" + * 而永远停在"部分完成"。 + */ + const complete = + total > 0 && + runtimeUnavailable === 0 && + !systemicFailure && + processed >= total && + (useScan || (counted !== null && counted.complete)) return { totalImageMessages: total, processed, @@ -613,22 +649,11 @@ export class ImageTextIndexService { pending: Math.max(0, total - processed - runtimeUnavailable), // 从未统计过总数 → 不算"已建立":不知道分母就不允许声称覆盖。 established: counted !== null && (processed > 0 || runtimeUnavailable > 0), - /** - * 覆盖完整性。 - * - * 分母取自流水线**真实走过**的集合时(`useScan`),不再要求 `counted.complete`: - * 那个标志表达的是"`countImageMessages()` 把每个会话都数上了",而进度现在已经 - * 不用那个分母了。继续要求它,会让一个**已经跑完**的索引因为"某个会话数不上" - * 而永远停在"部分完成"。 - */ - complete: - total > 0 && - runtimeUnavailable === 0 && - !systemicFailure && - processed >= total && - (useScan || (counted !== null && counted.complete)), + complete, systemicFailure, - countedAt: counted?.countedAt ?? null + countedAt: counted?.countedAt ?? null, + // recent-first 的时间维度:让调用方能回答"这一段时间能不能下确定性结论"。 + ...this.tierCoverageSnapshot(complete) } } @@ -672,7 +697,14 @@ export class ImageTextIndexService { cancellable: this.running, paused: this.runState === 'paused', ...this.rateSnapshot(coverage.processed, total), - ...(this.lastError ? { lastError: this.lastError } : {}) + ...(this.lastError ? { lastError: this.lastError } : {}), + // 阶段提示只在"真的在做这件事"时给:运行/暂停时报当前分段;整体完成时报完成。 + // 其余情况(idle / cancelled 且未完成)不给 —— 宁可不说,也不给一句过期的阶段。 + ...(this.running || this.runState === 'paused' + ? { currentPhase: this.currentPhase } + : coverage.complete + ? { currentPhase: 'complete' as ImageTextIndexPhase } + : {}) } } @@ -713,7 +745,9 @@ export class ImageTextIndexService { established: false, complete: false, systemicFailure: false, - countedAt: null + countedAt: null, + tiers: [], + coveredToMs: null }, storage: this.emptyStorage(), counting: this.counting @@ -1265,6 +1299,22 @@ export class ImageTextIndexService { return { started: true, state: this.runState } } + /** + * 一次索引 pass。 + * + * recent-first 的核心:**外层是时间分段,内层才是会话**。 + * + * 为什么不能只把会话列表按"最近活跃"排序就完事:那只保证"先把 A 群全部历史扫完", + * 而用户要的是"最近这几天的图片,不管在哪个群,都先能搜"。所以必须分段优先 —— + * 先把最近 7 天在所有会话上横着扫完,再退到下一个更老的分段。 + * + * 四条不可动摇的性质: + * 1. **锚点固定**:backfill 的 `anchorMs` 一旦落盘就不再变,分段边界因此稳定, + * 不会"跑几小时后 7 天窗口往前挪"。 + * 2. **新消息永远优先**:比锚点更新的图片由"增量补齐"负责,且它在每个调度点之前跑。 + * 3. **已完成的分段不重扫**;已 terminal 的 binding 永远跳过(不重复 OCR)。 + * 4. **进度不骗人**:总进度始终是 `processed / total`,分段只提供阶段文案。 + */ private async runPass(options: ImageTextIndexStartOptions): Promise { const store = this.ensureStore() if (!store) { @@ -1276,6 +1326,8 @@ export class ImageTextIndexService { // 进入图片流水线之前的一次性成本:单列出来,避免被摊进"每张图片"。 const preLoopStartedAt = this.now() + // `startupMs` 的参照点。逐会话逻辑被抽成独立方法之后,它必须放在实例上。 + this.passStartedAt = preLoopStartedAt const capability = (await this.deps.capability?.()) ?? null if (capability && !capability.available) { this.lastError = '当前系统不支持本地图片文字识别' @@ -1323,16 +1375,261 @@ export class ImageTextIndexService { * `startupMs` 的取样点必须是"第一张图片进入流水线的那一刻",不能在这里就记 —— * 否则会话级的准备成本会漏在外面,而那正是"单张很快、整遍很慢"的差额来源之一。 */ - let preLoopCaptured = false + const preLoopState = { captured: false } const scanState = store.readScanState() - let budget = options.messageLimit && options.messageLimit > 0 ? options.messageLimit : Infinity + const budget = { + remaining: options.messageLimit && options.messageLimit > 0 ? options.messageLimit : Infinity + } + /** + * 受控窗口(用于小样本验证):只跑这一个窗口,**不写任何分段状态**, + * 因此同一个窗口可以反复跑(checkpoint 是围绕全量集合建立的,混用会让"跳过" + * 变得不可解释)。 + */ + const windowed = Boolean(options.sinceMs && options.sinceMs > 0) + + const plan = this.resolveBackfillPlan(store, options) + + /** + * 升级场景:老版本可能已经把**全量**图片索引建完了。此时库里没有任何分段信息, + * 应当直接落成"全部完成",不重新回填 —— 用户已经拥有的东西不能被降级。 + * + * 但**不能只看库里自称的进度**:`readScanProgress()` 记的是"上一次跑完时留下了多少", + * 它不知道源里后来又新增了图片。只信它就会把"库里自称已完成、源侧其实有新增" + * 误判成"全部完成",于是那些新增的图片永远不会被索引。 + * + * 所以必须先向**源侧**核实:逐会话比对插入序水位,只有确实没有新内容时才算数。 + * 这次核实本身就是增量补齐(水位没涨的会话一条 SQL 就跳过),不会白跑。 + */ + if (!windowed && plan.created) { + const existing = this.coverageFromCounts(store.countByState()) + if (existing.complete) { + const verified = await this.processWindow({ + window: null, + incremental: true, + contacts, + provenance, + store, + budget, + scanState, + preLoopState, + marksConversationDone: true + }) + if (!verified.interrupted && !verified.truncated && verified.imageCount === 0) { + for (const tier of IMAGE_TEXT_BACKFILL_TIER_ORDER) { + store.writeBackfillTierState(tier, 'complete') + } + store.writeBackfillCoveredToMs(this.now()) + this.currentPhase = 'complete' + this.runState = 'completed' + this.running = false + await this.emit() + return + } + } + } + const lastSegment = plan.segments[plan.segments.length - 1] + /** + * 增量补齐跑过没有(本次 pass)。 + * + * 它必须**独立于"本分段要不要扫"**:所有分段都已完成时,仍然需要一次补齐, + * 否则"全部建完之后新到的图片"就再也没人接。 + */ + let swept = false + + for (const segment of plan.segments) { + if (this.cancelRequested || this.pauseRequested || budget.remaining <= 0) break + + const tierState = windowed ? null : store.readBackfillState().tierStates[segment.tier] + // 已完成的分段不再重扫 —— restart / resume 因此从**当前**分段继续, + // 而不是回到最近 7 天把已经做过的事再做一遍。 + const shouldScan = windowed || tierState !== 'complete' + + /** + * 增量补齐:**每个调度点先跑一次**,且本 pass 至少跑一次。 + * + * 排在历史分段之前,是为了"绝不会因为正在扫十年前的历史,让今天新收到的图片排队"; + * 即使分段全部完成、本 pass 没有任何分段要扫,也仍然要跑一次。 + */ + if (!windowed && (!swept || shouldScan)) { + const interrupted = await this.sweepIncremental({ + plan, + contacts, + provenance, + store, + budget, + scanState, + preLoopState + }) + swept = true + if (interrupted) break + } + + if (!shouldScan) continue + + this.currentPhase = windowed ? 'incremental' : segment.tier + if (!windowed) store.writeBackfillTierState(segment.tier, 'running') + await this.emit() + + const scanned = await this.processWindow({ + window: { sinceMs: segment.startMs, beforeMs: segment.endMs }, + incremental: false, + contacts, + provenance, + store, + budget, + scanState, + preLoopState, + marksConversationDone: !windowed && segment === lastSegment + }) + + // 被取消 / 暂停 / 预算截断 → 这一段**没有**跑完,绝不能标成 complete。 + if (scanned.interrupted || scanned.truncated) break + + if (!windowed) store.writeBackfillTierState(segment.tier, 'complete') + await this.emit() + } + + if (this.cancelRequested) this.runState = 'cancelled' + else if (this.pauseRequested) this.runState = 'paused' + else this.runState = 'completed' + // 只有真正跑完全部分段才把阶段切成"完成";中途停下时保留当前阶段(那才是实话)。 + if (this.runState === 'completed' && !windowed) this.currentPhase = 'complete' + this.running = false + await this.emit() + } + + /** + * 解析本次 pass 的分段计划。 + * + * 计划一律由**落盘的锚点**派生:锚点不随 pass 变化,所以"跑了几小时之后 + * 7 天窗口往前漂移、进而产生重复或遗漏"在结构上就不可能发生。 + */ + private resolveBackfillPlan( + store: ImageTextIndexStore, + options: ImageTextIndexStartOptions + ): { anchorMs: number; segments: ImageTextBackfillSegment[]; created: boolean } { + if (options.sinceMs && options.sinceMs > 0) { + // 受控窗口:单段、无上界、不落任何分段状态。 + return { + anchorMs: options.sinceMs, + segments: [ + { tier: 'recent_7d', startMs: options.sinceMs, endMs: Number.POSITIVE_INFINITY } + ], + created: false + } + } + const state = store.readBackfillState() + if (state.anchorMs !== null) { + return { + anchorMs: state.anchorMs, + segments: buildImageTextBackfillSegments(state.anchorMs), + created: false + } + } + const anchorMs = this.now() + store.writeBackfillAnchor(anchorMs) + for (const tier of IMAGE_TEXT_BACKFILL_TIER_ORDER) { + store.writeBackfillTierState(tier, 'pending') + } + return { anchorMs, segments: buildImageTextBackfillSegments(anchorMs), created: true } + } + + /** + * 增量补齐:把"锚点之后新到的东西"处理掉,永远排在历史分段之前。 + * + * 窗口 = `[max(锚点, 上次补齐水位), +∞)`,只覆盖新到的东西,所以刚建计划时 + * 它在时间上是空的、连枚举都省掉。水位可用时另有一层判据:插入序没涨的会话 + * 直接跳过,涨了的会话则**去掉时间窗**读(见 `processWindow`)。 + * + * 返回 true = 被取消 / 暂停 / 预算截断。 + */ + private async sweepIncremental(input: { + plan: { anchorMs: number; segments: ImageTextBackfillSegment[] } + contacts: Array<{ md5: string; m_nsUsrName: string; type: 'user' | 'group' }> + provenance: ImageOcrProvenance + store: ImageTextIndexStore + budget: { remaining: number } + scanState: Map< + string, + { state: string; imageTotal: number; processed: number; maxLocalId: number } + > + preLoopState: { captured: boolean } + }): Promise { + const incrementalSinceMs = Math.max( + input.plan.anchorMs, + input.store.readBackfillState().coveredToMs ?? 0 + ) + // 窗口在时间上必然为空 → 直接跳过,省掉一轮枚举。 + if (this.now() - incrementalSinceMs < 1_000) return false + + const swept = await this.processWindow({ + window: { sinceMs: incrementalSinceMs }, + incremental: true, + contacts: input.contacts, + provenance: input.provenance, + store: input.store, + budget: input.budget, + scanState: input.scanState, + preLoopState: input.preLoopState, + marksConversationDone: false + }) + // 只有真的扫完(没被取消 / 暂停 / 预算截断)才推进水位,否则会漏掉没扫到的部分。 + if (!swept.interrupted && !swept.truncated && input.budget.remaining > 0) { + input.store.writeBackfillCoveredToMs(this.now()) + } + return swept.interrupted || swept.truncated + } + + /** + * 处理一个窗口(`null` = 不看时间,用于受控小样本验证)。 + * + * 每个会话先做两条**纯 SQL 聚合**:源侧水位 + 窗口内图片条数。 + * 条数为 0 就整段跳过 —— 不读消息、不解密、不写任何"完成"标记。 + */ + private async processWindow(input: { + window: { sinceMs?: number; beforeMs?: number } | null + /** + * 增量补齐模式。 + * + * - 水位**可用**且水位涨了 → 去掉时间窗读这个会话(接得住晚到的旧时间消息); + * - 水位可用且没涨 → 跳过; + * - 水位**不可用** → 按时间窗兜底重扫:宁可慢,也不允许因为判据拿不到就漏。 + */ + incremental: boolean + contacts: Array<{ md5: string; m_nsUsrName: string; type: 'user' | 'group' }> + provenance: ImageOcrProvenance + store: ImageTextIndexStore + budget: { remaining: number } + scanState: Map< + string, + { state: string; imageTotal: number; processed: number; maxLocalId: number } + > + preLoopState: { captured: boolean } + /** 本窗口扫完是否意味着该会话**全部**图片都已定态(最后一个分段)。 */ + marksConversationDone: boolean + }): Promise<{ interrupted: boolean; imageCount: number; truncated: boolean }> { + const { incremental, contacts, provenance, store, budget, scanState } = input + let imageCount = 0 + /** + * 预算被截断。 + * + * 必须与"跑完了"区分开:被截断的分段**不能**被标记成 complete —— + * 否则下一次 pass 会以"这一段已完成"跳过,被截掉的那些图片就永远不会被处理。 + */ + let truncated = false for (const contact of contacts) { - if (this.cancelRequested || this.pauseRequested) break - if (budget <= 0) break + if (this.cancelRequested || this.pauseRequested) { + return { interrupted: true, imageCount, truncated } + } + if (budget.remaining <= 0) { + truncated = true + break + } const conversationId = contact.md5 + const previous = scanState.get(conversationId) /** * 会话级准备:水位 / 计数 / 让路 / 读消息。 * @@ -1341,45 +1638,66 @@ export class ImageTextIndexService { * 而 `perImageMs` 只覆盖 batch 循环,看不到它。 */ const setupStartedAt = this.now() + /** - * 是否只处理一个时间窗口(用于小样本验证)。 + * 源侧水位:`count` + `max(local_id)`,一条 SQL 聚合(**整会话**,不带窗口)。 * - * 带窗口时**不做增量跳过**:checkpoint 是围绕全量集合建立的, - * 窗口内的图片可能从未被处理过,继续按"该会话已完成"跳过会让窗口形同虚设。 + * 增量判据用 `maxLocalId` 而不是 create_time:`local_id` 是 WCDB 行内单调的 + * 插入序,因此"撤回一张旧图 + 新增一张新图"这种总数不变的变更也能被发现, + * 而 create_time 会被"晚到的旧时间消息"骗过。 */ - const windowed = Boolean(options.sinceMs && options.sinceMs > 0) - // 增量水位 = 条数 + 最大插入序。只比条数会漏掉「撤回一张旧图 + - // 新增一张新图」这种总数不变、集合却变了的会话。 - const watermark = await (this.deps.imageWatermark?.(conversationId, options.sinceMs) ?? - Promise.resolve(null)) - const imageTotal = - watermark?.count ?? - (await this.deps.countConversationImages?.(conversationId, options.sinceMs))?.count ?? - 0 - if (imageTotal === 0) { + const watermark = await (this.deps.imageWatermark?.(conversationId) ?? + Promise.resolve(null)) + + /** + * 这一段对这个会话实际要读的时间窗。 + * + * `null` = 不看时间(整会话,新 → 旧)。 + */ + let effectiveWindow = input.window + if (incremental && watermark !== null && previous !== undefined) { + // 水位可用:没涨就代表确实没有新内容,跳过(不读消息、不查窗口)。 + if (previous.maxLocalId > 0 && watermark.maxLocalId <= previous.maxLocalId) { + this.preLoop.conversationSetupMs += this.now() - setupStartedAt + continue + } + /** + * 判据可用且涨了 → **去掉时间窗**读这个会话。 + * + * 为什么不能只读"锚点之后":`local_id` 是插入序,而 `create_time` 是业务时间, + * 两者可以不一致 —— 网络补发、消息恢复、合并转发回填都会让一条**旧时间**的 + * 消息在今天才落库。只按时间窗读就永远接不到它,用户会"搜不到明明收到过的图"。 + * 代价只落在真的发生了插入的会话上,而每一轮之后水位即被推平。 + */ + effectiveWindow = null + } + const windowArg = effectiveWindow === null ? undefined : effectiveWindow + + /** + * 窗口内是否有图片:一条 SQL COUNT。 + * + * `count === null` 是**统计失败,不是 0 张** —— 必须跳过并且不写任何完成标记, + * 否则这段会被当成"已覆盖",把数不出来谎报成没有图片。 + */ + const probe = await (this.deps.countConversationImages?.( + conversationId, + windowArg + ) ?? Promise.resolve({ count: null, typeColumn: null })) + if (probe.count === null) { this.preLoop.conversationSetupMs += this.now() - setupStartedAt - store.writeScanState({ - conversationId, - state: 'done', - imageTotal: 0, - imageProcessed: 0, - maxLocalId: watermark?.maxLocalId ?? 0 - }) continue } - - // 增量:会话已完成且**水位完全未变** → 不读 WCDB、不 OCR。 - // 水位不可用时(数据库不支持该聚合)一律重扫:宁可慢,不可漏。 - const previous = scanState.get(conversationId) - if ( - !windowed && - watermark && - previous && - previous.state === 'done' && - previous.imageTotal === watermark.count && - previous.maxLocalId === watermark.maxLocalId - ) { + const imageTotal = probe.count + if (imageTotal === 0) { this.preLoop.conversationSetupMs += this.now() - setupStartedAt + this.rememberConversationWatermark({ + store, + scanState, + conversationId, + watermark, + processed: previous?.processed ?? 0, + marksConversationDone: input.marksConversationDone + }) continue } @@ -1401,34 +1719,47 @@ export class ImageTextIndexService { try { const source = await this.runStep('list-image-messages', () => this.deps.listImageMessages - ? this.deps.listImageMessages(conversationId) + ? this.deps.listImageMessages(conversationId, windowArg) : (this.deps.listMessages?.(conversationId) ?? Promise.resolve([])) ) imageMessages = source // 专用路径仍要过滤:召回归档合并可能补进非图片的撤回消息。 .filter(isImageMessage) - // 时间窗过滤:小样本验证时只看窗口内的图片,不然还是在跑全量。 - .filter((message) => - windowed ? (message.createTime || 0) * 1000 >= (options.sinceMs as number) : true - ) + // 时间窗过滤:兼容路径没有 SQL 层窗口,只能在这里筛;这也让小样本验证 + // 的语义与专用路径一致。 + .filter((message) => { + if (effectiveWindow === null) return true + const createTimeMs = (message.createTime || 0) * 1000 + if (effectiveWindow.sinceMs !== undefined && createTimeMs < effectiveWindow.sinceMs) { + return false + } + if (effectiveWindow.beforeMs !== undefined && createTimeMs >= effectiveWindow.beforeMs) { + return false + } + return true + }) } catch { imageMessages = [] } this.preLoop.listMessagesMs += this.now() - listStartedAt this.preLoop.conversationSetupMs += this.now() - setupStartedAt if (!imageMessages.length) { - store.writeScanState({ + this.rememberConversationWatermark({ + store, + scanState, conversationId, - state: 'done', - imageTotal: 0, - imageProcessed: 0, - maxLocalId: 0 + watermark, + processed: previous?.processed ?? 0, + marksConversationDone: input.marksConversationDone }) continue } - // 水位取**实际读到的**消息里最大的 local_id,而不是源侧水位: - // 万一在我们查水位之后、读消息之前又落了一条新图,用观测值会让下一轮 - // 发现"源水位更高"从而重扫(安全);用源侧水位则会把它永久跳过(漏索引)。 + imageCount += imageMessages.length + /** + * 水位取**实际读到的**消息里最大的 local_id,而不是源侧水位: + * 万一在我们查水位之后、读消息之前又落了一条新图,用观测值会让下一轮 + * 发现"源水位更高"从而重扫(安全);用源侧水位则会把它永久跳过(漏索引)。 + */ const observedMaxLocalId = imageMessages.reduce( (max, message) => Math.max(max, Number(message.localId) || 0), 0 @@ -1441,9 +1772,9 @@ export class ImageTextIndexService { this.knowledgeDirty = false for (let index = 0; index < imageMessages.length; index += IMAGE_TEXT_INDEX_BATCH_SIZE) { - if (!preLoopCaptured) { - preLoopCaptured = true - this.preLoop.startupMs = this.now() - preLoopStartedAt + if (!input.preLoopState.captured) { + input.preLoopState.captured = true + this.preLoop.startupMs = this.now() - this.passStartedAt } if (this.cancelRequested || this.pauseRequested) { interrupted = true @@ -1456,9 +1787,9 @@ export class ImageTextIndexService { conversationId, provenance, ocrByMessage, - () => budget > 0, + () => budget.remaining > 0, () => { - budget -= 1 + budget.remaining -= 1 } ) processedInConversation += batchResult.processed @@ -1481,23 +1812,39 @@ export class ImageTextIndexService { * 写 `done` 会让下一遍按水位错误跳过这些图片(**永久漏索引**), * 或者让用户以为这个会话已经处理完。预算耗尽只能记 `partial`。 */ - if (interrupted || budget <= 0) { + if (interrupted || budget.remaining <= 0) { store.writeScanState({ conversationId, state: 'partial', - imageTotal: imageMessages.length, + /** + * **整会话**的图片总数,不是本窗口的条数。 + * + * `image_ocr_scan_state.image_total` 是进度的分母(`readScanProgress()` 求和); + * 把某个分段的窗口条数写进去,分母就会缩到"这一段处理了多少", + * 于是总进度会突然跳到接近 100% —— 那是这个功能最不能犯的谎。 + */ + imageTotal: watermark?.count ?? previous?.imageTotal ?? imageMessages.length, imageProcessed: processedInConversation, maxLocalId: observedMaxLocalId }) - break + scanState.set(conversationId, { + state: 'partial', + imageTotal: imageMessages.length, + processed: previous?.processed ?? 0, + maxLocalId: observedMaxLocalId + }) + truncated = true + return { interrupted: true, imageCount, truncated } } - store.writeScanState({ + this.rememberConversationWatermark({ + store, + scanState, conversationId, - state: 'done', - imageTotal: imageMessages.length, - imageProcessed: processedInConversation, - maxLocalId: observedMaxLocalId + watermark, + processed: (previous?.processed ?? 0) + processedInConversation, + marksConversationDone: input.marksConversationDone, + observedMaxLocalId }) /** @@ -1530,13 +1877,77 @@ export class ImageTextIndexService { await this.emit() } - if (this.cancelRequested) this.runState = 'cancelled' - else if (this.pauseRequested) this.runState = 'paused' - else this.runState = 'completed' - this.running = false - await this.emit() + return { interrupted: false, imageCount, truncated } } + /** + * 写入会话 checkpoint。 + * + * 存的是**源侧水位**(整会话的 count / maxLocalId),不是窗口内的观测值: + * 增量补齐的判据必须是"源有没有变",用窗口观测值会让每次窗口扫描都改水位, + * 于是增量判断永远为真 —— 表现就是"每轮都重扫一遍"。 + */ + private rememberConversationWatermark(input: { + store: ImageTextIndexStore + scanState: Map< + string, + { state: string; imageTotal: number; processed: number; maxLocalId: number } + > + conversationId: string + watermark: ImageMessageWatermark | null + processed: number + marksConversationDone: boolean + observedMaxLocalId?: number + }): void { + const { store, scanState, conversationId, watermark, processed, marksConversationDone } = input + const previous = scanState.get(conversationId) + const state: 'done' | 'partial' = + marksConversationDone && previous?.state !== 'partial' ? 'done' : 'partial' + const imageTotal = watermark?.count ?? previous?.imageTotal ?? 0 + const maxLocalId = watermark?.maxLocalId ?? input.observedMaxLocalId ?? previous?.maxLocalId ?? 0 + store.writeScanState({ + conversationId, + state, + imageTotal, + imageProcessed: processed, + maxLocalId + }) + scanState.set(conversationId, { state, imageTotal, processed, maxLocalId }) + } + + /** + * 分段覆盖度(recent-first 的时间维度)。 + * + * 三种情形必须分清: + * - 从未规划过(新用户 / 旧版本)→ `tiers: []`,调用方只能按整体状态判断。 + * - 规划过 → 逐段给出真实运行态。 + * - **整体已经 complete** → 一律表达为"全部完成"。老版本已经把全量索引建完的账号, + * 升级后不能被重新拉回去做 backfill,也不能因为"没有分段信息"而让 Query Agent + * 以为历史还没扫完。 + */ + private tierCoverageSnapshot(complete: boolean): { + tiers: ImageTextTierCoverage[] + coveredToMs: number | null + } { + const store = this.store + if (!store) return { tiers: [], coveredToMs: null } + const state = store.readBackfillState() + const anchorMs = + state.anchorMs ?? (complete ? (store.readCountedTotal()?.countedAt ?? this.now()) : null) + if (anchorMs === null) return { tiers: [], coveredToMs: state.coveredToMs } + const tiers: ImageTextTierCoverage[] = buildImageTextBackfillSegments(anchorMs).map( + (segment) => ({ + tier: segment.tier, + state: complete ? 'complete' : (state.tierStates[segment.tier] ?? 'pending'), + startMs: segment.startMs, + endMs: segment.endMs + }) + ) + const coveredToMs = complete ? Math.max(state.coveredToMs ?? 0, anchorMs) : state.coveredToMs + return { tiers, coveredToMs } + } + + // --------------------------------------------------------------- 控制接口 pause(): { paused: boolean; state: ImageTextIndexRunState } { diff --git a/src/main/services/image-text-index-store.ts b/src/main/services/image-text-index-store.ts index 1dc2caf..446ad1d 100644 --- a/src/main/services/image-text-index-store.ts +++ b/src/main/services/image-text-index-store.ts @@ -13,11 +13,14 @@ import { mkdirSync, rmSync, statSync, existsSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { DatabaseSync } from 'node:sqlite' import { + IMAGE_TEXT_BACKFILL_TIER_ORDER, IMAGE_TEXT_INDEX_SCHEMA_VERSION, type ImageOcrArtifact, type ImageOcrBinding, type ImageOcrPersistedState, - type ImageTextIndexStorageStats + type ImageTextBackfillTier, + type ImageTextIndexStorageStats, + type ImageTextTierRunState } from '../../shared/image-text-index' const MAX_SAFE_ACCOUNT_SEGMENT = /^[a-f0-9]{32}$/ @@ -374,6 +377,76 @@ export class ImageTextIndexStore { this.writeMeta('total_image_messages_complete', input.complete ? '1' : '0') } + // ---------------------------------------------------- recent-first 回填状态 + // + // 全部走 `image_ocr_meta`(key/value),**不加表、不加列** —— 这是 old checkpoint + // 兼容性的来源:旧库没有这些 key 时读出来就是"没有计划",于是下一次 pass + // 以当前时刻为锚点重新建计划。已经处理过的图片由 terminal binding 兜住, + // 不会因为"计划是新的"而重新 OCR。 + + /** + * 读取回填计划状态。 + * + * `anchorMs === null` = 从未规划过(新用户,或从旧版本升级且没有这些 key)。 + * 调用方此时必须**新建**计划,而不是假设"已完成"。 + */ + readBackfillState(): { + anchorMs: number | null + tierStates: Partial> + coveredToMs: number | null + } { + const anchorRaw = this.readMeta('backfill_anchor_ms') + const anchorMs = anchorRaw === null ? null : Number(anchorRaw) + let tierStates: Partial> = {} + const statesRaw = this.readMeta('backfill_tier_states') + if (statesRaw) { + try { + const parsed = JSON.parse(statesRaw) as Record + for (const tier of IMAGE_TEXT_BACKFILL_TIER_ORDER) { + const value = parsed[tier] + if (value === 'pending' || value === 'running' || value === 'complete') { + tierStates[tier] = value + } + } + } catch { + // 状态串损坏时按"没有分段状态"处理:最坏情况是重扫一遍, + // 而 terminal binding 保证不会重复 OCR。 + tierStates = {} + } + } + const coveredRaw = this.readMeta('backfill_covered_to_ms') + const coveredToMs = coveredRaw === null ? null : Number(coveredRaw) + return { + anchorMs: anchorMs !== null && Number.isFinite(anchorMs) ? anchorMs : null, + tierStates, + coveredToMs: coveredToMs !== null && Number.isFinite(coveredToMs) ? coveredToMs : null + } + } + + writeBackfillAnchor(anchorMs: number): void { + this.writeMeta('backfill_anchor_ms', String(Math.floor(anchorMs))) + } + + /** + * 写入单个分段的运行态。 + * + * 一个 pass 是单线程串行推进分段,所以"读-改-写"整体落在一条 meta 行里; + * 这里额外做的只有"保留其它分段"。 + */ + writeBackfillTierState(tier: ImageTextBackfillTier, state: ImageTextTierRunState): void { + const current = this.readBackfillState().tierStates + const next = { ...current, [tier]: state } + this.writeMeta('backfill_tier_states', JSON.stringify(next)) + } + + /** 增量补齐水位:处理到哪儿了(`create_time <= ms` 都已就绪)。只增不减。 */ + writeBackfillCoveredToMs(coveredToMs: number): void { + const previous = this.readBackfillState().coveredToMs + if (previous !== null && previous >= coveredToMs) return + this.writeMeta('backfill_covered_to_ms', String(Math.floor(coveredToMs))) + } + + readScanState(): Map< string, { state: string; imageTotal: number; processed: number; maxLocalId: number } @@ -563,7 +636,10 @@ export class ImageTextIndexStore { DELETE FROM image_ocr_meta WHERE key IN ( 'total_image_messages', 'total_image_counted_at', - 'total_image_messages_complete' + 'total_image_messages_complete', + 'backfill_anchor_ms', + 'backfill_tier_states', + 'backfill_covered_to_ms' ); `) } diff --git a/src/main/services/local-query-api-service.ts b/src/main/services/local-query-api-service.ts index 156e7a2..0033e7b 100644 --- a/src/main/services/local-query-api-service.ts +++ b/src/main/services/local-query-api-service.ts @@ -29,7 +29,9 @@ import { normalizeMessageIdentity } from '../../shared/local-query-api' import { + IMAGE_TEXT_BACKFILL_TIER_LABEL, describeImageTextCoverage, + describeImageTextCoveredRanges, imageTextCoverageState, type ImageTextIndexCoverage } from '../../shared/image-text-index' @@ -144,6 +146,17 @@ export function buildImageOcrCoverage( ? `(图片数量统计于 ${formatLocalMinute(coverage.countedAt)})` : '' const base = describeImageTextCoverage(coverage) + /** + * recent-first 之后必须把"哪段时间能下确定性结论"一起给出。 + * + * 否则模型只看一个总百分比:30% 时它会以为连最近一周都不可信(过度保守没坏处), + * 但 99% 时它会以为"去年也能放心下结论"(这就把索引缺口说成了事实空缺)。 + */ + const tiers = coverage.tiers ?? [] + const coveredRanges = tiers + .filter((entry) => entry.state === 'complete') + .map((entry) => IMAGE_TEXT_BACKFILL_TIER_LABEL[entry.tier]) + const rangeNote = tiers.length ? describeImageTextCoveredRanges(coverage) : '' return { state, totalImageMessages: coverage.totalImageMessages, @@ -154,10 +167,11 @@ export function buildImageOcrCoverage( failed: coverage.failed, pending: coverage.pending, ...(coverage.countedAt ? { countedAtLabel: formatLocalMinute(coverage.countedAt) } : {}), + ...(coveredRanges.length ? { coveredRanges } : {}), summary: state === 'complete' ? `${base}${countedNote}` - : `${base}${countedNote}${IMAGE_OCR_ZERO_RESULT_CAUTION}` + : `${base}${countedNote}${rangeNote}${IMAGE_OCR_ZERO_RESULT_CAUTION}` } } diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index ac7a364..d111151 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -7,6 +7,7 @@ import { createConnection, Socket } from 'net' import { getResourceRoots } from './resource-paths' import { wcdbDebugLog } from './wcdb-debug' import type { ImageMessageCountProbe } from '../shared/image-text-index' +import { imageTextWindowToSeconds } from '../shared/image-text-index' export interface Wcdb4Session { username: string @@ -1352,13 +1353,53 @@ export class Wcdb4Client { return value } - /** 图片消息的 WHERE 片段;`sinceMs` 用于只统计某个时间点之后的消息(测试小窗口)。 */ - private imageMessageWhere(column: string, sinceMs?: number): string { - const clauses = [`(${this.quoteSqlIdentifier(column)} & 65535) = 3`] - // 微信的 create_time 是**秒**,调用方给的是毫秒。 - if (sinceMs && Number.isFinite(sinceMs) && sinceMs > 0) { - clauses.push(`"create_time" >= ${Math.floor(sinceMs / 1000)}`) + /** + * 归一化图片消息的时间范围参数。 + * + * 同时接受旧的裸 `sinceMs` 与新的 `{ sinceMs, beforeMs }`: + * 前者有若干既有调用点(统计卡片、增量水位),不该为了新功能去改它们; + * 后者是 recent-first 分段计划要的半开区间。 + */ + private normalizeImageRange( + input?: number | { sinceMs?: number; beforeMs?: number } + ): { sinceMs?: number; beforeMs?: number } { + if (typeof input === 'number') { + return Number.isFinite(input) && input > 0 ? { sinceMs: input } : {} } + if (!input) return {} + const range: { sinceMs?: number; beforeMs?: number } = {} + if (typeof input.sinceMs === 'number' && Number.isFinite(input.sinceMs) && input.sinceMs > 0) { + range.sinceMs = input.sinceMs + } + if ( + typeof input.beforeMs === 'number' && + Number.isFinite(input.beforeMs) && + input.beforeMs > 0 + ) { + range.beforeMs = input.beforeMs + } + return range + } + + /** + * 图片消息的 WHERE 片段;`[sinceMs, beforeMs)` 半开区间。 + * + * 边界换算**只走** `imageTextWindowToSeconds`:COUNT 与列表两条路径必须用 + * 同一份换算,否则"统计说有 3 张、列表却返回 2 张",而调用方会据此把一段 + * 标成"已覆盖"。 + */ + private imageMessageWhere( + column: string, + input?: number | { sinceMs?: number; beforeMs?: number } + ): string { + const clauses = [`(${this.quoteSqlIdentifier(column)} & 65535) = 3`] + // 微信的 create_time 是**秒**,`[sinceMs, beforeMs)` 转成秒闭区间 + // `[sinceSec, beforeSecInclusive]`;两端同一套下取整,保证不重不漏。 + const { sinceSec, beforeSecInclusive } = imageTextWindowToSeconds( + this.normalizeImageRange(input) + ) + if (sinceSec !== null) clauses.push(`"create_time" >= ${sinceSec}`) + if (beforeSecInclusive !== null) clauses.push(`"create_time" <= ${beforeSecInclusive}`) return clauses.join(' AND ') } @@ -1373,7 +1414,7 @@ export class Wcdb4Client { */ async countImageMessagesAsync( md5OrUsername: string, - sinceMs?: number + input?: number | { sinceMs?: number; beforeMs?: number } ): Promise { if (!this.wcdbGetMessageTableStats || !this.wcdbExecQuery) { return { count: null, typeColumn: null, error: '当前数据服务不支持消息表统计' } @@ -1406,7 +1447,7 @@ export class Wcdb4Client { this.wcdbExecQuery as unknown as KoffiAsyncFunction, 'message', table.dbPath, - `SELECT COUNT(*) AS "image_count" FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${this.imageMessageWhere(column, sinceMs)}` + `SELECT COUNT(*) AS "image_count" FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${this.imageMessageWhere(column, input)}` ) const value = Number(this.pickValue(rows[0] || {}, ['image_count', 'count', 'COUNT(*)'])) if (Number.isFinite(value)) total += value @@ -1433,7 +1474,7 @@ export class Wcdb4Client { */ async imageConversationWatermarkAsync( md5OrUsername: string, - sinceMs?: number + input?: number | { sinceMs?: number; beforeMs?: number } ): Promise<{ count: number; maxLocalId: number } | null> { if (!this.wcdbGetMessageTableStats || !this.wcdbExecQuery) return null // 与计数同因:必须先把会话 md5 解析成原生接口要的 username,否则永远匹配不到消息表。 @@ -1458,7 +1499,7 @@ export class Wcdb4Client { this.wcdbExecQuery as unknown as KoffiAsyncFunction, 'message', table.dbPath, - `SELECT COUNT(*) AS "image_count", MAX("local_id") AS "image_max_local_id" FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${this.imageMessageWhere(column, sinceMs)}` + `SELECT COUNT(*) AS "image_count", MAX("local_id") AS "image_max_local_id" FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${this.imageMessageWhere(column, input)}` ) const row = rows[0] || {} const tableCount = Number(this.pickValue(row, ['image_count', 'count', 'COUNT(*)'])) @@ -1548,7 +1589,21 @@ export class Wcdb4Client { */ async listImageMessagesAsync( md5OrUsername: string, - options: { sinceMs?: number; limit?: number; requestId?: string } = {} + options: { + sinceMs?: number + /** + * 开区间上界。recent-first 的分段窗口靠它把"最近 30 天"和"更早"切开, + * 与 `sinceMs` 一起构成 `[sinceMs, beforeMs)`。 + */ + beforeMs?: number + limit?: number + /** + * 行序。默认 `asc` 保持既有行为不变;recent-first 的窗口用 `desc`, + * 让同一窗口内**新的图片先被处理**(用户先受益,且断点续跑更有意义)。 + */ + order?: 'asc' | 'desc' + requestId?: string + } = {} ): Promise { if (!this.wcdbExecQuery) return [] const requestId = options.requestId ?? 'NO-REQUEST' @@ -1573,10 +1628,11 @@ export class Wcdb4Client { const column = this.resolveMessageTypeColumn(table) if (!column) continue try { - const where = this.imageMessageWhere(column, options.sinceMs) + const where = this.imageMessageWhere(column, options) // `local_id` 参与排序:`create_time` 同秒的消息需要一个稳定次序, // 否则多次读取的行序可能不同,调用方无法做稳定游标。 - const sql = `SELECT * FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${where} ORDER BY "create_time" ASC, "local_id" ASC LIMIT ${limit}` + const direction = options.order === 'desc' ? 'DESC' : 'ASC' + const sql = `SELECT * FROM ${this.quoteSqlIdentifier(table.tableName)} WHERE ${where} ORDER BY "create_time" ${direction}, "local_id" ${direction} LIMIT ${limit}` const queryStartedAt = Date.now() const rows = await this.callJsonAsync[]>( this.wcdbExecQuery as unknown as KoffiAsyncFunction, diff --git a/src/renderer/src/components/search/ImageTextIndexCard.tsx b/src/renderer/src/components/search/ImageTextIndexCard.tsx index 176d4e7..d2d2d3c 100644 --- a/src/renderer/src/components/search/ImageTextIndexCard.tsx +++ b/src/renderer/src/components/search/ImageTextIndexCard.tsx @@ -21,7 +21,10 @@ import { import { describeImageTextCoverage, imageTextCoverageState, - imageTextProcessedPercent + imageTextPhaseLabel, + imageTextProcessedPercent, + imageTextTierSearchableNotice, + type ImageTextBackfillTier } from '../../../../shared/image-text-index' import { useImageTextIndexStatus } from './hooks/useImageTextIndexStatus' @@ -89,6 +92,31 @@ export function ImageTextIndexCard({ dbReady, onNotice }: ImageTextIndexCardProp const percent = coverage ? imageTextProcessedPercent(coverage.processed, coverage.totalImageMessages) : 0 + /** + * 当前阶段文案(recent-first)。 + * + * 它**不参与进度计算**:进度永远是 `processed / totalImageMessages` + * (例如 12,800 / 349,838)。阶段只回答"现在在优先做什么", + * 绝不允许用"某个分段做完了"冒充整体完成。 + */ + const phaseLabel = progress?.currentPhase ? imageTextPhaseLabel(progress.currentPhase) : null + /** + * 已经**真正完整**的最新分段 —— 只有这时才敢告诉用户"这段时间已经能搜了"。 + * + * 判据是 `state === 'complete'`,不是"扫到过"。整体 complete 时不必重复宣告。 + */ + const searchableNotice = (() => { + if (!coverage || coverageState === 'complete') return null + const completed = new Set( + (coverage.tiers ?? []) + .filter((entry) => entry.state === 'complete') + .map((entry) => entry.tier) + ) + const newest = ( + ['recent_7d', 'recent_30d', 'recent_1y', 'archive'] as ImageTextBackfillTier[] + ).find((tier) => completed.has(tier)) + return newest ? imageTextTierSearchableNotice(newest) : null + })() const systemicFailure = coverage?.systemicFailure === true const visualState = progress?.state === 'error' || coverageState === 'failed' @@ -243,6 +271,15 @@ export function ImageTextIndexCard({ dbReady, onNotice }: ImageTextIndexCardProp }} /> + {/* + 阶段只是一句人话,没有百分比 —— 进度必须留在上面那行 + (processed / total 全量),否则用户会把"最近 7 天做完了"读成"整体做完了"。 + */} + {phaseLabel && ( +

+ {phaseLabel} +

+ )}

{`${progress.percent}% · 识别出文字 ${progress.indexed.toLocaleString()} · 没有文字 ${progress.empty.toLocaleString()} · 图片已清理 ${progress.missing.toLocaleString()} · 失败 ${progress.failed.toLocaleString()}`}

@@ -291,6 +328,18 @@ export function ImageTextIndexCard({ dbReady, onNotice }: ImageTextIndexCardProp )} + {/* + 只有**真正完整**的分段才敢说"已可搜索"。这是一句承诺,不是进度提示 —— + 所以判据是 coverage 里该分段的 state === 'complete',而不是"扫到过"。 + */} + {searchableNotice && ( +

+ {searchableNotice} +

+ )}

{describeImageTextCoverage(coverage)}

)} diff --git a/src/shared/image-text-index.ts b/src/shared/image-text-index.ts index 2738863..f1ab0c7 100644 --- a/src/shared/image-text-index.ts +++ b/src/shared/image-text-index.ts @@ -70,6 +70,308 @@ export function resolveImageTextOcrConcurrency(raw?: string | number | null): nu /** 已完成一批之后、回到会话循环前的让出时间。 */ export const IMAGE_TEXT_INDEX_YIELD_MS = 0 +/** + * 单段时间窗口的图片消息读取上限。 + * + * 必须显式给 limit:底层在不给 limit 时按 `create_time ASC` 排序并截断 —— + * 那是"从最老开始读",正好与 recent-first 相反,而且会把窗口内较新的图片悄悄丢掉。 + */ +export const IMAGE_TEXT_SEGMENT_MESSAGE_LIMIT = 200_000 + +// ---------------------------------------------------------------- recent-first +// +// 首次建立索引时,用户要的不是"从十年前开始扫",而是"最近聊天的图片先能搜"。 +// 这里把"最近优先"固化成**显式时间分段计划**,而不是靠反转全库排序碰运气。 +// +// 三条硬约束(任何实现都必须同时满足): +// 1. 分段唯一来源是本文件:`[startInclusive, endExclusive)`,不允许别的模块自己推边界。 +// 2. 一次 backfill session 的 `anchorMs` **固定不变**,否则跑几小时后窗口会漂移, +// 产生重复 / 遗漏 / checkpoint 不稳定。 +// 3. 比锚点更新的消息由**增量补齐**单独负责,永远排在所有历史分段之前。 + +/** 历史回填分段。刻意用时间语义而不是 Tier 编号(内部也不要出现 Tier 1/2)。 */ +export type ImageTextBackfillTier = 'recent_7d' | 'recent_30d' | 'recent_1y' | 'archive' + +/** 处理顺序 = 优先级顺序:越靠前越新。 */ +export const IMAGE_TEXT_BACKFILL_TIER_ORDER: readonly ImageTextBackfillTier[] = [ + 'recent_7d', + 'recent_30d', + 'recent_1y', + 'archive' +] + +/** + * 「最近」窗口的天数。 + * + * 取 7 天而不是 3 天:用户对"最近"的直觉通常是"这一周",而 7 天窗口在真实库里 + * 通常只有几百到几千张图,几分钟内就能把"最近聊天里的图"变成可搜索。 + */ +export const IMAGE_TEXT_RECENT_WINDOW_DAYS = 7 + +/** 各段下界的回看天数;`archive` 无下界。 */ +export const IMAGE_TEXT_BACKFILL_WINDOW_DAYS: Record = { + recent_7d: IMAGE_TEXT_RECENT_WINDOW_DAYS, + recent_30d: 30, + recent_1y: 365, + archive: Number.POSITIVE_INFINITY +} + +/** 各段的用户可见名称。 */ +export const IMAGE_TEXT_BACKFILL_TIER_LABEL: Record = { + recent_7d: '最近图片', + recent_30d: '最近 30 天', + recent_1y: '近一年图片', + archive: '更早图片' +} + +const DAY_MS = 24 * 60 * 60 * 1000 + +/** 一段回填窗口。`endMs` 为开区间上界;`archive` 的 `startMs` 是 -∞。 */ +export interface ImageTextBackfillSegment { + tier: ImageTextBackfillTier + /** epoch ms,**闭**下界。 */ + startMs: number + /** epoch ms,**开**上界。 */ + endMs: number +} + +/** + * 由固定锚点推出全部分段。 + * + * 结果按优先级从新到旧排列,且**互不重叠、无空隙**: + * `[now-7d, now)` → `[now-30d, now-7d)` → `[now-365d, now-30d)` → `(-∞, now-365d)`。 + * + * 相邻段的边界由同一个锚点派生,所以不会 off-by-one:上一段的 `endMs` 恒等于下一段的 + * `startMs`,而区间语义是"下界闭、上界开",落在边界上的消息只会被**一段**认领。 + */ +export function buildImageTextBackfillSegments(anchorMs: number): ImageTextBackfillSegment[] { + const segments: ImageTextBackfillSegment[] = [] + let upper = anchorMs + for (const tier of IMAGE_TEXT_BACKFILL_TIER_ORDER) { + const days = IMAGE_TEXT_BACKFILL_WINDOW_DAYS[tier] + const lower = Number.isFinite(days) ? anchorMs - days * DAY_MS : Number.NEGATIVE_INFINITY + segments.push({ tier, startMs: lower, endMs: upper }) + upper = lower + } + return segments +} + +/** + * 把 ms 半开区间转换成底层查询要的**秒**闭区间。 + * + * **唯一的换算实现**:`sinceMs` 下取整做闭下界,`beforeMs` 下取整减一放开区间上界。 + * 两端必须用同一种取整方式 —— 一边 ceil 一边 floor 的话,边界那一秒会被 + * 相邻两段同时认领(重复 OCR)或被同时跳过(漏索引)。 + * + * 微信的 `create_time` 只有秒级精度,所以 1 秒以内的边界歧义无法在源头消除; + * 能做的是让它**确定且不重叠**。 + */ +export function imageTextWindowToSeconds(range: { sinceMs?: number; beforeMs?: number }): { + sinceSec: number | null + beforeSecInclusive: number | null +} { + const { sinceMs, beforeMs } = range + const sinceSec = + typeof sinceMs === 'number' && Number.isFinite(sinceMs) && sinceMs > 0 + ? Math.floor(sinceMs / 1000) + : null + const beforeSecInclusive = + typeof beforeMs === 'number' && Number.isFinite(beforeMs) && beforeMs > 0 + ? Math.floor(beforeMs / 1000) - 1 + : null + return { sinceSec, beforeSecInclusive } +} + +/** 分段 → 秒闭区间;语义同 `imageTextWindowToSeconds`。 */ +export function imageTextSegmentToSecondRange(segment: ImageTextBackfillSegment): { + sinceSec: number | null + beforeSecInclusive: number | null +} { + return imageTextWindowToSeconds({ + ...(Number.isFinite(segment.startMs) ? { sinceMs: segment.startMs } : {}), + ...(Number.isFinite(segment.endMs) ? { beforeMs: segment.endMs } : {}) + }) +} + +// ---------------------------------------------------------------- 进度阶段 + +/** 索引运行阶段:增量补齐 / 某个历史分段 / 全部完成。 */ +export type ImageTextIndexPhase = 'incremental' | ImageTextBackfillTier | 'complete' + +export function imageTextPhaseLabel(phase: ImageTextIndexPhase): string { + switch (phase) { + case 'incremental': + case 'recent_7d': + return '正在优先索引最近图片' + case 'recent_30d': + return '正在补齐最近 30 天' + case 'recent_1y': + return '正在补齐近一年图片' + case 'archive': + return '正在补齐更早图片' + case 'complete': + return '图片文字索引已完成' + } +} + +/** + * 「某段已可搜索」的宣告文案。 + * + * 只允许在**该段真的 complete** 时使用 —— 它是一句承诺,不是进度提示。 + */ +export function imageTextTierSearchableNotice(tier: ImageTextBackfillTier): string { + switch (tier) { + case 'recent_7d': + return '最近图片已可搜索' + case 'recent_30d': + return '最近 30 天图片已可搜索' + case 'recent_1y': + return '近一年图片已可搜索' + case 'archive': + return '更早图片已可搜索' + } +} + +/** 分段的运行态。只有 `complete` 才允许被当作"这段已经完整可搜"。 */ +export type ImageTextTierRunState = 'pending' | 'running' | 'complete' + +/** 单个历史分段的完成状态与边界。 */ +export interface ImageTextTierCoverage { + tier: ImageTextBackfillTier + state: ImageTextTierRunState + /** epoch ms,闭下界;`archive` 为 -∞。 */ + startMs: number + /** epoch ms,开上界。 */ + endMs: number +} + +/** 时间范围询问的结论。 */ +export interface ImageTextRangeCoverage { + state: 'not_built' | 'partial' | 'complete' + /** 请求范围内**已真正完整**的子区间;无交集时为 null。 */ + coveredFromMs: number | null + coveredToMs: number | null + /** 请求范围是否有一部分落在"尚未覆盖"的区域。 */ + hasUncovered: boolean +} + +/** + * 给定查询时间范围,回答"这一段的图片文字覆盖是否完整"。 + * + * 判据刻意严格:只有当**整个请求区间**都落在已完成的覆盖并集里才回 `complete`。 + * 请求范围开放(不传 sinceMs / beforeMs)时视为"全部历史",只要有任何一段没完成 + * 就是 `partial` —— 这正是零结果诚实性要的:0 条证据不能回答"没有"。 + */ +export function imageTextRangeCoverage( + coverage: ImageTextIndexCoverage, + range: { sinceMs?: number; beforeMs?: number } = {} +): ImageTextRangeCoverage { + if (!coverage.established) { + return { state: 'not_built', coveredFromMs: null, coveredToMs: null, hasUncovered: true } + } + // 整体 complete = 全部图片消息都已定态 → 任意时间范围都完整,开放区间也一样。 + // 少了这一条,"全历史"这种没有上界的查询会永远因为"上界之后还没覆盖"被判成 partial。 + if (coverage.complete) { + return { state: 'complete', coveredFromMs: null, coveredToMs: null, hasUncovered: false } + } + // 已完成分段 + 增量水位合并成"已覆盖并集"。 + // + // `?? []` 不是多余的防御:覆盖度快照可能来自**旧版本落盘的库**(那时还没有分段概念), + // 也可能来自手写的夹具。缺字段只应该让"时间维度"暂时不可用,绝不能让查询直接崩。 + const tiers = coverage.tiers ?? [] + const covered: { startMs: number; endMs: number }[] = tiers + .filter((entry) => entry.state === 'complete') + .map((entry) => ({ startMs: entry.startMs, endMs: entry.endMs })) + if (coverage.coveredToMs !== null && coverage.coveredToMs !== undefined && tiers.length) { + const anchorMs = tiers[0]?.endMs ?? coverage.coveredToMs + covered.push({ startMs: anchorMs, endMs: coverage.coveredToMs }) + } + const merged = mergeImageTextRanges(covered) + + // 查询范围:不给边界 = 全历史(-∞, +∞)。 + const queryStart = range.sinceMs ?? Number.NEGATIVE_INFINITY + const queryEnd = range.beforeMs ?? Number.POSITIVE_INFINITY + if (!(queryEnd > queryStart)) { + return { state: 'complete', coveredFromMs: null, coveredToMs: null, hasUncovered: false } + } + + let cursor = queryStart + let coveredFromMs: number | null = null + let coveredToMs: number | null = null + for (const span of merged) { + if (span.endMs <= cursor) continue + if (span.startMs > cursor) break + if (coveredFromMs === null) coveredFromMs = Math.max(span.startMs, queryStart) + coveredToMs = Math.min(span.endMs, queryEnd) + cursor = Math.min(span.endMs, queryEnd) + if (cursor >= queryEnd) break + } + const hasUncovered = cursor < queryEnd + return { + state: hasUncovered ? 'partial' : 'complete', + coveredFromMs: coveredFromMs === null ? null : coveredFromMs, + coveredToMs, + hasUncovered + } +} + +function mergeImageTextRanges( + ranges: { startMs: number; endMs: number }[] +): { startMs: number; endMs: number }[] { + const sorted = [...ranges].sort((left, right) => left.startMs - right.startMs) + const merged: { startMs: number; endMs: number }[] = [] + for (const current of sorted) { + const last = merged[merged.length - 1] + if (last && current.startMs <= last.endMs) { + if (current.endMs > last.endMs) last.endMs = current.endMs + continue + } + merged.push({ ...current }) + } + return merged +} + +/** + * 「哪些时间段已经**真正**可以放心搜」的人话描述。 + * + * recent-first 之后,"索引建了多少"和"哪段时间能下确定结论"是两件事: + * 最近 7 天可能已经 100% 可用,而更早的历史还在补齐。只给一个总百分比, + * 模型会把"最近能搜"误当成"全历史都搜过了",于是对"去年有没有发过 XXX" + * 给出"没有"这种不该下的结论。 + */ +export function describeImageTextCoveredRanges(coverage: ImageTextIndexCoverage): string { + if (!coverage.established) return '还没有任何一个时间段完成图片文字索引。' + const completed = (coverage.tiers ?? []).filter((entry) => entry.state === 'complete') + if (coverage.complete) return '全部历史时间的图片文字都已可搜索。' + if (!completed.length) return '还没有任何一个时间段完成图片文字索引。' + const labels = completed.map((entry) => IMAGE_TEXT_BACKFILL_TIER_LABEL[entry.tier]) + // 只有归档段也完成才可能覆盖到最早;否则一定还有更老的历史没扫。 + const hasArchive = completed.some((entry) => entry.tier === 'archive') + return hasArchive + ? `已完整覆盖:${labels.join('、')}。` + : `已完整覆盖:${labels.join('、')};更早的图片仍在补齐。` +} + +/** + * 时间范围覆盖度的人话结论(Query Agent 只引用,不自己换算)。 + * + * 与 `describeImageTextCoverage` 的分工:那个回答"整体建了多少", + * 这个回答"**这次查的这个时间段**能不能下确定性结论"。 + */ +export function describeImageTextRangeCoverage( + coverage: ImageTextIndexCoverage, + range: { sinceMs?: number; beforeMs?: number } = {} +): string { + const result = imageTextRangeCoverage(coverage, range) + if (result.state === 'not_built') { + return '图片文字索引尚未建立:当前范围内图片里的文字还搜不到,不能据此回答"没有"。' + } + if (result.state === 'complete') { + return '图片文字索引已覆盖该时间范围:范围内没有匹配的图片文字,可以据此回答。' + } + return '图片文字历史仍在补齐,这次查询的时间范围尚未完整索引:当前结果不能排除尚未索引的图片。' +} + /** 单张图片的 OCR 结果状态。 */ export type ImageOcrState = /** 尚未处理 */ @@ -269,6 +571,13 @@ export interface ImageTextIndexProgress { speedPerSec?: number | null /** 按当前窗口速度估算的剩余时间(毫秒);速度不可用或分母不可信时为 null。 */ etaMs?: number | null + /** + * 当前正在处理的阶段(recent-first 的可见性)。 + * + * **它只是阶段提示,不是进度**:总进度仍然必须是 `processed / totalImageMessages` + * (全量图片消息),绝不允许用"某个分段处理完了"冒充整体完成。 + */ + currentPhase?: ImageTextIndexPhase } /** @@ -311,6 +620,19 @@ export interface ImageTextIndexCoverage { * 冒充成「现在完整」。 */ countedAt: number | null + /** + * 各历史分段的完成状态与边界(新 → 旧)。未建立 / 旧快照时为 `[]`。 + * + * 边界来自**当时固定的锚点**,因此可以和用户查询的时间范围直接求交。 + */ + tiers: ImageTextTierCoverage[] + /** + * 增量补齐水位:`create_time <= coveredToMs` 的新图片都已处理完;null = 尚无。 + * + * 它单独存在的原因:锚点之后新到的图片不属于任何历史分段,必须有独立水位 + * 才能回答"最近这几个小时是否已经可搜"。 + */ + coveredToMs: number | null } /** 覆盖度状态(外加"未建立")。UI 与 Query Agent 共用同一判据,避免两处各推一套口径漂移。 */ diff --git a/src/shared/local-query-api.ts b/src/shared/local-query-api.ts index 3ae1784..2be8bb7 100644 --- a/src/shared/local-query-api.ts +++ b/src/shared/local-query-api.ts @@ -369,6 +369,14 @@ export interface QueryImageTextCoverage { pending: number /** 图片数量统计时刻(本地时间 `MM-DD HH:mm`);从未统计时为 undefined。 */ countedAtLabel?: string + /** + * 已经**真正完整**的时间段描述(新 → 旧),例如"最近 7 天"。 + * + * recent-first 之后必须有这一维:总进度 30% 不代表"最近一周不可信", + * 反过来总进度 99% 也不代表"去年可以下确定性结论"。模型只能引用这里列出的 + * 时间段去下"没有"的结论,其余范围一律只能说"仍在补齐"。 + */ + coveredRanges?: string[] /** 可直接引用的结论句;模型只引用,不要自己换算或推断。 */ summary: string } diff --git a/tests/component/image-text-index-card.test.tsx b/tests/component/image-text-index-card.test.tsx index 03d6679..decc234 100644 --- a/tests/component/image-text-index-card.test.tsx +++ b/tests/component/image-text-index-card.test.tsx @@ -445,3 +445,105 @@ describe('图片文字索引卡片 — 修复图片搜索索引', () => { expect(String(onNotice.mock.calls.at(-1)?.[0])).toContain('正在进行中') }) }) + +/** + * recent-first 的阶段可见性。 + * + * 这里要守住的是两件**不能混**的事: + * - **总进度**永远是 `processed / totalImageMessages`(全量图片消息); + * - **阶段文案**只回答"现在在优先做什么"。 + * + * 用"某个分段做完了"去冒充整体完成,是这次改动最容易撒的谎,所以两条都断言。 + */ +describe('recent-first 阶段显示', () => { + /** 已建立、且不在运行/暂停 —— 卡片明细块的渲染条件。 */ + const settled = ( + coverageOverride: Partial + ): ImageTextIndexStatus => + status({ + ...running, + progress: { ...running.progress, state: 'idle', cancellable: false, paused: false }, + coverage: { ...running.coverage, ...coverageOverride } + }) + + const tiers = ( + completeTier: 'recent_7d' | 'recent_30d' | 'recent_1y' | 'archive' | null + ): ImageTextIndexStatus['coverage']['tiers'] => [ + { + tier: 'recent_7d', + state: completeTier === 'recent_7d' ? 'complete' : 'running', + startMs: 7, + endMs: 8 + }, + { + tier: 'recent_30d', + state: completeTier === 'recent_30d' ? 'complete' : 'pending', + startMs: 6, + endMs: 7 + }, + { + tier: 'recent_1y', + state: completeTier === 'recent_1y' ? 'complete' : 'pending', + startMs: 5, + endMs: 6 + }, + { + tier: 'archive', + state: completeTier === 'archive' ? 'complete' : 'pending', + startMs: 4, + endMs: 5 + } + ] + + it('运行中显示阶段文案,同时总进度仍以全量为分母', async () => { + api.getImageTextIndexStatus.mockResolvedValue( + status({ + ...running, + progress: { ...running.progress, currentPhase: 'recent_30d' } + }) + ) + + await renderCard() + + expect(screen.getByTestId('image-text-index-phase').textContent).toBe('正在补齐最近 30 天') + // 阶段 ≠ 进度:分子分母仍然是全量数字,不是"这一段处理了多少张"。 + expect(screen.getByTestId('image-text-index-progress').textContent).toBe( + `${running.progress.processed.toLocaleString()} / ${running.progress.totalImageMessages.toLocaleString()}` + ) + }) + + it('阶段文案是用户语言,不出现工程术语', async () => { + api.getImageTextIndexStatus.mockResolvedValue( + status({ ...running, progress: { ...running.progress, currentPhase: 'archive' } }) + ) + + await renderCard() + + const text = screen.getByTestId('image-text-index-phase').textContent ?? '' + expect(text).toBe('正在补齐更早图片') + expect(text).not.toMatch(/Tier/i) + }) + + it('分段真的完成时才宣告"已可搜索"', async () => { + api.getImageTextIndexStatus.mockResolvedValue( + settled({ tiers: tiers('recent_7d'), coveredToMs: 8 }) + ) + + await renderCard() + + expect(screen.getByTestId('image-text-index-searchable-notice').textContent).toBe( + '最近图片已可搜索' + ) + }) + + it('分段还没完成时不得宣告"已可搜索"', async () => { + api.getImageTextIndexStatus.mockResolvedValue( + settled({ tiers: tiers(null), coveredToMs: 8 }) + ) + + await renderCard() + + // 这是一句承诺,不是进度提示:没有真正 complete 就不许说。 + expect(screen.queryByTestId('image-text-index-searchable-notice')).toBeNull() + }) +}) diff --git a/tests/integration/image-text-index-checkpoint-and-coverage.test.ts b/tests/integration/image-text-index-checkpoint-and-coverage.test.ts index f8fd2d4..aca6c8b 100644 --- a/tests/integration/image-text-index-checkpoint-and-coverage.test.ts +++ b/tests/integration/image-text-index-checkpoint-and-coverage.test.ts @@ -19,6 +19,7 @@ import { } from '../../src/main/services/image-text-index-store' import { buildImageOcrCoverage } from '../../src/main/services/local-query-api-service' import type { ImageTextIndexCoverage } from '../../src/shared/image-text-index' +import { sourceMessageId } from '../../src/main/knowledge/message-identity' const ACCOUNT = 'wxid_fixture_account' const CONVERSATION = 'conversation-md5-fixture' @@ -93,72 +94,161 @@ function makeHarness(options: { messages?: chat.FormattedMessage[] } = {}): Harn } } -describe('增量水位:只比条数会漏掉「等量替换」', () => { - it('水位(条数 + 最大插入序)都没变时才跳过,不读 WCDB', async () => { - const harness = makeHarness({ messages: [imageMessage(10, 1000), imageMessage(20, 2000)] }) - harness.watermark.count = 2 - harness.watermark.maxLocalId = 20 - - await harness.service.startPass() - // 走到完成态需要等内部 promise 收敛。 - await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) - expect(harness.listMessages).toHaveBeenCalledTimes(1) - - // 第二遍:水位完全一致 → 跳过,不再读会话消息。 - await harness.service.startPass() - await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) - expect(harness.listMessages).toHaveBeenCalledTimes(1) - }) - - it('总数相同但最大插入序前进 → 必须重扫(撤回一张旧图 + 新增一张新图)', async () => { - const harness = makeHarness({ messages: [imageMessage(10, 1000), imageMessage(20, 2000)] }) - harness.watermark.count = 2 - harness.watermark.maxLocalId = 20 - - await harness.service.startPass() - await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) - expect(harness.listMessages).toHaveBeenCalledTimes(1) - - // 集合变了、条数没变:localId 10 被撤回,新增 localId 30。 - harness.listMessages.mockImplementation(async () => [ - imageMessage(20, 2000), - imageMessage(30, 3000) - ]) - harness.watermark.maxLocalId = 30 - - await harness.service.startPass() - await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) - // 只看 count 的实现会在这里静默跳过 —— 那正是会漏掉新图片的洞。 - expect(harness.listMessages).toHaveBeenCalledTimes(2) - }) - - it('水位不可用(数据库不支持该聚合)时一律重扫,宁可慢也不漏', async () => { - const harness = makeHarness({ messages: [imageMessage(10, 1000)] }) - harness.watermark.count = 1 +/** + * 增量判据。 + * + * recent-first 之后,"什么时候读 WCDB"由**分段完成状态 + 增量水位**共同决定, + * 但两条硬约束一个字都没变: + * 1. 没有新内容时**不得**重复读会话消息、更不得重复 OCR; + * 2. 有新内容(含"总数不变但集合变了")时**必须**处理到 —— 宁可慢,也不漏。 + * + * 这里的夹具刻意做成**窗口感知**的:新架构的"这段没有图片就整段跳过"完全依赖 + * 计数说实话;忽略窗口的夹具测出来的只是"夹具不过滤",不是调度器的行为。 + */ +describe('增量判据:水位不得漏掉新图片', () => { + function makeIncrementalHarness(options: { + messages: chat.FormattedMessage[] + withWatermark?: boolean + }): { + service: ImageTextIndexService + databasePath: string + state: { messages: chat.FormattedMessage[]; count: number; maxLocalId: number; now: number } + listMessages: ReturnType + listImageMessages: ReturnType + } { + const databaseRoot = makeDatabaseRoot() + const databasePath = getImageTextIndexDatabasePath(databaseRoot, ACCOUNT) + const state = { + messages: [...options.messages], + count: options.messages.length, + maxLocalId: options.messages.reduce((max, m) => Math.max(max, Number(m.localId) || 0), 0), + now: 1_800_000_000_000 + } + const inWindow = ( + message: chat.FormattedMessage, + window?: { sinceMs?: number; beforeMs?: number } + ): boolean => { + const createTimeMs = (message.createTime || 0) * 1000 + if (window?.sinceMs !== undefined && createTimeMs < window.sinceMs) return false + if (window?.beforeMs !== undefined && createTimeMs >= window.beforeMs) return false + return true + } + const listMessages = vi.fn(async () => state.messages) + const listImageMessages = vi.fn( + async (_conversationId: string, window?: { sinceMs?: number; beforeMs?: number }) => + state.messages.filter((message) => inWindow(message, window)) + ) const service = new ImageTextIndexService() - const listMessages = vi.fn(async () => [imageMessage(10, 1000)]) service.bind({ - databaseRoot: harness.databaseRoot, + databaseRoot, resolveAccountId: () => ACCOUNT, - listContacts: async () => [{ md5: CONVERSATION, m_nsUsrName: 'fixture', type: 'group' }], + resolveAccountRoot: () => 'C:/fixture/account', + now: () => state.now, + listContacts: async () => [ + { md5: CONVERSATION, m_nsUsrName: 'fixture', type: 'group' as const } + ], listMessages, - countConversationImages: async () => ({ count: 1, typeColumn: 'local_type' }), - // 关键:不提供 imageWatermark + listImageMessages, + countConversationImages: async ( + _conversationId: string, + window?: { sinceMs?: number; beforeMs?: number } + ) => ({ + count: state.messages.filter((message) => inWindow(message, window)).length, + typeColumn: 'local_type' + }), + ...(options.withWatermark === false + ? {} + : { imageWatermark: async () => ({ count: state.count, maxLocalId: state.maxLocalId }) }), + // 没有解密服务 → 每张图片都会被判成 image_missing。这样测试完全不碰真实图片。 decryptService: () => ({ findImageFile: () => null, decryptImage: () => null }) as never, capability: async () => ({ available: true, engine: 'windows-system-ocr', platform: 'win32', - runtimeVersion: null, - language: null - }) + runtimeVersion: '1.2.0', + language: 'zh-Hans-CN' + }), + recognize: async () => ({ success: true, text: '', language: 'zh-Hans-CN' }) + }) + return { service, databasePath, state, listMessages, listImageMessages } + } + + /** 直接读派生库的绑定,回答"到底处理了哪几条",而不是只看调用次数。 */ + const bindingIds = (databasePath: string): string[] => { + const store = new ImageTextIndexStore(databasePath, ACCOUNT) + const ids = [...store.getConversationOcr(CONVERSATION).keys()] + store.close() + return ids + } + + it('全部完成之后:第二遍不再读会话消息,也不重复 OCR', async () => { + const harness = makeIncrementalHarness({ + messages: [imageMessage(10, 1000), imageMessage(20, 2000)] }) - await service.startPass() - await vi.waitFor(() => expect(service.isRunning()).toBe(false)) - await service.startPass() - await vi.waitFor(() => expect(service.isRunning()).toBe(false)) - expect(listMessages).toHaveBeenCalledTimes(2) + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + // 两条图片的 create_time 都很老 → 只落在归档段,只被那一段读到。 + expect(harness.listImageMessages).toHaveBeenCalledTimes(1) + const firstIds = bindingIds(harness.databasePath) + expect(firstIds).toHaveLength(2) + + // 第二遍:水位完全一致 + 各分段已完成 → 一个字都不再读。 + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + expect(harness.listImageMessages).toHaveBeenCalledTimes(1) + expect(bindingIds(harness.databasePath)).toEqual(firstIds) + }) + + it('总数相同但最大插入序前进 → 必须处理到新图片(即使是旧时间)', async () => { + const harness = makeIncrementalHarness({ + messages: [imageMessage(10, 1000), imageMessage(20, 2000)] + }) + + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + expect(bindingIds(harness.databasePath)).toHaveLength(2) + + // 集合变了、条数没变:localId 10 被撤回,新增 localId 30 —— 而且它带着**旧时间**, + // 只按时间窗读的实现在这里就会漏掉它。 + harness.state.messages = [imageMessage(20, 2000), imageMessage(30, 2000)] + harness.state.count = 2 + harness.state.maxLocalId = 30 + harness.state.now += 5_000 + + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + + // 只看 count 的实现会在这里静默跳过 —— 那正是会漏掉新图片的洞。 + const ids = bindingIds(harness.databasePath) + expect(ids).toHaveLength(3) + expect(ids).toContain(sourceMessageId(imageMessage(30, 2000))) + }) + + it('水位不可用(数据库不支持该聚合)时按时间窗兜底重扫,宁可慢也不漏', async () => { + const harness = makeIncrementalHarness({ + messages: [imageMessage(10, 1000)], + withWatermark: false + }) + const anchorSeconds = Math.floor(harness.state.now / 1000) + + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + expect(bindingIds(harness.databasePath)).toHaveLength(1) + + // 锚点之后新到一张图;并且时间确实往前走了一段(否则"窗口必然为空"的短路会生效, + // 那不是漏,而是正确地判定"还没有新东西")。 + harness.state.messages = [imageMessage(10, 1000), imageMessage(40, anchorSeconds + 10)] + harness.state.count = 2 + harness.state.maxLocalId = 40 + harness.state.now += 20_000 + + await harness.service.startPass() + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false)) + + const ids = bindingIds(harness.databasePath) + expect(ids).toHaveLength(2) + expect(ids).toContain(sourceMessageId(imageMessage(40, anchorSeconds + 10))) }) }) diff --git a/tests/integration/image-text-index-image-query.test.ts b/tests/integration/image-text-index-image-query.test.ts index fe75500..8d4d948 100644 --- a/tests/integration/image-text-index-image-query.test.ts +++ b/tests/integration/image-text-index-image-query.test.ts @@ -86,8 +86,24 @@ function createHarness(): Harness { const all = mixedMessages() const imagesOnly = all.filter((message) => message.contentData?.type === 'image') + /** + * recent-first 之后,"读哪些行"由**时间分段**决定,所以夹具必须像真 WCDB 一样 + * 按窗口说话。忽略窗口的夹具测不出调度行为,只会让"读了几次"变成 4 次。 + */ + const inWindow = ( + message: chat.FormattedMessage, + window?: { sinceMs?: number; beforeMs?: number } + ): boolean => { + const createTimeMs = (message.createTime || 0) * 1000 + if (window?.sinceMs !== undefined && createTimeMs < window.sinceMs) return false + if (window?.beforeMs !== undefined && createTimeMs >= window.beforeMs) return false + return true + } const listMessages = vi.fn(async () => all) - const listImageMessages = vi.fn(async () => imagesOnly) + const listImageMessages = vi.fn( + async (_conversationId: string, window?: { sinceMs?: number; beforeMs?: number }) => + imagesOnly.filter((message) => inWindow(message, window)) + ) const service = new ImageTextIndexService() service.bind({ @@ -98,7 +114,13 @@ function createHarness(): Harness { listContacts: async () => [ { md5: CONVERSATION, m_nsUsrName: 'boundary', type: 'user' as const } ], - countConversationImages: async () => ({ count: IMAGE_COUNT, typeColumn: 'local_type' }), + countConversationImages: async ( + _conversationId: string, + window?: { sinceMs?: number; beforeMs?: number } + ) => ({ + count: imagesOnly.filter((message) => inWindow(message, window)).length, + typeColumn: 'local_type' + }), imageWatermark: async () => ({ count: IMAGE_COUNT, maxLocalId: TEXT_COUNT + IMAGE_COUNT }), listMessages, listImageMessages, diff --git a/tests/integration/image-text-index-progress-notify.test.ts b/tests/integration/image-text-index-progress-notify.test.ts index 008be9a..eb88757 100644 --- a/tests/integration/image-text-index-progress-notify.test.ts +++ b/tests/integration/image-text-index-progress-notify.test.ts @@ -117,8 +117,15 @@ function createService(options: { count: number; notifyIntervalMs: number; perIm return { service, notifications } } +/** + * 等 pass 收尾。 + * + * 显式给足超时:这些用例故意让 240 张图各睡几毫秒来制造可观测的持续时间, + * 而 `vi.waitFor` 的默认超时是 1000ms —— 机器稍慢就会以**断言失败**而不是 + * "超时"的形态报出来。这里等的是"跑完",不是"跑得快"。 + */ const finish = async (service: ImageTextIndexService): Promise => { - await vi.waitFor(() => expect(service.isRunning()).toBe(false)) + await vi.waitFor(() => expect(service.isRunning()).toBe(false), { timeout: 30_000 }) } describe('进度通知节流', () => { diff --git a/tests/integration/image-text-index-recent-first.test.ts b/tests/integration/image-text-index-recent-first.test.ts new file mode 100644 index 0000000..92f71a6 --- /dev/null +++ b/tests/integration/image-text-index-recent-first.test.ts @@ -0,0 +1,543 @@ +/** + * 「recent-first 图片文字索引」的行为测试。 + * + * 这一轮改的是**调度**:不再从最老的历史往下扫,而是先让"最近聊天的图片"可搜索。 + * 调度改动最容易骗人的地方是"看起来更快了,其实漏了东西",所以这里的断言都落在 + * **可观察的结果**上(读了哪些窗口、处理了哪些消息、覆盖度怎么说),而不是内部变量。 + * + * 被测的性质: + * - 分段计划:互不重叠、无空隙,边界唯一; + * - 处理顺序按时间分段从新到旧; + * - 断点续跑不重复 OCR; + * - 历史分段跑着的时候,新图片仍然优先; + * - 覆盖度能按时间范围回答"这段能不能下确定性结论"; + * - 老版本既有索引 / 旧 checkpoint 不被降级,源侧有新增时必须补上; + * - UI 文案不暴露工程术语。 + */ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type * as chat from '../../src/main/services/chat-service' +import { ImageTextIndexService } from '../../src/main/services/image-text-index-service' +import { + ImageTextIndexStore, + getImageTextIndexDatabasePath +} from '../../src/main/services/image-text-index-store' +import { sourceMessageId } from '../../src/main/knowledge/message-identity' +import { + IMAGE_TEXT_BACKFILL_WINDOW_DAYS, + buildImageTextBackfillSegments, + describeImageTextRangeCoverage, + imageTextPhaseLabel, + imageTextRangeCoverage, + imageTextWindowToSeconds, + type ImageTextBackfillTier, + type ImageTextIndexCoverage, + type ImageTextTierRunState +} from '../../src/shared/image-text-index' + +const ACCOUNT = 'wxid_recent_first_fixture' +const CONVERSATION = 'conversation-fixture-md5' +const DAY_MS = 24 * 60 * 60 * 1000 +const ANCHOR_MS = 1_800_000_000_000 +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) { + try { + rmSync(root, { recursive: true, force: true }) + } catch { + // 测试收尾尽力而为。 + } + } +}) + +function makeDatabaseRoot(): string { + const root = mkdtempSync(join(tmpdir(), 'tm-recent-first-')) + roots.push(root) + return root +} + +function imageMessage(localId: number, createTimeSeconds: number): chat.FormattedMessage { + return { + localId: String(localId), + createTime: createTimeSeconds, + content: '[图片]', + contentData: { type: 'image', md5: `md5-${localId}`, datName: `dat-${localId}` } + } as unknown as chat.FormattedMessage +} + +type Window = { sinceMs?: number; beforeMs?: number } + +interface HarnessState { + messages: chat.FormattedMessage[] + count: number + maxLocalId: number + now: number +} + +interface HarnessResult { + service: ImageTextIndexService + databasePath: string + state: HarnessState + /** 每次向底层请求图片消息时记录的**时间边界**(不含分页上限)。 */ + windows: Window[] + listImageMessages: ReturnType + /** 真正"过了一遍处理"的图片数 —— 用它证明断点续跑没有重做。 */ + findImageFile: ReturnType +} + +function inWindow(message: chat.FormattedMessage, window?: Window): boolean { + const createTimeMs = (message.createTime || 0) * 1000 + if (window?.sinceMs !== undefined && createTimeMs < window.sinceMs) return false + if (window?.beforeMs !== undefined && createTimeMs >= window.beforeMs) return false + return true +} + +function makeHarness(options: { + messages: chat.FormattedMessage[] + onWindow?: (window: Window | undefined, callIndex: number) => void +}): HarnessResult { + const databaseRoot = makeDatabaseRoot() + const databasePath = getImageTextIndexDatabasePath(databaseRoot, ACCOUNT) + const state = { + messages: [...options.messages], + count: options.messages.length, + maxLocalId: options.messages.reduce((max, m) => Math.max(max, Number(m.localId) || 0), 0), + now: ANCHOR_MS + } + const windows: Window[] = [] + const findImageFile = vi.fn(() => null) + + const listImageMessages = vi.fn(async (_conversationId: string, window?: Window) => { + // 只记时间边界:调用方还会带一个分页上限,那与"读了哪个时间窗"无关。 + const bounds: Window = {} + if (window?.sinceMs !== undefined) bounds.sinceMs = window.sinceMs + if (window?.beforeMs !== undefined) bounds.beforeMs = window.beforeMs + windows.push(bounds) + options.onWindow?.(window, windows.length - 1) + return state.messages.filter((message) => inWindow(message, window)) + }) + + const service = new ImageTextIndexService() + service.bind({ + databaseRoot, + resolveAccountId: () => ACCOUNT, + resolveAccountRoot: () => 'C:/fixture/account', + now: () => state.now, + listContacts: async () => [ + { md5: CONVERSATION, m_nsUsrName: 'fixture', type: 'group' as const } + ], + listImageMessages, + // 窗口感知的计数:新架构"这段没图片就整段跳过"完全依赖它说实话。 + countConversationImages: async (_conversationId: string, window?: Window) => ({ + count: state.messages.filter((message) => inWindow(message, window)).length, + typeColumn: 'local_type' + }), + imageWatermark: async () => ({ count: state.count, maxLocalId: state.maxLocalId }), + // 没有解密能力 → 每张图片都会被判成 image_missing,测试完全不碰真实图片。 + decryptService: () => ({ findImageFile, decryptImage: () => null }) as never, + capability: async () => ({ + available: true, + engine: 'windows-system-ocr', + platform: 'win32', + runtimeVersion: '1.2.0', + language: 'zh-Hans-CN' + }), + recognize: async () => ({ success: true, text: '', language: 'zh-Hans-CN' }) + }) + + return { service, databasePath, state, windows, listImageMessages, findImageFile } +} + +const bindingIds = (databasePath: string): string[] => { + const store = new ImageTextIndexStore(databasePath, ACCOUNT) + const ids = [...store.getConversationOcr(CONVERSATION).keys()] + store.close() + return ids +} + +const tierStates = ( + databasePath: string +): Partial> => { + const store = new ImageTextIndexStore(databasePath, ACCOUNT) + const result = store.readBackfillState() + store.close() + return result.tierStates +} + +const runPass = async ( + harness: HarnessResult, + options?: Parameters[0] +): Promise => { + harness.service.startPass(options) + await vi.waitFor(() => expect(harness.service.isRunning()).toBe(false), { timeout: 15_000 }) +} + +/** 合成数据集:今天 / 3 天前 / 15 天前 / 6 个月前 / 3 年前,各 10 张。 */ +function syntheticDataset(anchorMs: number): { + messages: chat.FormattedMessage[] + byBucket: Map +} { + const buckets: Array<{ key: string; offsetDays: number }> = [ + { key: 'today', offsetDays: 0 }, + { key: '3d', offsetDays: 3 }, + { key: '15d', offsetDays: 15 }, + { key: '6mo', offsetDays: 183 }, + { key: '3y', offsetDays: 1095 } + ] + const messages: chat.FormattedMessage[] = [] + const byBucket = new Map() + let localId = 1 + for (const bucket of buckets) { + const ids: number[] = [] + const baseSeconds = Math.floor((anchorMs - bucket.offsetDays * DAY_MS) / 1000) - 60 + for (let index = 0; index < 10; index += 1) { + messages.push(imageMessage(localId, baseSeconds + index)) + ids.push(localId) + localId += 1 + } + byBucket.set(bucket.key, ids) + } + return { messages, byBucket } +} + +describe('分段计划:唯一边界、不重不漏', () => { + it('四段从新到旧、相邻且互不重叠', () => { + const segments = buildImageTextBackfillSegments(ANCHOR_MS) + expect(segments.map((segment) => segment.tier)).toEqual([ + 'recent_7d', + 'recent_30d', + 'recent_1y', + 'archive' + ]) + expect(segments[0].endMs).toBe(ANCHOR_MS) + for (let index = 1; index < segments.length; index += 1) { + // 上一段的上界必须等于下一段的下界:半开区间才既不重叠也不留缝。 + expect(segments[index].endMs).toBe(segments[index - 1].startMs) + } + expect(segments[2].startMs).toBe( + ANCHOR_MS - IMAGE_TEXT_BACKFILL_WINDOW_DAYS.recent_1y * DAY_MS + ) + // 归档段没有下界。 + expect(segments[3].startMs).toBe(Number.NEGATIVE_INFINITY) + }) + + it('边界那一秒只属于一段(两端同一套取整)', () => { + const boundaryMs = ANCHOR_MS - 7 * DAY_MS + const { beforeSecInclusive } = imageTextWindowToSeconds({ beforeMs: boundaryMs }) + const { sinceSec } = imageTextWindowToSeconds({ sinceMs: boundaryMs }) + // 上界段的"含"到 beforeSecInclusive,下界段从 sinceSec 起 —— 必须正好衔接。 + expect(sinceSec! - beforeSecInclusive!).toBe(1) + }) +}) + +describe('recent-first:合成数据集的真实处理顺序', () => { + it('今天 / 3 天 → 15 天 → 6 个月前 → 3 年前,先新后旧', async () => { + const harness = makeHarness({ messages: [] }) + const { messages, byBucket } = syntheticDataset(ANCHOR_MS) + harness.state.messages = messages + harness.state.count = messages.length + harness.state.maxLocalId = Math.max(...messages.map((m) => Number(m.localId))) + + await runPass(harness) + + // 只有"有图片的分段"才会去读消息:今天+3 天 / 15 天 / 6 个月 / 3 年,共 4 次。 + expect(harness.windows).toHaveLength(4) + const [first, second, third, fourth] = harness.windows + expect(first).toEqual({ sinceMs: ANCHOR_MS - 7 * DAY_MS, beforeMs: ANCHOR_MS }) + expect(second).toEqual({ sinceMs: ANCHOR_MS - 30 * DAY_MS, beforeMs: ANCHOR_MS - 7 * DAY_MS }) + expect(third).toEqual({ sinceMs: ANCHOR_MS - 365 * DAY_MS, beforeMs: ANCHOR_MS - 30 * DAY_MS }) + expect(fourth).toEqual({ sinceMs: Number.NEGATIVE_INFINITY, beforeMs: ANCHOR_MS - 365 * DAY_MS }) + + // 第一次读就拿到了"最近的图"—— 这正是用户要的价值。 + const ids = bindingIds(harness.databasePath) + expect(ids).toHaveLength(50) + const recentIds = [...byBucket.get('today')!, ...byBucket.get('3d')!] + for (const id of recentIds) expect(ids).toContain(sourceMessageId(imageMessage(id, 0))) + expect(Math.max(...byBucket.get('today')!)).toBeLessThan(Math.min(...byBucket.get('3y')!)) + for (const id of byBucket.get('3y')!) { + expect(ids).toContain(sourceMessageId(imageMessage(id, 0))) + } + }) +}) + +describe('断点续跑:不重复 OCR', () => { + it('第一批被预算截断后,重启只补剩下的,已完成的不再走一遍', async () => { + const harness = makeHarness({ messages: [] }) + const { messages } = syntheticDataset(ANCHOR_MS) + harness.state.messages = messages + harness.state.count = messages.length + harness.state.maxLocalId = Math.max(...messages.map((m) => Number(m.localId))) + + // 第一轮:只允许处理 6 张 → 必然停在"最近图片"这一段中间。 + await runPass(harness, { messageLimit: 6 }) + expect(harness.findImageFile).toHaveBeenCalledTimes(6) + expect(harness.windows).toHaveLength(1) + let states = tierStates(harness.databasePath) + // 该分段没跑完 → 不允许被标成 complete(否则剩下的图永远不会被处理)。 + expect(states.recent_7d).not.toBe('complete') + + // 第二轮:预算放开 → 先把最近这段的剩余 4 张补完,再继续往下。 + await runPass(harness) + expect(harness.findImageFile).toHaveBeenCalledTimes(50) + states = tierStates(harness.databasePath) + expect(states.recent_7d).toBe('complete') + expect(states.archive).toBe('complete') + expect(bindingIds(harness.databasePath)).toHaveLength(50) + }) +}) + +describe('增量优先:历史分段跑着的时候新图片先处理', () => { + it('新图片在下一次调度点被处理,而不是排到历史之后', async () => { + let injected = false + const harness = makeHarness({ + messages: [], + onWindow: (window) => { + // 第二轮窗口请求(recent_30d)之后,模拟"用户刚收到一张新图"。 + if (injected || window?.sinceMs === undefined) return + if (window.beforeMs === ANCHOR_MS - 7 * DAY_MS) { + injected = true + harness.state.messages = [ + ...harness.state.messages, + imageMessage(999, Math.floor((ANCHOR_MS + 5_000) / 1000)) + ] + harness.state.count += 1 + harness.state.maxLocalId = 999 + harness.state.now = ANCHOR_MS + 5_000 + } + } + }) + const { messages } = syntheticDataset(ANCHOR_MS) + harness.state.messages = messages + harness.state.count = messages.length + harness.state.maxLocalId = Math.max(...messages.map((m) => Number(m.localId))) + + await runPass(harness) + + const newMessageId = sourceMessageId(imageMessage(999, 0)) + expect(bindingIds(harness.databasePath)).toContain(newMessageId) + /** + * 新图片必须在"更老的分段"之前被处理。 + * + * 增量补齐对"插入序涨了"的会话是**不设时间窗**读取的(这样才接得住晚到的旧时间消息), + * 所以这里找的是"没有任何时间边界的那个窗口",它必须早于 1 年 / 归档分段。 + */ + const sweepWindowIndex = harness.windows.findIndex( + (window) => window.sinceMs === undefined && window.beforeMs === undefined + ) + const yearWindowIndex = harness.windows.findIndex( + (window) => window?.beforeMs === ANCHOR_MS - 30 * DAY_MS + ) + expect(sweepWindowIndex).toBeGreaterThanOrEqual(0) + expect(yearWindowIndex).toBeGreaterThan(sweepWindowIndex) + }) +}) + +describe('兼容性:老索引不被降级,源侧有新增必须补上', () => { + it('老版本已经全量建完且源侧无变化 → 保持完成,一个字都不读', async () => { + const harness = makeHarness({ messages: [] }) + // 造一份"旧版本建完"的库:总数 3、三条 binding 全部 terminal、没有任何分段元数据。 + const store = new ImageTextIndexStore(harness.databasePath, ACCOUNT) + store.writeCountedTotal({ total: 3, countedAt: ANCHOR_MS - 1000, complete: true }) + // 旧版本每跑完一个会话都会留下 scan_state —— 升级后正是靠它证明源侧没动过。 + store.writeScanState({ + conversationId: CONVERSATION, + state: 'done', + imageTotal: 3, + imageProcessed: 3, + maxLocalId: 3 + }) + for (let index = 1; index <= 3; index += 1) { + store.putBinding({ + accountId: ACCOUNT, + conversationId: CONVERSATION, + messageId: `local:${index}`, + createTime: ANCHOR_MS - 1000, + imageIdentity: `sha256:${index}`, + artifactKey: `sha256:${index}|legacy`, + state: 'indexed', + updatedAt: 1 + }) + } + store.close() + + harness.state.messages = [imageMessage(1, 1000), imageMessage(2, 2000), imageMessage(3, 3000)] + harness.state.count = 3 + harness.state.maxLocalId = 3 + + await runPass(harness) + + // 源侧水位一致 → 一个字都不该读:用户已经拥有的东西不能被降级成"重新建立"。 + expect(harness.listImageMessages).not.toHaveBeenCalled() + expect(harness.findImageFile).not.toHaveBeenCalled() + const states = tierStates(harness.databasePath) + expect(states.recent_7d).toBe('complete') + expect(states.archive).toBe('complete') + }) + + it('老版本自称"已建完"但源侧已新增 → 必须补上新增,不能信库里的进度', async () => { + const harness = makeHarness({ messages: [] }) + const store = new ImageTextIndexStore(harness.databasePath, ACCOUNT) + // 库里自称 2/2 已完成(旧版本跑完时的状态)。 + store.writeCountedTotal({ total: 2, countedAt: ANCHOR_MS - 1000, complete: true }) + for (const localId of [1, 2]) { + store.writeScanState({ + conversationId: CONVERSATION, + state: 'done', + imageTotal: 2, + imageProcessed: 2, + maxLocalId: 2 + }) + store.putBinding({ + accountId: ACCOUNT, + conversationId: CONVERSATION, + messageId: `local:${localId}`, + createTime: ANCHOR_MS - 1000, + imageIdentity: `sha256:old-${localId}`, + artifactKey: `sha256:old-${localId}|legacy`, + state: 'indexed', + updatedAt: 1 + }) + } + store.close() + + // 源侧现在是 3 张(插入序 1..3)—— 只看库里自称的进度会把它误判成"已完成"。 + harness.state.messages = [imageMessage(1, 1000), imageMessage(2, 2000), imageMessage(3, 3000)] + harness.state.count = 3 + harness.state.maxLocalId = 3 + + await runPass(harness) + + const ids = bindingIds(harness.databasePath) + expect(ids).toHaveLength(3) + expect(ids).toContain(sourceMessageId(imageMessage(3, 3000))) + // 老的 terminal 结果必须被复用,不能重新 OCR。 + expect(harness.findImageFile).toHaveBeenCalledTimes(1) + }) +}) + +describe('时间范围覆盖度', () => { + const coverageOf = (input: Partial): ImageTextIndexCoverage => ({ + totalImageMessages: 100, + processed: 20, + indexed: 18, + empty: 2, + missing: 0, + failed: 0, + runtimeUnavailable: 0, + pending: 80, + established: true, + complete: false, + systemicFailure: false, + countedAt: ANCHOR_MS, + tiers: [ + { tier: 'recent_7d', state: 'complete', startMs: ANCHOR_MS - 7 * DAY_MS, endMs: ANCHOR_MS }, + { + tier: 'recent_30d', + state: 'pending', + startMs: ANCHOR_MS - 30 * DAY_MS, + endMs: ANCHOR_MS - 7 * DAY_MS + }, + { + tier: 'recent_1y', + state: 'pending', + startMs: ANCHOR_MS - 365 * DAY_MS, + endMs: ANCHOR_MS - 30 * DAY_MS + }, + { + tier: 'archive', + state: 'pending', + startMs: Number.NEGATIVE_INFINITY, + endMs: ANCHOR_MS - 365 * DAY_MS + } + ], + coveredToMs: ANCHOR_MS, + ...input + }) + + it('A. 最近 7 天已完成 → 查询最近 24h 可以下确定性结论', () => { + const coverage = coverageOf({}) + const range = { sinceMs: ANCHOR_MS - DAY_MS, beforeMs: ANCHOR_MS } + const result = imageTextRangeCoverage(coverage, range) + expect(result.state).toBe('complete') + expect(result.hasUncovered).toBe(false) + expect(describeImageTextRangeCoverage(coverage, range)).toContain('已覆盖该时间范围') + }) + + it('B. 更早历史未完成 → 全历史查询只能是 partial,并明确"不能排除尚未索引的图片"', () => { + const coverage = coverageOf({}) + expect(imageTextRangeCoverage(coverage, {}).state).toBe('partial') + expect(describeImageTextRangeCoverage(coverage, {})).toContain('不能排除尚未索引的图片') + }) + + it('C. 全部完成 → 任意范围都是 complete', () => { + const coverage = coverageOf({ + processed: 100, + indexed: 98, + empty: 2, + pending: 0, + complete: true, + tiers: coverageOf({}).tiers.map((entry) => ({ ...entry, state: 'complete' as const })) + }) + expect(imageTextRangeCoverage(coverage, {}).state).toBe('complete') + expect( + imageTextRangeCoverage(coverage, { sinceMs: ANCHOR_MS - 900 * DAY_MS }).state + ).toBe('complete') + }) + + it('D. 在更老的历史里被取消 → 最近一段仍可完整,全历史仍是 partial', () => { + const coverage = coverageOf({}) + expect( + imageTextRangeCoverage(coverage, { + sinceMs: ANCHOR_MS - 3 * DAY_MS, + beforeMs: ANCHOR_MS + }).state + ).toBe('complete') + expect(imageTextRangeCoverage(coverage, {}).state).toBe('partial') + }) + + it('未建立时不得声称任何范围完整', () => { + const coverage = coverageOf({ established: false, tiers: [], coveredToMs: null }) + expect(imageTextRangeCoverage(coverage, { sinceMs: ANCHOR_MS - DAY_MS }).state).toBe( + 'not_built' + ) + }) +}) + +describe('UI 文案与进度', () => { + it('阶段文案是用户语言,不出现工程术语', () => { + expect(imageTextPhaseLabel('recent_7d')).toBe('正在优先索引最近图片') + expect(imageTextPhaseLabel('incremental')).toBe('正在优先索引最近图片') + expect(imageTextPhaseLabel('recent_30d')).toBe('正在补齐最近 30 天') + expect(imageTextPhaseLabel('recent_1y')).toBe('正在补齐近一年图片') + expect(imageTextPhaseLabel('archive')).toBe('正在补齐更早图片') + expect(imageTextPhaseLabel('complete')).toBe('图片文字索引已完成') + const phases = ['incremental', 'recent_7d', 'recent_30d', 'recent_1y', 'archive', 'complete'] as const + for (const phase of phases) { + expect(imageTextPhaseLabel(phase)).not.toMatch(/Tier/i) + } + }) + + it('阶段字段在运行中出现,且总进度仍以全量图片数为分母', async () => { + const harness = makeHarness({ messages: [] }) + const { messages } = syntheticDataset(ANCHOR_MS) + harness.state.messages = messages + harness.state.count = messages.length + harness.state.maxLocalId = Math.max(...messages.map((m) => Number(m.localId))) + + await runPass(harness, { messageLimit: 3 }) + const status = await harness.service.getStatus() + // 总分母必须是全部图片消息,而不是"这一段处理了多少"。 + expect(status.progress.totalImageMessages).toBe(50) + expect(status.progress.processed).toBeGreaterThan(0) + expect(status.progress.percent).toBeLessThan(100) + expect(status.coverage.tiers.map((entry) => entry.tier)).toEqual([ + 'recent_7d', + 'recent_30d', + 'recent_1y', + 'archive' + ]) + }) +})