feat: merge favorites into export and search_messages

This commit is contained in:
wuyouMaster
2026-09-24 23:55:52 +08:00
parent 19bf497af0
commit afb876250e
5 changed files with 90 additions and 2 deletions
+23
View File
@@ -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<string>()
@@ -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)
+28
View File
@@ -30,6 +30,34 @@ export class FavoritesService {
} as Message
})
}
/** 只读关键词搜索收藏文本(title/desc/正文)。 */
async search(query: string, limit = 50): Promise<ParsedContent[]> {
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<Array<{ text: string; timestamp?: number }>> {
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 {
+7
View File
@@ -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(
+30 -2
View File
@@ -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<Array<{ text: string; timestamp?: number }>>)
| undefined
constructor(
private readonly knowledge?: KnowledgeSearchService,
private readonly nowProvider: () => Date = () => new Date()
) {}
/** 注入收藏检索(`FavoritesService.searchHits`),供 `search_messages` 合并只读命中。 */
setFavoritesSearchProvider(
provider: (query: string, limit: number) => Promise<Array<{ text: string; timestamp?: number }>>
): 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,
+2
View File
@@ -38,6 +38,8 @@ export interface ExportRequest {
kinds: ExportMessageKind[]
includeMedia: boolean
includeVoiceTranscripts?: boolean
/** 合并本机收藏(favorite.db)只读卡片。 */
includeFavorites?: boolean
preferOriginal?: boolean
fallbackThumbnail?: boolean
keepMissing?: boolean