From afb876250efe693add5d8fcfb0c6ce742d639fc7 Mon Sep 17 00:00:00 2001 From: wuyouMaster Date: Thu, 24 Sep 2026 23:55:52 +0800 Subject: [PATCH] feat: merge favorites into export and search_messages --- src/main/export-service.ts | 23 ++++++++++++++ src/main/favorites-service.ts | 28 +++++++++++++++++ src/main/index.ts | 7 +++++ src/main/services/local-query-api-service.ts | 32 ++++++++++++++++++-- src/shared/export.ts | 2 ++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/main/export-service.ts b/src/main/export-service.ts index 08b8801..d707146 100644 --- a/src/main/export-service.ts +++ b/src/main/export-service.ts @@ -25,6 +25,7 @@ import { mergeCachedSelfInfo, type CachedSelfInfo } from './services/bootstrap-c import type { VoiceRecognitionUseCase } from './voice-pipeline/voice-recognition-use-case' import { imageFileQuality } from '../shared/image-quality' import { resolveMemberName } from '../shared/member-names' +import { FavoritesService } from './favorites-service' import { filesystemSafeName } from '../shared/contact-name' const jobs = new Set() @@ -853,6 +854,28 @@ async function runSingleExport( percent: Math.max(1, Math.round(((targetOrder + 1) / targets.length) * 10)) }) } + if (request.includeFavorites) { + const wcdb = chat.getChatDb()?.getWcdb4Client() + if (wcdb) { + try { + const favoriteMessages = await new FavoritesService(wcdb).listExportMessages(500) + for (const [messageOrder, message] of favoriteMessages.entries()) { + if (!request.kinds.includes(kindOf(message))) continue + messageEntries.push({ + message: { + ...message, + exportConversationId: 'favorites', + exportConversationName: '收藏' + }, + targetOrder: targets.length, + messageOrder + }) + } + } catch (error) { + console.warn('[export] favorites merge skipped:', error) + } + } + } const messages = messageEntries .sort((left, right) => { const byTime = Number(left.message.createTime || 0) - Number(right.message.createTime || 0) diff --git a/src/main/favorites-service.ts b/src/main/favorites-service.ts index b8ad5d0..83e9b81 100644 --- a/src/main/favorites-service.ts +++ b/src/main/favorites-service.ts @@ -30,6 +30,34 @@ export class FavoritesService { } as Message }) } + + /** 只读关键词搜索收藏文本(title/desc/正文)。 */ + async search(query: string, limit = 50): Promise { + const needle = String(query || '').trim().toLowerCase() + if (!needle) return [] + const items = await this.listContents(500) + return items.filter((item) => textOf(item).toLowerCase().includes(needle)).slice(0, limit) + } + + /** Local Query API 适配:只要文本与时间戳。 */ + async searchHits( + query: string, + limit = 20 + ): Promise> { + const rows = await this.wcdb4Client.listFavoriteItems(500) + const needle = String(query || '').trim().toLowerCase() + if (!needle) return [] + return rows + .map((row) => { + const content = favoriteRowToContent(row) + return { + text: textOf(content), + timestamp: Number(row.update_time) || undefined + } + }) + .filter((hit) => hit.text.toLowerCase().includes(needle)) + .slice(0, limit) + } } function textOf(content: ParsedContent): string { diff --git a/src/main/index.ts b/src/main/index.ts index a493a53..2f67d91 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -771,6 +771,13 @@ app.whenReady().then(async () => { localQueryApiService.setImageOcrEntryProvider((conversationId, messageId) => imageTextIndexService.getConversationOcr(conversationId).get(messageId) ) + // 收藏只读检索:search_messages 合并 favorite.db 命中(无库时返回空)。 + localQueryApiService.setFavoritesSearchProvider(async (query, limit) => { + const wcdb = chat.getChatDb()?.getWcdb4Client() + if (!wcdb) return [] + const { FavoritesService } = await import('./favorites-service') + return new FavoritesService(wcdb).searchHits(query, limit) + }) setLocalQueryApiService(localQueryApiService) // Query Agent:生产 Runtime 只在这里实例化一次,桌面问问微信与 Agent Hub 共用同一个实例。 queryAgentService = new QueryAgentService( diff --git a/src/main/services/local-query-api-service.ts b/src/main/services/local-query-api-service.ts index 0033e7b..e2288b4 100644 --- a/src/main/services/local-query-api-service.ts +++ b/src/main/services/local-query-api-service.ts @@ -320,11 +320,23 @@ export class LocalQueryApiService { | ((conversationId: string, messageId: string) => { state: string; text: string } | undefined) | undefined + /** 收藏关键词检索(favorite.db,只读)。 */ + private favoritesSearch: + | ((query: string, limit: number) => Promise>) + | undefined + constructor( private readonly knowledge?: KnowledgeSearchService, private readonly nowProvider: () => Date = () => new Date() ) {} + /** 注入收藏检索(`FavoritesService.searchHits`),供 `search_messages` 合并只读命中。 */ + setFavoritesSearchProvider( + provider: (query: string, limit: number) => Promise> + ): void { + this.favoritesSearch = provider + } + /** * 注入图片文字索引覆盖度提供者。 * @@ -507,14 +519,30 @@ export class LocalQueryApiService { enrichmentMs, ...(knowledgeTiming ? { knowledge: knowledgeTiming } : {}) } + let favoriteEvidence: QueryEvidenceItem[] = [] + if (this.favoritesSearch && !targetView) { + try { + const hits = await this.favoritesSearch(request.query, Math.min(10, limit)) + favoriteEvidence = hits.map((hit, index) => ({ + messageRef: `fav-${index}`, + conversationName: '收藏', + timestamp: hit.timestamp ?? 0, + sender: '收藏', + sourceKind: 'other' as QueryMessageType, + text: hit.text + })) + } catch { + favoriteEvidence = [] + } + } return { status: 'completed' as const, ...(targetView ? { target: targetView } : {}), resolvedTimeRange: range, coverage: { state: this.searchCoverage(found, requestedEnd) }, probeCount: probes.length, - evidenceCount: found.evidence.size, - evidence: Array.from(found.evidence.values()).slice(0, limit), + evidenceCount: found.evidence.size + favoriteEvidence.length, + evidence: [...Array.from(found.evidence.values()).slice(0, limit), ...favoriteEvidence], scope: corpus.scope, indexLatestAt: found.indexLatestAt, sourceLatestAt: found.sourceLatestAt, diff --git a/src/shared/export.ts b/src/shared/export.ts index 0fe2bf3..ba1e558 100644 --- a/src/shared/export.ts +++ b/src/shared/export.ts @@ -38,6 +38,8 @@ export interface ExportRequest { kinds: ExportMessageKind[] includeMedia: boolean includeVoiceTranscripts?: boolean + /** 合并本机收藏(favorite.db)只读卡片。 */ + includeFavorites?: boolean preferOriginal?: boolean fallbackThumbnail?: boolean keepMissing?: boolean