From 5ab0466a87e8fb381582fbd2c1d23ce1340619ac Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 21 Feb 2026 23:06:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=94=E7=B3=BB=E4=BA=BA=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AE=97=E6=B3=95=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=8E=B7=E5=8F=96=E6=9B=BE=E7=BB=8F=E7=9A=84?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=EF=BC=9B=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E4=BA=BA=E9=A1=B5=E9=9D=A2=E6=89=93=E5=BC=80?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E4=BC=9A=E8=AF=9D=EF=BC=9B=E6=9C=8B=E5=8F=8B?= =?UTF-8?q?=E5=9C=88=E9=A1=B5=E9=9D=A2=E4=BC=98=E5=8C=96=EF=BC=9B=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=A3=80=E6=B5=8B=E5=B9=B6=E6=A0=87=E8=AE=B0=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B7=B2=E5=88=A0=E9=99=A4=E7=9A=84=E6=9C=8B=E5=8F=8B?= =?UTF-8?q?=E5=9C=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 14 +++++++ electron/services/chatService.ts | 45 +++++--------------- src/components/Sns/SnsMediaGrid.tsx | 60 +++++++++++++++++++------- src/components/Sns/SnsPostItem.tsx | 65 +++++++++++++++++++++++------ src/pages/ChatPage.tsx | 29 +++++++++++-- src/pages/ContactsPage.scss | 11 ++--- src/pages/ContactsPage.tsx | 14 +++---- src/pages/SnsPage.scss | 47 +++++++++++++++++++-- 8 files changed, 203 insertions(+), 82 deletions(-) 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,让