mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-20 03:23:21 +08:00
TS 实现简易 LRU 缓存:新增缓存类 + 核心方法 + 容量满自动淘汰 LRU 项
This commit is contained in:
@@ -1644,6 +1644,85 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 文件消息卡片
|
||||
.file-message {
|
||||
width: 240px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #4a90d9 0%, #357abd 100%);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送的文件消息样式
|
||||
.message-bubble.sent .file-message {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
.file-name {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 转账消息卡片
|
||||
.transfer-message {
|
||||
width: 240px;
|
||||
|
||||
+218
-58
@@ -1,14 +1,23 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback, memo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Image as ImageIcon, Play, Video, Copy, ZoomIn, CheckSquare, Check, Edit, Link, Sparkles } from 'lucide-react'
|
||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Image as ImageIcon, Play, Video, Copy, ZoomIn, CheckSquare, Check, Edit, Link, Sparkles, FileText, FileArchive } from 'lucide-react'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import { useUpdateStatusStore } from '../stores/updateStatusStore'
|
||||
import ChatBackground from '../components/ChatBackground'
|
||||
import MessageContent from '../components/MessageContent'
|
||||
import { getImageXorKey, getImageAesKey, getQuoteStyle } from '../services/config'
|
||||
import { LRUCache } from '../utils/lruCache'
|
||||
import type { ChatSession, Message } from '../types/models'
|
||||
import { List, RowComponentProps } from 'react-window'
|
||||
import './ChatPage.scss'
|
||||
|
||||
interface SessionRowData {
|
||||
sessions: ChatSession[]
|
||||
currentSessionId: string | null
|
||||
onSelect: (s: ChatSession) => void
|
||||
formatTime: (t: number) => string
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface ChatPageProps {
|
||||
@@ -155,6 +164,48 @@ function SessionAvatar({ session, size = 48 }: { session: ChatSession; size?: nu
|
||||
)
|
||||
}
|
||||
|
||||
// 会话列表行组件(使用 memo 优化性能)
|
||||
const SessionRow = (props: RowComponentProps<SessionRowData>) => {
|
||||
const { index, style, sessions, currentSessionId, onSelect, formatTime } = props
|
||||
const session = sessions[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={`session-item ${currentSessionId === session.username ? 'active' : ''}`}
|
||||
onClick={() => onSelect(session)}
|
||||
>
|
||||
<SessionAvatar session={session} size={48} />
|
||||
<div className="session-info">
|
||||
<div className="session-top">
|
||||
<span className="session-name">{session.displayName || session.username}</span>
|
||||
<span className="session-time">{formatTime(session.lastTimestamp || session.sortTimestamp)}</span>
|
||||
</div>
|
||||
<div className="session-bottom">
|
||||
<span className="session-summary">
|
||||
{(() => {
|
||||
const summary = session.summary || '暂无消息'
|
||||
const firstLine = summary.split('\n')[0]
|
||||
const hasMoreLines = summary.includes('\n')
|
||||
return (
|
||||
<>
|
||||
<MessageContent content={firstLine} disableLinks={true} />
|
||||
{hasMoreLines && <span>...</span>}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
{session.unreadCount > 0 && (
|
||||
<span className="unread-badge">
|
||||
{session.unreadCount > 99 ? '99+' : session.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatPage(_props: ChatPageProps) {
|
||||
const [quoteStyle, setQuoteStyle] = useState<'default' | 'wechat'>('default')
|
||||
|
||||
@@ -322,14 +373,14 @@ function ChatPage(_props: ChatPageProps) {
|
||||
// 合并:保留顺序,只更新变化的字段
|
||||
const merged = result.sessions!.map(newSession => {
|
||||
const oldSession = oldSessionsMap.get(newSession.username)
|
||||
|
||||
|
||||
// 如果是新会话,直接返回
|
||||
if (!oldSession) {
|
||||
return newSession
|
||||
}
|
||||
|
||||
// 检查是否有实质性变化
|
||||
const hasChanges =
|
||||
const hasChanges =
|
||||
oldSession.summary !== newSession.summary ||
|
||||
oldSession.lastTimestamp !== newSession.lastTimestamp ||
|
||||
oldSession.unreadCount !== newSession.unreadCount ||
|
||||
@@ -439,6 +490,40 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听增量消息推送
|
||||
useEffect(() => {
|
||||
// 告知后端当前会话
|
||||
window.electronAPI.chat.setCurrentSession(currentSessionId)
|
||||
|
||||
const cleanup = window.electronAPI.chat.onNewMessages((data: { sessionId: string; messages: Message[] }) => {
|
||||
if (data.sessionId === currentSessionId && data.messages && data.messages.length > 0) {
|
||||
setMessages((prev: Message[]) => {
|
||||
// 使用 sortSeq 去重
|
||||
const newMsgs = data.messages.filter((nm: Message) =>
|
||||
!prev.some((pm: Message) => pm.sortSeq === nm.sortSeq)
|
||||
)
|
||||
if (newMsgs.length === 0) return prev
|
||||
|
||||
return [...prev, ...newMsgs]
|
||||
})
|
||||
|
||||
// 平滑滚动到底部
|
||||
requestAnimationFrame(() => scrollToBottom(true))
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
}
|
||||
}, [currentSessionId])
|
||||
|
||||
// 组件卸载时取消当前会话
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.electronAPI.chat.setCurrentSession(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 选择会话
|
||||
const handleSelectSession = (session: ChatSession) => {
|
||||
if (session.username === currentSessionId) {
|
||||
@@ -559,11 +644,11 @@ function ChatPage(_props: ChatPageProps) {
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
|
||||
// 监听会话列表更新
|
||||
// 监听会话列表更新
|
||||
const removeSessionsListener = window.electronAPI.chat.onSessionsUpdated?.(async (newSessions) => {
|
||||
// 更新增量更新时间戳
|
||||
lastIncrementalUpdateTime = Date.now()
|
||||
|
||||
|
||||
// 智能合并更新会话列表,避免闪烁
|
||||
setSessions((prevSessions: ChatSession[]) => {
|
||||
// 如果之前没有会话,直接设置
|
||||
@@ -579,14 +664,14 @@ function ChatPage(_props: ChatPageProps) {
|
||||
// 合并:保留顺序,只更新变化的字段
|
||||
const merged = newSessions.map(newSession => {
|
||||
const oldSession = oldSessionsMap.get(newSession.username)
|
||||
|
||||
|
||||
// 如果是新会话,直接返回
|
||||
if (!oldSession) {
|
||||
return newSession
|
||||
}
|
||||
|
||||
// 检查是否有实质性变化
|
||||
const hasChanges =
|
||||
const hasChanges =
|
||||
oldSession.summary !== newSession.summary ||
|
||||
oldSession.lastTimestamp !== newSession.lastTimestamp ||
|
||||
oldSession.unreadCount !== newSession.unreadCount ||
|
||||
@@ -817,43 +902,22 @@ function ChatPage(_props: ChatPageProps) {
|
||||
))}
|
||||
</div>
|
||||
) : filteredSessions.length > 0 ? (
|
||||
<div className="session-list">
|
||||
{filteredSessions.map(session => (
|
||||
<div
|
||||
key={session.username}
|
||||
className={`session-item ${currentSessionId === session.username ? 'active' : ''}`}
|
||||
onClick={() => handleSelectSession(session)}
|
||||
>
|
||||
<SessionAvatar session={session} size={48} />
|
||||
<div className="session-info">
|
||||
<div className="session-top">
|
||||
<span className="session-name">{session.displayName || session.username}</span>
|
||||
<span className="session-time">{formatSessionTime(session.lastTimestamp || session.sortTimestamp)}</span>
|
||||
</div>
|
||||
<div className="session-bottom">
|
||||
<span className="session-summary">
|
||||
{(() => {
|
||||
const summary = session.summary || '暂无消息'
|
||||
const firstLine = summary.split('\n')[0]
|
||||
const hasMoreLines = summary.includes('\n')
|
||||
return (
|
||||
<>
|
||||
<MessageContent content={firstLine} disableLinks={true} />
|
||||
{hasMoreLines && <span>...</span>}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
{session.unreadCount > 0 && (
|
||||
<span className="unread-badge">
|
||||
{session.unreadCount > 99 ? '99+' : session.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="session-list" style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
|
||||
{/* @ts-ignore - 类型定义不匹配但不影响运行 */}
|
||||
<List
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
rowCount={filteredSessions.length}
|
||||
rowHeight={72}
|
||||
rowProps={{
|
||||
sessions: filteredSessions,
|
||||
currentSessionId,
|
||||
onSelect: handleSelectSession,
|
||||
formatTime: formatSessionTime
|
||||
}}
|
||||
rowComponent={SessionRow}
|
||||
/>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<div className="empty-sessions">
|
||||
<MessageSquare />
|
||||
@@ -1357,10 +1421,10 @@ function ChatPage(_props: ChatPageProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// 前端表情包缓存
|
||||
const emojiDataUrlCache = new Map<string, string>()
|
||||
// 前端图片缓存
|
||||
const imageDataUrlCache = new Map<string, string>()
|
||||
// 前端表情包缓存 (LRU 限制)
|
||||
const emojiDataUrlCache = new LRUCache<string, string>(200)
|
||||
// 前端图片缓存 (LRU 限制)
|
||||
const imageDataUrlCache = new LRUCache<string, string>(50)
|
||||
|
||||
// 图片解密队列管理
|
||||
const imageDecryptQueue: Array<() => Promise<void>> = []
|
||||
@@ -1387,7 +1451,7 @@ function enqueueDecrypt(fn: () => Promise<void>) {
|
||||
}
|
||||
|
||||
// 视频信息缓存(带时间戳)
|
||||
const videoInfoCache = new Map<string, {
|
||||
const videoInfoCache = new Map<string, {
|
||||
videoUrl?: string
|
||||
coverUrl?: string
|
||||
thumbUrl?: string
|
||||
@@ -1652,12 +1716,12 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, h
|
||||
if (cached) {
|
||||
// 智能缓存失效:如果视频不存在,且缓存时间早于最后一次增量更新,则重新获取
|
||||
const shouldRefetch = !cached.exists && cached.cachedAt < lastIncrementalUpdateTime
|
||||
|
||||
|
||||
if (!shouldRefetch) {
|
||||
setVideoInfo(cached)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 需要重新获取,清除旧缓存
|
||||
videoInfoCache.delete(message.videoMd5)
|
||||
}
|
||||
@@ -1845,7 +1909,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, h
|
||||
|
||||
// 群聊中获取发送者信息
|
||||
const [isLoadingSender, setIsLoadingSender] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isGroupChat && !isSent && message.senderUsername) {
|
||||
setIsLoadingSender(true)
|
||||
@@ -1855,7 +1919,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, h
|
||||
setSenderName(result.displayName)
|
||||
}
|
||||
setIsLoadingSender(false)
|
||||
}).catch(() => {
|
||||
}).catch(() => {
|
||||
setIsLoadingSender(false)
|
||||
})
|
||||
}
|
||||
@@ -2436,26 +2500,122 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, h
|
||||
)
|
||||
}
|
||||
|
||||
// 文件消息 (type=6):渲染为文件卡片
|
||||
if (appMsgType === '6') {
|
||||
// 优先使用从接口获取的文件信息,否则从 XML 解析
|
||||
const fileName = message.fileName || title || '文件'
|
||||
const fileSize = message.fileSize
|
||||
const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || ''
|
||||
const fileMd5 = message.fileMd5
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number | undefined): string => {
|
||||
if (!bytes) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
// 根据扩展名选择图标
|
||||
const getFileIcon = (ext: string) => {
|
||||
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
|
||||
if (archiveExts.includes(ext)) {
|
||||
return <FileArchive size={28} />
|
||||
}
|
||||
return <FileText size={28} />
|
||||
}
|
||||
|
||||
// 点击文件消息,定位到文件所在文件夹并选中文件
|
||||
const handleFileClick = async () => {
|
||||
try {
|
||||
// 获取用户设置的微信原始存储目录(不是解密缓存目录)
|
||||
const wechatDir = await window.electronAPI.config.get('dbPath') as string
|
||||
if (!wechatDir) {
|
||||
console.error('未设置微信存储目录')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const userInfo = await window.electronAPI.chat.getMyUserInfo()
|
||||
if (!userInfo.success || !userInfo.userInfo) {
|
||||
console.error('无法获取用户信息')
|
||||
return
|
||||
}
|
||||
|
||||
const wxid = userInfo.userInfo.wxid
|
||||
|
||||
// 文件存储在 {微信存储目录}\{账号文件夹}\msg\file\{年-月}\ 目录下
|
||||
// 根据消息创建时间计算日期目录
|
||||
const msgDate = new Date(message.createTime * 1000)
|
||||
const year = msgDate.getFullYear()
|
||||
const month = String(msgDate.getMonth() + 1).padStart(2, '0')
|
||||
const dateFolder = `${year}-${month}`
|
||||
|
||||
// 构建完整文件路径(包括文件名)
|
||||
const filePath = `${wechatDir}\\${wxid}\\msg\\file\\${dateFolder}\\${fileName}`
|
||||
|
||||
// 使用 showItemInFolder 在文件管理器中定位并选中文件
|
||||
try {
|
||||
await window.electronAPI.shell.showItemInFolder(filePath)
|
||||
} catch (err) {
|
||||
// 如果文件不存在或路径错误,尝试只打开文件夹
|
||||
console.warn('无法定位到具体文件,尝试打开文件夹:', err)
|
||||
const fileDir = `${wechatDir}\\${wxid}\\msg\\file\\${dateFolder}`
|
||||
const result = await window.electronAPI.shell.openPath(fileDir)
|
||||
|
||||
// 如果还是失败,打开上级目录
|
||||
if (result) {
|
||||
console.warn('无法打开月份文件夹,尝试打开上级目录')
|
||||
const parentDir = `${wechatDir}\\${wxid}\\msg\\file`
|
||||
await window.electronAPI.shell.openPath(parentDir)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('打开文件夹失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="file-message"
|
||||
onClick={handleFileClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="点击定位到文件所在文件夹"
|
||||
>
|
||||
<div className="file-icon">
|
||||
{getFileIcon(fileExt)}
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title={fileName}>{fileName}</div>
|
||||
<div className="file-meta">
|
||||
{fileSize ? formatFileSize(fileSize) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 转账消息 (type=2000):渲染为转账卡片
|
||||
if (appMsgType === '2000') {
|
||||
try {
|
||||
const content = message.rawContent || message.parsedContent || ''
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(content, 'text/xml')
|
||||
|
||||
|
||||
const feedesc = doc.querySelector('feedesc')?.textContent || ''
|
||||
const payMemo = doc.querySelector('pay_memo')?.textContent || ''
|
||||
const paysubtype = doc.querySelector('paysubtype')?.textContent || '1'
|
||||
|
||||
|
||||
// paysubtype: 1=待收款, 3=已收款
|
||||
const isReceived = paysubtype === '3'
|
||||
|
||||
|
||||
return (
|
||||
<div className={`transfer-message ${isReceived ? 'received' : ''}`}>
|
||||
<div className="transfer-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="white" strokeWidth="2"/>
|
||||
<path d="M12 20h16M20 12l8 8-8 8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<circle cx="20" cy="20" r="18" stroke="white" strokeWidth="2" />
|
||||
<path d="M12 20h16M20 12l8 8-8 8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="transfer-info">
|
||||
|
||||
@@ -111,6 +111,17 @@ function DataManagementPage() {
|
||||
}
|
||||
|
||||
const handleDecryptAll = async () => {
|
||||
// 先检查是否配置了解密密钥
|
||||
const decryptKey = await window.electronAPI.config.get('decryptKey')
|
||||
if (!decryptKey) {
|
||||
showMessage('请先在设置页面配置解密密钥', false)
|
||||
// 3秒后自动跳转到设置页面
|
||||
setTimeout(() => {
|
||||
window.location.hash = '#/settings'
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查聊天窗口是否打开
|
||||
const isChatOpen = await window.electronAPI.window.isChatWindowOpen()
|
||||
if (isChatOpen) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface ChatState {
|
||||
setFilteredSessions: (sessions: ChatSession[]) => void
|
||||
setCurrentSession: (sessionId: string | null) => void
|
||||
setLoadingSessions: (loading: boolean) => void
|
||||
setMessages: (messages: Message[]) => void
|
||||
setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void
|
||||
appendMessages: (messages: Message[], prepend?: boolean) => void
|
||||
setLoadingMessages: (loading: boolean) => void
|
||||
setLoadingMore: (loading: boolean) => void
|
||||
@@ -82,24 +82,26 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
setLoadingSessions: (loading) => set({ isLoadingSessions: loading }),
|
||||
|
||||
setMessages: (messages) => set({ messages }),
|
||||
setMessages: (messages) => set((state) => ({
|
||||
messages: typeof messages === 'function' ? messages(state.messages) : messages
|
||||
})),
|
||||
|
||||
appendMessages: (newMessages, prepend = false) => set((state) => {
|
||||
// 使用与后端一致的多维 Key (serverId + localId + createTime + sortSeq) 进行去重
|
||||
const existingKeys = new Set(
|
||||
state.messages.map(m => `${m.serverId}-${m.localId}-${m.createTime}-${m.sortSeq}`)
|
||||
)
|
||||
|
||||
|
||||
// 过滤掉已存在的消息
|
||||
const uniqueNewMessages = newMessages.filter(
|
||||
msg => !existingKeys.has(`${msg.serverId}-${msg.localId}-${msg.createTime}-${msg.sortSeq}`)
|
||||
)
|
||||
|
||||
|
||||
// 如果没有新消息,直接返回原状态
|
||||
if (uniqueNewMessages.length === 0) {
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
messages: prepend
|
||||
? [...uniqueNewMessages, ...state.messages]
|
||||
|
||||
Vendored
+4
-1
@@ -46,6 +46,7 @@ export interface ElectronAPI {
|
||||
shell: {
|
||||
openPath: (path: string) => Promise<string>
|
||||
openExternal: (url: string) => Promise<void>
|
||||
showItemInFolder: (fullPath: string) => Promise<void>
|
||||
}
|
||||
app: {
|
||||
getDownloadsPath: () => Promise<string>
|
||||
@@ -202,6 +203,8 @@ export interface ElectronAPI {
|
||||
downloadEmoji: (cdnUrl: string, md5?: string, productId?: string, createTime?: number) => Promise<{ success: boolean; localPath?: string; error?: string }>
|
||||
close: () => Promise<boolean>
|
||||
refreshCache: () => Promise<boolean>
|
||||
setCurrentSession: (sessionId: string | null) => Promise<boolean>
|
||||
onNewMessages: (callback: (data: { sessionId: string; messages: Message[] }) => void) => () => void
|
||||
getSessionDetail: (sessionId: string) => Promise<{
|
||||
success: boolean
|
||||
detail?: {
|
||||
@@ -387,7 +390,7 @@ export interface ElectronAPI {
|
||||
successCount?: number
|
||||
error?: string
|
||||
}>
|
||||
onProgress: (callback: (data: {
|
||||
onProgress: (callback: (data: {
|
||||
current?: number
|
||||
total?: number
|
||||
currentSession?: string
|
||||
|
||||
@@ -55,6 +55,11 @@ export interface Message {
|
||||
videoMd5?: string
|
||||
rawContent?: string
|
||||
productId?: string
|
||||
// 文件消息相关
|
||||
fileName?: string // 文件名
|
||||
fileSize?: number // 文件大小(字节)
|
||||
fileExt?: string // 文件扩展名
|
||||
fileMd5?: string // 文件 MD5
|
||||
}
|
||||
|
||||
// 分析数据
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 简单的 LRU (Least Recently Used) 缓存实现
|
||||
* 用于限制内存中缓存对象的数量,防止内存泄漏
|
||||
*/
|
||||
export class LRUCache<K, V> {
|
||||
private capacity: number;
|
||||
private cache: Map<K, V>;
|
||||
|
||||
constructor(capacity: number) {
|
||||
this.capacity = capacity;
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
if (!this.cache.has(key)) return undefined;
|
||||
|
||||
// 刷新项目:先删除再添加,使其成为最新的(排在 Map 末尾)
|
||||
const value = this.cache.get(key)!;
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
if (this.cache.has(key)) {
|
||||
// 如果已存在,删除旧的以便重新添加到末尾
|
||||
this.cache.delete(key);
|
||||
} else if (this.cache.size >= this.capacity) {
|
||||
// 如果达到容量上限,删除第一个项目(最久未使用的)
|
||||
// Map.keys().next().value 获取的是插入顺序最早的那个
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user