From 2ad88df3c0f2085b6a764b49382a48269b19463e Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:50:42 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=B4?= =?UTF-8?q?=E5=83=8Fbase64=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 36 +++++- electron/services/exportService.ts | 173 ++++++++++++++++++++++++++++- src/pages/ExportPage.tsx | 19 +++- 3 files changed, 220 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fbead4..71b7605 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,8 @@ jobs: steps: - name: Check out git repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 @@ -30,7 +32,39 @@ jobs: npx tsc npx vite build + # --- 生成更新日志步骤 --- + - name: Build Changelog + id: build_changelog + uses: mikepenz/release-changelog-builder-action@v4 + with: + outputFile: "release-notes.md" + configurationJson: | + { + "categories": [ + { + "title": "## 🚀 Features", + "labels": ["feat", "feature"] + }, + { + "title": "## 🐛 Fixes", + "labels": ["fix", "bug"] + }, + { + "title": "## 🧰 Maintenance", + "labels": ["chore", "refactor", "docs", "perf"] + } + ], + "template": "# Release Notes\n\n{{CHANGELOG}}" + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # --- 打包并发布步骤 --- - name: Package and Publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx electron-builder --publish always \ No newline at end of file + run: > + npx electron-builder + --publish always + -c.releaseInfo.releaseNotesFile=release-notes.md + -c.publish.releaseType=release \ No newline at end of file diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 8a2613c..98f6363 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -1,5 +1,8 @@ import * as fs from 'fs' import * as path from 'path' +import * as http from 'http' +import * as https from 'https' +import { fileURLToPath } from 'url' import { ConfigService } from './config' import { wcdbService } from './wcdbService' @@ -298,9 +301,9 @@ class ExportService { sessionId: string, cleanedMyWxid: string, dateRange?: { start: number; end: number } | null - ): Promise<{ rows: any[]; memberSet: Map; firstTime: number | null; lastTime: number | null }> { + ): Promise<{ rows: any[]; memberSet: Map; firstTime: number | null; lastTime: number | null }> { const rows: any[] = [] - const memberSet = new Map() + const memberSet = new Map() let firstTime: number | null = null let lastTime: number | null = null @@ -336,8 +339,11 @@ class ExportService { const memberInfo = await this.getContactInfo(actualSender) if (!memberSet.has(actualSender)) { memberSet.set(actualSender, { - platformId: actualSender, - accountName: memberInfo.displayName + member: { + platformId: actualSender, + accountName: memberInfo.displayName + }, + avatarUrl: memberInfo.avatarUrl }) } @@ -361,6 +367,121 @@ class ExportService { return { rows, memberSet, firstTime, lastTime } } + private resolveAvatarFile(avatarUrl?: string): { data?: Buffer; sourcePath?: string; sourceUrl?: string; ext: string; mime?: string } | null { + if (!avatarUrl) return null + if (avatarUrl.startsWith('data:')) { + const match = /^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/i.exec(avatarUrl) + if (!match) return null + const mime = match[1].toLowerCase() + const data = Buffer.from(match[2], 'base64') + const ext = mime.includes('png') ? '.png' + : mime.includes('gif') ? '.gif' + : mime.includes('webp') ? '.webp' + : '.jpg' + return { data, ext, mime } + } + if (avatarUrl.startsWith('file://')) { + try { + const sourcePath = fileURLToPath(avatarUrl) + const ext = path.extname(sourcePath) || '.jpg' + return { sourcePath, ext } + } catch { + return null + } + } + if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { + const url = new URL(avatarUrl) + const ext = path.extname(url.pathname) || '.jpg' + return { sourceUrl: avatarUrl, ext } + } + const sourcePath = avatarUrl + const ext = path.extname(sourcePath) || '.jpg' + return { sourcePath, ext } + } + + private async downloadToBuffer(url: string, remainingRedirects = 2): Promise<{ data: Buffer; mime?: string } | null> { + const client = url.startsWith('https:') ? https : http + return new Promise((resolve) => { + const request = client.get(url, (res) => { + const status = res.statusCode || 0 + if (status >= 300 && status < 400 && res.headers.location && remainingRedirects > 0) { + res.resume() + const redirectedUrl = new URL(res.headers.location, url).href + this.downloadToBuffer(redirectedUrl, remainingRedirects - 1) + .then(resolve) + return + } + if (status < 200 || status >= 300) { + res.resume() + resolve(null) + return + } + const chunks: Buffer[] = [] + res.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) + res.on('end', () => { + const data = Buffer.concat(chunks) + const mime = typeof res.headers['content-type'] === 'string' ? res.headers['content-type'] : undefined + resolve({ data, mime }) + }) + }) + request.on('error', () => resolve(null)) + request.setTimeout(15000, () => { + request.destroy() + resolve(null) + }) + }) + } + + private async exportAvatars( + members: Array<{ username: string; avatarUrl?: string }> + ): Promise> { + const result = new Map() + if (members.length === 0) return result + + for (const member of members) { + const fileInfo = this.resolveAvatarFile(member.avatarUrl) + if (!fileInfo) continue + try { + let data: Buffer | null = null + let mime = fileInfo.mime + if (fileInfo.data) { + data = fileInfo.data + } else if (fileInfo.sourcePath && fs.existsSync(fileInfo.sourcePath)) { + data = await fs.promises.readFile(fileInfo.sourcePath) + } else if (fileInfo.sourceUrl) { + const downloaded = await this.downloadToBuffer(fileInfo.sourceUrl) + if (downloaded) { + data = downloaded.data + mime = downloaded.mime || mime + } + } + if (!data) continue + const finalMime = mime || this.inferImageMime(fileInfo.ext) + const base64 = data.toString('base64') + result.set(member.username, `data:${finalMime};base64,${base64}`) + } catch { + continue + } + } + + return result + } + + private inferImageMime(ext: string): string { + switch (ext.toLowerCase()) { + case '.png': + return 'image/png' + case '.gif': + return 'image/gif' + case '.webp': + return 'image/webp' + case '.bmp': + return 'image/bmp' + default: + return 'image/jpeg' + } + } + /** * 导出单个会话为 ChatLab 格式 */ @@ -399,7 +520,7 @@ class ExportService { }) const chatLabMessages: ChatLabMessage[] = allMessages.map((msg) => { - const memberInfo = collected.memberSet.get(msg.senderUsername) || { + const memberInfo = collected.memberSet.get(msg.senderUsername)?.member || { platformId: msg.senderUsername, accountName: msg.senderUsername } @@ -412,6 +533,23 @@ class ExportService { } }) + const avatarMap = options.exportAvatars + ? await this.exportAvatars( + [ + ...Array.from(collected.memberSet.entries()).map(([username, info]) => ({ + username, + avatarUrl: info.avatarUrl + })), + { username: sessionId, avatarUrl: sessionInfo.avatarUrl } + ] + ) + : new Map() + + const members = Array.from(collected.memberSet.values()).map((info) => { + const avatar = avatarMap.get(info.member.platformId) + return avatar ? { ...info.member, avatar } : info.member + }) + const chatLabExport: ChatLabExport = { chatlab: { version: '0.0.1', @@ -424,7 +562,7 @@ class ExportService { type: isGroup ? 'group' : 'private', ...(isGroup && { groupId: sessionId }) }, - members: Array.from(collected.memberSet.values()), + members, messages: chatLabMessages } @@ -538,6 +676,29 @@ class ExportService { messages: allMessages } + if (options.exportAvatars) { + const avatarMap = await this.exportAvatars( + [ + ...Array.from(collected.memberSet.entries()).map(([username, info]) => ({ + username, + avatarUrl: info.avatarUrl + })), + { username: sessionId, avatarUrl: sessionInfo.avatarUrl } + ] + ) + const avatars: Record = {} + for (const [username, relPath] of avatarMap.entries()) { + avatars[username] = relPath + } + if (Object.keys(avatars).length > 0) { + detailedExport.session = { + ...detailedExport.session, + avatar: avatars[sessionId] + } + ;(detailedExport as any).avatars = avatars + } + } + fs.writeFileSync(outputPath, JSON.stringify(detailedExport, null, 2), 'utf-8') onProgress?.({ diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index e39dd07..105d29c 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -15,6 +15,7 @@ interface ExportOptions { format: 'chatlab' | 'chatlab-jsonl' | 'json' | 'html' | 'txt' | 'excel' | 'sql' dateRange: { start: Date; end: Date } | null useAllTime: boolean + exportAvatars: boolean } interface ExportResult { @@ -41,7 +42,8 @@ function ExportPage() { start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), end: new Date() }, - useAllTime: true + useAllTime: true, + exportAvatars: true }) const loadSessions = useCallback(async () => { @@ -140,6 +142,7 @@ function ExportPage() { const sessionList = Array.from(selectedSessions) const exportOptions = { format: options.format, + exportAvatars: options.exportAvatars, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), end: Math.floor(options.dateRange.end.getTime() / 1000) @@ -289,6 +292,20 @@ function ExportPage() { +
+

导出头像

+
+ +
+
+

导出位置

From ea29e77a5756166cc2d7ec77155224afc072ba20 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:51:26 +0800 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16fa836..6aa76d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.0.0", + "version": "1.0.1", "description": "WeFlow - 微信聊天记录查看工具", "main": "dist-electron/main.js", "scripts": { From bb7730ff2fe48e70b37de4eb9676add84a7166e4 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:54:58 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20action=E9=85=8D=E7=BD=AE=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71b7605..0149390 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,8 +63,5 @@ jobs: - name: Package and Publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: > - npx electron-builder - --publish always - -c.releaseInfo.releaseNotesFile=release-notes.md - -c.publish.releaseType=release \ No newline at end of file + run: | + npx electron-builder --publish always "-c.releaseInfo.releaseNotesFile=release-notes.md" "-c.publish.releaseType=release" \ No newline at end of file From e27270f134af8e325eb38abe311d7e6750ddd123 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:00:22 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=B8=80=E4=BA=9B=E5=B0=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 23 ++++++----------------- package.json | 5 +++++ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0149390..282e40b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 @@ -32,7 +32,6 @@ jobs: npx tsc npx vite build - # --- 生成更新日志步骤 --- - name: Build Changelog id: build_changelog uses: mikepenz/release-changelog-builder-action@v4 @@ -41,27 +40,17 @@ jobs: configurationJson: | { "categories": [ - { - "title": "## 🚀 Features", - "labels": ["feat", "feature"] - }, - { - "title": "## 🐛 Fixes", - "labels": ["fix", "bug"] - }, - { - "title": "## 🧰 Maintenance", - "labels": ["chore", "refactor", "docs", "perf"] - } + { "title": "## 🚀 Features", "labels": ["feat", "feature"] }, + { "title": "## 🐛 Fixes", "labels": ["fix", "bug"] }, + { "title": "## 🧰 Maintenance", "labels": ["chore", "refactor", "docs", "perf"] } ], "template": "# Release Notes\n\n{{CHANGELOG}}" } env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # --- 打包并发布步骤 --- - name: Package and Publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - npx electron-builder --publish always "-c.releaseInfo.releaseNotesFile=release-notes.md" "-c.publish.releaseType=release" \ No newline at end of file + + run: npx electron-builder --publish always "-c.releaseInfo.releaseNotesFile=release-notes.md" \ No newline at end of file diff --git a/package.json b/package.json index 6aa76d7..7b598cb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.1", "description": "WeFlow - 微信聊天记录查看工具", "main": "dist-electron/main.js", + "author": "cc", "scripts": { "dev": "vite", "build": "tsc && vite build && electron-builder", @@ -46,6 +47,10 @@ }, "build": { "appId": "com.WeFlow.app", + "publish": { + "provider": "github", + "releaseType": "release" + }, "productName": "WeFlow", "artifactName": "${productName}-${version}-Setup.${ext}", "directories": { From f321c465d5dee548b6165ad16435ab676f780a2f Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:10:34 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E5=87=8F=E5=B0=91=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=8C=85=E4=BD=93=E7=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 282e40b..7391e59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,12 +39,24 @@ jobs: outputFile: "release-notes.md" configurationJson: | { + "template": "# v${{ github.ref_name }} 版本发布\n\n{{CHANGELOG}}\n\n---\n> 此更新由系统自动构建", "categories": [ - { "title": "## 🚀 Features", "labels": ["feat", "feature"] }, - { "title": "## 🐛 Fixes", "labels": ["fix", "bug"] }, - { "title": "## 🧰 Maintenance", "labels": ["chore", "refactor", "docs", "perf"] } + { + "title": "## 新功能", + "filter": { "pattern": "^feat:.*", "flags": "i" } + }, + { + "title": "## 修复", + "filter": { "pattern": "^fix:.*", "flags": "i" } + }, + { + "title": "## 性能与维护", + "filter": { "pattern": "^(chore|docs|perf|refactor):.*", "flags": "i" } + } ], - "template": "# Release Notes\n\n{{CHANGELOG}}" + "ignore_labels": true, + "commitMode": true, + "empty_summary": "## 更新详情\n- 常规代码优化与维护" } env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -52,5 +64,5 @@ jobs: - name: Package and Publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: npx electron-builder --publish always "-c.releaseInfo.releaseNotesFile=release-notes.md" \ No newline at end of file + run: | + npx electron-builder --publish always "-c.releaseInfo.releaseNotesFile=release-notes.md" \ No newline at end of file From f864189407a20b9a492e82b188a3fcd9f1854a23 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:11:01 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96action=E6=89=93?= =?UTF-8?q?=E5=8C=85=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b598cb..f47737c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.0.1", + "version": "1.0.2", "description": "WeFlow - 微信聊天记录查看工具", "main": "dist-electron/main.js", "author": "cc", @@ -64,6 +64,7 @@ }, "nsis": { "oneClick": false, + "differentialPackage":false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "unicode": true,