diff --git a/.github/workflows/dev-daily-fixed.yml b/.github/workflows/dev-daily-fixed.yml index e867f46..cdd756f 100644 --- a/.github/workflows/dev-daily-fixed.yml +++ b/.github/workflows/dev-daily-fixed.yml @@ -12,6 +12,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" FIXED_DEV_TAG: nightly-dev + TARGET_BRANCH: dev ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/ jobs: @@ -23,6 +24,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -36,11 +38,10 @@ jobs: shell: bash run: | set -euo pipefail - BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")" YEAR_2="$(TZ=Asia/Shanghai date +%y)" MONTH="$(TZ=Asia/Shanghai date +%-m)" DAY="$(TZ=Asia/Shanghai date +%-d)" - DEV_VERSION="${BASE_VERSION}-dev.${YEAR_2}.${MONTH}.${DAY}" + DEV_VERSION="${YEAR_2}.${MONTH}.${DAY}" echo "dev_version=$DEV_VERSION" >> "$GITHUB_OUTPUT" echo "Dev version: $DEV_VERSION" @@ -63,6 +64,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -95,7 +97,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - mapfile -t assets < <(find release -maxdepth 1 -type f | sort) + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) if [ "${#assets[@]}" -eq 0 ]; then echo "No release files found in ./release" exit 1 @@ -109,6 +114,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -138,7 +144,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - mapfile -t assets < <(find release -maxdepth 1 -type f | sort) + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) if [ "${#assets[@]}" -eq 0 ]; then echo "No release files found in ./release" exit 1 @@ -152,6 +161,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -181,7 +191,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - mapfile -t assets < <(find release -maxdepth 1 -type f | sort) + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) if [ "${#assets[@]}" -eq 0 ]; then echo "No release files found in ./release" exit 1 @@ -195,6 +208,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -224,7 +238,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - mapfile -t assets < <(find release -maxdepth 1 -type f | sort) + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) if [ "${#assets[@]}" -eq 0 ]; then echo "No release files found in ./release" exit 1 diff --git a/.github/workflows/preview-nightly-main.yml b/.github/workflows/preview-nightly-main.yml index 9ff6a19..c67048d 100644 --- a/.github/workflows/preview-nightly-main.yml +++ b/.github/workflows/preview-nightly-main.yml @@ -11,6 +11,8 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + FIXED_PREVIEW_TAG: nightly-preview + TARGET_BRANCH: main ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/ jobs: @@ -23,6 +25,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -50,15 +53,34 @@ jobs: SHOULD_BUILD=false fi - BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")" YEAR_2="$(TZ=Asia/Shanghai date +%y)" - EXISTING_COUNT="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" --jq "[.[].tag_name | select(test(\"^v${BASE_VERSION}-preview[.]${YEAR_2}[.][0-9]+$\"))] | length")" - NEXT_COUNT=$((EXISTING_COUNT + 1)) - PREVIEW_VERSION="${BASE_VERSION}-preview.${YEAR_2}.${NEXT_COUNT}" + YEARLY_RUN_COUNT=1 + LAST_VERSION="$(gh release view "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" --json body --jq '.body' 2>/dev/null | grep -Eo '0\.[0-9]{2}\.[0-9]+' | head -n 1 || true)" + if [[ "$LAST_VERSION" =~ ^0\.([0-9]{2})\.([0-9]+)$ ]]; then + LAST_YEAR="${BASH_REMATCH[1]}" + LAST_COUNT="${BASH_REMATCH[2]}" + if [ "$LAST_YEAR" = "$YEAR_2" ]; then + YEARLY_RUN_COUNT=$((LAST_COUNT + 1)) + fi + fi + + PREVIEW_VERSION="0.${YEAR_2}.${YEARLY_RUN_COUNT}" echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT" echo "preview_version=$PREVIEW_VERSION" >> "$GITHUB_OUTPUT" - echo "Preview version: $PREVIEW_VERSION (commits in last 24h on main: $COMMITS_24H)" + echo "Preview version: $PREVIEW_VERSION (commits in last 24h on main: $COMMITS_24H, yearly count: $YEARLY_RUN_COUNT)" + + - name: Ensure fixed preview prerelease exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if gh release view "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release edit "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" --title "Preview Nightly Build" --prerelease + else + gh release create "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" --title "Preview Nightly Build" --notes "预览版发布页" --prerelease + fi preview-mac-arm64: needs: prepare @@ -68,6 +90,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -88,15 +111,29 @@ jobs: npx tsc npx vite build - - name: Package and Publish macOS arm64 preview + - name: Package macOS arm64 preview artifacts env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_IDENTITY_AUTO_DISCOVERY: "false" shell: bash run: | export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/" echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR" - npx electron-builder --mac dmg --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' + npx electron-builder --mac dmg --arm64 --publish never '--config.publish.channel=preview' '--config.artifactName=${productName}-preview-arm64.${ext}' + + - name: Upload macOS arm64 assets to fixed preview release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + gh release upload "$FIXED_PREVIEW_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber preview-linux: needs: prepare @@ -106,6 +143,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -126,11 +164,27 @@ jobs: npx tsc npx vite build - - name: Package and Publish Linux preview + - name: Package Linux preview artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --linux --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' + npx electron-builder --linux --publish never '--config.publish.channel=preview' '--config.artifactName=${productName}-preview-linux.${ext}' + + - name: Upload Linux assets to fixed preview release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + gh release upload "$FIXED_PREVIEW_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber preview-win-x64: needs: prepare @@ -140,6 +194,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -160,11 +215,27 @@ jobs: npx tsc npx vite build - - name: Package and Publish Windows x64 preview + - name: Package Windows x64 preview artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --win nsis --x64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' + npx electron-builder --win nsis --x64 --publish never '--config.publish.channel=preview' '--config.artifactName=${productName}-preview-x64-Setup.${ext}' + + - name: Upload Windows x64 assets to fixed preview release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + gh release upload "$FIXED_PREVIEW_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber preview-win-arm64: needs: prepare @@ -174,6 +245,7 @@ jobs: - name: Check out git repository uses: actions/checkout@v5 with: + ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 - name: Install Node.js @@ -194,11 +266,27 @@ jobs: npx tsc npx vite build - - name: Package and Publish Windows arm64 preview + - name: Package Windows arm64 preview artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --win nsis --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' + npx electron-builder --win nsis --arm64 --publish never '--config.publish.channel=preview-arm64' '--config.artifactName=${productName}-preview-arm64-Setup.${ext}' + + - name: Upload Windows arm64 assets to fixed preview release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + gh release upload "$FIXED_PREVIEW_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber update-preview-release-notes: needs: @@ -217,7 +305,8 @@ jobs: run: | set -euo pipefail - TAG="v${{ needs.prepare.outputs.preview_version }}" + TAG="$FIXED_PREVIEW_TAG" + CURRENT_PREVIEW_VERSION="${{ needs.prepare.outputs.preview_version }}" REPO="$GITHUB_REPOSITORY" RELEASE_PAGE="https://github.com/$REPO/releases/tag/$TAG" @@ -259,6 +348,7 @@ jobs: ## Preview Nightly 说明 - 该版本为 **预览版**,用于提前体验即将发布的功能与修复。 - 可能包含尚未完全稳定的改动,不建议长期使用 + - 当前版本号:\`$CURRENT_PREVIEW_VERSION\` ## 下载 - Windows x64: ${WINDOWS_URL:-$RELEASE_PAGE} diff --git a/electron/main.ts b/electron/main.ts index 355693a..980c972 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -30,7 +30,7 @@ import { cloudControlService } from './services/cloudControlService' import { destroyNotificationWindow, registerNotificationHandlers, showNotification } from './windows/notificationWindow' import { httpService } from './services/httpService' import { messagePushService } from './services/messagePushService' - +import { bizService } from './services/bizService' // 配置自动更新 autoUpdater.autoDownload = false @@ -38,18 +38,28 @@ autoUpdater.autoInstallOnAppQuit = true autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载 // 更新通道策略: // - 稳定版(如 4.3.0)默认走 latest -// - 预览版(如 4.3.0-preview.26.1)默认走 preview -// - 开发版(如 4.3.0-dev.26.3.4)默认走 dev +// - 预览版(如 0.26.2)默认走 preview(0.年.当年发布序号) +// - 开发版(如 26.4.5)默认走 dev(年.月.日) // - 用户可在设置页切换稳定/预览/开发,切换后即时生效 // 同时区分 Windows x64 / arm64,避免更新清单互相覆盖。 const appVersion = app.getVersion() +const inferUpdateTrackFromVersion = (version: string): 'stable' | 'preview' | 'dev' => { + const normalized = String(version || '').trim().replace(/^v/i, '') + if (/^0\.\d{2}\.\d+$/i.test(normalized)) return 'preview' + if (/^\d{2}\.\d{1,2}\.\d{1,2}$/i.test(normalized)) return 'dev' + // 兼容旧版命名(如 4.3.0-preview.26.1 / 4.3.0-dev.26.3.4) + if (/-preview\.\d+\.\d+$/i.test(normalized)) return 'preview' + if (/-dev\.\d+\.\d+\.\d+$/i.test(normalized)) return 'dev' + // 兼容 alpha/beta/rc 预发布 + if (/(alpha|beta|rc)/i.test(normalized)) return 'dev' + return 'stable' +} + const defaultUpdateTrack: 'stable' | 'preview' | 'dev' = (() => { - if (/-preview\.\d+\.\d+$/i.test(appVersion)) return 'preview' - if (/-dev\.\d+\.\d+\.\d+$/i.test(appVersion)) return 'dev' - if (/(alpha|beta|rc)/i.test(appVersion)) return 'dev' + const inferred = inferUpdateTrackFromVersion(appVersion) + if (inferred === 'preview' || inferred === 'dev') return inferred return 'stable' })() -const isPrereleaseBuild = defaultUpdateTrack !== 'stable' let configService: ConfigService | null = null const normalizeUpdateTrack = (raw: unknown): 'stable' | 'preview' | 'dev' | null => { @@ -62,16 +72,81 @@ const getEffectiveUpdateTrack = (): 'stable' | 'preview' | 'dev' => { return configuredTrack || defaultUpdateTrack } +const isRemoteVersionNewer = (latestVersion: string, currentVersion: string): boolean => { + const latest = String(latestVersion || '').trim() + const current = String(currentVersion || '').trim() + if (!latest || !current) return false + + const parseVersion = (version: string) => { + const normalized = version.replace(/^v/i, '') + const [main, pre = ''] = normalized.split('-', 2) + const core = main.split('.').map((segment) => Number.parseInt(segment, 10) || 0) + const prerelease = pre ? pre.split('.').map((segment) => /^\d+$/.test(segment) ? Number.parseInt(segment, 10) : segment) : [] + return { core, prerelease } + } + + const compareParsedVersion = (a: ReturnType, b: ReturnType): number => { + const maxLen = Math.max(a.core.length, b.core.length) + for (let i = 0; i < maxLen; i += 1) { + const left = a.core[i] || 0 + const right = b.core[i] || 0 + if (left > right) return 1 + if (left < right) return -1 + } + + const aPre = a.prerelease + const bPre = b.prerelease + if (aPre.length === 0 && bPre.length === 0) return 0 + if (aPre.length === 0) return 1 + if (bPre.length === 0) return -1 + + const preMaxLen = Math.max(aPre.length, bPre.length) + for (let i = 0; i < preMaxLen; i += 1) { + const left = aPre[i] + const right = bPre[i] + if (left === undefined) return -1 + if (right === undefined) return 1 + if (left === right) continue + + const leftNum = typeof left === 'number' + const rightNum = typeof right === 'number' + if (leftNum && rightNum) return left > right ? 1 : -1 + if (leftNum) return -1 + if (rightNum) return 1 + return String(left) > String(right) ? 1 : -1 + } + + return 0 + } + + try { + return autoUpdater.currentVersion.compare(latest) < 0 + } catch { + return compareParsedVersion(parseVersion(latest), parseVersion(current)) > 0 + } +} + +const shouldOfferUpdateForTrack = (latestVersion: string, currentVersion: string): boolean => { + if (isRemoteVersionNewer(latestVersion, currentVersion)) return true + const effectiveTrack = getEffectiveUpdateTrack() + const currentTrack = inferUpdateTrackFromVersion(currentVersion) + // 切换通道后,目标通道最新版本与当前版本不同即提示更新(即使是降级) + if (effectiveTrack !== currentTrack && latestVersion !== currentVersion) return true + return false +} + const applyAutoUpdateChannel = (reason: 'startup' | 'settings' = 'startup') => { const track = getEffectiveUpdateTrack() + const currentTrack = inferUpdateTrackFromVersion(appVersion) const baseUpdateChannel = track === 'stable' ? 'latest' : track autoUpdater.allowPrerelease = track !== 'stable' - autoUpdater.allowDowngrade = isPrereleaseBuild && track === 'stable' + // 只要用户当前选择的目标通道与当前安装版本所属通道不同,就允许跨通道更新(含降级) + autoUpdater.allowDowngrade = track !== currentTrack autoUpdater.channel = process.platform === 'win32' && process.arch === 'arm64' ? `${baseUpdateChannel}-arm64` : baseUpdateChannel - console.log(`[Update](${reason}) 当前版本 ${appVersion},渠道偏好: ${track},更新通道: ${autoUpdater.channel}`) + console.log(`[Update](${reason}) 当前版本 ${appVersion},当前轨道: ${currentTrack},渠道偏好: ${track},更新通道: ${autoUpdater.channel},allowDowngrade=${autoUpdater.allowDowngrade}`) } applyAutoUpdateChannel('startup') @@ -1152,6 +1227,7 @@ const removeMatchedEntriesInDir = async ( // 注册 IPC 处理器 function registerIpcHandlers() { registerNotificationHandlers() + bizService.registerHandlers() // 配置相关 ipcMain.handle('config:get', async (_, key: string) => { return configService?.get(key as any) @@ -1283,7 +1359,7 @@ function registerIpcHandlers() { if (result && result.updateInfo) { const currentVersion = app.getVersion() const latestVersion = result.updateInfo.version - if (latestVersion !== currentVersion) { + if (shouldOfferUpdateForTrack(latestVersion, currentVersion)) { return { hasUpdate: true, version: latestVersion, @@ -2741,7 +2817,7 @@ function checkForUpdatesOnStartup() { const latestVersion = result.updateInfo.version // 检查是否有新版本 - if (latestVersion !== currentVersion && mainWindow) { + if (shouldOfferUpdateForTrack(latestVersion, currentVersion) && mainWindow) { // 检查该版本是否被用户忽略 const ignoredVersion = configService?.get('ignoredUpdateVersion') if (ignoredVersion === latestVersion) { diff --git a/electron/preload.ts b/electron/preload.ts index bfa151d..38e722f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -413,6 +413,14 @@ contextBridge.exposeInMainWorld('electronAPI', { downloadEmoji: (params: { url: string; encryptUrl?: string; aesKey?: string }) => ipcRenderer.invoke('sns:downloadEmoji', params) }, + biz: { + listAccounts: (account?: string) => ipcRenderer.invoke('biz:listAccounts', account), + listMessages: (username: string, account?: string, limit?: number, offset?: number) => + ipcRenderer.invoke('biz:listMessages', username, account, limit, offset), + listPayRecords: (account?: string, limit?: number, offset?: number) => + ipcRenderer.invoke('biz:listPayRecords', account, limit, offset) + }, + // 数据收集 cloud: { diff --git a/electron/services/bizService.ts b/electron/services/bizService.ts new file mode 100644 index 0000000..110b22c --- /dev/null +++ b/electron/services/bizService.ts @@ -0,0 +1,221 @@ +import { join } from 'path' +import { readdirSync, existsSync } from 'fs' +import { wcdbService } from './wcdbService' +import { ConfigService } from './config' +import { chatService, Message } from './chatService' +import { ipcMain } from 'electron' +import { createHash } from 'crypto' + +export interface BizAccount { + username: string + name: string + avatar: string + type: number + last_time: number + formatted_last_time: string +} + +export interface BizMessage { + local_id: number + create_time: number + title: string + des: string + url: string + cover: string + content_list: any[] +} + +export interface BizPayRecord { + local_id: number + create_time: number + title: string + description: string + merchant_name: string + merchant_icon: string + timestamp: number + formatted_time: string +} + +export class BizService { + private configService: ConfigService + + constructor() { + this.configService = new ConfigService() + } + + private extractXmlValue(xml: string, tagName: string): string { + const regex = new RegExp(`<${tagName}>([\\s\\S]*?)`, 'i') + const match = regex.exec(xml) + if (match) { + return match[1].replace(//g, '').trim() + } + return '' + } + + private parseBizContentList(xmlStr: string): any[] { + if (!xmlStr) return [] + const contentList: any[] = [] + try { + const itemRegex = /([\s\S]*?)<\/item>/gi + let match: RegExpExecArray | null + while ((match = itemRegex.exec(xmlStr)) !== null) { + const itemXml = match[1] + const itemStruct = { + title: this.extractXmlValue(itemXml, 'title'), + url: this.extractXmlValue(itemXml, 'url'), + cover: this.extractXmlValue(itemXml, 'cover') || this.extractXmlValue(itemXml, 'thumburl'), + summary: this.extractXmlValue(itemXml, 'summary') || this.extractXmlValue(itemXml, 'digest') + } + if (itemStruct.title) contentList.push(itemStruct) + } + } catch (e) { } + return contentList + } + + private parsePayXml(xmlStr: string): any { + if (!xmlStr) return null + try { + const title = this.extractXmlValue(xmlStr, 'title') + const description = this.extractXmlValue(xmlStr, 'des') + const merchantName = this.extractXmlValue(xmlStr, 'display_name') || '微信支付' + const merchantIcon = this.extractXmlValue(xmlStr, 'icon_url') + const pubTime = parseInt(this.extractXmlValue(xmlStr, 'pub_time') || '0') + if (!title && !description) return null + return { title, description, merchant_name: merchantName, merchant_icon: merchantIcon, timestamp: pubTime } + } catch (e) { return null } + } + + async listAccounts(account?: string): Promise { + try { + const contactsResult = await chatService.getContacts({ lite: true }) + if (!contactsResult.success || !contactsResult.contacts) return [] + + const officialContacts = contactsResult.contacts.filter(c => c.type === 'official') + const usernames = officialContacts.map(c => c.username) + const enrichment = await chatService.enrichSessionsContactInfo(usernames) + const contactInfoMap = enrichment.success && enrichment.contacts ? enrichment.contacts : {} + + const root = this.configService.get('dbPath') + const myWxid = this.configService.get('myWxid') + const accountWxid = account || myWxid + if (!root || !accountWxid) return [] + + const dbDir = join(root, accountWxid, 'db_storage', 'message') + const bizLatestTime: Record = {} + + if (existsSync(dbDir)) { + const bizDbFiles = readdirSync(dbDir).filter(f => f.startsWith('biz_message') && f.endsWith('.db')) + for (const file of bizDbFiles) { + const dbPath = join(dbDir, file) + const name2idRes = await wcdbService.execQuery('message', dbPath, 'SELECT username FROM Name2Id') + if (name2idRes.success && name2idRes.rows) { + for (const row of name2idRes.rows) { + const uname = row.username || row.user_name + if (uname) { + const md5 = createHash('md5').update(uname).digest('hex').toLowerCase() + const tName = `Msg_${md5}` + const timeRes = await wcdbService.execQuery('message', dbPath, `SELECT MAX(create_time) as max_time FROM ${tName}`) + if (timeRes.success && timeRes.rows && timeRes.rows[0]?.max_time) { + const t = parseInt(timeRes.rows[0].max_time) + if (!isNaN(t)) bizLatestTime[uname] = Math.max(bizLatestTime[uname] || 0, t) + } + } + } + } + } + } + + const result: BizAccount[] = officialContacts.map(contact => { + const uname = contact.username + const info = contactInfoMap[uname] + const lastTime = bizLatestTime[uname] || 0 + return { + username: uname, + name: info?.displayName || contact.displayName || uname, + avatar: info?.avatarUrl || '', + type: 0, + last_time: lastTime, + formatted_last_time: lastTime ? new Date(lastTime * 1000).toISOString().split('T')[0] : '' + } + }) + + const contactDbPath = join(root, accountWxid, 'contact.db') + if (existsSync(contactDbPath)) { + const bizInfoRes = await wcdbService.execQuery('contact', contactDbPath, 'SELECT username, type FROM biz_info') + if (bizInfoRes.success && bizInfoRes.rows) { + const typeMap: Record = {} + for (const r of bizInfoRes.rows) typeMap[r.username] = r.type + for (const acc of result) if (typeMap[acc.username] !== undefined) acc.type = typeMap[acc.username] + } + } + + return result + .filter(acc => !acc.name.includes('朋友圈广告')) + .sort((a, b) => { + if (a.username === 'gh_3dfda90e39d6') return -1 + if (b.username === 'gh_3dfda90e39d6') return 1 + return b.last_time - a.last_time + }) + } catch (e) { return [] } + } + + async listMessages(username: string, account?: string, limit: number = 20, offset: number = 0): Promise { + try { + // 仅保留核心路径:利用 chatService 的自动路由能力 + const res = await chatService.getMessages(username, offset, limit) + if (!res.success || !res.messages) return [] + + return res.messages.map(msg => { + const bizMsg: BizMessage = { + local_id: msg.localId, + create_time: msg.createTime, + title: msg.linkTitle || msg.parsedContent || '', + des: msg.appMsgDesc || '', + url: msg.linkUrl || '', + cover: msg.linkThumb || msg.appMsgThumbUrl || '', + content_list: [] + } + if (msg.rawContent) { + bizMsg.content_list = this.parseBizContentList(msg.rawContent) + if (bizMsg.content_list.length > 0 && !bizMsg.title) { + bizMsg.title = bizMsg.content_list[0].title + bizMsg.cover = bizMsg.cover || bizMsg.content_list[0].cover + } + } + return bizMsg + }) + } catch (e) { return [] } + } + + async listPayRecords(account?: string, limit: number = 20, offset: number = 0): Promise { + const username = 'gh_3dfda90e39d6' + try { + const res = await chatService.getMessages(username, offset, limit) + if (!res.success || !res.messages) return [] + + const records: BizPayRecord[] = [] + for (const msg of res.messages) { + if (!msg.rawContent) continue + const parsedData = this.parsePayXml(msg.rawContent) + if (parsedData) { + records.push({ + local_id: msg.localId, + create_time: msg.createTime, + ...parsedData, + timestamp: parsedData.timestamp || msg.createTime, + formatted_time: new Date((parsedData.timestamp || msg.createTime) * 1000).toLocaleString() + }) + } + } + return records + } catch (e) { return [] } + } + + registerHandlers() { + ipcMain.handle('biz:listAccounts', (_, account) => this.listAccounts(account)) + ipcMain.handle('biz:listMessages', (_, username, account, limit, offset) => this.listMessages(username, account, limit, offset)) + ipcMain.handle('biz:listPayRecords', (_, account, limit, offset) => this.listPayRecords(account, limit, offset)) + } +} + +export const bizService = new BizService() diff --git a/package-lock.json b/package-lock.json index 0ae8f81..8fa56e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", - "electron-store": "^10.0.0", + "electron-store": "^11.0.2", "electron-updater": "^6.3.9", "exceljs": "^4.4.0", "ffmpeg-static": "^5.3.0", @@ -24,7 +24,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "react-virtuoso": "^4.18.1", "remark-gfm": "^4.0.1", "sherpa-onnx-node": "^1.10.38", @@ -38,7 +38,7 @@ "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.3.4", - "electron": "^39.2.7", + "electron": "^41.1.1", "electron-builder": "^26.8.1", "sass": "^1.98.0", "sharp": "^0.34.5", @@ -2948,13 +2948,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/plist": { @@ -4260,20 +4260,20 @@ } }, "node_modules/conf": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz", - "integrity": "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", "license": "MIT", "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", - "dot-prop": "^9.0.0", + "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", - "uint8array-extras": "^1.4.0" + "uint8array-extras": "^1.5.0" }, "engines": { "node": ">=20" @@ -4733,15 +4733,15 @@ } }, "node_modules/dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", "license": "MIT", "dependencies": { - "type-fest": "^4.18.2" + "type-fest": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4878,15 +4878,15 @@ } }, "node_modules/electron": { - "version": "39.8.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.6.tgz", - "integrity": "sha512-uWX6Jh5LmwL13VwOSKBjebI+ck+03GOwc8V2Sgbmr9pJVJ/cHfli/PkjXuRDr+hq+SLHQuT9mGHSIfScebApRA==", + "version": "41.1.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.1.1.tgz", + "integrity": "sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", + "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -5029,13 +5029,13 @@ } }, "node_modules/electron-store": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-10.1.0.tgz", - "integrity": "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-11.0.2.tgz", + "integrity": "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ==", "license": "MIT", "dependencies": { - "conf": "^14.0.0", - "type-fest": "^4.41.0" + "conf": "^15.0.2", + "type-fest": "^5.0.1" }, "engines": { "node": ">=20" @@ -8522,9 +8522,9 @@ } }, "node_modules/react-router": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", - "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -8544,12 +8544,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", - "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", "license": "MIT", "dependencies": { - "react-router": "7.13.2" + "react-router": "7.14.0" }, "engines": { "node": ">=20.0.0" @@ -9489,6 +9489,18 @@ "node": ">=8" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tar": { "version": "7.5.13", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", @@ -9713,12 +9725,15 @@ "license": "0BSD" }, "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9757,9 +9772,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 0f5d54e..28736e3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", - "electron-store": "^10.0.0", + "electron-store": "^11.0.2", "electron-updater": "^6.3.9", "exceljs": "^4.4.0", "ffmpeg-static": "^5.3.0", @@ -38,7 +38,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "react-virtuoso": "^4.18.1", "remark-gfm": "^4.0.1", "sherpa-onnx-node": "^1.10.38", @@ -52,7 +52,7 @@ "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.3.4", - "electron": "^39.2.7", + "electron": "^41.1.1", "electron-builder": "^26.8.1", "sass": "^1.98.0", "sharp": "^0.34.5", diff --git a/src/App.tsx b/src/App.tsx index d5f2512..4ad162e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import ExportPage from './pages/ExportPage' import VideoWindow from './pages/VideoWindow' import ImageWindow from './pages/ImageWindow' import SnsPage from './pages/SnsPage' +import BizPage from './pages/BizPage' import ContactsPage from './pages/ContactsPage' import ChatHistoryPage from './pages/ChatHistoryPage' import NotificationWindow from './pages/NotificationWindow' @@ -736,6 +737,7 @@ function App() {