From 3b7590d8ce5f89babd5314e8b75942f0397939f6 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Tue, 17 Feb 2026 01:59:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E5=8F=8D=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 ++-- package.json | 2 +- src/pages/AnalyticsPage.scss | 32 +++++++++++++++++++++++++++++++- src/pages/AnalyticsPage.tsx | 18 +++++++++++++++++- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7110b7..84cdacd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weflow", - "version": "1.5.4", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "weflow", - "version": "1.5.4", + "version": "2.0.1", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.5.0", diff --git a/package.json b/package.json index 282c41c..155eeb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.5.4", + "version": "2.0.1", "description": "WeFlow", "main": "dist-electron/main.js", "author": "cc", diff --git a/src/pages/AnalyticsPage.scss b/src/pages/AnalyticsPage.scss index c45c74e..81102df 100644 --- a/src/pages/AnalyticsPage.scss +++ b/src/pages/AnalyticsPage.scss @@ -482,13 +482,43 @@ margin-top: 16px; } + .exclude-footer-left { + display: flex; + align-items: center; + gap: 12px; + } + .exclude-count { font-size: 12px; color: var(--text-tertiary); } + .btn-text { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: none; + cursor: pointer; + font-size: 12px; + color: var(--text-secondary); + padding: 4px 8px; + border-radius: 6px; + transition: all 0.15s; + + &:hover { + color: var(--primary); + background: var(--primary-light); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + .exclude-actions { display: flex; gap: 8px; } -} +} \ No newline at end of file diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index 9e56515..df8a6c7 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -146,6 +146,17 @@ function AnalyticsPage() { }) } + const toggleInvertSelection = () => { + setDraftExcluded((prev) => { + const allUsernames = new Set(excludeCandidates.map(c => normalizeUsername(c.username))) + const inverted = new Set() + for (const u of allUsernames) { + if (!prev.has(u)) inverted.add(u) + } + return inverted + }) + } + const handleApplyExcluded = async () => { const payload = Array.from(draftExcluded) setIsExcludeDialogOpen(false) @@ -493,7 +504,12 @@ function AnalyticsPage() { )}
- 已排除 {draftExcluded.size} 人 +
+ 已排除 {draftExcluded.size} 人 + +
-
- - 用于朋友圈在线图片原生解密,优先使用这里配置的 DLL - { - const value = e.target.value - setWeixinDllPath(value) - scheduleConfigSave('weixinDllPath', () => configService.setWeixinDllPath(value)) - }} - /> -
- - -
-
+
diff --git a/src/services/config.ts b/src/services/config.ts index 684bdcf..089a78e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -12,7 +12,7 @@ export const CONFIG_KEYS = { LAST_SESSION: 'lastSession', WINDOW_BOUNDS: 'windowBounds', CACHE_PATH: 'cachePath', - WEIXIN_DLL_PATH: 'weixinDllPath', + EXPORT_PATH: 'exportPath', AGREEMENT_ACCEPTED: 'agreementAccepted', LOG_ENABLED: 'logEnabled', @@ -163,16 +163,7 @@ export async function setCachePath(path: string): Promise { } -// 获取 Weixin.dll 路径 -export async function getWeixinDllPath(): Promise { - const value = await config.get(CONFIG_KEYS.WEIXIN_DLL_PATH) - return value as string | null -} -// 设置 Weixin.dll 路径 -export async function setWeixinDllPath(path: string): Promise { - await config.set(CONFIG_KEYS.WEIXIN_DLL_PATH, path) -} // 获取导出路径 export async function getExportPath(): Promise { From 5f868d193c5cabf16bd564df1da5f294269d48fb Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Wed, 18 Feb 2026 13:49:56 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=87=BA=E6=9D=A5?= =?UTF-8?q?=E7=9A=84=E8=A1=A8=E6=83=85=E5=8C=85=E4=B9=9F=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/chatService.ts | 103 ++++++++++++++++++++--------- electron/services/exportService.ts | 45 ++++--------- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index afa4a4b..dc9c50b 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -50,6 +50,9 @@ export interface Message { emojiCdnUrl?: string emojiMd5?: string emojiLocalPath?: string // 本地缓存 castle 路径 + emojiThumbUrl?: string + emojiEncryptUrl?: string + emojiAesKey?: string // 引用消息相关 quotedContent?: string quotedSender?: string @@ -1151,6 +1154,9 @@ class ChatService { const emojiInfo = this.parseEmojiInfo(content) emojiCdnUrl = emojiInfo.cdnUrl emojiMd5 = emojiInfo.md5 + cdnThumbUrl = emojiInfo.thumbUrl // 复用 cdnThumbUrl 字段或使用 emojiThumbUrl + // 注意:Message 接口定义的 emojiThumbUrl,这里我们统一一下 + // 如果 Message 接口有 emojiThumbUrl,则使用它 } else if (localType === 3 && content) { const imageInfo = this.parseImageInfo(content) imageMd5 = imageInfo.md5 @@ -1373,7 +1379,7 @@ class ChatService { /** * 解析表情包信息 */ - private parseEmojiInfo(content: string): { cdnUrl?: string; md5?: string } { + private parseEmojiInfo(content: string): { cdnUrl?: string; md5?: string; thumbUrl?: string; encryptUrl?: string; aesKey?: string } { try { // 提取 cdnurl let cdnUrl: string | undefined @@ -1387,16 +1393,15 @@ class ChatService { } } - // 如果没有 cdnurl,尝试 thumburl - if (!cdnUrl) { - const thumbUrlMatch = /thumburl\s*=\s*['"]([^'"]+)['"]/i.exec(content) || /thumburl\s*=\s*([^'"]+?)(?=\s|\/|>)/i.exec(content) - if (thumbUrlMatch) { - cdnUrl = thumbUrlMatch[1].replace(/&/g, '&') - if (cdnUrl.includes('%')) { - try { - cdnUrl = decodeURIComponent(cdnUrl) - } catch { } - } + // 提取 thumburl + let thumbUrl: string | undefined + const thumbUrlMatch = /thumburl\s*=\s*['"]([^'"]+)['"]/i.exec(content) || /thumburl\s*=\s*([^'"]+?)(?=\s|\/|>)/i.exec(content) + if (thumbUrlMatch) { + thumbUrl = thumbUrlMatch[1].replace(/&/g, '&') + if (thumbUrl.includes('%')) { + try { + thumbUrl = decodeURIComponent(thumbUrl) + } catch { } } } @@ -1404,9 +1409,23 @@ class ChatService { const md5Match = /md5\s*=\s*['"]([a-fA-F0-9]+)['"]/i.exec(content) || /md5\s*=\s*([a-fA-F0-9]+)/i.exec(content) const md5 = md5Match ? md5Match[1] : undefined - // 不构造假 URL,只返回真正的 cdnurl - // 没有 cdnUrl 时保持静默,交由后续回退逻辑处理 - return { cdnUrl, md5 } + // 提取 encrypturl + let encryptUrl: string | undefined + const encryptUrlMatch = /encrypturl\s*=\s*['"]([^'"]+)['"]/i.exec(content) || /encrypturl\s*=\s*([^'"]+?)(?=\s|\/|>)/i.exec(content) + if (encryptUrlMatch) { + encryptUrl = encryptUrlMatch[1].replace(/&/g, '&') + if (encryptUrl.includes('%')) { + try { + encryptUrl = decodeURIComponent(encryptUrl) + } catch { } + } + } + + // 提取 aeskey + const aesKeyMatch = /aeskey\s*=\s*['"]([a-zA-Z0-9]+)['"]/i.exec(content) || /aeskey\s*=\s*([a-zA-Z0-9]+)/i.exec(content) + const aesKey = aesKeyMatch ? aesKeyMatch[1] : undefined + + return { cdnUrl, md5, thumbUrl, encryptUrl, aesKey } } catch (e) { console.error('[ChatService] 表情包解析失败:', e, { xml: content }) return {} @@ -2622,11 +2641,7 @@ class ChatService { // 检查内存缓存 const cached = emojiCache.get(cacheKey) if (cached && existsSync(cached)) { - // 读取文件并转为 data URL - const dataUrl = this.fileToDataUrl(cached) - if (dataUrl) { - return { success: true, localPath: dataUrl } - } + return { success: true, localPath: cached } } // 检查是否正在下载 @@ -2634,10 +2649,7 @@ class ChatService { if (downloading) { const result = await downloading if (result) { - const dataUrl = this.fileToDataUrl(result) - if (dataUrl) { - return { success: true, localPath: dataUrl } - } + return { success: true, localPath: result } } return { success: false, error: '下载失败' } } @@ -2654,10 +2666,7 @@ class ChatService { const filePath = join(cacheDir, `${cacheKey}${ext}`) if (existsSync(filePath)) { emojiCache.set(cacheKey, filePath) - const dataUrl = this.fileToDataUrl(filePath) - if (dataUrl) { - return { success: true, localPath: dataUrl } - } + return { success: true, localPath: filePath } } } @@ -2671,10 +2680,7 @@ class ChatService { if (localPath) { emojiCache.set(cacheKey, localPath) - const dataUrl = this.fileToDataUrl(localPath) - if (dataUrl) { - return { success: true, localPath: dataUrl } - } + return { success: true, localPath } } return { success: false, error: '下载失败' } } catch (e) { @@ -3917,6 +3923,13 @@ class ChatService { const imgInfo = this.parseImageInfo(rawContent) Object.assign(msg, imgInfo) msg.imageDatName = this.parseImageDatNameFromRow(row) + } else if (msg.localType === 47) { // Emoji + const emojiInfo = this.parseEmojiInfo(rawContent) + msg.emojiCdnUrl = emojiInfo.cdnUrl + msg.emojiMd5 = emojiInfo.md5 + msg.emojiThumbUrl = emojiInfo.thumbUrl + msg.emojiEncryptUrl = emojiInfo.encryptUrl + msg.emojiAesKey = emojiInfo.aesKey } return msg @@ -4227,6 +4240,34 @@ class ChatService { return { success: false, error: String(e) } } } + + + /** + * 下载表情包文件(用于导出,返回文件路径) + */ + async downloadEmojiFile(msg: Message): Promise { + if (!msg.emojiMd5) return null + let url = msg.emojiCdnUrl + + // 尝试获取 URL + if (!url && msg.emojiEncryptUrl) { + console.warn('[ChatService] Emoji has only encryptUrl:', msg.emojiMd5) + } + + if (!url) { + await this.fallbackEmoticon(msg) + url = msg.emojiCdnUrl + } + + if (!url) return null + + // Reuse existing downloadEmoji method + const result = await this.downloadEmoji(url, msg.emojiMd5) + if (result.success && result.localPath) { + return result.localPath + } + return null + } } export const chatService = new ChatService() diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 5814efd..ab0eb67 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -1479,49 +1479,30 @@ class ExportService { fs.mkdirSync(emojisDir, { recursive: true }) } - // 使用消息对象中已提取的字段 - const emojiUrl = msg.emojiCdnUrl - const emojiMd5 = msg.emojiMd5 - - if (!emojiUrl && !emojiMd5) { + // 使用 chatService 下载表情包 (利用其重试和 fallback 逻辑) + const localPath = await chatService.downloadEmojiFile(msg) + if (!localPath || !fs.existsSync(localPath)) { return null } - - - const key = emojiMd5 || String(msg.localId) - // 根据 URL 判断扩展名 - let ext = '.gif' - if (emojiUrl) { - if (emojiUrl.includes('.png')) ext = '.png' - else if (emojiUrl.includes('.jpg') || emojiUrl.includes('.jpeg')) ext = '.jpg' - } + // 确定目标文件名 + const ext = path.extname(localPath) || '.gif' + const key = msg.emojiMd5 || String(msg.localId) const fileName = `${key}${ext}` const destPath = path.join(emojisDir, fileName) - // 如果已存在则跳过 - if (fs.existsSync(destPath)) { - return { - relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName), - kind: 'emoji' - } + // 复制文件到导出目录 (如果不存在) + if (!fs.existsSync(destPath)) { + fs.copyFileSync(localPath, destPath) } - // 下载表情 - if (emojiUrl) { - const downloaded = await this.downloadFile(emojiUrl, destPath) - if (downloaded) { - return { - relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName), - kind: 'emoji' - } - } else { - } + return { + relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName), + kind: 'emoji' } - - return null } catch (e) { + console.error('ExportService: exportEmoji failed', e) return null } }