perf(chat): split session detail into fast and extra loading

This commit is contained in:
tisonhuang
2026-03-01 18:41:06 +08:00
parent 22b6a07749
commit a5ae22d2a5
6 changed files with 344 additions and 96 deletions

View File

@@ -986,6 +986,14 @@ function registerIpcHandlers() {
return chatService.getSessionDetail(sessionId) return chatService.getSessionDetail(sessionId)
}) })
ipcMain.handle('chat:getSessionDetailFast', async (_, sessionId: string) => {
return chatService.getSessionDetailFast(sessionId)
})
ipcMain.handle('chat:getSessionDetailExtra', async (_, sessionId: string) => {
return chatService.getSessionDetailExtra(sessionId)
})
ipcMain.handle('chat:getExportSessionStats', async (_, sessionIds: string[]) => { ipcMain.handle('chat:getExportSessionStats', async (_, sessionIds: string[]) => {
return chatService.getExportSessionStats(sessionIds) return chatService.getExportSessionStats(sessionIds)
}) })

View File

@@ -154,6 +154,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId), getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
close: () => ipcRenderer.invoke('chat:close'), close: () => ipcRenderer.invoke('chat:close'),
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId), getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
getSessionDetailFast: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailFast', sessionId),
getSessionDetailExtra: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailExtra', sessionId),
getExportSessionStats: (sessionIds: string[]) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds), getExportSessionStats: (sessionIds: string[]) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds),
getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId), getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId),
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) => getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>

View File

@@ -159,6 +159,24 @@ interface ExportTabCounts {
former_friend: number former_friend: number
} }
interface SessionDetailFast {
wxid: string
displayName: string
remark?: string
nickName?: string
alias?: string
avatarUrl?: string
messageCount: number
}
interface SessionDetailExtra {
firstMessageTime?: number
latestMessageTime?: number
messageTables: { dbName: string; tableName: string; count: number }[]
}
type SessionDetail = SessionDetailFast & SessionDetailExtra
// 表情包缓存 // 表情包缓存
const emojiCache: Map<string, string> = new Map() const emojiCache: Map<string, string> = new Map()
const emojiDownloading: Map<string, Promise<string | null>> = new Map() const emojiDownloading: Map<string, Promise<string | null>> = new Map()
@@ -201,6 +219,10 @@ class ChatService {
private sessionMessageCountHintCache = new Map<string, number>() private sessionMessageCountHintCache = new Map<string, number>()
private sessionMessageCountCacheScope = '' private sessionMessageCountCacheScope = ''
private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000 private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000
private sessionDetailFastCache = new Map<string, { detail: SessionDetailFast; updatedAt: number }>()
private sessionDetailExtraCache = new Map<string, { detail: SessionDetailExtra; updatedAt: number }>()
private readonly sessionDetailFastCacheTtlMs = 60 * 1000
private readonly sessionDetailExtraCacheTtlMs = 5 * 60 * 1000
private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>() private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>()
private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000 private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000
@@ -1565,6 +1587,8 @@ class ChatService {
this.sessionMessageCountCacheScope = scope this.sessionMessageCountCacheScope = scope
this.sessionMessageCountCache.clear() this.sessionMessageCountCache.clear()
this.sessionMessageCountHintCache.clear() this.sessionMessageCountHintCache.clear()
this.sessionDetailFastCache.clear()
this.sessionDetailExtraCache.clear()
this.sessionStatusCache.clear() this.sessionStatusCache.clear()
} }
@@ -3819,20 +3843,9 @@ class ChatService {
/** /**
* 获取会话详情信息 * 获取会话详情信息
*/ */
async getSessionDetail(sessionId: string): Promise<{ async getSessionDetailFast(sessionId: string): Promise<{
success: boolean success: boolean
detail?: { detail?: SessionDetailFast
wxid: string
displayName: string
remark?: string
nickName?: string
alias?: string
avatarUrl?: string
messageCount: number
firstMessageTime?: number
latestMessageTime?: number
messageTables: { dbName: string; tableName: string; count: number }[]
}
error?: string error?: string
}> { }> {
try { try {
@@ -3840,53 +3853,152 @@ class ChatService {
if (!connectResult.success) { if (!connectResult.success) {
return { success: false, error: connectResult.error || '数据库未连接' } return { success: false, error: connectResult.error || '数据库未连接' }
} }
this.refreshSessionMessageCountCacheScope()
let displayName = sessionId const normalizedSessionId = String(sessionId || '').trim()
if (!normalizedSessionId) {
return { success: false, error: '会话ID不能为空' }
}
const now = Date.now()
const cachedDetail = this.sessionDetailFastCache.get(normalizedSessionId)
if (cachedDetail && now - cachedDetail.updatedAt <= this.sessionDetailFastCacheTtlMs) {
return { success: true, detail: cachedDetail.detail }
}
let displayName = normalizedSessionId
let remark: string | undefined let remark: string | undefined
let nickName: string | undefined let nickName: string | undefined
let alias: string | undefined let alias: string | undefined
let avatarUrl: string | undefined let avatarUrl: string | undefined
const cachedContact = this.avatarCache.get(normalizedSessionId)
const contactResult = await wcdbService.getContact(sessionId) if (cachedContact) {
if (contactResult.success && contactResult.contact) { displayName = cachedContact.displayName || normalizedSessionId
remark = contactResult.contact.remark || undefined avatarUrl = cachedContact.avatarUrl
nickName = contactResult.contact.nickName || undefined
alias = contactResult.contact.alias || undefined
displayName = remark || nickName || alias || sessionId
}
const avatarResult = await wcdbService.getAvatarUrls([sessionId])
if (avatarResult.success && avatarResult.map) {
avatarUrl = avatarResult.map[sessionId]
} }
const countResult = await wcdbService.getMessageCount(sessionId) const [contactResult, avatarResult] = await Promise.allSettled([
const totalMessageCount = countResult.success && countResult.count ? countResult.count : 0 wcdbService.getContact(normalizedSessionId),
avatarUrl ? Promise.resolve({ success: true, map: { [normalizedSessionId]: avatarUrl } }) : wcdbService.getAvatarUrls([normalizedSessionId])
])
let firstMessageTime: number | undefined if (contactResult.status === 'fulfilled' && contactResult.value.success && contactResult.value.contact) {
let latestMessageTime: number | undefined remark = contactResult.value.contact.remark || undefined
nickName = contactResult.value.contact.nickName || undefined
const earliestCursor = await wcdbService.openMessageCursor(sessionId, 1, true, 0, 0) alias = contactResult.value.contact.alias || undefined
if (earliestCursor.success && earliestCursor.cursor) { displayName = remark || nickName || alias || displayName
const batch = await wcdbService.fetchMessageBatch(earliestCursor.cursor)
if (batch.success && batch.rows && batch.rows.length > 0) {
firstMessageTime = parseInt(batch.rows[0].create_time || '0', 10) || undefined
}
await wcdbService.closeMessageCursor(earliestCursor.cursor)
} }
const latestCursor = await wcdbService.openMessageCursor(sessionId, 1, false, 0, 0) if (avatarResult.status === 'fulfilled' && avatarResult.value.success && avatarResult.value.map) {
if (latestCursor.success && latestCursor.cursor) { avatarUrl = avatarResult.value.map[normalizedSessionId]
const batch = await wcdbService.fetchMessageBatch(latestCursor.cursor)
if (batch.success && batch.rows && batch.rows.length > 0) {
latestMessageTime = parseInt(batch.rows[0].create_time || '0', 10) || undefined
} }
await wcdbService.closeMessageCursor(latestCursor.cursor)
let messageCount: number | undefined
const cachedCount = this.sessionMessageCountCache.get(normalizedSessionId)
if (cachedCount && now - cachedCount.updatedAt <= this.sessionMessageCountCacheTtlMs) {
messageCount = cachedCount.count
} else {
const hintCount = this.sessionMessageCountHintCache.get(normalizedSessionId)
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
messageCount = Math.floor(hintCount)
this.sessionMessageCountCache.set(normalizedSessionId, {
count: messageCount,
updatedAt: now
})
} }
}
if (!Number.isFinite(messageCount)) {
const countResult = await wcdbService.getMessageCount(normalizedSessionId)
messageCount = countResult.success && Number.isFinite(countResult.count)
? Math.max(0, Math.floor(countResult.count || 0))
: 0
this.sessionMessageCountCache.set(normalizedSessionId, {
count: messageCount,
updatedAt: Date.now()
})
}
const detail: SessionDetailFast = {
wxid: normalizedSessionId,
displayName,
remark,
nickName,
alias,
avatarUrl,
messageCount: Math.max(0, Math.floor(messageCount || 0))
}
this.sessionDetailFastCache.set(normalizedSessionId, {
detail,
updatedAt: Date.now()
})
return { success: true, detail }
} catch (e) {
console.error('ChatService: 获取会话详情快速信息失败:', e)
return { success: false, error: String(e) }
}
}
private async getBoundaryMessageTime(sessionId: string, ascending: boolean): Promise<number | undefined> {
const cursorResult = await wcdbService.openMessageCursor(sessionId, 1, ascending, 0, 0)
if (!cursorResult.success || !cursorResult.cursor) {
return undefined
}
const cursor = cursorResult.cursor
try {
const batch = await wcdbService.fetchMessageBatch(cursor)
if (!batch.success || !batch.rows || batch.rows.length === 0) {
return undefined
}
const ts = parseInt(batch.rows[0].create_time || '0', 10)
return Number.isFinite(ts) && ts > 0 ? ts : undefined
} finally {
await wcdbService.closeMessageCursor(cursor)
}
}
async getSessionDetailExtra(sessionId: string): Promise<{
success: boolean
detail?: SessionDetailExtra
error?: string
}> {
try {
const connectResult = await this.ensureConnected()
if (!connectResult.success) {
return { success: false, error: connectResult.error || '数据库未连接' }
}
this.refreshSessionMessageCountCacheScope()
const normalizedSessionId = String(sessionId || '').trim()
if (!normalizedSessionId) {
return { success: false, error: '会话ID不能为空' }
}
const now = Date.now()
const cachedDetail = this.sessionDetailExtraCache.get(normalizedSessionId)
if (cachedDetail && now - cachedDetail.updatedAt <= this.sessionDetailExtraCacheTtlMs) {
return { success: true, detail: cachedDetail.detail }
}
const [firstMessageTimeResult, latestMessageTimeResult, tableStatsResult] = await Promise.allSettled([
this.getBoundaryMessageTime(normalizedSessionId, true),
this.getBoundaryMessageTime(normalizedSessionId, false),
wcdbService.getMessageTableStats(normalizedSessionId)
])
const firstMessageTime = firstMessageTimeResult.status === 'fulfilled'
? firstMessageTimeResult.value
: undefined
const latestMessageTime = latestMessageTimeResult.status === 'fulfilled'
? latestMessageTimeResult.value
: undefined
const messageTables: { dbName: string; tableName: string; count: number }[] = [] const messageTables: { dbName: string; tableName: string; count: number }[] = []
const tableStats = await wcdbService.getMessageTableStats(sessionId) if (tableStatsResult.status === 'fulfilled' && tableStatsResult.value.success && tableStatsResult.value.tables) {
if (tableStats.success && tableStats.tables) { for (const row of tableStatsResult.value.tables) {
for (const row of tableStats.tables) {
messageTables.push({ messageTables.push({
dbName: basename(row.db_path || ''), dbName: basename(row.db_path || ''),
tableName: row.table_name || '', tableName: row.table_name || '',
@@ -3895,21 +4007,49 @@ class ChatService {
} }
} }
return { const detail: SessionDetailExtra = {
success: true,
detail: {
wxid: sessionId,
displayName,
remark,
nickName,
alias,
avatarUrl,
messageCount: totalMessageCount,
firstMessageTime, firstMessageTime,
latestMessageTime, latestMessageTime,
messageTables messageTables
} }
this.sessionDetailExtraCache.set(normalizedSessionId, {
detail,
updatedAt: Date.now()
})
return {
success: true,
detail
} }
} catch (e) {
console.error('ChatService: 获取会话详情补充统计失败:', e)
return { success: false, error: String(e) }
}
}
async getSessionDetail(sessionId: string): Promise<{
success: boolean
detail?: SessionDetail
error?: string
}> {
try {
const fastResult = await this.getSessionDetailFast(sessionId)
if (!fastResult.success || !fastResult.detail) {
return { success: false, error: fastResult.error || '获取会话详情失败' }
}
const extraResult = await this.getSessionDetailExtra(sessionId)
const detail: SessionDetail = {
...fastResult.detail,
firstMessageTime: extraResult.success ? extraResult.detail?.firstMessageTime : undefined,
latestMessageTime: extraResult.success ? extraResult.detail?.latestMessageTime : undefined,
messageTables: extraResult.success && extraResult.detail?.messageTables
? extraResult.detail.messageTables
: []
}
return { success: true, detail }
} catch (e) { } catch (e) {
console.error('ChatService: 获取会话详情失败:', e) console.error('ChatService: 获取会话详情失败:', e)
return { success: false, error: String(e) } return { success: false, error: String(e) }

View File

@@ -2766,6 +2766,14 @@
gap: 8px; gap: 8px;
} }
.detail-table-placeholder {
padding: 10px 12px;
background: var(--bg-secondary);
border-radius: 8px;
font-size: 12px;
color: var(--text-secondary);
}
.table-item { .table-item {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -312,6 +312,7 @@ function ChatPage(_props: ChatPageProps) {
const [showDetailPanel, setShowDetailPanel] = useState(false) const [showDetailPanel, setShowDetailPanel] = useState(false)
const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null) const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null)
const [isLoadingDetail, setIsLoadingDetail] = useState(false) const [isLoadingDetail, setIsLoadingDetail] = useState(false)
const [isLoadingDetailExtra, setIsLoadingDetailExtra] = useState(false)
const [copiedField, setCopiedField] = useState<string | null>(null) const [copiedField, setCopiedField] = useState<string | null>(null)
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([]) const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false) const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
@@ -386,6 +387,7 @@ function ChatPage(_props: ChatPageProps) {
const searchKeywordRef = useRef('') const searchKeywordRef = useRef('')
const preloadImageKeysRef = useRef<Set<string>>(new Set()) const preloadImageKeysRef = useRef<Set<string>>(new Set())
const lastPreloadSessionRef = useRef<string | null>(null) const lastPreloadSessionRef = useRef<string | null>(null)
const detailRequestSeqRef = useRef(0)
// 加载当前用户头像 // 加载当前用户头像
const loadMyAvatar = useCallback(async () => { const loadMyAvatar = useCallback(async () => {
@@ -401,25 +403,91 @@ function ChatPage(_props: ChatPageProps) {
// 加载会话详情 // 加载会话详情
const loadSessionDetail = useCallback(async (sessionId: string) => { const loadSessionDetail = useCallback(async (sessionId: string) => {
const normalizedSessionId = String(sessionId || '').trim()
if (!normalizedSessionId) return
const requestSeq = ++detailRequestSeqRef.current
const mappedSession = sessionMapRef.current.get(normalizedSessionId) || sessionsRef.current.find((s) => s.username === normalizedSessionId)
const hintedCount = typeof mappedSession?.messageCountHint === 'number' && Number.isFinite(mappedSession.messageCountHint) && mappedSession.messageCountHint >= 0
? Math.floor(mappedSession.messageCountHint)
: undefined
setSessionDetail((prev) => {
const sameSession = prev?.wxid === normalizedSessionId
return {
wxid: normalizedSessionId,
displayName: mappedSession?.displayName || prev?.displayName || normalizedSessionId,
remark: sameSession ? prev?.remark : undefined,
nickName: sameSession ? prev?.nickName : undefined,
alias: sameSession ? prev?.alias : undefined,
avatarUrl: mappedSession?.avatarUrl || (sameSession ? prev?.avatarUrl : undefined),
messageCount: hintedCount ?? (sameSession ? prev.messageCount : Number.NaN),
firstMessageTime: sameSession ? prev?.firstMessageTime : undefined,
latestMessageTime: sameSession ? prev?.latestMessageTime : undefined,
messageTables: sameSession && Array.isArray(prev?.messageTables) ? prev.messageTables : []
}
})
setIsLoadingDetail(true) setIsLoadingDetail(true)
setIsLoadingDetailExtra(true)
try { try {
const result = await window.electronAPI.chat.getSessionDetail(sessionId) const result = await window.electronAPI.chat.getSessionDetailFast(normalizedSessionId)
if (requestSeq !== detailRequestSeqRef.current) return
if (result.success && result.detail) { if (result.success && result.detail) {
setSessionDetail(result.detail) setSessionDetail((prev) => ({
wxid: normalizedSessionId,
displayName: result.detail!.displayName || prev?.displayName || normalizedSessionId,
remark: result.detail!.remark,
nickName: result.detail!.nickName,
alias: result.detail!.alias,
avatarUrl: result.detail!.avatarUrl || prev?.avatarUrl,
messageCount: Number.isFinite(result.detail!.messageCount) ? result.detail!.messageCount : prev?.messageCount ?? Number.NaN,
firstMessageTime: prev?.firstMessageTime,
latestMessageTime: prev?.latestMessageTime,
messageTables: Array.isArray(prev?.messageTables) ? (prev?.messageTables || []) : []
}))
} }
} catch (e) { } catch (e) {
console.error('加载会话详情失败:', e) console.error('加载会话详情失败:', e)
} finally { } finally {
if (requestSeq === detailRequestSeqRef.current) {
setIsLoadingDetail(false) setIsLoadingDetail(false)
} }
}
try {
const result = await window.electronAPI.chat.getSessionDetailExtra(normalizedSessionId)
if (requestSeq !== detailRequestSeqRef.current) return
if (result.success && result.detail) {
setSessionDetail((prev) => {
if (!prev || prev.wxid !== normalizedSessionId) return prev
return {
...prev,
firstMessageTime: result.detail!.firstMessageTime,
latestMessageTime: result.detail!.latestMessageTime,
messageTables: Array.isArray(result.detail!.messageTables) ? result.detail!.messageTables : []
}
})
}
} catch (e) {
console.error('加载会话详情补充统计失败:', e)
} finally {
if (requestSeq === detailRequestSeqRef.current) {
setIsLoadingDetailExtra(false)
}
}
}, []) }, [])
// 切换详情面板 // 切换详情面板
const toggleDetailPanel = useCallback(() => { const toggleDetailPanel = useCallback(() => {
if (!showDetailPanel && currentSessionId) { if (showDetailPanel) {
loadSessionDetail(currentSessionId) setShowDetailPanel(false)
return
}
setShowDetailPanel(true)
if (currentSessionId) {
void loadSessionDetail(currentSessionId)
} }
setShowDetailPanel(!showDetailPanel)
}, [showDetailPanel, currentSessionId, loadSessionDetail]) }, [showDetailPanel, currentSessionId, loadSessionDetail])
// 复制字段值到剪贴板 // 复制字段值到剪贴板
@@ -1107,7 +1175,7 @@ function ChatPage(_props: ChatPageProps) {
// 重置详情面板 // 重置详情面板
setSessionDetail(null) setSessionDetail(null)
if (showDetailPanel) { if (showDetailPanel) {
loadSessionDetail(session.username) void loadSessionDetail(session.username)
} }
} }
@@ -2475,7 +2543,7 @@ function ChatPage(_props: ChatPageProps) {
<X size={16} /> <X size={16} />
</button> </button>
</div> </div>
{isLoadingDetail ? ( {isLoadingDetail && !sessionDetail ? (
<div className="detail-loading"> <div className="detail-loading">
<Loader2 size={20} className="spin" /> <Loader2 size={20} className="spin" />
<span>...</span> <span>...</span>
@@ -2530,39 +2598,35 @@ function ChatPage(_props: ChatPageProps) {
<span className="value highlight"> <span className="value highlight">
{Number.isFinite(sessionDetail.messageCount) {Number.isFinite(sessionDetail.messageCount)
? sessionDetail.messageCount.toLocaleString() ? sessionDetail.messageCount.toLocaleString()
: '—'} : (isLoadingDetail ? '统计中...' : '—')}
</span> </span>
</div> </div>
{sessionDetail.firstMessageTime && (
<div className="detail-item"> <div className="detail-item">
<Calendar size={14} /> <Calendar size={14} />
<span className="label"></span> <span className="label"></span>
<span className="value"> <span className="value">
{Number.isFinite(sessionDetail.firstMessageTime) {Number.isFinite(sessionDetail.firstMessageTime)
? new Date(sessionDetail.firstMessageTime * 1000).toLocaleDateString('zh-CN') ? new Date((sessionDetail.firstMessageTime as number) * 1000).toLocaleDateString('zh-CN')
: '—'} : (isLoadingDetailExtra ? '统计中...' : '—')}
</span> </span>
</div> </div>
)}
{sessionDetail.latestMessageTime && (
<div className="detail-item"> <div className="detail-item">
<Calendar size={14} /> <Calendar size={14} />
<span className="label"></span> <span className="label"></span>
<span className="value"> <span className="value">
{Number.isFinite(sessionDetail.latestMessageTime) {Number.isFinite(sessionDetail.latestMessageTime)
? new Date(sessionDetail.latestMessageTime * 1000).toLocaleDateString('zh-CN') ? new Date((sessionDetail.latestMessageTime as number) * 1000).toLocaleDateString('zh-CN')
: '—'} : (isLoadingDetailExtra ? '统计中...' : '—')}
</span> </span>
</div> </div>
)}
</div> </div>
{Array.isArray(sessionDetail.messageTables) && sessionDetail.messageTables.length > 0 && (
<div className="detail-section"> <div className="detail-section">
<div className="section-title"> <div className="section-title">
<Database size={14} /> <Database size={14} />
<span></span> <span></span>
</div> </div>
{Array.isArray(sessionDetail.messageTables) && sessionDetail.messageTables.length > 0 ? (
<div className="table-list"> <div className="table-list">
{sessionDetail.messageTables.map((t, i) => ( {sessionDetail.messageTables.map((t, i) => (
<div key={i} className="table-item"> <div key={i} className="table-item">
@@ -2571,9 +2635,13 @@ function ChatPage(_props: ChatPageProps) {
</div> </div>
))} ))}
</div> </div>
) : (
<div className="detail-table-placeholder">
{isLoadingDetailExtra ? '统计中...' : '暂无统计数据'}
</div> </div>
)} )}
</div> </div>
</div>
) : ( ) : (
<div className="detail-empty"></div> <div className="detail-empty"></div>
)} )}

View File

@@ -144,6 +144,28 @@ export interface ElectronAPI {
} }
error?: string error?: string
}> }>
getSessionDetailFast: (sessionId: string) => Promise<{
success: boolean
detail?: {
wxid: string
displayName: string
remark?: string
nickName?: string
alias?: string
avatarUrl?: string
messageCount: number
}
error?: string
}>
getSessionDetailExtra: (sessionId: string) => Promise<{
success: boolean
detail?: {
firstMessageTime?: number
latestMessageTime?: number
messageTables: { dbName: string; tableName: string; count: number }[]
}
error?: string
}>
getExportSessionStats: (sessionIds: string[]) => Promise<{ getExportSessionStats: (sessionIds: string[]) => Promise<{
success: boolean success: boolean
data?: Record<string, { data?: Record<string, {