mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
perf(export): further optimize detail loading and prioritize session stats
This commit is contained in:
@@ -87,6 +87,7 @@ let onboardingWindow: BrowserWindow | null = null
|
|||||||
// Splash 启动窗口
|
// Splash 启动窗口
|
||||||
let splashWindow: BrowserWindow | null = null
|
let splashWindow: BrowserWindow | null = null
|
||||||
const sessionChatWindows = new Map<string, BrowserWindow>()
|
const sessionChatWindows = new Map<string, BrowserWindow>()
|
||||||
|
const sessionChatWindowSources = new Map<string, 'chat' | 'export'>()
|
||||||
const keyService = new KeyService()
|
const keyService = new KeyService()
|
||||||
|
|
||||||
let mainWindowReady = false
|
let mainWindowReady = false
|
||||||
@@ -123,6 +124,32 @@ interface AnnualReportYearsTaskState {
|
|||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpenSessionChatWindowOptions {
|
||||||
|
source?: 'chat' | 'export'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeSessionChatWindowSource = (source: unknown): 'chat' | 'export' => {
|
||||||
|
return String(source || '').trim().toLowerCase() === 'export' ? 'export' : 'chat'
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSessionChatWindowContent = (
|
||||||
|
win: BrowserWindow,
|
||||||
|
sessionId: string,
|
||||||
|
source: 'chat' | 'export'
|
||||||
|
) => {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
sessionId,
|
||||||
|
source
|
||||||
|
}).toString()
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
win.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/chat-window?${query}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
win.loadFile(join(__dirname, '../dist/index.html'), {
|
||||||
|
hash: `/chat-window?${query}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const annualReportYearsLoadTasks = new Map<string, AnnualReportYearsTaskState>()
|
const annualReportYearsLoadTasks = new Map<string, AnnualReportYearsTaskState>()
|
||||||
const annualReportYearsTaskByCacheKey = new Map<string, string>()
|
const annualReportYearsTaskByCacheKey = new Map<string, string>()
|
||||||
const annualReportYearsSnapshotCache = new Map<string, { snapshot: AnnualReportYearsProgressPayload; updatedAt: number; taskId: string }>()
|
const annualReportYearsSnapshotCache = new Map<string, { snapshot: AnnualReportYearsProgressPayload; updatedAt: number; taskId: string }>()
|
||||||
@@ -688,12 +715,18 @@ function createChatHistoryWindow(sessionId: string, messageId: number) {
|
|||||||
/**
|
/**
|
||||||
* 创建独立的会话聊天窗口(单会话,复用聊天页右侧消息区域)
|
* 创建独立的会话聊天窗口(单会话,复用聊天页右侧消息区域)
|
||||||
*/
|
*/
|
||||||
function createSessionChatWindow(sessionId: string) {
|
function createSessionChatWindow(sessionId: string, options?: OpenSessionChatWindowOptions) {
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
if (!normalizedSessionId) return null
|
if (!normalizedSessionId) return null
|
||||||
|
const normalizedSource = normalizeSessionChatWindowSource(options?.source)
|
||||||
|
|
||||||
const existing = sessionChatWindows.get(normalizedSessionId)
|
const existing = sessionChatWindows.get(normalizedSessionId)
|
||||||
if (existing && !existing.isDestroyed()) {
|
if (existing && !existing.isDestroyed()) {
|
||||||
|
const trackedSource = sessionChatWindowSources.get(normalizedSessionId) || 'chat'
|
||||||
|
if (trackedSource !== normalizedSource) {
|
||||||
|
loadSessionChatWindowContent(existing, normalizedSessionId, normalizedSource)
|
||||||
|
sessionChatWindowSources.set(normalizedSessionId, normalizedSource)
|
||||||
|
}
|
||||||
if (existing.isMinimized()) {
|
if (existing.isMinimized()) {
|
||||||
existing.restore()
|
existing.restore()
|
||||||
}
|
}
|
||||||
@@ -730,10 +763,9 @@ function createSessionChatWindow(sessionId: string) {
|
|||||||
autoHideMenuBar: true
|
autoHideMenuBar: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionParam = `sessionId=${encodeURIComponent(normalizedSessionId)}`
|
loadSessionChatWindowContent(win, normalizedSessionId, normalizedSource)
|
||||||
if (process.env.VITE_DEV_SERVER_URL) {
|
|
||||||
win.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/chat-window?${sessionParam}`)
|
|
||||||
|
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
win.webContents.on('before-input-event', (event, input) => {
|
win.webContents.on('before-input-event', (event, input) => {
|
||||||
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
|
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
|
||||||
if (win.webContents.isDevToolsOpened()) {
|
if (win.webContents.isDevToolsOpened()) {
|
||||||
@@ -744,10 +776,6 @@ function createSessionChatWindow(sessionId: string) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
win.loadFile(join(__dirname, '../dist/index.html'), {
|
|
||||||
hash: `/chat-window?${sessionParam}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
win.once('ready-to-show', () => {
|
||||||
@@ -759,10 +787,12 @@ function createSessionChatWindow(sessionId: string) {
|
|||||||
const tracked = sessionChatWindows.get(normalizedSessionId)
|
const tracked = sessionChatWindows.get(normalizedSessionId)
|
||||||
if (tracked === win) {
|
if (tracked === win) {
|
||||||
sessionChatWindows.delete(normalizedSessionId)
|
sessionChatWindows.delete(normalizedSessionId)
|
||||||
|
sessionChatWindowSources.delete(normalizedSessionId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionChatWindows.set(normalizedSessionId, win)
|
sessionChatWindows.set(normalizedSessionId, win)
|
||||||
|
sessionChatWindowSources.set(normalizedSessionId, normalizedSource)
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,8 +1101,8 @@ function registerIpcHandlers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 打开会话聊天窗口(同会话仅保留一个窗口并聚焦)
|
// 打开会话聊天窗口(同会话仅保留一个窗口并聚焦)
|
||||||
ipcMain.handle('window:openSessionChatWindow', (_, sessionId: string) => {
|
ipcMain.handle('window:openSessionChatWindow', (_, sessionId: string, options?: OpenSessionChatWindowOptions) => {
|
||||||
const win = createSessionChatWindow(sessionId)
|
const win = createSessionChatWindow(sessionId, options)
|
||||||
return Boolean(win)
|
return Boolean(win)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1410,6 +1440,7 @@ function registerIpcHandlers() {
|
|||||||
forceRefresh?: boolean
|
forceRefresh?: boolean
|
||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
|
cacheOnly?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return chatService.getExportSessionStats(sessionIds, options)
|
return chatService.getExportSessionStats(sessionIds, options)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.invoke('window:openImageViewerWindow', imagePath, liveVideoPath),
|
ipcRenderer.invoke('window:openImageViewerWindow', imagePath, liveVideoPath),
|
||||||
openChatHistoryWindow: (sessionId: string, messageId: number) =>
|
openChatHistoryWindow: (sessionId: string, messageId: number) =>
|
||||||
ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId),
|
ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId),
|
||||||
openSessionChatWindow: (sessionId: string) =>
|
openSessionChatWindow: (sessionId: string, options?: { source?: 'chat' | 'export' }) =>
|
||||||
ipcRenderer.invoke('window:openSessionChatWindow', sessionId)
|
ipcRenderer.invoke('window:openSessionChatWindow', sessionId, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 数据库路径
|
// 数据库路径
|
||||||
@@ -174,7 +174,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getSessionDetailExtra: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailExtra', sessionId),
|
getSessionDetailExtra: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailExtra', sessionId),
|
||||||
getExportSessionStats: (
|
getExportSessionStats: (
|
||||||
sessionIds: string[],
|
sessionIds: string[],
|
||||||
options?: { includeRelations?: boolean; forceRefresh?: boolean; allowStaleCache?: boolean; preferAccurateSpecialTypes?: boolean }
|
options?: {
|
||||||
|
includeRelations?: boolean
|
||||||
|
forceRefresh?: boolean
|
||||||
|
allowStaleCache?: boolean
|
||||||
|
preferAccurateSpecialTypes?: boolean
|
||||||
|
cacheOnly?: boolean
|
||||||
|
}
|
||||||
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
||||||
getGroupMyMessageCountHint: (chatroomId: string) =>
|
getGroupMyMessageCountHint: (chatroomId: string) =>
|
||||||
ipcRenderer.invoke('chat:getGroupMyMessageCountHint', chatroomId),
|
ipcRenderer.invoke('chat:getGroupMyMessageCountHint', chatroomId),
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ interface ExportSessionStatsOptions {
|
|||||||
forceRefresh?: boolean
|
forceRefresh?: boolean
|
||||||
allowStaleCache?: boolean
|
allowStaleCache?: boolean
|
||||||
preferAccurateSpecialTypes?: boolean
|
preferAccurateSpecialTypes?: boolean
|
||||||
|
cacheOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExportSessionStatsCacheMeta {
|
interface ExportSessionStatsCacheMeta {
|
||||||
@@ -5354,6 +5355,7 @@ class ChatService {
|
|||||||
const forceRefresh = options.forceRefresh === true
|
const forceRefresh = options.forceRefresh === true
|
||||||
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 normalizedSessionIds = Array.from(
|
const normalizedSessionIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -5377,32 +5379,34 @@ class ChatService {
|
|||||||
? this.getGroupMyMessageCountHintEntry(sessionId)
|
? this.getGroupMyMessageCountHintEntry(sessionId)
|
||||||
: null
|
: null
|
||||||
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
||||||
if (!forceRefresh && !preferAccurateSpecialTypes) {
|
const canUseCache = cacheOnly || (!forceRefresh && !preferAccurateSpecialTypes)
|
||||||
if (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) {
|
if (!stale || allowStaleCache || cacheOnly) {
|
||||||
resultMap[sessionId] = this.fromSessionStatsCacheStats(cachedResult.entry.stats)
|
resultMap[sessionId] = this.fromSessionStatsCacheStats(cachedResult.entry.stats)
|
||||||
if (groupMyMessagesHint && Number.isFinite(groupMyMessagesHint.entry.messageCount)) {
|
if (groupMyMessagesHint && Number.isFinite(groupMyMessagesHint.entry.messageCount)) {
|
||||||
resultMap[sessionId].groupMyMessages = groupMyMessagesHint.entry.messageCount
|
resultMap[sessionId].groupMyMessages = groupMyMessagesHint.entry.messageCount
|
||||||
}
|
}
|
||||||
cacheMeta[sessionId] = {
|
cacheMeta[sessionId] = {
|
||||||
updatedAt: cachedResult.entry.updatedAt,
|
updatedAt: cachedResult.entry.updatedAt,
|
||||||
stale,
|
stale,
|
||||||
includeRelations: cachedResult.entry.includeRelations,
|
includeRelations: cachedResult.entry.includeRelations,
|
||||||
source: cachedResult.source
|
source: cachedResult.source
|
||||||
}
|
}
|
||||||
if (stale) {
|
if (stale) {
|
||||||
needsRefreshSet.add(sessionId)
|
needsRefreshSet.add(sessionId)
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// allowStaleCache 仅对“已有缓存”生效;无缓存会话仍需进入计算流程。
|
|
||||||
if (allowStaleCache && cachedResult) {
|
|
||||||
needsRefreshSet.add(sessionId)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// allowStaleCache/cacheOnly 仅对“已有缓存”生效;无缓存会话不会直接算重查询。
|
||||||
|
if (canUseCache && allowStaleCache && cachedResult) {
|
||||||
|
needsRefreshSet.add(sessionId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (cacheOnly) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
pendingSessionIds.push(sessionId)
|
pendingSessionIds.push(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -402,8 +402,10 @@ function App() {
|
|||||||
|
|
||||||
// 独立会话聊天窗口(仅显示聊天内容区域)
|
// 独立会话聊天窗口(仅显示聊天内容区域)
|
||||||
if (isStandaloneChatWindow) {
|
if (isStandaloneChatWindow) {
|
||||||
const sessionId = new URLSearchParams(location.search).get('sessionId') || ''
|
const params = new URLSearchParams(location.search)
|
||||||
return <ChatPage standaloneSessionWindow initialSessionId={sessionId} />
|
const sessionId = params.get('sessionId') || ''
|
||||||
|
const standaloneSource = params.get('source')
|
||||||
|
return <ChatPage standaloneSessionWindow initialSessionId={sessionId} standaloneSource={standaloneSource} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// 独立通知窗口
|
// 独立通知窗口
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ function formatYmdHmDateTime(timestamp?: number): string {
|
|||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
standaloneSessionWindow?: boolean
|
standaloneSessionWindow?: boolean
|
||||||
initialSessionId?: string | null
|
initialSessionId?: string | null
|
||||||
|
standaloneSource?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -408,8 +409,10 @@ const SessionItem = React.memo(function SessionItem({
|
|||||||
|
|
||||||
|
|
||||||
function ChatPage(props: ChatPageProps) {
|
function ChatPage(props: ChatPageProps) {
|
||||||
const { standaloneSessionWindow = false, initialSessionId = null } = props
|
const { standaloneSessionWindow = false, initialSessionId = null, standaloneSource = null } = props
|
||||||
const normalizedInitialSessionId = useMemo(() => String(initialSessionId || '').trim(), [initialSessionId])
|
const normalizedInitialSessionId = useMemo(() => String(initialSessionId || '').trim(), [initialSessionId])
|
||||||
|
const normalizedStandaloneSource = useMemo(() => String(standaloneSource || '').trim().toLowerCase(), [standaloneSource])
|
||||||
|
const shouldHideStandaloneDetailButton = standaloneSessionWindow && normalizedStandaloneSource === 'export'
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -3863,13 +3866,15 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
>
|
>
|
||||||
<RefreshCw size={18} className={isRefreshingMessages ? 'spin' : ''} />
|
<RefreshCw size={18} className={isRefreshingMessages ? 'spin' : ''} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
{!shouldHideStandaloneDetailButton && (
|
||||||
className={`icon-btn detail-btn ${showDetailPanel ? 'active' : ''}`}
|
<button
|
||||||
onClick={toggleDetailPanel}
|
className={`icon-btn detail-btn ${showDetailPanel ? 'active' : ''}`}
|
||||||
title="会话详情"
|
onClick={toggleDetailPanel}
|
||||||
>
|
title="会话详情"
|
||||||
<Info size={18} />
|
>
|
||||||
</button>
|
<Info size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ interface TimeRangeDialogDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content']
|
const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content']
|
||||||
|
const DETAIL_PRECISE_REFRESH_COOLDOWN_MS = 10 * 60 * 1000
|
||||||
const contentTypeLabels: Record<ContentType, string> = {
|
const contentTypeLabels: Record<ContentType, string> = {
|
||||||
text: '聊天文本',
|
text: '聊天文本',
|
||||||
voice: '语音',
|
voice: '语音',
|
||||||
@@ -1307,6 +1308,8 @@ function ExportPage() {
|
|||||||
const hasBaseConfigReadyRef = useRef(false)
|
const hasBaseConfigReadyRef = useRef(false)
|
||||||
const sessionCountRequestIdRef = useRef(0)
|
const sessionCountRequestIdRef = useRef(0)
|
||||||
const activeTabRef = useRef<ConversationTab>('private')
|
const activeTabRef = useRef<ConversationTab>('private')
|
||||||
|
const detailStatsPriorityRef = useRef(false)
|
||||||
|
const sessionPreciseRefreshAtRef = useRef<Record<string, number>>({})
|
||||||
|
|
||||||
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
||||||
if (exportCacheScopeReadyRef.current) {
|
if (exportCacheScopeReadyRef.current) {
|
||||||
@@ -1894,6 +1897,9 @@ function ExportPage() {
|
|||||||
|
|
||||||
setIsLoadingSessionCounts(true)
|
setIsLoadingSessionCounts(true)
|
||||||
try {
|
try {
|
||||||
|
if (detailStatsPriorityRef.current) {
|
||||||
|
return { ...accumulatedCounts }
|
||||||
|
}
|
||||||
if (prioritizedSessionIds.length > 0) {
|
if (prioritizedSessionIds.length > 0) {
|
||||||
const priorityResult = await window.electronAPI.chat.getSessionMessageCounts(prioritizedSessionIds)
|
const priorityResult = await window.electronAPI.chat.getSessionMessageCounts(prioritizedSessionIds)
|
||||||
if (isStale()) return { ...accumulatedCounts }
|
if (isStale()) return { ...accumulatedCounts }
|
||||||
@@ -1902,6 +1908,9 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (detailStatsPriorityRef.current) {
|
||||||
|
return { ...accumulatedCounts }
|
||||||
|
}
|
||||||
if (remainingSessionIds.length > 0) {
|
if (remainingSessionIds.length > 0) {
|
||||||
const remainingResult = await window.electronAPI.chat.getSessionMessageCounts(remainingSessionIds)
|
const remainingResult = await window.electronAPI.chat.getSessionMessageCounts(remainingSessionIds)
|
||||||
if (isStale()) return { ...accumulatedCounts }
|
if (isStale()) return { ...accumulatedCounts }
|
||||||
@@ -1930,6 +1939,7 @@ function ExportPage() {
|
|||||||
const loadToken = Date.now()
|
const loadToken = Date.now()
|
||||||
sessionLoadTokenRef.current = loadToken
|
sessionLoadTokenRef.current = loadToken
|
||||||
sessionsHydratedAtRef.current = 0
|
sessionsHydratedAtRef.current = 0
|
||||||
|
sessionPreciseRefreshAtRef.current = {}
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
sessionCountRequestIdRef.current += 1
|
sessionCountRequestIdRef.current += 1
|
||||||
@@ -2027,12 +2037,14 @@ function ExportPage() {
|
|||||||
setIsSessionEnriching(true)
|
setIsSessionEnriching(true)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
if (detailStatsPriorityRef.current) return
|
||||||
let contactMap = { ...cachedContactMap }
|
let contactMap = { ...cachedContactMap }
|
||||||
let avatarEntries = { ...cachedAvatarEntries }
|
let avatarEntries = { ...cachedAvatarEntries }
|
||||||
let hasFreshNetworkData = false
|
let hasFreshNetworkData = false
|
||||||
let hasNetworkContactsSnapshot = false
|
let hasNetworkContactsSnapshot = false
|
||||||
|
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
if (detailStatsPriorityRef.current) return
|
||||||
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
@@ -2091,6 +2103,7 @@ function ExportPage() {
|
|||||||
if (needsEnrichment.length > 0) {
|
if (needsEnrichment.length > 0) {
|
||||||
for (let i = 0; i < needsEnrichment.length; i += EXPORT_AVATAR_ENRICH_BATCH_SIZE) {
|
for (let i = 0; i < needsEnrichment.length; i += EXPORT_AVATAR_ENRICH_BATCH_SIZE) {
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
if (detailStatsPriorityRef.current) return
|
||||||
const batch = needsEnrichment.slice(i, i + EXPORT_AVATAR_ENRICH_BATCH_SIZE)
|
const batch = needsEnrichment.slice(i, i + EXPORT_AVATAR_ENRICH_BATCH_SIZE)
|
||||||
if (batch.length === 0) continue
|
if (batch.length === 0) continue
|
||||||
try {
|
try {
|
||||||
@@ -3399,6 +3412,11 @@ function ExportPage() {
|
|||||||
const loadSessionDetail = useCallback(async (sessionId: string) => {
|
const loadSessionDetail = useCallback(async (sessionId: string) => {
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
if (!normalizedSessionId) return
|
if (!normalizedSessionId) return
|
||||||
|
const preciseCacheKey = `${exportCacheScopeRef.current}::${normalizedSessionId}`
|
||||||
|
|
||||||
|
detailStatsPriorityRef.current = true
|
||||||
|
sessionCountRequestIdRef.current += 1
|
||||||
|
setIsLoadingSessionCounts(false)
|
||||||
|
|
||||||
const requestSeq = ++detailRequestSeqRef.current
|
const requestSeq = ++detailRequestSeqRef.current
|
||||||
const mappedSession = sessionRowByUsername.get(normalizedSessionId)
|
const mappedSession = sessionRowByUsername.get(normalizedSessionId)
|
||||||
@@ -3510,19 +3528,13 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [extraResultSettled, statsResultSettled] = await Promise.allSettled([
|
const extraPromise = window.electronAPI.chat.getSessionDetailExtra(normalizedSessionId)
|
||||||
window.electronAPI.chat.getSessionDetailExtra(normalizedSessionId),
|
void (async () => {
|
||||||
window.electronAPI.chat.getExportSessionStats(
|
try {
|
||||||
[normalizedSessionId],
|
const extraResult = await extraPromise
|
||||||
{ includeRelations: false, allowStaleCache: true }
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
)
|
if (!extraResult.success || !extraResult.detail) return
|
||||||
])
|
const detail = extraResult.detail
|
||||||
|
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
|
||||||
|
|
||||||
if (extraResultSettled.status === 'fulfilled' && extraResultSettled.value.success) {
|
|
||||||
const detail = extraResultSettled.value.detail
|
|
||||||
if (detail) {
|
|
||||||
setSessionDetail((prev) => {
|
setSessionDetail((prev) => {
|
||||||
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
||||||
return {
|
return {
|
||||||
@@ -3532,62 +3544,86 @@ function ExportPage() {
|
|||||||
messageTables: Array.isArray(detail.messageTables) ? detail.messageTables : []
|
messageTables: Array.isArray(detail.messageTables) ? detail.messageTables : []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statsResultSettled.status === 'fulfilled' && statsResultSettled.value.success) {
|
|
||||||
const metric = statsResultSettled.value.data?.[normalizedSessionId] as SessionExportMetric | undefined
|
|
||||||
const cacheMeta = statsResultSettled.value.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
|
|
||||||
if (metric) {
|
|
||||||
applySessionDetailStats(normalizedSessionId, metric, cacheMeta, false)
|
|
||||||
} else if (cacheMeta) {
|
|
||||||
setSessionDetail((prev) => {
|
|
||||||
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
statsUpdatedAt: cacheMeta.updatedAt,
|
|
||||||
statsStale: cacheMeta.stale
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsRefreshingSessionDetailStats(true)
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
// 后台精确补算三类重字段(转账/红包/通话),不阻塞首屏基础统计显示。
|
|
||||||
const freshResult = await window.electronAPI.chat.getExportSessionStats(
|
|
||||||
[normalizedSessionId],
|
|
||||||
{ includeRelations: false, forceRefresh: true, preferAccurateSpecialTypes: true }
|
|
||||||
)
|
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
|
||||||
if (freshResult.success && freshResult.data) {
|
|
||||||
const metric = freshResult.data[normalizedSessionId] as SessionExportMetric | undefined
|
|
||||||
const cacheMeta = freshResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
|
|
||||||
if (metric) {
|
|
||||||
applySessionDetailStats(normalizedSessionId, metric, cacheMeta, false)
|
|
||||||
} else if (cacheMeta) {
|
|
||||||
setSessionDetail((prev) => {
|
|
||||||
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
statsUpdatedAt: cacheMeta.updatedAt,
|
|
||||||
statsStale: cacheMeta.stale
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出页刷新会话统计失败:', error)
|
console.error('导出页加载会话详情补充信息失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
if (requestSeq === detailRequestSeqRef.current) {
|
if (requestSeq === detailRequestSeqRef.current) {
|
||||||
setIsRefreshingSessionDetailStats(false)
|
setIsLoadingSessionDetailExtra(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
let quickMetric: SessionExportMetric | undefined
|
||||||
|
let quickCacheMeta: SessionExportCacheMeta | undefined
|
||||||
|
try {
|
||||||
|
const quickStatsResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
|
[normalizedSessionId],
|
||||||
|
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
||||||
|
)
|
||||||
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
if (quickStatsResult.success) {
|
||||||
|
quickMetric = quickStatsResult.data?.[normalizedSessionId] as SessionExportMetric | undefined
|
||||||
|
quickCacheMeta = quickStatsResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
|
||||||
|
if (quickMetric) {
|
||||||
|
applySessionDetailStats(normalizedSessionId, quickMetric, quickCacheMeta, false)
|
||||||
|
} else if (quickCacheMeta) {
|
||||||
|
const cacheMeta = quickCacheMeta
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
statsUpdatedAt: cacheMeta.updatedAt,
|
||||||
|
statsStale: cacheMeta.stale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出页读取会话统计缓存失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPreciseAt = sessionPreciseRefreshAtRef.current[preciseCacheKey] || 0
|
||||||
|
const hasRecentPrecise = Date.now() - lastPreciseAt <= DETAIL_PRECISE_REFRESH_COOLDOWN_MS
|
||||||
|
const shouldRunPreciseRefresh = !hasRecentPrecise && (!quickMetric || Boolean(quickCacheMeta?.stale))
|
||||||
|
|
||||||
|
if (shouldRunPreciseRefresh) {
|
||||||
|
setIsRefreshingSessionDetailStats(true)
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
// 后台精确补算三类重字段(转账/红包/通话),不阻塞首屏基础统计显示。
|
||||||
|
const freshResult = await window.electronAPI.chat.getExportSessionStats(
|
||||||
|
[normalizedSessionId],
|
||||||
|
{ includeRelations: false, forceRefresh: true, preferAccurateSpecialTypes: true }
|
||||||
|
)
|
||||||
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
if (freshResult.success && freshResult.data) {
|
||||||
|
const metric = freshResult.data[normalizedSessionId] as SessionExportMetric | undefined
|
||||||
|
const cacheMeta = freshResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
|
||||||
|
if (metric) {
|
||||||
|
applySessionDetailStats(normalizedSessionId, metric, cacheMeta, false)
|
||||||
|
sessionPreciseRefreshAtRef.current[preciseCacheKey] = Date.now()
|
||||||
|
} else if (cacheMeta) {
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
statsUpdatedAt: cacheMeta.updatedAt,
|
||||||
|
statsStale: cacheMeta.stale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出页刷新会话统计失败:', error)
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === detailRequestSeqRef.current) {
|
||||||
|
setIsRefreshingSessionDetailStats(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出页加载会话详情补充统计失败:', error)
|
console.error('导出页加载会话详情补充统计失败:', error)
|
||||||
} finally {
|
|
||||||
if (requestSeq === detailRequestSeqRef.current) {
|
if (requestSeq === detailRequestSeqRef.current) {
|
||||||
setIsLoadingSessionDetailExtra(false)
|
setIsLoadingSessionDetailExtra(false)
|
||||||
}
|
}
|
||||||
@@ -3627,6 +3663,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
const closeSessionDetailPanel = useCallback(() => {
|
const closeSessionDetailPanel = useCallback(() => {
|
||||||
detailRequestSeqRef.current += 1
|
detailRequestSeqRef.current += 1
|
||||||
|
detailStatsPriorityRef.current = false
|
||||||
setShowSessionDetailPanel(false)
|
setShowSessionDetailPanel(false)
|
||||||
setIsLoadingSessionDetail(false)
|
setIsLoadingSessionDetail(false)
|
||||||
setIsLoadingSessionDetailExtra(false)
|
setIsLoadingSessionDetailExtra(false)
|
||||||
@@ -3636,6 +3673,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
const openSessionDetail = useCallback((sessionId: string) => {
|
const openSessionDetail = useCallback((sessionId: string) => {
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
detailStatsPriorityRef.current = true
|
||||||
setShowSessionDetailPanel(true)
|
setShowSessionDetailPanel(true)
|
||||||
void loadSessionDetail(sessionId)
|
void loadSessionDetail(sessionId)
|
||||||
}, [loadSessionDetail])
|
}, [loadSessionDetail])
|
||||||
@@ -3827,7 +3865,7 @@ function ExportPage() {
|
|||||||
title={canExport ? '在新窗口打开该会话' : '该联系人暂无会话记录'}
|
title={canExport ? '在新窗口打开该会话' : '该联系人暂无会话记录'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!canExport) return
|
if (!canExport) return
|
||||||
void window.electronAPI.window.openSessionChatWindow(contact.username)
|
void window.electronAPI.window.openSessionChatWindow(contact.username, { source: 'export' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExternalLink size={13} />
|
<ExternalLink size={13} />
|
||||||
|
|||||||
14
src/types/electron.d.ts
vendored
14
src/types/electron.d.ts
vendored
@@ -1,5 +1,9 @@
|
|||||||
import type { ChatSession, Message, Contact, ContactInfo } from './models'
|
import type { ChatSession, Message, Contact, ContactInfo } from './models'
|
||||||
|
|
||||||
|
export interface SessionChatWindowOpenOptions {
|
||||||
|
source?: 'chat' | 'export'
|
||||||
|
}
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
window: {
|
window: {
|
||||||
minimize: () => void
|
minimize: () => void
|
||||||
@@ -13,7 +17,7 @@ export interface ElectronAPI {
|
|||||||
resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise<void>
|
resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise<void>
|
||||||
openImageViewerWindow: (imagePath: string, liveVideoPath?: string) => Promise<void>
|
openImageViewerWindow: (imagePath: string, liveVideoPath?: string) => Promise<void>
|
||||||
openChatHistoryWindow: (sessionId: string, messageId: number) => Promise<boolean>
|
openChatHistoryWindow: (sessionId: string, messageId: number) => Promise<boolean>
|
||||||
openSessionChatWindow: (sessionId: string) => Promise<boolean>
|
openSessionChatWindow: (sessionId: string, options?: SessionChatWindowOpenOptions) => Promise<boolean>
|
||||||
}
|
}
|
||||||
config: {
|
config: {
|
||||||
get: (key: string) => Promise<unknown>
|
get: (key: string) => Promise<unknown>
|
||||||
@@ -250,7 +254,13 @@ export interface ElectronAPI {
|
|||||||
}>
|
}>
|
||||||
getExportSessionStats: (
|
getExportSessionStats: (
|
||||||
sessionIds: string[],
|
sessionIds: string[],
|
||||||
options?: { includeRelations?: boolean; forceRefresh?: boolean; allowStaleCache?: boolean; preferAccurateSpecialTypes?: boolean }
|
options?: {
|
||||||
|
includeRelations?: boolean
|
||||||
|
forceRefresh?: boolean
|
||||||
|
allowStaleCache?: boolean
|
||||||
|
preferAccurateSpecialTypes?: boolean
|
||||||
|
cacheOnly?: boolean
|
||||||
|
}
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
data?: Record<string, {
|
data?: Record<string, {
|
||||||
|
|||||||
Reference in New Issue
Block a user