diff --git a/electron/main.ts b/electron/main.ts index 81186a2..718579f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -173,6 +173,20 @@ function createWindow(options: { autoShow?: boolean } = {}) { } ) + // 忽略微信 CDN 域名的证书错误(部分节点证书配置不正确) + win.webContents.on('certificate-error', (event, url, _error, _cert, callback) => { + const trusted = ['.qq.com', '.qpic.cn', '.weixin.qq.com', '.wechat.com'] + try { + const host = new URL(url).hostname + if (trusted.some(d => host.endsWith(d))) { + event.preventDefault() + callback(true) + return + } + } catch {} + callback(false) + }) + return win } diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 81ea83a..c36d6ff 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -103,7 +103,7 @@ export interface ContactInfo { remark?: string nickname?: string avatarUrl?: string - type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' + type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } // 表情包缓存 @@ -603,7 +603,7 @@ class ChatService { // 使用execQuery直接查询加密的contact.db // kind='contact', path=null表示使用已打开的contact.db const contactQuery = ` - SELECT username, remark, nick_name, alias, local_type, flag + SELECT username, remark, nick_name, alias, local_type, flag, quan_pin FROM contact ` @@ -651,48 +651,23 @@ class ChatService { for (const row of rows) { const username = row.username || '' - // 过滤系统账号和特殊账号 - 完全复制cipher的逻辑 if (!username) continue - if (username === 'filehelper' || username === 'fmessage' || username === 'floatbottle' || - username === 'medianote' || username === 'newsapp' || username.startsWith('fake_') || - username === 'weixin' || username === 'qmessage' || username === 'qqmail' || - username === 'tmessage' || username.startsWith('wxid_') === false && - username.includes('@') === false && username.startsWith('gh_') === false && - /^[a-zA-Z0-9_-]+$/.test(username) === false) { - continue - } - - // 判断类型 - 正确规则:wxid开头且有alias的是好友 - let type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' = 'other' - const localType = row.local_type || 0 + const excludeNames = ['medianote', 'floatbottle', 'qmessage', 'qqmail', 'fmessage'] + let type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' = 'other' + const localType = this.getRowInt(row, ['local_type', 'localType', 'WCDB_CT_local_type'], 0) const flag = Number(row.flag ?? 0) + const quanPin = this.getRowField(row, ['quan_pin', 'quanPin', 'WCDB_CT_quan_pin']) || '' if (username.includes('@chatroom')) { type = 'group' } else if (username.startsWith('gh_')) { - if (flag === 0) continue type = 'official' - } else if (localType === 3 || localType === 4) { - if (flag === 0) continue - if (flag === 4) continue - type = 'official' - } else if (username.startsWith('wxid_') && row.alias) { - type = flag === 0 ? 'deleted_friend' : 'friend' - } else if (localType === 1) { - type = flag === 0 ? 'deleted_friend' : 'friend' - } else if (localType === 2) { - // local_type=2 是群成员但非好友,跳过 - continue - } else if (localType === 0) { - // local_type=0 可能是好友或其他,检查是否有备注或昵称 - if (row.remark || row.nick_name) { - type = flag === 0 ? 'deleted_friend' : 'friend' - } else { - continue - } + } else if (/^(?!.*(gh_|@chatroom)).*$/.test(username) && localType === 1 && !excludeNames.includes(username)) { + type = 'friend' + } else if (/^(?!.*(gh_|@chatroom)).*$/.test(username) && localType === 0 && quanPin) { + type = 'former_friend' } else { - // 其他未知类型,跳过 continue } diff --git a/src/components/Sns/SnsMediaGrid.tsx b/src/components/Sns/SnsMediaGrid.tsx index a9ce6a9..a2b832f 100644 --- a/src/components/Sns/SnsMediaGrid.tsx +++ b/src/components/Sns/SnsMediaGrid.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react' -import { Play, Lock, Download } from 'lucide-react' +import React, { useState, useRef } from 'react' +import { Play, Lock, Download, ImageOff } from 'lucide-react' import { LivePhotoIcon } from '../../components/LivePhotoIcon' import { RefreshCw } from 'lucide-react' @@ -22,6 +22,7 @@ interface SnsMedia { interface SnsMediaGridProps { mediaList: SnsMedia[] onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void + onMediaDeleted?: () => void } const isSnsVideoUrl = (url?: string): boolean => { @@ -79,9 +80,13 @@ const extractVideoFrame = async (videoPath: string): Promise => { }) } -const MediaItem = ({ media, onPreview }: { media: SnsMedia; onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void }) => { +const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void; onMediaDeleted?: () => void }) => { const [error, setError] = useState(false) + const [deleted, setDeleted] = useState(false) const [loading, setLoading] = useState(true) + const markDeleted = () => { setDeleted(true); onMediaDeleted?.() } + const retryCount = useRef(0) + const [retryKey, setRetryKey] = useState(0) const [thumbSrc, setThumbSrc] = useState('') const [videoPath, setVideoPath] = useState('') const [liveVideoPath, setLiveVideoPath] = useState('') @@ -92,6 +97,16 @@ const MediaItem = ({ media, onPreview }: { media: SnsMedia; onPreview: (src: str const isLive = !!media.livePhoto const targetUrl = media.thumb || media.url + // 视频重试:失败时重试最多2次,耗尽才标记删除 + const videoRetryOrDelete = () => { + if (retryCount.current < 2) { + retryCount.current++ + setRetryKey(k => k + 1) + } else { + markDeleted() + } + } + // Simple effect to load image/decrypt // Simple effect to load image/decrypt React.useEffect(() => { @@ -112,7 +127,7 @@ const MediaItem = ({ media, onPreview }: { media: SnsMedia; onPreview: (src: str if (result.dataUrl) setThumbSrc(result.dataUrl) else if (result.videoPath) setThumbSrc(`file://${result.videoPath.replace(/\\/g, '/')}`) } else { - setThumbSrc(targetUrl) + markDeleted() } // Pre-load live photo video if needed @@ -149,11 +164,11 @@ const MediaItem = ({ media, onPreview }: { media: SnsMedia; onPreview: (src: str if (!cancelled) setThumbSrc(coverDataUrl) } catch (err) { console.error('Frame extraction failed', err) - // Fallback to video path if extraction fails, though it might be black - // Only set thumbSrc if extraction fails, so we don't override the generated one + // 封面提取失败,用视频路径作为 fallback,让