mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +00:00
优化底层游标索引性能;优化HTTPAPI索引逻辑;优化导出图片的索引写入逻辑
This commit is contained in:
@@ -2390,6 +2390,8 @@ function registerIpcHandlers() {
|
|||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
cacheOnly?: boolean
|
cacheOnly?: boolean
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
}) => {
|
}) => {
|
||||||
return chatService.getExportSessionStats(sessionIds, options)
|
return chatService.getExportSessionStats(sessionIds, options)
|
||||||
})
|
})
|
||||||
@@ -3935,4 +3937,3 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
cacheOnly?: boolean
|
cacheOnly?: boolean
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
}
|
}
|
||||||
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
||||||
getGroupMyMessageCountHint: (chatroomId: string) =>
|
getGroupMyMessageCountHint: (chatroomId: string) =>
|
||||||
@@ -565,4 +567,3 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
validateWeiboUid: (uid: string) => ipcRenderer.invoke('social:validateWeiboUid', uid)
|
validateWeiboUid: (uid: string) => ipcRenderer.invoke('social:validateWeiboUid', uid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ interface ExportSessionStatsOptions {
|
|||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
cacheOnly?: boolean
|
cacheOnly?: boolean
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExportSessionStatsCacheMeta {
|
interface ExportSessionStatsCacheMeta {
|
||||||
@@ -2178,28 +2180,31 @@ class ChatService {
|
|||||||
return { success: false, error: connectResult.error || '数据库未连接' }
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchSize = Math.max(1, limit)
|
// 聊天页首屏优先走稳定路径:直接拉取固定窗口并做本地确定性排序,
|
||||||
const cursorResult = await wcdbService.openMessageCursor(sessionId, batchSize, false, 0, 0)
|
// 避免游标首批在极端数据分布下出现不稳定边界。
|
||||||
if (!cursorResult.success || !cursorResult.cursor) {
|
const pageLimit = Math.max(1, Math.floor(limit || this.messageBatchDefault))
|
||||||
return { success: false, error: cursorResult.error || '打开消息游标失败' }
|
const probeLimit = Math.min(500, pageLimit + 1)
|
||||||
|
const result = await wcdbService.getMessages(sessionId, probeLimit, 0)
|
||||||
|
if (!result.success || !Array.isArray(result.messages)) {
|
||||||
|
return { success: false, error: result.error || '获取最新消息失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const rawRows = result.messages as Record<string, any>[]
|
||||||
const collected = await this.collectVisibleMessagesFromCursor(sessionId, cursorResult.cursor, limit)
|
const hasMore = rawRows.length > pageLimit
|
||||||
if (!collected.success) {
|
const selectedRows = hasMore ? rawRows.slice(0, pageLimit) : rawRows
|
||||||
return { success: false, error: collected.error || '获取消息失败' }
|
const mapped = this.mapRowsToMessages(selectedRows)
|
||||||
}
|
const visible = mapped.filter((msg) => this.isMessageVisibleForSession(sessionId, msg))
|
||||||
|
const normalized = this.normalizeMessageOrder(visible)
|
||||||
|
await this.repairEmojiMessages(normalized)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[ChatService] getLatestMessages session=${sessionId} rawRowsConsumed=${collected.rawRowsConsumed || 0} visibleMessagesReturned=${collected.messages?.length || 0} filteredOut=${collected.filteredOut || 0} nextOffset=${collected.rawRowsConsumed || 0} hasMore=${collected.hasMore === true}`
|
`[ChatService] getLatestMessages(stable) session=${sessionId} rawRows=${rawRows.length} visibleMessagesReturned=${normalized.length} nextOffset=${selectedRows.length} hasMore=${hasMore}`
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
messages: collected.messages,
|
messages: normalized,
|
||||||
hasMore: collected.hasMore,
|
hasMore,
|
||||||
nextOffset: collected.rawRowsConsumed || 0
|
nextOffset: selectedRows.length
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await wcdbService.closeMessageCursor(cursorResult.cursor)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 获取最新消息失败:', e)
|
console.error('ChatService: 获取最新消息失败:', e)
|
||||||
@@ -2241,16 +2246,59 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private compareMessagesByTimeline(a: Message, b: Message): number {
|
||||||
|
const aSortSeq = Math.max(0, Number(a.sortSeq || 0))
|
||||||
|
const bSortSeq = Math.max(0, Number(b.sortSeq || 0))
|
||||||
|
const aCreateTime = Math.max(0, Number(a.createTime || 0))
|
||||||
|
const bCreateTime = Math.max(0, Number(b.createTime || 0))
|
||||||
|
const aLocalId = Math.max(0, Number(a.localId || 0))
|
||||||
|
const bLocalId = Math.max(0, Number(b.localId || 0))
|
||||||
|
const aServerId = Math.max(0, Number(a.serverId || 0))
|
||||||
|
const bServerId = Math.max(0, Number(b.serverId || 0))
|
||||||
|
|
||||||
|
// 与 C++ 侧归并规则一致:当两侧都有 sortSeq 时优先 sortSeq,否则先看 createTime。
|
||||||
|
if (aSortSeq > 0 && bSortSeq > 0 && aSortSeq !== bSortSeq) {
|
||||||
|
return aSortSeq - bSortSeq
|
||||||
|
}
|
||||||
|
if (aCreateTime !== bCreateTime) {
|
||||||
|
return aCreateTime - bCreateTime
|
||||||
|
}
|
||||||
|
if (aSortSeq !== bSortSeq) {
|
||||||
|
return aSortSeq - bSortSeq
|
||||||
|
}
|
||||||
|
if (aLocalId !== bLocalId) {
|
||||||
|
return aLocalId - bLocalId
|
||||||
|
}
|
||||||
|
if (aServerId !== bServerId) {
|
||||||
|
return aServerId - bServerId
|
||||||
|
}
|
||||||
|
|
||||||
|
const aKey = String(a.messageKey || '')
|
||||||
|
const bKey = String(b.messageKey || '')
|
||||||
|
if (aKey < bKey) return -1
|
||||||
|
if (aKey > bKey) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeMessageOrder(messages: Message[]): Message[] {
|
private normalizeMessageOrder(messages: Message[]): Message[] {
|
||||||
if (messages.length < 2) return messages
|
if (messages.length < 2) return messages
|
||||||
const first = messages[0]
|
|
||||||
const last = messages[messages.length - 1]
|
const withIndex = messages.map((msg, index) => ({ msg, index }))
|
||||||
const firstKey = first.sortSeq || first.createTime || first.localId || 0
|
withIndex.sort((left, right) => {
|
||||||
const lastKey = last.sortSeq || last.createTime || last.localId || 0
|
const diff = this.compareMessagesByTimeline(left.msg, right.msg)
|
||||||
if (firstKey > lastKey) {
|
if (diff !== 0) return diff
|
||||||
return [...messages].reverse()
|
return left.index - right.index
|
||||||
|
})
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
for (let index = 0; index < withIndex.length; index += 1) {
|
||||||
|
if (withIndex[index].msg !== messages[index]) {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return messages
|
}
|
||||||
|
if (!changed) return messages
|
||||||
|
return withIndex.map((entry) => entry.msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
private encodeMessageKeySegment(value: unknown): string {
|
private encodeMessageKeySegment(value: unknown): string {
|
||||||
@@ -2436,6 +2484,95 @@ class ChatService {
|
|||||||
return Number.isFinite(parsed) ? parsed : fallback
|
return Number.isFinite(parsed) ? parsed : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseCompactDateTimeDigitsToSeconds(raw: string): number {
|
||||||
|
const text = String(raw || '').trim()
|
||||||
|
if (!/^\d{8}(?:\d{4}(?:\d{2})?)?$/.test(text)) return 0
|
||||||
|
|
||||||
|
const year = Number.parseInt(text.slice(0, 4), 10)
|
||||||
|
const month = Number.parseInt(text.slice(4, 6), 10)
|
||||||
|
const day = Number.parseInt(text.slice(6, 8), 10)
|
||||||
|
const hour = text.length >= 12 ? Number.parseInt(text.slice(8, 10), 10) : 0
|
||||||
|
const minute = text.length >= 12 ? Number.parseInt(text.slice(10, 12), 10) : 0
|
||||||
|
const second = text.length >= 14 ? Number.parseInt(text.slice(12, 14), 10) : 0
|
||||||
|
|
||||||
|
if (!Number.isFinite(year) || year < 1990 || year > 2200) return 0
|
||||||
|
if (!Number.isFinite(month) || month < 1 || month > 12) return 0
|
||||||
|
if (!Number.isFinite(day) || day < 1 || day > 31) return 0
|
||||||
|
if (!Number.isFinite(hour) || hour < 0 || hour > 23) return 0
|
||||||
|
if (!Number.isFinite(minute) || minute < 0 || minute > 59) return 0
|
||||||
|
if (!Number.isFinite(second) || second < 0 || second > 59) return 0
|
||||||
|
|
||||||
|
const dt = new Date(year, month - 1, day, hour, minute, second)
|
||||||
|
if (
|
||||||
|
dt.getFullYear() !== year ||
|
||||||
|
dt.getMonth() !== month - 1 ||
|
||||||
|
dt.getDate() !== day ||
|
||||||
|
dt.getHours() !== hour ||
|
||||||
|
dt.getMinutes() !== minute ||
|
||||||
|
dt.getSeconds() !== second
|
||||||
|
) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const ts = Math.floor(dt.getTime() / 1000)
|
||||||
|
return Number.isFinite(ts) && ts > 0 ? ts : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDateTimeTextToSeconds(raw: unknown): number {
|
||||||
|
const text = String(raw ?? '').trim()
|
||||||
|
if (!text) return 0
|
||||||
|
|
||||||
|
const compactDigits = this.parseCompactDateTimeDigitsToSeconds(text)
|
||||||
|
if (compactDigits > 0) return compactDigits
|
||||||
|
|
||||||
|
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(text)) {
|
||||||
|
const parsed = Date.parse(text)
|
||||||
|
const seconds = Math.floor(parsed / 1000)
|
||||||
|
if (Number.isFinite(seconds) && seconds > 0) return seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = text.replace('T', ' ').replace(/\.\d+$/, '').replace(/\//g, '-')
|
||||||
|
const match = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/)
|
||||||
|
if (!match) return 0
|
||||||
|
|
||||||
|
const year = Number.parseInt(match[1], 10)
|
||||||
|
const month = Number.parseInt(match[2], 10)
|
||||||
|
const day = Number.parseInt(match[3], 10)
|
||||||
|
const hour = Number.parseInt(match[4] || '0', 10)
|
||||||
|
const minute = Number.parseInt(match[5] || '0', 10)
|
||||||
|
const second = Number.parseInt(match[6] || '0', 10)
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return 0
|
||||||
|
const dt = new Date(year, month - 1, day, hour, minute, second)
|
||||||
|
const ts = Math.floor(dt.getTime() / 1000)
|
||||||
|
return Number.isFinite(ts) && ts > 0 ? ts : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeTimestampLikeToSeconds(raw: unknown): number {
|
||||||
|
if (raw === undefined || raw === null || raw === '') return 0
|
||||||
|
const text = String(raw ?? '').trim()
|
||||||
|
if (!text) return 0
|
||||||
|
|
||||||
|
const compactDigits = this.parseCompactDateTimeDigitsToSeconds(text)
|
||||||
|
if (compactDigits > 0) return compactDigits
|
||||||
|
|
||||||
|
const parsed = this.coerceRowNumber(raw)
|
||||||
|
if (Number.isFinite(parsed) && parsed > 0) {
|
||||||
|
let normalized = Math.floor(parsed)
|
||||||
|
while (normalized > 10000000000) {
|
||||||
|
normalized = Math.floor(normalized / 1000)
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parseDateTimeTextToSeconds(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRowTimestampSeconds(row: Record<string, any>, keys: string[], fallback = 0): number {
|
||||||
|
const raw = this.getRowField(row, keys)
|
||||||
|
if (raw === undefined || raw === null || raw === '') return fallback
|
||||||
|
const parsed = this.normalizeTimestampLikeToSeconds(raw)
|
||||||
|
return parsed > 0 ? parsed : fallback
|
||||||
|
}
|
||||||
|
|
||||||
private hasAnyContactExtendedFieldKey(row: Record<string, any>): boolean {
|
private hasAnyContactExtendedFieldKey(row: Record<string, any>): boolean {
|
||||||
for (const key of Object.keys(row || {})) {
|
for (const key of Object.keys(row || {})) {
|
||||||
if (this.contactExtendedFieldCandidateSet.has(String(key || '').toLowerCase())) {
|
if (this.contactExtendedFieldCandidateSet.has(String(key || '').toLowerCase())) {
|
||||||
@@ -3066,13 +3203,13 @@ class ChatService {
|
|||||||
if (typeof raw === 'number') return raw
|
if (typeof raw === 'number') return raw
|
||||||
if (typeof raw === 'bigint') return Number(raw)
|
if (typeof raw === 'bigint') return Number(raw)
|
||||||
if (Buffer.isBuffer(raw)) {
|
if (Buffer.isBuffer(raw)) {
|
||||||
return parseInt(raw.toString('utf-8'), 10)
|
return this.coerceRowNumber(raw.toString('utf-8'))
|
||||||
}
|
}
|
||||||
if (raw instanceof Uint8Array) {
|
if (raw instanceof Uint8Array) {
|
||||||
return parseInt(Buffer.from(raw).toString('utf-8'), 10)
|
return this.coerceRowNumber(Buffer.from(raw).toString('utf-8'))
|
||||||
}
|
}
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
return parseInt(Buffer.from(raw).toString('utf-8'), 10)
|
return this.coerceRowNumber(Buffer.from(raw).toString('utf-8'))
|
||||||
}
|
}
|
||||||
if (typeof raw === 'object') {
|
if (typeof raw === 'object') {
|
||||||
if ('value' in raw) return this.coerceRowNumber(raw.value)
|
if ('value' in raw) return this.coerceRowNumber(raw.value)
|
||||||
@@ -3088,14 +3225,22 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
const text = raw.toString ? String(raw) : ''
|
const text = raw.toString ? String(raw) : ''
|
||||||
if (text && text !== '[object Object]') {
|
if (text && text !== '[object Object]') {
|
||||||
const parsed = parseInt(text, 10)
|
return this.coerceRowNumber(text)
|
||||||
return Number.isFinite(parsed) ? parsed : NaN
|
|
||||||
}
|
}
|
||||||
return NaN
|
return NaN
|
||||||
}
|
}
|
||||||
const parsed = parseInt(String(raw), 10)
|
const text = String(raw).trim()
|
||||||
|
if (!text) return NaN
|
||||||
|
if (/^[+-]?\d+$/.test(text)) {
|
||||||
|
const parsed = Number(text)
|
||||||
return Number.isFinite(parsed) ? parsed : NaN
|
return Number.isFinite(parsed) ? parsed : NaN
|
||||||
}
|
}
|
||||||
|
if (/^[+-]?\d+\.\d+$/.test(text)) {
|
||||||
|
const parsed = Number(text)
|
||||||
|
return Number.isFinite(parsed) ? parsed : NaN
|
||||||
|
}
|
||||||
|
return NaN
|
||||||
|
}
|
||||||
|
|
||||||
private buildIdentityKeys(raw: string): string[] {
|
private buildIdentityKeys(raw: string): string[] {
|
||||||
const value = String(raw || '').trim()
|
const value = String(raw || '').trim()
|
||||||
@@ -3656,7 +3801,11 @@ class ChatService {
|
|||||||
return this.extractXmlValue(content, 'type')
|
return this.extractXmlValue(content, 'type')
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectSpecialMessageCountsByCursorScan(sessionId: string): Promise<{
|
private async collectSpecialMessageCountsByCursorScan(
|
||||||
|
sessionId: string,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
|
): Promise<{
|
||||||
transferMessages: number
|
transferMessages: number
|
||||||
redPacketMessages: number
|
redPacketMessages: number
|
||||||
callMessages: number
|
callMessages: number
|
||||||
@@ -3667,7 +3816,7 @@ class ChatService {
|
|||||||
callMessages: 0
|
callMessages: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursorResult = await wcdbService.openMessageCursorLite(sessionId, 500, false, 0, 0)
|
const cursorResult = await wcdbService.openMessageCursorLite(sessionId, 500, false, beginTimestamp, endTimestamp)
|
||||||
if (!cursorResult.success || !cursorResult.cursor) {
|
if (!cursorResult.success || !cursorResult.cursor) {
|
||||||
return counters
|
return counters
|
||||||
}
|
}
|
||||||
@@ -3713,7 +3862,9 @@ class ChatService {
|
|||||||
|
|
||||||
private async collectSessionExportStatsByCursorScan(
|
private async collectSessionExportStatsByCursorScan(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
selfIdentitySet: Set<string>
|
selfIdentitySet: Set<string>,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
): Promise<ExportSessionStats> {
|
): Promise<ExportSessionStats> {
|
||||||
const stats: ExportSessionStats = {
|
const stats: ExportSessionStats = {
|
||||||
totalMessages: 0,
|
totalMessages: 0,
|
||||||
@@ -3731,7 +3882,7 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const senderIdentities = new Set<string>()
|
const senderIdentities = new Set<string>()
|
||||||
const cursorResult = await wcdbService.openMessageCursorLite(sessionId, 500, false, 0, 0)
|
const cursorResult = await wcdbService.openMessageCursorLite(sessionId, 500, false, beginTimestamp, endTimestamp)
|
||||||
if (!cursorResult.success || !cursorResult.cursor) {
|
if (!cursorResult.success || !cursorResult.cursor) {
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
@@ -3806,7 +3957,7 @@ class ChatService {
|
|||||||
|
|
||||||
if (sessionId.endsWith('@chatroom')) {
|
if (sessionId.endsWith('@chatroom')) {
|
||||||
stats.groupActiveSpeakers = senderIdentities.size
|
stats.groupActiveSpeakers = senderIdentities.size
|
||||||
if (Number.isFinite(stats.groupMyMessages)) {
|
if ((beginTimestamp <= 0 && endTimestamp <= 0) && Number.isFinite(stats.groupMyMessages)) {
|
||||||
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3816,7 +3967,9 @@ class ChatService {
|
|||||||
private async collectSessionExportStats(
|
private async collectSessionExportStats(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
selfIdentitySet: Set<string>,
|
selfIdentitySet: Set<string>,
|
||||||
preferAccurateSpecialTypes: boolean = false
|
preferAccurateSpecialTypes: boolean = false,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
): Promise<ExportSessionStats> {
|
): Promise<ExportSessionStats> {
|
||||||
const stats: ExportSessionStats = {
|
const stats: ExportSessionStats = {
|
||||||
totalMessages: 0,
|
totalMessages: 0,
|
||||||
@@ -3834,9 +3987,9 @@ class ChatService {
|
|||||||
stats.groupActiveSpeakers = 0
|
stats.groupActiveSpeakers = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeResult = await wcdbService.getSessionMessageTypeStats(sessionId, 0, 0)
|
const nativeResult = await wcdbService.getSessionMessageTypeStats(sessionId, beginTimestamp, endTimestamp)
|
||||||
if (!nativeResult.success || !nativeResult.data) {
|
if (!nativeResult.success || !nativeResult.data) {
|
||||||
return this.collectSessionExportStatsByCursorScan(sessionId, selfIdentitySet)
|
return this.collectSessionExportStatsByCursorScan(sessionId, selfIdentitySet, beginTimestamp, endTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = nativeResult.data as Record<string, any>
|
const data = nativeResult.data as Record<string, any>
|
||||||
@@ -3856,7 +4009,7 @@ class ChatService {
|
|||||||
|
|
||||||
if (preferAccurateSpecialTypes) {
|
if (preferAccurateSpecialTypes) {
|
||||||
try {
|
try {
|
||||||
const preciseCounters = await this.collectSpecialMessageCountsByCursorScan(sessionId)
|
const preciseCounters = await this.collectSpecialMessageCountsByCursorScan(sessionId, beginTimestamp, endTimestamp)
|
||||||
stats.transferMessages = preciseCounters.transferMessages
|
stats.transferMessages = preciseCounters.transferMessages
|
||||||
stats.redPacketMessages = preciseCounters.redPacketMessages
|
stats.redPacketMessages = preciseCounters.redPacketMessages
|
||||||
stats.callMessages = preciseCounters.callMessages
|
stats.callMessages = preciseCounters.callMessages
|
||||||
@@ -3868,14 +4021,19 @@ class ChatService {
|
|||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
stats.groupMyMessages = Math.max(0, Math.floor(Number(data.group_my_messages || 0)))
|
stats.groupMyMessages = Math.max(0, Math.floor(Number(data.group_my_messages || 0)))
|
||||||
stats.groupActiveSpeakers = Math.max(0, Math.floor(Number(data.group_sender_count || 0)))
|
stats.groupActiveSpeakers = Math.max(0, Math.floor(Number(data.group_sender_count || 0)))
|
||||||
if (Number.isFinite(stats.groupMyMessages)) {
|
if ((beginTimestamp <= 0 && endTimestamp <= 0) && Number.isFinite(stats.groupMyMessages)) {
|
||||||
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
private toExportSessionStatsFromNativeTypeRow(sessionId: string, row: Record<string, any>): ExportSessionStats {
|
private toExportSessionStatsFromNativeTypeRow(
|
||||||
|
sessionId: string,
|
||||||
|
row: Record<string, any>,
|
||||||
|
options?: { updateGroupHint?: boolean }
|
||||||
|
): ExportSessionStats {
|
||||||
|
const updateGroupHint = options?.updateGroupHint !== false
|
||||||
const stats: ExportSessionStats = {
|
const stats: ExportSessionStats = {
|
||||||
totalMessages: Math.max(0, Math.floor(Number(row?.total_messages || 0))),
|
totalMessages: Math.max(0, Math.floor(Number(row?.total_messages || 0))),
|
||||||
voiceMessages: Math.max(0, Math.floor(Number(row?.voice_messages || 0))),
|
voiceMessages: Math.max(0, Math.floor(Number(row?.voice_messages || 0))),
|
||||||
@@ -3895,7 +4053,7 @@ class ChatService {
|
|||||||
if (sessionId.endsWith('@chatroom')) {
|
if (sessionId.endsWith('@chatroom')) {
|
||||||
stats.groupMyMessages = Math.max(0, Math.floor(Number(row?.group_my_messages || 0)))
|
stats.groupMyMessages = Math.max(0, Math.floor(Number(row?.group_my_messages || 0)))
|
||||||
stats.groupActiveSpeakers = Math.max(0, Math.floor(Number(row?.group_sender_count || 0)))
|
stats.groupActiveSpeakers = Math.max(0, Math.floor(Number(row?.group_sender_count || 0)))
|
||||||
if (Number.isFinite(stats.groupMyMessages)) {
|
if (updateGroupHint && Number.isFinite(stats.groupMyMessages)) {
|
||||||
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4025,9 +4183,17 @@ class ChatService {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
selfIdentitySet: Set<string>,
|
selfIdentitySet: Set<string>,
|
||||||
includeRelations: boolean,
|
includeRelations: boolean,
|
||||||
preferAccurateSpecialTypes: boolean = false
|
preferAccurateSpecialTypes: boolean = false,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
): Promise<ExportSessionStats> {
|
): Promise<ExportSessionStats> {
|
||||||
const stats = await this.collectSessionExportStats(sessionId, selfIdentitySet, preferAccurateSpecialTypes)
|
const stats = await this.collectSessionExportStats(
|
||||||
|
sessionId,
|
||||||
|
selfIdentitySet,
|
||||||
|
preferAccurateSpecialTypes,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp
|
||||||
|
)
|
||||||
const isGroup = sessionId.endsWith('@chatroom')
|
const isGroup = sessionId.endsWith('@chatroom')
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
@@ -4066,7 +4232,9 @@ class ChatService {
|
|||||||
sessionIds: string[],
|
sessionIds: string[],
|
||||||
includeRelations: boolean,
|
includeRelations: boolean,
|
||||||
selfIdentitySet: Set<string>,
|
selfIdentitySet: Set<string>,
|
||||||
preferAccurateSpecialTypes: boolean = false
|
preferAccurateSpecialTypes: boolean = false,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
): Promise<Record<string, ExportSessionStats>> {
|
): Promise<Record<string, ExportSessionStats>> {
|
||||||
const normalizedSessionIds = Array.from(
|
const normalizedSessionIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -4127,8 +4295,8 @@ class ChatService {
|
|||||||
try {
|
try {
|
||||||
const quickMode = !includeRelations && normalizedSessionIds.length > 1
|
const quickMode = !includeRelations && normalizedSessionIds.length > 1
|
||||||
const nativeBatch = await wcdbService.getSessionMessageTypeStatsBatch(normalizedSessionIds, {
|
const nativeBatch = await wcdbService.getSessionMessageTypeStatsBatch(normalizedSessionIds, {
|
||||||
beginTimestamp: 0,
|
beginTimestamp,
|
||||||
endTimestamp: 0,
|
endTimestamp,
|
||||||
quickMode,
|
quickMode,
|
||||||
includeGroupSenderCount: true
|
includeGroupSenderCount: true
|
||||||
})
|
})
|
||||||
@@ -4136,7 +4304,9 @@ class ChatService {
|
|||||||
for (const sessionId of normalizedSessionIds) {
|
for (const sessionId of normalizedSessionIds) {
|
||||||
const row = nativeBatch.data?.[sessionId] as Record<string, any> | undefined
|
const row = nativeBatch.data?.[sessionId] as Record<string, any> | undefined
|
||||||
if (!row || typeof row !== 'object') continue
|
if (!row || typeof row !== 'object') continue
|
||||||
nativeBatchStats[sessionId] = this.toExportSessionStatsFromNativeTypeRow(sessionId, row)
|
nativeBatchStats[sessionId] = this.toExportSessionStatsFromNativeTypeRow(sessionId, row, {
|
||||||
|
updateGroupHint: beginTimestamp <= 0 && endTimestamp <= 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
hasNativeBatchStats = Object.keys(nativeBatchStats).length > 0
|
hasNativeBatchStats = Object.keys(nativeBatchStats).length > 0
|
||||||
} else {
|
} else {
|
||||||
@@ -4151,7 +4321,13 @@ class ChatService {
|
|||||||
try {
|
try {
|
||||||
const stats = hasNativeBatchStats && nativeBatchStats[sessionId]
|
const stats = hasNativeBatchStats && nativeBatchStats[sessionId]
|
||||||
? { ...nativeBatchStats[sessionId] }
|
? { ...nativeBatchStats[sessionId] }
|
||||||
: await this.collectSessionExportStats(sessionId, selfIdentitySet, preferAccurateSpecialTypes)
|
: await this.collectSessionExportStats(
|
||||||
|
sessionId,
|
||||||
|
selfIdentitySet,
|
||||||
|
preferAccurateSpecialTypes,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp
|
||||||
|
)
|
||||||
if (sessionId.endsWith('@chatroom')) {
|
if (sessionId.endsWith('@chatroom')) {
|
||||||
if (shouldLoadGroupMemberCount) {
|
if (shouldLoadGroupMemberCount) {
|
||||||
stats.groupMemberCount = typeof memberCountMap[sessionId] === 'number'
|
stats.groupMemberCount = typeof memberCountMap[sessionId] === 'number'
|
||||||
@@ -4181,10 +4357,12 @@ class ChatService {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
includeRelations: boolean,
|
includeRelations: boolean,
|
||||||
selfIdentitySet: Set<string>,
|
selfIdentitySet: Set<string>,
|
||||||
preferAccurateSpecialTypes: boolean = false
|
preferAccurateSpecialTypes: boolean = false,
|
||||||
|
beginTimestamp: number = 0,
|
||||||
|
endTimestamp: number = 0
|
||||||
): Promise<ExportSessionStats> {
|
): Promise<ExportSessionStats> {
|
||||||
if (preferAccurateSpecialTypes) {
|
if (preferAccurateSpecialTypes) {
|
||||||
return this.computeSessionExportStats(sessionId, selfIdentitySet, includeRelations, true)
|
return this.computeSessionExportStats(sessionId, selfIdentitySet, includeRelations, true, beginTimestamp, endTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopedKey = this.buildScopedSessionStatsKey(sessionId)
|
const scopedKey = this.buildScopedSessionStatsKey(sessionId)
|
||||||
@@ -4199,8 +4377,13 @@ class ChatService {
|
|||||||
if (pendingFull) return pendingFull
|
if (pendingFull) return pendingFull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldUsePendingPool = beginTimestamp <= 0 && endTimestamp <= 0
|
||||||
|
if (!shouldUsePendingPool) {
|
||||||
|
return this.computeSessionExportStats(sessionId, selfIdentitySet, includeRelations, false, beginTimestamp, endTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
const targetMap = includeRelations ? this.sessionStatsPendingFull : this.sessionStatsPendingBasic
|
const targetMap = includeRelations ? this.sessionStatsPendingFull : this.sessionStatsPendingBasic
|
||||||
const pending = this.computeSessionExportStats(sessionId, selfIdentitySet, includeRelations, false)
|
const pending = this.computeSessionExportStats(sessionId, selfIdentitySet, includeRelations, false, beginTimestamp, endTimestamp)
|
||||||
targetMap.set(scopedKey, pending)
|
targetMap.set(scopedKey, pending)
|
||||||
try {
|
try {
|
||||||
return await pending
|
return await pending
|
||||||
@@ -4216,6 +4399,55 @@ class ChatService {
|
|||||||
return this.mapRowsToMessages(rows)
|
return this.mapRowsToMessages(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapRowsToMessagesLiteForApi(rows: Record<string, any>[]): Message[] {
|
||||||
|
const myWxid = String(this.configService.get('myWxid') || '').trim()
|
||||||
|
const messages: Message[] = []
|
||||||
|
for (const row of rows) {
|
||||||
|
const sourceInfo = this.getMessageSourceInfo(row)
|
||||||
|
const localType = this.getRowInt(row, ['local_type'], 1)
|
||||||
|
const createTime = this.getRowTimestampSeconds(row, ['create_time', 'createTime', 'msg_time', 'msgTime', 'time'], 0)
|
||||||
|
const sortSeq = this.getRowInt(row, ['sort_seq'], createTime > 0 ? createTime * 1000 : 0)
|
||||||
|
const localId = this.getRowInt(row, ['local_id'], 0)
|
||||||
|
const serverId = this.getRowInt(row, ['server_id'], 0)
|
||||||
|
const content = this.decodeMessageContent(row.message_content, row.compress_content)
|
||||||
|
|
||||||
|
const isSendRaw = row.computed_is_send ?? row.is_send
|
||||||
|
const parsedRawIsSend = isSendRaw === null || isSendRaw === undefined
|
||||||
|
? null
|
||||||
|
: parseInt(String(isSendRaw), 10)
|
||||||
|
const normalizedIsSend = typeof parsedRawIsSend === 'number' && Number.isFinite(parsedRawIsSend)
|
||||||
|
? parsedRawIsSend
|
||||||
|
: null
|
||||||
|
const senderFromRow = String(row.sender_username || '').trim() || this.extractSenderUsernameFromContent(content) || null
|
||||||
|
const { isSend } = this.resolveMessageIsSend(normalizedIsSend, senderFromRow)
|
||||||
|
const senderUsername = senderFromRow || (isSend === 1 && myWxid ? myWxid : null)
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
messageKey: this.buildMessageKey({
|
||||||
|
localId,
|
||||||
|
serverId,
|
||||||
|
createTime,
|
||||||
|
sortSeq,
|
||||||
|
senderUsername,
|
||||||
|
localType,
|
||||||
|
...sourceInfo
|
||||||
|
}),
|
||||||
|
localId,
|
||||||
|
serverId,
|
||||||
|
localType,
|
||||||
|
createTime,
|
||||||
|
sortSeq,
|
||||||
|
isSend,
|
||||||
|
senderUsername,
|
||||||
|
parsedContent: '',
|
||||||
|
rawContent: content,
|
||||||
|
content,
|
||||||
|
_db_path: sourceInfo.dbPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
private mapRowsToMessages(rows: Record<string, any>[]): Message[] {
|
private mapRowsToMessages(rows: Record<string, any>[]): Message[] {
|
||||||
const myWxid = this.configService.get('myWxid')
|
const myWxid = this.configService.get('myWxid')
|
||||||
|
|
||||||
@@ -4233,7 +4465,7 @@ class ChatService {
|
|||||||
|| this.extractSenderUsernameFromContent(content)
|
|| this.extractSenderUsernameFromContent(content)
|
||||||
|| null
|
|| null
|
||||||
const { isSend } = this.resolveMessageIsSend(parsedRawIsSend, senderUsername)
|
const { isSend } = this.resolveMessageIsSend(parsedRawIsSend, senderUsername)
|
||||||
const createTime = this.getRowInt(row, ['create_time'], 0)
|
const createTime = this.getRowTimestampSeconds(row, ['create_time', 'createTime', 'msg_time', 'msgTime', 'time'], 0)
|
||||||
|
|
||||||
if (senderUsername && !myWxid) {
|
if (senderUsername && !myWxid) {
|
||||||
// [DEBUG] Issue #34: 未配置 myWxid,无法判断是否发送
|
// [DEBUG] Issue #34: 未配置 myWxid,无法判断是否发送
|
||||||
@@ -7042,6 +7274,9 @@ class ChatService {
|
|||||||
const allowStaleCache = options.allowStaleCache === true
|
const allowStaleCache = options.allowStaleCache === true
|
||||||
const preferAccurateSpecialTypes = options.preferAccurateSpecialTypes === true
|
const preferAccurateSpecialTypes = options.preferAccurateSpecialTypes === true
|
||||||
const cacheOnly = options.cacheOnly === true
|
const cacheOnly = options.cacheOnly === true
|
||||||
|
const beginTimestamp = this.normalizeTimestampSeconds(Number(options.beginTimestamp || 0))
|
||||||
|
const endTimestamp = this.normalizeTimestampSeconds(Number(options.endTimestamp || 0))
|
||||||
|
const useRangeFilter = beginTimestamp > 0 || endTimestamp > 0
|
||||||
|
|
||||||
const normalizedSessionIds = Array.from(
|
const normalizedSessionIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -7065,7 +7300,7 @@ class ChatService {
|
|||||||
? this.getGroupMyMessageCountHintEntry(sessionId)
|
? this.getGroupMyMessageCountHintEntry(sessionId)
|
||||||
: null
|
: null
|
||||||
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
||||||
const canUseCache = cacheOnly || (!forceRefresh && !preferAccurateSpecialTypes)
|
const canUseCache = !useRangeFilter && (cacheOnly || (!forceRefresh && !preferAccurateSpecialTypes))
|
||||||
if (canUseCache && cachedResult && this.supportsRequestedRelation(cachedResult.entry, includeRelations)) {
|
if (canUseCache && cachedResult && this.supportsRequestedRelation(cachedResult.entry, includeRelations)) {
|
||||||
const stale = now - cachedResult.entry.updatedAt > this.sessionStatsCacheTtlMs
|
const stale = now - cachedResult.entry.updatedAt > this.sessionStatsCacheTtlMs
|
||||||
if (!stale || allowStaleCache || cacheOnly) {
|
if (!stale || allowStaleCache || cacheOnly) {
|
||||||
@@ -7103,8 +7338,16 @@ class ChatService {
|
|||||||
if (pendingSessionIds.length === 1) {
|
if (pendingSessionIds.length === 1) {
|
||||||
const sessionId = pendingSessionIds[0]
|
const sessionId = pendingSessionIds[0]
|
||||||
try {
|
try {
|
||||||
const stats = await this.getOrComputeSessionExportStats(sessionId, includeRelations, selfIdentitySet, preferAccurateSpecialTypes)
|
const stats = await this.getOrComputeSessionExportStats(
|
||||||
|
sessionId,
|
||||||
|
includeRelations,
|
||||||
|
selfIdentitySet,
|
||||||
|
preferAccurateSpecialTypes,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp
|
||||||
|
)
|
||||||
resultMap[sessionId] = stats
|
resultMap[sessionId] = stats
|
||||||
|
if (!useRangeFilter) {
|
||||||
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
||||||
cacheMeta[sessionId] = {
|
cacheMeta[sessionId] = {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
@@ -7112,6 +7355,7 @@ class ChatService {
|
|||||||
includeRelations,
|
includeRelations,
|
||||||
source: 'fresh'
|
source: 'fresh'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
usedBatchedCompute = true
|
usedBatchedCompute = true
|
||||||
} catch {
|
} catch {
|
||||||
usedBatchedCompute = false
|
usedBatchedCompute = false
|
||||||
@@ -7122,12 +7366,15 @@ class ChatService {
|
|||||||
pendingSessionIds,
|
pendingSessionIds,
|
||||||
includeRelations,
|
includeRelations,
|
||||||
selfIdentitySet,
|
selfIdentitySet,
|
||||||
preferAccurateSpecialTypes
|
preferAccurateSpecialTypes,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp
|
||||||
)
|
)
|
||||||
for (const sessionId of pendingSessionIds) {
|
for (const sessionId of pendingSessionIds) {
|
||||||
const stats = batchedStatsMap[sessionId]
|
const stats = batchedStatsMap[sessionId]
|
||||||
if (!stats) continue
|
if (!stats) continue
|
||||||
resultMap[sessionId] = stats
|
resultMap[sessionId] = stats
|
||||||
|
if (!useRangeFilter) {
|
||||||
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
||||||
cacheMeta[sessionId] = {
|
cacheMeta[sessionId] = {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
@@ -7136,6 +7383,7 @@ class ChatService {
|
|||||||
source: 'fresh'
|
source: 'fresh'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
usedBatchedCompute = true
|
usedBatchedCompute = true
|
||||||
} catch {
|
} catch {
|
||||||
usedBatchedCompute = false
|
usedBatchedCompute = false
|
||||||
@@ -7145,8 +7393,16 @@ class ChatService {
|
|||||||
if (!usedBatchedCompute) {
|
if (!usedBatchedCompute) {
|
||||||
await this.forEachWithConcurrency(pendingSessionIds, 3, async (sessionId) => {
|
await this.forEachWithConcurrency(pendingSessionIds, 3, async (sessionId) => {
|
||||||
try {
|
try {
|
||||||
const stats = await this.getOrComputeSessionExportStats(sessionId, includeRelations, selfIdentitySet, preferAccurateSpecialTypes)
|
const stats = await this.getOrComputeSessionExportStats(
|
||||||
|
sessionId,
|
||||||
|
includeRelations,
|
||||||
|
selfIdentitySet,
|
||||||
|
preferAccurateSpecialTypes,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp
|
||||||
|
)
|
||||||
resultMap[sessionId] = stats
|
resultMap[sessionId] = stats
|
||||||
|
if (!useRangeFilter) {
|
||||||
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
|
||||||
cacheMeta[sessionId] = {
|
cacheMeta[sessionId] = {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
@@ -7154,6 +7410,7 @@ class ChatService {
|
|||||||
includeRelations,
|
includeRelations,
|
||||||
source: 'fresh'
|
source: 'fresh'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
resultMap[sessionId] = this.buildEmptyExportSessionStats(sessionId, includeRelations)
|
resultMap[sessionId] = this.buildEmptyExportSessionStats(sessionId, includeRelations)
|
||||||
}
|
}
|
||||||
@@ -8892,7 +9149,11 @@ class ChatService {
|
|||||||
private normalizeTimestampSeconds(value: number): number {
|
private normalizeTimestampSeconds(value: number): number {
|
||||||
const numeric = Number(value || 0)
|
const numeric = Number(value || 0)
|
||||||
if (!Number.isFinite(numeric) || numeric <= 0) return 0
|
if (!Number.isFinite(numeric) || numeric <= 0) return 0
|
||||||
return numeric > 1e12 ? Math.floor(numeric / 1000) : Math.floor(numeric)
|
let normalized = Math.floor(numeric)
|
||||||
|
while (normalized > 10000000000) {
|
||||||
|
normalized = Math.floor(normalized / 1000)
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
private toSafeInt(value: unknown, fallback = 0): number {
|
private toSafeInt(value: unknown, fallback = 0): number {
|
||||||
@@ -10532,8 +10793,8 @@ class ChatService {
|
|||||||
const serverIdRaw = this.normalizeUnsignedIntegerToken(row.server_id)
|
const serverIdRaw = this.normalizeUnsignedIntegerToken(row.server_id)
|
||||||
const serverId = this.getRowInt(row, ['server_id'], 0)
|
const serverId = this.getRowInt(row, ['server_id'], 0)
|
||||||
const localType = this.getRowInt(row, ['local_type'], 0)
|
const localType = this.getRowInt(row, ['local_type'], 0)
|
||||||
const createTime = this.getRowInt(row, ['create_time'], 0)
|
const createTime = this.getRowTimestampSeconds(row, ['create_time', 'createTime', 'msg_time', 'msgTime', 'time'], 0)
|
||||||
const sortSeq = this.getRowInt(row, ['sort_seq'], createTime)
|
const sortSeq = this.getRowInt(row, ['sort_seq'], createTime > 0 ? createTime * 1000 : 0)
|
||||||
const rawIsSend = row.computed_is_send ?? row.is_send
|
const rawIsSend = row.computed_is_send ?? row.is_send
|
||||||
const senderUsername = await this.resolveSenderUsernameForMessageRow(row, rawContent)
|
const senderUsername = await this.resolveSenderUsernameForMessageRow(row, rawContent)
|
||||||
const sendState = this.resolveMessageIsSend(rawIsSend === null ? null : parseInt(rawIsSend, 10), senderUsername)
|
const sendState = this.resolveMessageIsSend(rawIsSend === null ? null : parseInt(rawIsSend, 10), senderUsername)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -516,27 +516,29 @@ class HttpService {
|
|||||||
limit: number,
|
limit: number,
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
ascending: boolean
|
ascending: boolean,
|
||||||
|
useLiteMapping: boolean = true
|
||||||
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// 使用固定 batch 大小(与 limit 相同或最多 500)来减少循环次数
|
// 深分页时放大 batch,避免 offset 很大时出现大量小批次循环。
|
||||||
const batchSize = Math.min(limit, 500)
|
const batchSize = Math.min(2000, Math.max(500, limit))
|
||||||
const beginTimestamp = startTime > 10000000000 ? Math.floor(startTime / 1000) : startTime
|
const beginTimestamp = startTime > 10000000000 ? Math.floor(startTime / 1000) : startTime
|
||||||
const endTimestamp = endTime > 10000000000 ? Math.floor(endTime / 1000) : endTime
|
const endTimestamp = endTime > 10000000000 ? Math.floor(endTime / 1000) : endTime
|
||||||
|
|
||||||
const cursorResult = await wcdbService.openMessageCursor(talker, batchSize, ascending, beginTimestamp, endTimestamp)
|
const cursorResult = await wcdbService.openMessageCursorLite(talker, batchSize, ascending, beginTimestamp, endTimestamp)
|
||||||
if (!cursorResult.success || !cursorResult.cursor) {
|
if (!cursorResult.success || !cursorResult.cursor) {
|
||||||
return { success: false, error: cursorResult.error || '打开消息游标失败' }
|
return { success: false, error: cursorResult.error || '打开消息游标失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursor = cursorResult.cursor
|
const cursor = cursorResult.cursor
|
||||||
try {
|
try {
|
||||||
const allRows: Record<string, any>[] = []
|
const collectedRows: Record<string, any>[] = []
|
||||||
let hasMore = true
|
let hasMore = true
|
||||||
let skipped = 0
|
let skipped = 0
|
||||||
|
let reachedLimit = false
|
||||||
|
|
||||||
// 循环获取消息,处理 offset 跳过 + limit 累积
|
// 循环获取消息,处理 offset 跳过 + limit 累积
|
||||||
while (allRows.length < limit && hasMore) {
|
while (collectedRows.length < limit && hasMore) {
|
||||||
const batch = await wcdbService.fetchMessageBatch(cursor)
|
const batch = await wcdbService.fetchMessageBatch(cursor)
|
||||||
if (!batch.success || !batch.rows || batch.rows.length === 0) {
|
if (!batch.success || !batch.rows || batch.rows.length === 0) {
|
||||||
hasMore = false
|
hasMore = false
|
||||||
@@ -557,12 +559,20 @@ class HttpService {
|
|||||||
skipped = offset
|
skipped = offset
|
||||||
}
|
}
|
||||||
|
|
||||||
allRows.push(...rows)
|
const remainingCapacity = limit - collectedRows.length
|
||||||
|
if (rows.length > remainingCapacity) {
|
||||||
|
collectedRows.push(...rows.slice(0, remainingCapacity))
|
||||||
|
reachedLimit = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedRows = allRows.slice(0, limit)
|
collectedRows.push(...rows)
|
||||||
const finalHasMore = hasMore || allRows.length > limit
|
}
|
||||||
const messages = chatService.mapRowsToMessagesForApi(trimmedRows)
|
|
||||||
|
const finalHasMore = hasMore || reachedLimit
|
||||||
|
const messages = useLiteMapping
|
||||||
|
? chatService.mapRowsToMessagesLiteForApi(collectedRows)
|
||||||
|
: chatService.mapRowsToMessagesForApi(collectedRows)
|
||||||
await this.backfillMissingSenderUsernames(talker, messages)
|
await this.backfillMissingSenderUsernames(talker, messages)
|
||||||
return { success: true, messages, hasMore: finalHasMore }
|
return { success: true, messages, hasMore: finalHasMore }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -590,9 +600,35 @@ class HttpService {
|
|||||||
if (targets.length === 0) return
|
if (targets.length === 0) return
|
||||||
|
|
||||||
const myWxid = (this.configService.get('myWxid') || '').trim()
|
const myWxid = (this.configService.get('myWxid') || '').trim()
|
||||||
|
const MAX_DETAIL_BACKFILL = 120
|
||||||
|
if (targets.length > MAX_DETAIL_BACKFILL) {
|
||||||
for (const msg of targets) {
|
for (const msg of targets) {
|
||||||
|
if (!msg.senderUsername && msg.isSend === 1 && myWxid) {
|
||||||
|
msg.senderUsername = myWxid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = [...targets]
|
||||||
|
const workerCount = Math.max(1, Math.min(6, queue.length))
|
||||||
|
const state = {
|
||||||
|
attempted: 0,
|
||||||
|
hydrated: 0,
|
||||||
|
consecutiveMiss: 0
|
||||||
|
}
|
||||||
|
const MAX_DETAIL_LOOKUPS = 80
|
||||||
|
const MAX_CONSECUTIVE_MISS = 36
|
||||||
|
const runWorker = async (): Promise<void> => {
|
||||||
|
while (queue.length > 0) {
|
||||||
|
if (state.attempted >= MAX_DETAIL_LOOKUPS) break
|
||||||
|
if (state.consecutiveMiss >= MAX_CONSECUTIVE_MISS && state.hydrated <= 0) break
|
||||||
|
const msg = queue.shift()
|
||||||
|
if (!msg) break
|
||||||
|
|
||||||
const localId = Number(msg.localId || 0)
|
const localId = Number(msg.localId || 0)
|
||||||
if (Number.isFinite(localId) && localId > 0) {
|
if (Number.isFinite(localId) && localId > 0) {
|
||||||
|
state.attempted += 1
|
||||||
try {
|
try {
|
||||||
const detail = await wcdbService.getMessageById(talker, localId)
|
const detail = await wcdbService.getMessageById(talker, localId)
|
||||||
if (detail.success && detail.message) {
|
if (detail.success && detail.message) {
|
||||||
@@ -606,9 +642,18 @@ class HttpService {
|
|||||||
if (!msg.rawContent && hydrated?.rawContent) {
|
if (!msg.rawContent && hydrated?.rawContent) {
|
||||||
msg.rawContent = hydrated.rawContent
|
msg.rawContent = hydrated.rawContent
|
||||||
}
|
}
|
||||||
|
if (msg.senderUsername) {
|
||||||
|
state.hydrated += 1
|
||||||
|
state.consecutiveMiss = 0
|
||||||
|
} else {
|
||||||
|
state.consecutiveMiss += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.consecutiveMiss += 1
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[HttpService] backfill sender failed:', error)
|
console.warn('[HttpService] backfill sender failed:', error)
|
||||||
|
state.consecutiveMiss += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +663,9 @@ class HttpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: workerCount }, () => runWorker()))
|
||||||
|
}
|
||||||
|
|
||||||
private parseBooleanParam(url: URL, keys: string[], defaultValue: boolean = false): boolean {
|
private parseBooleanParam(url: URL, keys: string[], defaultValue: boolean = false): boolean {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const raw = url.searchParams.get(key)
|
const raw = url.searchParams.get(key)
|
||||||
@@ -663,7 +711,7 @@ class HttpService {
|
|||||||
const talker = (url.searchParams.get('talker') || '').trim()
|
const talker = (url.searchParams.get('talker') || '').trim()
|
||||||
const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000)
|
const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000)
|
||||||
const offset = this.parseIntParam(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
|
const offset = this.parseIntParam(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
|
||||||
const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase()
|
const keyword = (url.searchParams.get('keyword') || '').trim()
|
||||||
const startParam = url.searchParams.get('start')
|
const startParam = url.searchParams.get('start')
|
||||||
const endParam = url.searchParams.get('end')
|
const endParam = url.searchParams.get('end')
|
||||||
const chatlab = this.parseBooleanParam(url, ['chatlab'], false)
|
const chatlab = this.parseBooleanParam(url, ['chatlab'], false)
|
||||||
@@ -683,26 +731,41 @@ class HttpService {
|
|||||||
|
|
||||||
const startTime = this.parseTimeParam(startParam)
|
const startTime = this.parseTimeParam(startParam)
|
||||||
const endTime = this.parseTimeParam(endParam, true)
|
const endTime = this.parseTimeParam(endParam, true)
|
||||||
const queryOffset = keyword ? 0 : offset
|
let messages: Message[] = []
|
||||||
const queryLimit = keyword ? 10000 : limit
|
let hasMore = false
|
||||||
|
|
||||||
const result = await this.fetchMessagesBatch(talker, queryOffset, queryLimit, startTime, endTime, false)
|
if (keyword) {
|
||||||
|
const searchLimit = Math.max(1, limit) + 1
|
||||||
|
const searchResult = await chatService.searchMessages(
|
||||||
|
keyword,
|
||||||
|
talker,
|
||||||
|
searchLimit,
|
||||||
|
offset,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
)
|
||||||
|
if (!searchResult.success || !searchResult.messages) {
|
||||||
|
this.sendError(res, 500, searchResult.error || 'Failed to search messages')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasMore = searchResult.messages.length > limit
|
||||||
|
messages = hasMore ? searchResult.messages.slice(0, limit) : searchResult.messages
|
||||||
|
} else {
|
||||||
|
const result = await this.fetchMessagesBatch(
|
||||||
|
talker,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
false,
|
||||||
|
!mediaOptions.enabled
|
||||||
|
)
|
||||||
if (!result.success || !result.messages) {
|
if (!result.success || !result.messages) {
|
||||||
this.sendError(res, 500, result.error || 'Failed to get messages')
|
this.sendError(res, 500, result.error || 'Failed to get messages')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
messages = result.messages
|
||||||
let messages = result.messages
|
hasMore = result.hasMore === true
|
||||||
let hasMore = result.hasMore === true
|
|
||||||
|
|
||||||
if (keyword) {
|
|
||||||
const filtered = messages.filter((msg) => {
|
|
||||||
const content = (msg.parsedContent || msg.rawContent || '').toLowerCase()
|
|
||||||
return content.includes(keyword)
|
|
||||||
})
|
|
||||||
const endIndex = offset + limit
|
|
||||||
hasMore = filtered.length > endIndex
|
|
||||||
messages = filtered.slice(offset, endIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaMap = mediaOptions.enabled
|
const mediaMap = mediaOptions.enabled
|
||||||
@@ -812,7 +875,7 @@ class HttpService {
|
|||||||
const endTime = endParam ? this.parseTimeParam(endParam, true) : 0
|
const endTime = endParam ? this.parseTimeParam(endParam, true) : 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.fetchMessagesBatch(sessionId, offset, limit, startTime, endTime, true)
|
const result = await this.fetchMessagesBatch(sessionId, offset, limit, startTime, endTime, true, true)
|
||||||
if (!result.success || !result.messages) {
|
if (!result.success || !result.messages) {
|
||||||
this.sendError(res, 500, result.error || 'Failed to get messages')
|
this.sendError(res, 500, result.error || 'Failed to get messages')
|
||||||
return
|
return
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3445,10 +3445,10 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
if (result.success && result.messages) {
|
if (result.success && result.messages) {
|
||||||
const resultMessages = result.messages
|
const resultMessages = result.messages
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
|
setNoMessageTable(false)
|
||||||
setMessages(resultMessages)
|
setMessages(resultMessages)
|
||||||
persistSessionPreviewCache(sessionId, resultMessages)
|
persistSessionPreviewCache(sessionId, resultMessages)
|
||||||
if (resultMessages.length === 0) {
|
if (resultMessages.length === 0) {
|
||||||
setNoMessageTable(true)
|
|
||||||
setHasMoreMessages(false)
|
setHasMoreMessages(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3549,7 +3549,10 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
: offset + resultMessages.length
|
: offset + resultMessages.length
|
||||||
setCurrentOffset(nextOffset)
|
setCurrentOffset(nextOffset)
|
||||||
} else if (!result.success) {
|
} else if (!result.success) {
|
||||||
setNoMessageTable(true)
|
const errorText = String(result.error || '')
|
||||||
|
const shouldMarkNoTable =
|
||||||
|
/schema mismatch|no message db|no table|消息数据库未找到|消息表|message schema/i.test(errorText)
|
||||||
|
setNoMessageTable(shouldMarkNoTable)
|
||||||
setHasMoreMessages(false)
|
setHasMoreMessages(false)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -3557,6 +3560,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
setConnectionError('加载消息失败')
|
setConnectionError('加载消息失败')
|
||||||
setHasMoreMessages(false)
|
setHasMoreMessages(false)
|
||||||
if (offset === 0 && currentSessionRef.current === sessionId) {
|
if (offset === 0 && currentSessionRef.current === sessionId) {
|
||||||
|
setNoMessageTable(false)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1899,7 +1899,7 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
? `缓存命中 ${mediaCacheHitFiles}/${mediaCacheTotal}`
|
? `缓存命中 ${mediaCacheHitFiles}/${mediaCacheTotal}`
|
||||||
: ''
|
: ''
|
||||||
const mediaMissMetricLabel = mediaCacheMissFiles > 0
|
const mediaMissMetricLabel = mediaCacheMissFiles > 0
|
||||||
? `未导出 ${mediaCacheMissFiles} 个文件/媒体`
|
? `缓存未命中 ${mediaCacheMissFiles}`
|
||||||
: ''
|
: ''
|
||||||
const mediaDedupMetricLabel = mediaDedupReuseFiles > 0
|
const mediaDedupMetricLabel = mediaDedupReuseFiles > 0
|
||||||
? `复用 ${mediaDedupReuseFiles}`
|
? `复用 ${mediaDedupReuseFiles}`
|
||||||
@@ -1914,7 +1914,7 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
)
|
)
|
||||||
: ''
|
: ''
|
||||||
const mediaLiveMetricLabel = task.progress.phase === 'exporting-media'
|
const mediaLiveMetricLabel = task.progress.phase === 'exporting-media'
|
||||||
? (mediaDoneFiles > 0 ? `已处理 ${mediaDoneFiles}` : '')
|
? (mediaDoneFiles > 0 ? `已写入 ${mediaDoneFiles}` : '')
|
||||||
: ''
|
: ''
|
||||||
const sessionProgressLabel = completedSessionTotal > 0
|
const sessionProgressLabel = completedSessionTotal > 0
|
||||||
? `会话 ${completedSessionCount}/${completedSessionTotal}`
|
? `会话 ${completedSessionCount}/${completedSessionTotal}`
|
||||||
@@ -2238,6 +2238,27 @@ function ExportPage() {
|
|||||||
exportConcurrency: 2
|
exportConcurrency: 2
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const exportStatsRangeOptions = useMemo(() => {
|
||||||
|
if (options.useAllTime || !options.dateRange) return null
|
||||||
|
const beginTimestamp = Math.floor(options.dateRange.start.getTime() / 1000)
|
||||||
|
const endTimestamp = Math.floor(options.dateRange.end.getTime() / 1000)
|
||||||
|
if (!Number.isFinite(beginTimestamp) || !Number.isFinite(endTimestamp)) return null
|
||||||
|
if (beginTimestamp <= 0 && endTimestamp <= 0) return null
|
||||||
|
return {
|
||||||
|
beginTimestamp: Math.max(0, beginTimestamp),
|
||||||
|
endTimestamp: Math.max(0, endTimestamp)
|
||||||
|
}
|
||||||
|
}, [options.useAllTime, options.dateRange])
|
||||||
|
|
||||||
|
const withExportStatsRange = useCallback((statsOptions: Record<string, any>): Record<string, any> => {
|
||||||
|
if (!exportStatsRangeOptions) return statsOptions
|
||||||
|
return {
|
||||||
|
...statsOptions,
|
||||||
|
beginTimestamp: exportStatsRangeOptions.beginTimestamp,
|
||||||
|
endTimestamp: exportStatsRangeOptions.endTimestamp
|
||||||
|
}
|
||||||
|
}, [exportStatsRangeOptions])
|
||||||
|
|
||||||
const [exportDialog, setExportDialog] = useState<ExportDialogState>({
|
const [exportDialog, setExportDialog] = useState<ExportDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
intent: 'manual',
|
intent: 'manual',
|
||||||
@@ -4003,7 +4024,7 @@ function ExportPage() {
|
|||||||
const cacheResult = await withTimeout(
|
const cacheResult = await withTimeout(
|
||||||
window.electronAPI.chat.getExportSessionStats(
|
window.electronAPI.chat.getExportSessionStats(
|
||||||
batchSessionIds,
|
batchSessionIds,
|
||||||
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
withExportStatsRange({ includeRelations: false, allowStaleCache: true, cacheOnly: true })
|
||||||
),
|
),
|
||||||
12000,
|
12000,
|
||||||
'cacheOnly'
|
'cacheOnly'
|
||||||
@@ -4018,7 +4039,7 @@ function ExportPage() {
|
|||||||
const freshResult = await withTimeout(
|
const freshResult = await withTimeout(
|
||||||
window.electronAPI.chat.getExportSessionStats(
|
window.electronAPI.chat.getExportSessionStats(
|
||||||
missingSessionIds,
|
missingSessionIds,
|
||||||
{ includeRelations: false, allowStaleCache: true }
|
withExportStatsRange({ includeRelations: false, allowStaleCache: true })
|
||||||
),
|
),
|
||||||
45000,
|
45000,
|
||||||
'fresh'
|
'fresh'
|
||||||
@@ -4062,7 +4083,7 @@ function ExportPage() {
|
|||||||
void runSessionMediaMetricWorker(runId)
|
void runSessionMediaMetricWorker(runId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [applySessionMediaMetricsFromStats, isSessionMediaMetricReady, patchSessionLoadTraceStage])
|
}, [applySessionMediaMetricsFromStats, isSessionMediaMetricReady, patchSessionLoadTraceStage, withExportStatsRange])
|
||||||
|
|
||||||
const scheduleSessionMediaMetricWorker = useCallback(() => {
|
const scheduleSessionMediaMetricWorker = useCallback(() => {
|
||||||
if (activeTaskCountRef.current > 0) return
|
if (activeTaskCountRef.current > 0) return
|
||||||
@@ -7243,7 +7264,7 @@ function ExportPage() {
|
|||||||
try {
|
try {
|
||||||
const quickStatsResult = await window.electronAPI.chat.getExportSessionStats(
|
const quickStatsResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
[normalizedSessionId],
|
[normalizedSessionId],
|
||||||
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
withExportStatsRange({ includeRelations: false, allowStaleCache: true, cacheOnly: true })
|
||||||
)
|
)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
if (quickStatsResult.success) {
|
if (quickStatsResult.success) {
|
||||||
@@ -7270,7 +7291,7 @@ function ExportPage() {
|
|||||||
try {
|
try {
|
||||||
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
|
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
[normalizedSessionId],
|
[normalizedSessionId],
|
||||||
{ includeRelations: true, allowStaleCache: true, cacheOnly: true }
|
withExportStatsRange({ includeRelations: true, allowStaleCache: true, cacheOnly: true })
|
||||||
)
|
)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
if (relationCacheResult.success && relationCacheResult.data) {
|
if (relationCacheResult.success && relationCacheResult.data) {
|
||||||
@@ -7295,7 +7316,7 @@ function ExportPage() {
|
|||||||
// 后台补齐非关系统计,不走精确特型扫描,避免阻塞列表统计队列。
|
// 后台补齐非关系统计,不走精确特型扫描,避免阻塞列表统计队列。
|
||||||
const freshResult = await window.electronAPI.chat.getExportSessionStats(
|
const freshResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
[normalizedSessionId],
|
[normalizedSessionId],
|
||||||
{ includeRelations: false, forceRefresh: true }
|
withExportStatsRange({ includeRelations: false, forceRefresh: true })
|
||||||
)
|
)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
if (freshResult.success && freshResult.data) {
|
if (freshResult.success && freshResult.data) {
|
||||||
@@ -7330,7 +7351,7 @@ function ExportPage() {
|
|||||||
setIsLoadingSessionDetailExtra(false)
|
setIsLoadingSessionDetailExtra(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername])
|
}, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername, withExportStatsRange])
|
||||||
|
|
||||||
const loadSessionRelationStats = useCallback(async (options?: { forceRefresh?: boolean }) => {
|
const loadSessionRelationStats = useCallback(async (options?: { forceRefresh?: boolean }) => {
|
||||||
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
|
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
|
||||||
@@ -7343,7 +7364,7 @@ function ExportPage() {
|
|||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
|
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
[normalizedSessionId],
|
[normalizedSessionId],
|
||||||
{ includeRelations: true, allowStaleCache: true, cacheOnly: true }
|
withExportStatsRange({ includeRelations: true, allowStaleCache: true, cacheOnly: true })
|
||||||
)
|
)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
|
||||||
@@ -7361,7 +7382,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
const relationResult = await window.electronAPI.chat.getExportSessionStats(
|
const relationResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
[normalizedSessionId],
|
[normalizedSessionId],
|
||||||
{ includeRelations: true, forceRefresh, preferAccurateSpecialTypes: true }
|
withExportStatsRange({ includeRelations: true, forceRefresh, preferAccurateSpecialTypes: true })
|
||||||
)
|
)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
|
||||||
@@ -7381,7 +7402,7 @@ function ExportPage() {
|
|||||||
setIsLoadingSessionRelationStats(false)
|
setIsLoadingSessionRelationStats(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid])
|
}, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid, withExportStatsRange])
|
||||||
|
|
||||||
const handleRefreshTableData = useCallback(async () => {
|
const handleRefreshTableData = useCallback(async () => {
|
||||||
const scopeKey = await ensureExportCacheScope()
|
const scopeKey = await ensureExportCacheScope()
|
||||||
|
|||||||
3
src/types/electron.d.ts
vendored
3
src/types/electron.d.ts
vendored
@@ -311,6 +311,8 @@ export interface ElectronAPI {
|
|||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
cacheOnly?: boolean
|
cacheOnly?: boolean
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
}
|
}
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
@@ -1220,4 +1222,3 @@ declare global {
|
|||||||
export { }
|
export { }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user