mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-03 15:08:25 +00:00
29
.github/workflows/dev-daily-fixed.yml
vendored
29
.github/workflows/dev-daily-fixed.yml
vendored
@@ -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
|
||||
|
||||
120
.github/workflows/preview-nightly-main.yml
vendored
120
.github/workflows/preview-nightly-main.yml
vendored
@@ -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}
|
||||
|
||||
@@ -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<typeof parseVersion>, b: ReturnType<typeof parseVersion>): 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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
221
electron/services/bizService.ts
Normal file
221
electron/services/bizService.ts
Normal file
@@ -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]*?)</${tagName}>`, 'i')
|
||||
const match = regex.exec(xml)
|
||||
if (match) {
|
||||
return match[1].replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
private parseBizContentList(xmlStr: string): any[] {
|
||||
if (!xmlStr) return []
|
||||
const contentList: any[] = []
|
||||
try {
|
||||
const itemRegex = /<item>([\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<BizAccount[]> {
|
||||
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<string, number> = {}
|
||||
|
||||
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<string, number> = {}
|
||||
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<BizMessage[]> {
|
||||
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<BizPayRecord[]> {
|
||||
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()
|
||||
95
package-lock.json
generated
95
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<Route path="/export" element={<div className="export-route-anchor" aria-hidden="true" />} />
|
||||
<Route path="/sns" element={<SnsPage />} />
|
||||
<Route path="/biz" element={<BizPage />} />
|
||||
<Route path="/contacts" element={<ContactsPage />} />
|
||||
<Route path="/chat-history/:sessionId/:messageId" element={<ChatHistoryPage />} />
|
||||
<Route path="/chat-history-inline/:payloadId" element={<ChatHistoryPage />} />
|
||||
|
||||
345
src/pages/BizPage.scss
Normal file
345
src/pages/BizPage.scss
Normal file
@@ -0,0 +1,345 @@
|
||||
.biz-account-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: var(--bg-secondary); // 对齐会话列表背景
|
||||
|
||||
.biz-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.biz-account-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--primary-light) !important;
|
||||
border-left: 3px solid var(--primary);
|
||||
padding-left: 13px; // 补偿 border-left
|
||||
}
|
||||
|
||||
&.pay-account {
|
||||
background-color: var(--bg-primary);
|
||||
&.active {
|
||||
background-color: var(--primary-light) !important;
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.biz-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px; // 对齐会话列表头像圆角
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.biz-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.biz-info-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.biz-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.biz-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.biz-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
margin-top: 2px;
|
||||
|
||||
&.type-service { color: #07c160; background: rgba(7, 193, 96, 0.1); }
|
||||
&.type-sub { color: var(--primary); background: var(--primary-light); }
|
||||
&.type-enterprise { color: #f5222d; background: rgba(245, 34, 45, 0.1); }
|
||||
&.type-unknown { color: var(--text-tertiary); background: var(--bg-tertiary); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.biz-main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-secondary); // 对齐聊天页背景
|
||||
|
||||
.main-header {
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--card-bg);
|
||||
flex-shrink: 0;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.message-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 16px;
|
||||
background: var(--chat-pattern);
|
||||
background-color: var(--bg-tertiary); // 对齐聊天背景色
|
||||
|
||||
.messages-wrapper {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 占位状态:对齐 Chat 页面风格
|
||||
.biz-no-record-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
background: var(--bg-tertiary);
|
||||
|
||||
.no-record-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.5;
|
||||
|
||||
svg { width: 32px; height: 32px; }
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 280px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.biz-loading-more {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.pay-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
|
||||
.pay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.pay-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.pay-icon-placeholder {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-title {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.pay-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.pay-footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.article-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
|
||||
.main-article {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.article-cover {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.article-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||
|
||||
.article-title {
|
||||
color: white;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-digest {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sub-articles {
|
||||
.sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { background-color: var(--bg-hover); }
|
||||
|
||||
.sub-title {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: var(--text-primary);
|
||||
padding-right: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.biz-empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
background: var(--bg-tertiary); // 对齐 Chat 页面空白背景
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-tertiary);
|
||||
|
||||
svg { width: 40px; height: 40px; }
|
||||
}
|
||||
|
||||
p { color: var(--text-tertiary); font-size: 14px; }
|
||||
}
|
||||
261
src/pages/BizPage.tsx
Normal file
261
src/pages/BizPage.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { useThemeStore } from '../stores/themeStore';
|
||||
import { Newspaper, MessageSquareOff } from 'lucide-react';
|
||||
import './BizPage.scss';
|
||||
|
||||
export interface BizAccount {
|
||||
username: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
type: number;
|
||||
last_time: number;
|
||||
formatted_last_time: string;
|
||||
}
|
||||
|
||||
export const BizAccountList: React.FC<{
|
||||
onSelect: (account: BizAccount) => void;
|
||||
selectedUsername?: string;
|
||||
searchKeyword?: string;
|
||||
}> = ({ onSelect, selectedUsername, searchKeyword }) => {
|
||||
const [accounts, setAccounts] = useState<BizAccount[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [myWxid, setMyWxid] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const initWxid = async () => {
|
||||
try {
|
||||
const wxid = await window.electronAPI.config.get('myWxid');
|
||||
if (wxid) {
|
||||
setMyWxid(wxid as string);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("获取 myWxid 失败:", e);
|
||||
}
|
||||
};
|
||||
initWxid().then(_r => { });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
if (!myWxid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await window.electronAPI.biz.listAccounts(myWxid)
|
||||
setAccounts(res || []);
|
||||
} catch (err) {
|
||||
console.error('获取服务号列表失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetch().then(_r => { } );
|
||||
}, [myWxid]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!searchKeyword) return accounts;
|
||||
const q = searchKeyword.toLowerCase();
|
||||
return accounts.filter(a =>
|
||||
(a.name && a.name.toLowerCase().includes(q)) ||
|
||||
(a.username && a.username.toLowerCase().includes(q))
|
||||
);
|
||||
}, [accounts, searchKeyword]);
|
||||
|
||||
if (loading) return <div className="biz-loading">加载中...</div>;
|
||||
|
||||
return (
|
||||
<div className="biz-account-list">
|
||||
{filtered.map(item => (
|
||||
<div
|
||||
key={item.username}
|
||||
onClick={() => onSelect(item)}
|
||||
className={`biz-account-item ${selectedUsername === item.username ? 'active' : ''} ${item.username === 'gh_3dfda90e39d6' ? 'pay-account' : ''}`}
|
||||
>
|
||||
<img
|
||||
src={item.avatar}
|
||||
className="biz-avatar"
|
||||
alt=""
|
||||
/>
|
||||
<div className="biz-info">
|
||||
<div className="biz-info-top">
|
||||
<span className="biz-name">{item.name || item.username}</span>
|
||||
<span className="biz-time">{item.formatted_last_time}</span>
|
||||
</div>
|
||||
{item.username === 'gh_3dfda90e39d6' && (
|
||||
<div className="biz-badge type-service">服务号</div>
|
||||
)}
|
||||
|
||||
{/* 我看了下没有接口获取相关type,如果exec没法用的话确实无能为力,后面再适配吧 */}
|
||||
{/*<div className={`biz-badge ${*/}
|
||||
{/* item.type === 1 ? 'type-service' :*/}
|
||||
{/* item.type === 0 ? 'type-sub' :*/}
|
||||
{/* item.type === 2 ? 'type-enterprise' : 'type-unknown'*/}
|
||||
{/*}`}>*/}
|
||||
{/* {item.type === 1 ? '服务号' : item.type === 0 ? '订阅号' : item.type === 2 ? '企业号' : '未知'}*/}
|
||||
{/*</div>*/}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 公众号消息区域组件
|
||||
export const BizMessageArea: React.FC<{
|
||||
account: BizAccount | null;
|
||||
}> = ({ account }) => {
|
||||
const themeMode = useThemeStore((state) => state.themeMode);
|
||||
const [messages, setMessages] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const limit = 20;
|
||||
const messageListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [myWxid, setMyWxid] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const initWxid = async () => {
|
||||
try {
|
||||
const wxid = await window.electronAPI.config.get('myWxid');
|
||||
if (wxid) {
|
||||
setMyWxid(wxid as string);
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
initWxid();
|
||||
}, []);
|
||||
|
||||
const isDark = useMemo(() => {
|
||||
if (themeMode === 'dark') return true;
|
||||
if (themeMode === 'system') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return false;
|
||||
}, [themeMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (account && myWxid) {
|
||||
setMessages([]);
|
||||
setOffset(0);
|
||||
setHasMore(true);
|
||||
loadMessages(account.username, 0);
|
||||
}
|
||||
}, [account, myWxid]);
|
||||
|
||||
const loadMessages = async (username: string, currentOffset: number) => {
|
||||
if (loading || !myWxid) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
let res;
|
||||
if (username === 'gh_3dfda90e39d6') {
|
||||
res = await window.electronAPI.biz.listPayRecords(myWxid, limit, currentOffset);
|
||||
} else {
|
||||
res = await window.electronAPI.biz.listMessages(username, myWxid, limit, currentOffset);
|
||||
}
|
||||
if (res) {
|
||||
if (res.length < limit) setHasMore(false);
|
||||
setMessages(prev => currentOffset === 0 ? res : [...prev, ...res]);
|
||||
setOffset(currentOffset + limit);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载消息失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.currentTarget;
|
||||
if (target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight < 50) {
|
||||
if (!loading && hasMore && account) {
|
||||
loadMessages(account.username, offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className="biz-empty-state">
|
||||
<div className="empty-icon"><Newspaper size={40} /></div>
|
||||
<p>请选择一个服务号查看消息</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMTgwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjE4MCIgZmlsbD0iI2Y1ZjVmNSIvPjwvc3ZnPg==';
|
||||
|
||||
return (
|
||||
<div className={`biz-main ${isDark ? 'dark' : ''}`}>
|
||||
<div className="main-header">
|
||||
<h2>{account.name}</h2>
|
||||
</div>
|
||||
<div className="message-container" onScroll={handleScroll} ref={messageListRef}>
|
||||
<div className="messages-wrapper">
|
||||
{!loading && messages.length === 0 && (
|
||||
<div className="biz-no-record-container">
|
||||
<div className="no-record-icon">
|
||||
<MessageSquareOff size={48} />
|
||||
</div>
|
||||
<h3>暂无本地记录</h3>
|
||||
<p>该公众号在当前数据库中没有可显示的聊天历史</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.local_id}>
|
||||
{account.username === 'gh_3dfda90e39d6' ? (
|
||||
<div className="pay-card">
|
||||
<div className="pay-header">
|
||||
{msg.merchant_icon ? <img src={msg.merchant_icon} className="pay-icon" alt=""/> : <div className="pay-icon-placeholder">¥</div>}
|
||||
<span>{msg.merchant_name || '微信支付'}</span>
|
||||
</div>
|
||||
<div className="pay-title">{msg.title}</div>
|
||||
<div className="pay-desc">{msg.description}</div>
|
||||
<div className="pay-footer">{msg.formatted_time}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="article-card">
|
||||
<div onClick={() => window.electronAPI.shell.openExternal(msg.url)} className="main-article">
|
||||
<img src={msg.cover || defaultImage} className="article-cover" alt=""/>
|
||||
<div className="article-overlay"><h3 className="article-title">{msg.title}</h3></div>
|
||||
</div>
|
||||
{msg.des && <div className="article-digest">{msg.des}</div>}
|
||||
{msg.content_list && msg.content_list.length > 1 && (
|
||||
<div className="sub-articles">
|
||||
{msg.content_list.slice(1).map((item: any, idx: number) => (
|
||||
<div key={idx} onClick={() => window.electronAPI.shell.openExternal(item.url)} className="sub-item">
|
||||
<span className="sub-title">{item.title}</span>
|
||||
{item.cover && <img src={item.cover} className="sub-cover" alt=""/>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="biz-loading-more">加载中...</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BizPage: React.FC = () => {
|
||||
const [selectedAccount, setSelectedAccount] = useState<BizAccount | null>(null);
|
||||
return (
|
||||
<div className="biz-page">
|
||||
<div className="biz-sidebar">
|
||||
<BizAccountList onSelect={setSelectedAccount} selectedUsername={selectedAccount?.username} />
|
||||
</div>
|
||||
<BizMessageArea account={selectedAccount} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BizPage;
|
||||
@@ -4487,6 +4487,32 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 公众号入口样式
|
||||
.session-item.biz-entry {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg, rgba(0,0,0,0.05));
|
||||
}
|
||||
|
||||
.biz-entry-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
// 消息信息弹窗
|
||||
.message-info-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture } from 'lucide-react'
|
||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture, Newspaper } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||
@@ -16,6 +16,7 @@ import JumpToDatePopover from '../components/JumpToDatePopover'
|
||||
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
|
||||
import { type ContactSnsTimelineTarget, isSingleContactSession } from '../components/Sns/contactSnsTimeline'
|
||||
import * as configService from '../services/config'
|
||||
import BizPage, { BizAccountList, BizMessageArea, BizAccount } from './BizPage'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
isBackgroundTaskCancelRequested,
|
||||
@@ -36,6 +37,8 @@ const SYSTEM_MESSAGE_TYPES = [
|
||||
266287972401, // 拍一拍
|
||||
]
|
||||
|
||||
const OFFICIAL_ACCOUNTS_VIRTUAL_ID = 'official_accounts_virtual'
|
||||
|
||||
interface PendingInSessionSearchPayload {
|
||||
sessionId: string
|
||||
keyword: string
|
||||
@@ -983,6 +986,7 @@ const SessionItem = React.memo(function SessionItem({
|
||||
)
|
||||
|
||||
const isFoldEntry = session.username.toLowerCase().includes('placeholder_foldgroup')
|
||||
const isBizEntry = session.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID
|
||||
|
||||
// 折叠入口:专属名称和图标
|
||||
if (isFoldEntry) {
|
||||
@@ -1007,6 +1011,29 @@ const SessionItem = React.memo(function SessionItem({
|
||||
)
|
||||
}
|
||||
|
||||
// 公众号入口:专属名称和图标
|
||||
if (isBizEntry) {
|
||||
return (
|
||||
<div
|
||||
className={`session-item biz-entry ${isActive ? 'active' : ''}`}
|
||||
onClick={() => onSelect(session)}
|
||||
>
|
||||
<div className="biz-entry-avatar">
|
||||
<Newspaper size={22} />
|
||||
</div>
|
||||
<div className="session-info">
|
||||
<div className="session-top">
|
||||
<span className="session-name">订阅号/服务号</span>
|
||||
<span className="session-time">{timeText}</span>
|
||||
</div>
|
||||
<div className="session-bottom">
|
||||
<span className="session-summary">{session.summary || '查看公众号历史消息'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 根据匹配字段显示不同的 summary
|
||||
const summaryContent = useMemo(() => {
|
||||
if (session.matchedField === 'wxid') {
|
||||
@@ -1204,6 +1231,8 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
|
||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
||||
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
|
||||
const [bizView, setBizView] = useState(false) // 是否在"公众号"视图
|
||||
const [selectedBizAccount, setSelectedBizAccount] = useState<BizAccount | null>(null)
|
||||
const [hasInitialMessages, setHasInitialMessages] = useState(false)
|
||||
const [isSessionSwitching, setIsSessionSwitching] = useState(false)
|
||||
const [noMessageTable, setNoMessageTable] = useState(false)
|
||||
@@ -2691,6 +2720,9 @@ function ChatPage(props: ChatPageProps) {
|
||||
setConnected(false)
|
||||
setConnecting(false)
|
||||
setHasMoreMessages(true)
|
||||
setFoldedView(false)
|
||||
setBizView(false)
|
||||
setSelectedBizAccount(null)
|
||||
setHasMoreLater(false)
|
||||
const scope = await resolveChatCacheScope()
|
||||
hydrateSessionListCache(scope)
|
||||
@@ -3964,6 +3996,12 @@ function ChatPage(props: ChatPageProps) {
|
||||
setFoldedView(true)
|
||||
return
|
||||
}
|
||||
// 点击公众号入口,切换到公众号视图
|
||||
if (session.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID) {
|
||||
setBizView(true)
|
||||
setSelectedBizAccount(null) // 切入时默认不选中任何公众号
|
||||
return
|
||||
}
|
||||
selectSessionById(session.username)
|
||||
}
|
||||
|
||||
@@ -4946,14 +4984,30 @@ function ChatPage(props: ChatPageProps) {
|
||||
const foldedGroups = sessions.filter(s => s.isFolded && !s.username.toLowerCase().includes('placeholder_foldgroup'))
|
||||
const hasFoldedGroups = foldedGroups.length > 0
|
||||
|
||||
const visible = sessions.filter(s => {
|
||||
let visible = sessions.filter(s => {
|
||||
if (s.isFolded && !s.username.toLowerCase().includes('placeholder_foldgroup')) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// 如果有折叠的群聊,但列表中没有入口,则插入入口
|
||||
const bizEntry: ChatSession = {
|
||||
username: OFFICIAL_ACCOUNTS_VIRTUAL_ID,
|
||||
displayName: '公众号',
|
||||
summary: '查看公众号历史消息',
|
||||
type: 0,
|
||||
sortTimestamp: 9999999999, // 放到最前面? 目前还没有严格的对时间进行排序, 后面可以改一下
|
||||
lastTimestamp: 0,
|
||||
lastMsgType: 0,
|
||||
unreadCount: 0,
|
||||
isMuted: false,
|
||||
isFolded: false
|
||||
}
|
||||
|
||||
if (!visible.some(s => s.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID)) {
|
||||
visible.unshift(bizEntry)
|
||||
}
|
||||
|
||||
if (hasFoldedGroups && !visible.some(s => s.username.toLowerCase().includes('placeholder_foldgroup'))) {
|
||||
// 找到最新的折叠消息
|
||||
|
||||
const latestFolded = foldedGroups.reduce((latest, current) => {
|
||||
const latestTime = latest.sortTimestamp || latest.lastTimestamp
|
||||
const currentTime = current.sortTimestamp || current.lastTimestamp
|
||||
@@ -6031,7 +6085,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
ref={sidebarRef}
|
||||
style={{ width: sidebarWidth, minWidth: sidebarWidth, maxWidth: sidebarWidth }}
|
||||
>
|
||||
<div className={`session-header session-header-viewport ${foldedView ? 'folded' : ''}`}>
|
||||
<div className={`session-header session-header-viewport ${foldedView || bizView ? 'folded' : ''}`}>
|
||||
{/* 普通 header */}
|
||||
<div className="session-header-panel main-header">
|
||||
<div className="search-row">
|
||||
@@ -6061,12 +6115,18 @@ function ChatPage(props: ChatPageProps) {
|
||||
{/* 折叠群 header */}
|
||||
<div className="session-header-panel folded-header">
|
||||
<div className="folded-view-header">
|
||||
<button className="icon-btn back-btn" onClick={() => setFoldedView(false)}>
|
||||
<button className="icon-btn back-btn" onClick={() => {
|
||||
setFoldedView(false)
|
||||
setBizView(false)
|
||||
}}>
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
<span className="folded-view-title">
|
||||
<Users size={14} />
|
||||
折叠的群聊
|
||||
{foldedView ? (
|
||||
<><Users size={14} /> 折叠的群聊</>
|
||||
) : bizView ? (
|
||||
<><Newspaper size={14} /> 订阅号/服务号</>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6173,7 +6233,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`session-list-viewport ${foldedView ? 'folded' : ''}`}>
|
||||
<div className={`session-list-viewport ${foldedView || bizView ? 'folded' : ''}`}>
|
||||
{/* 普通会话列表 */}
|
||||
<div className="session-list-panel main-panel">
|
||||
{Array.isArray(filteredSessions) && filteredSessions.length > 0 ? (
|
||||
@@ -6199,7 +6259,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
<SessionItem
|
||||
key={session.username}
|
||||
session={session}
|
||||
isActive={currentSessionId === session.username}
|
||||
isActive={currentSessionId === session.username || (bizView && session.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID)}
|
||||
onSelect={handleSelectSession}
|
||||
formatTime={formatSessionTime}
|
||||
searchKeyword={searchKeyword}
|
||||
@@ -6218,13 +6278,14 @@ function ChatPage(props: ChatPageProps) {
|
||||
|
||||
{/* 折叠群列表 */}
|
||||
<div className="session-list-panel folded-panel">
|
||||
{foldedSessions.length > 0 ? (
|
||||
{foldedView && (
|
||||
foldedSessions.length > 0 ? (
|
||||
<div className="session-list">
|
||||
{foldedSessions.map(session => (
|
||||
<SessionItem
|
||||
key={session.username}
|
||||
session={session}
|
||||
isActive={currentSessionId === session.username}
|
||||
isActive={currentSessionId === session.username || (bizView && session.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID)}
|
||||
onSelect={handleSelectSession}
|
||||
formatTime={formatSessionTime}
|
||||
searchKeyword={searchKeyword}
|
||||
@@ -6236,6 +6297,17 @@ function ChatPage(props: ChatPageProps) {
|
||||
<Users size={32} />
|
||||
<p>没有折叠的群聊</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{bizView && (
|
||||
<div style={{ height: '100%', overflowY: 'auto' }}>
|
||||
<BizAccountList
|
||||
onSelect={setSelectedBizAccount}
|
||||
selectedUsername={selectedBizAccount?.username}
|
||||
searchKeyword={searchKeyword}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -6247,7 +6319,9 @@ function ChatPage(props: ChatPageProps) {
|
||||
|
||||
{/* 右侧消息区域 */}
|
||||
<div className="message-area">
|
||||
{currentSession ? (
|
||||
{bizView ? (
|
||||
<BizMessageArea account={selectedBizAccount} />
|
||||
) : currentSession ? (
|
||||
<>
|
||||
<div className="message-header">
|
||||
<Avatar
|
||||
|
||||
@@ -392,9 +392,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
setUpdateChannel(savedUpdateChannel)
|
||||
} else {
|
||||
const currentVersion = await window.electronAPI.app.getVersion()
|
||||
if (/-preview\.\d+\.\d+$/i.test(currentVersion)) {
|
||||
if (/^0\.\d{2}\.\d+$/i.test(currentVersion) || /-preview\.\d+\.\d+$/i.test(currentVersion)) {
|
||||
setUpdateChannel('preview')
|
||||
} else if (/-dev\.\d+\.\d+\.\d+$/i.test(currentVersion) || /(alpha|beta|rc)/i.test(currentVersion)) {
|
||||
} else if (/^\d{2}\.\d{1,2}\.\d{1,2}$/i.test(currentVersion) || /-dev\.\d+\.\d+\.\d+$/i.test(currentVersion) || /(alpha|beta|rc)/i.test(currentVersion)) {
|
||||
setUpdateChannel('dev')
|
||||
} else {
|
||||
setUpdateChannel('stable')
|
||||
|
||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
@@ -326,6 +326,11 @@ export interface ElectronAPI {
|
||||
getMessage: (sessionId: string, localId: number) => Promise<{ success: boolean; message?: Message; error?: string }>
|
||||
onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => () => void
|
||||
}
|
||||
biz: {
|
||||
listAccounts: (account?: string) => Promise<any[]>
|
||||
listMessages: (username: string, account?: string, limit?: number, offset?: number) => Promise<any[]>
|
||||
listPayRecords: (account?: string, limit?: number, offset?: number) => Promise<any[]>
|
||||
}
|
||||
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||
|
||||
@@ -18,38 +18,6 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 900,
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) return
|
||||
|
||||
if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('/react-router')) {
|
||||
return 'vendor-react'
|
||||
}
|
||||
|
||||
if (id.includes('/echarts') || id.includes('/echarts-for-react')) {
|
||||
return 'vendor-echarts'
|
||||
}
|
||||
|
||||
if (
|
||||
id.includes('/react-markdown') ||
|
||||
id.includes('/remark-gfm') ||
|
||||
id.includes('/mdast-') ||
|
||||
id.includes('/micromark-') ||
|
||||
id.includes('/unified') ||
|
||||
id.includes('/vfile')
|
||||
) {
|
||||
return 'vendor-markdown'
|
||||
}
|
||||
|
||||
if (id.includes('/jszip') || id.includes('/exceljs')) {
|
||||
return 'vendor-export'
|
||||
}
|
||||
|
||||
return 'vendor-misc'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
@@ -204,6 +172,7 @@ export default defineConfig({
|
||||
renderer()
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom'],
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user