mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-27 15:07:55 +00:00
Compare commits
6 Commits
dependabot
...
v4.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8e0160d5c | ||
|
|
ac40a81901 | ||
|
|
ca38a68a75 | ||
|
|
64be2dd562 | ||
|
|
ea2abb6c72 | ||
|
|
011e2ff37a |
139
.github/workflows/release.yml
vendored
139
.github/workflows/release.yml
vendored
@@ -12,8 +12,27 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark release as pre-release (building)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="$GITHUB_REF_NAME"
|
||||
REPO="$GITHUB_REPOSITORY"
|
||||
# Create or update the release as a pre-release with a placeholder note
|
||||
if gh release view "$TAG" --repo "$REPO" > /dev/null 2>&1; then
|
||||
gh release edit "$TAG" --repo "$REPO" --prerelease --notes $'## ⚠️ 正在自动构建中,请勿下载\n\n各平台安装包正在构建,完成后将自动更新本页面并正式发布。\n\n**请勿在此期间下载任何文件。**'
|
||||
else
|
||||
gh release create "$TAG" --repo "$REPO" --prerelease --title "$TAG" --notes $'## ⚠️ 正在自动构建中,请勿下载\n\n各平台安装包正在构建,完成后将自动更新本页面并正式发布。\n\n**请勿在此期间下载任何文件。**'
|
||||
fi
|
||||
|
||||
release-mac-arm64:
|
||||
runs-on: macos-14
|
||||
needs: prepare-release
|
||||
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
@@ -49,24 +68,9 @@ jobs:
|
||||
run: |
|
||||
npx electron-builder --mac dmg --arm64 --publish always
|
||||
|
||||
- name: Inject minimumVersion into latest yml
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
REPO=${{ github.repository }}
|
||||
MINIMUM_VERSION="4.1.7"
|
||||
for YML_FILE in latest-mac.yml latest-arm64-mac.yml; do
|
||||
gh release download "$TAG" --repo "$REPO" --pattern "$YML_FILE" --output "/tmp/$YML_FILE" 2>/dev/null || continue
|
||||
if ! grep -q 'minimumVersion' "/tmp/$YML_FILE"; then
|
||||
echo "minimumVersion: $MINIMUM_VERSION" >> "/tmp/$YML_FILE"
|
||||
fi
|
||||
gh release upload "$TAG" --repo "$REPO" "/tmp/$YML_FILE" --clobber
|
||||
done
|
||||
|
||||
release-linux:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-release
|
||||
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
@@ -101,22 +105,9 @@ jobs:
|
||||
run: |
|
||||
npx electron-builder --linux --publish always
|
||||
|
||||
- name: Inject minimumVersion into latest yml
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
REPO=${{ github.repository }}
|
||||
MINIMUM_VERSION="4.1.7"
|
||||
gh release download "$TAG" --repo "$REPO" --pattern "latest-linux.yml" --output "/tmp/latest-linux.yml" 2>/dev/null
|
||||
if [ -f /tmp/latest-linux.yml ] && ! grep -q 'minimumVersion' /tmp/latest-linux.yml; then
|
||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-linux.yml
|
||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest-linux.yml --clobber
|
||||
fi
|
||||
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
needs: prepare-release
|
||||
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
@@ -146,27 +137,15 @@ jobs:
|
||||
npx vite build
|
||||
|
||||
- name: Package and Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npx electron-builder --win nsis --x64 --publish always '--config.artifactName=${productName}-${version}-x64-Setup.${ext}'
|
||||
|
||||
- name: Inject minimumVersion into latest yml
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
REPO=${{ github.repository }}
|
||||
MINIMUM_VERSION="4.1.7"
|
||||
gh release download "$TAG" --repo "$REPO" --pattern "latest.yml" --output "/tmp/latest.yml" 2>/dev/null
|
||||
if [ -f /tmp/latest.yml ] && ! grep -q 'minimumVersion' /tmp/latest.yml; then
|
||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest.yml
|
||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
|
||||
fi
|
||||
npx electron-builder --win nsis --x64 --publish always "-c.artifactName=\${productName}-\${version}-x64-Setup.\${ext}"
|
||||
|
||||
release-windows-arm64:
|
||||
runs-on: windows-latest
|
||||
needs: prepare-release
|
||||
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
@@ -196,24 +175,11 @@ jobs:
|
||||
npx vite build
|
||||
|
||||
- name: Package and Publish Windows arm64
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npx electron-builder --win nsis --arm64 --publish always '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
|
||||
|
||||
- name: Inject minimumVersion into latest yml
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
REPO=${{ github.repository }}
|
||||
MINIMUM_VERSION="4.1.7"
|
||||
gh release download "$TAG" --repo "$REPO" --pattern "latest-arm64.yml" --output "/tmp/latest-arm64.yml" 2>/dev/null
|
||||
if [ -f /tmp/latest-arm64.yml ] && ! grep -q 'minimumVersion' /tmp/latest-arm64.yml; then
|
||||
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-arm64.yml
|
||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest-arm64.yml --clobber
|
||||
fi
|
||||
npx electron-builder --win nsis --arm64 --publish always -c.publish.channel=latest-arm64 "-c.artifactName=\${productName}-\${version}-arm64-Setup.\${ext}"
|
||||
|
||||
update-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -224,6 +190,53 @@ jobs:
|
||||
- release-windows-arm64
|
||||
|
||||
steps:
|
||||
- name: Fix latest.yml to point to x64 installer
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="$GITHUB_REF_NAME"
|
||||
VERSION="${TAG#v}"
|
||||
REPO="$GITHUB_REPOSITORY"
|
||||
|
||||
# Find the x64 exe asset name
|
||||
ASSETS_JSON="$(gh release view "$TAG" --repo "$REPO" --json assets)"
|
||||
X64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("x64.*\\.exe$"))][0] // ""')"
|
||||
if [ -z "$X64_ASSET" ]; then
|
||||
X64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("\\.exe$")) | select(test("arm64") | not)][0] // ""')"
|
||||
fi
|
||||
|
||||
if [ -z "$X64_ASSET" ]; then
|
||||
echo "ERROR: Could not find x64 exe asset"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading x64 installer: $X64_ASSET"
|
||||
gh release download "$TAG" --repo "$REPO" --pattern "$X64_ASSET" --dir /tmp/weflow-x64
|
||||
|
||||
SHA512_B64="$(sha512sum "/tmp/weflow-x64/$X64_ASSET" | awk '{print $1}' | xxd -r -p | base64 -w 0)"
|
||||
SIZE="$(stat -c%s "/tmp/weflow-x64/$X64_ASSET")"
|
||||
RELEASE_DATE="$(gh release view "$TAG" --repo "$REPO" --json publishedAt -q .publishedAt)"
|
||||
|
||||
cat > /tmp/latest.yml <<YMLEOF
|
||||
version: $VERSION
|
||||
files:
|
||||
- url: $X64_ASSET
|
||||
sha512: $SHA512_B64
|
||||
size: $SIZE
|
||||
path: $X64_ASSET
|
||||
sha512: $SHA512_B64
|
||||
releaseDate: '$RELEASE_DATE'
|
||||
YMLEOF
|
||||
|
||||
# Strip leading spaces (heredoc indentation)
|
||||
sed -i 's/^ //' /tmp/latest.yml
|
||||
cat /tmp/latest.yml
|
||||
|
||||
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
|
||||
echo "latest.yml updated successfully to point to $X64_ASSET"
|
||||
|
||||
- name: Generate release notes with platform download links
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -281,10 +294,18 @@ jobs:
|
||||
## macOS 安装提示(未知来源)
|
||||
- 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。
|
||||
- 如果仍被系统拦截,请在终端执行以下命令去除隔离标记:
|
||||
- `xattr -dr com.apple.quarantine "/Applications/WeFlow.app"`
|
||||
- xattr -rd com.apple.quarantine /Applications/WeFlow.app
|
||||
- 执行后重新打开 WeFlow。
|
||||
|
||||
> 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE
|
||||
EOF
|
||||
|
||||
gh release edit "$TAG" --repo "$REPO" --notes-file release_notes.md
|
||||
|
||||
- name: Mark release as published (no longer pre-release)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --latest --no-fail-on-no-release --draft=false --prerelease=false
|
||||
|
||||
95
.github/workflows/security-scan.yml
vendored
95
.github/workflows/security-scan.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 每天 UTC 02:00(北京时间 10:00)
|
||||
workflow_dispatch: # 支持手动触发
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
name: Security Scan (${{ matrix.branch }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch:
|
||||
- main
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ matrix.branch }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
# 1. npm audit - 检查依赖漏洞
|
||||
- name: Dependency vulnerability audit
|
||||
run: pnpm audit --audit-level=moderate
|
||||
continue-on-error: true
|
||||
|
||||
# 2. CodeQL 静态分析
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript, typescript
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:javascript-typescript/branch:${{ matrix.branch }}'
|
||||
|
||||
# 3. 密钥/敏感信息扫描
|
||||
- name: Secret scanning with Gitleaks
|
||||
uses: gitleaks/gitleaks-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
# 动态获取所有分支并扫描(排除已在 matrix 中的)
|
||||
scan-all-branches:
|
||||
name: Scan additional branches
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: List all branches
|
||||
id: branches
|
||||
run: |
|
||||
git branch -r | grep -v HEAD | sed 's|origin/||' | tr -d ' ' | while read branch; do
|
||||
echo "Branch: $branch"
|
||||
done
|
||||
|
||||
- name: Run pnpm audit on all branches
|
||||
run: |
|
||||
git branch -r | grep -v HEAD | sed 's|origin/||' | tr -d ' ' | while read branch; do
|
||||
echo "===== Auditing branch: $branch ====="
|
||||
git checkout "$branch" 2>/dev/null || continue
|
||||
pnpm install --frozen-lockfile --silent 2>/dev/null || npm install --silent 2>/dev/null || true
|
||||
pnpm audit --audit-level=moderate 2>/dev/null || true
|
||||
done
|
||||
continue-on-error: true
|
||||
@@ -1,23 +0,0 @@
|
||||
title = "Gitleaks Config"
|
||||
|
||||
[extend]
|
||||
# 继承默认规则
|
||||
useDefault = true
|
||||
|
||||
# 排除误报路径
|
||||
[[rules]]
|
||||
id = "curl-auth-header"
|
||||
[rules.allowlist]
|
||||
paths = [
|
||||
'''docs/HTTP-API\.md'''
|
||||
]
|
||||
regexes = [
|
||||
'''YOUR_TOKEN'''
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "generic-api-key"
|
||||
[rules.allowlist]
|
||||
paths = [
|
||||
'''src/pages/ChatPage\.tsx'''
|
||||
]
|
||||
@@ -146,27 +146,67 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => {
|
||||
|
||||
if (!merged.trim()) return ''
|
||||
|
||||
const shouldStripReleaseSection = (headingRaw: string): boolean => {
|
||||
const heading = headingRaw.trim().toLowerCase()
|
||||
if (heading === '下载' || heading === 'download') return true
|
||||
const normalizeHeadingText = (raw: string): string => {
|
||||
return raw
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, '\'')
|
||||
.replace(/'/gi, '\'')
|
||||
.toLowerCase()
|
||||
.replace(/[::]/g, '')
|
||||
.replace(/\s+/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
const compactHeading = heading.replace(/\s+/g, '')
|
||||
if (compactHeading.startsWith('macos安装提示')) return true
|
||||
if (compactHeading.startsWith('mac安装提示')) return true
|
||||
const shouldStripReleaseSection = (headingRaw: string): boolean => {
|
||||
const heading = normalizeHeadingText(headingRaw)
|
||||
if (!heading) return false
|
||||
if (heading.startsWith('下载') || heading.startsWith('download')) return true
|
||||
|
||||
if ((heading.includes('macos') || heading.startsWith('mac')) && heading.includes('安装提示')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// 兼容 electron-updater 直接返回 HTML 的场景
|
||||
// 兼容 electron-updater 直接返回 HTML 的场景(含 dir/anchor 等标签嵌套)
|
||||
const removeDownloadSectionFromHtml = (input: string): string => {
|
||||
return input
|
||||
.replace(
|
||||
/<h[1-6][^>]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/<h[1-6][^>]*>\s*(?:mac\s*os|mac)\s*安装提示(?:\s*[((]\s*未知来源\s*[))])?\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
|
||||
''
|
||||
)
|
||||
const headingPattern = /<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi
|
||||
const headings: Array<{ start: number; end: number; headingText: string }> = []
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = headingPattern.exec(input)) !== null) {
|
||||
const full = match[0]
|
||||
headings.push({
|
||||
start: match.index,
|
||||
end: match.index + full.length,
|
||||
headingText: match[2] || ''
|
||||
})
|
||||
}
|
||||
|
||||
if (headings.length === 0) return input
|
||||
|
||||
const rangesToRemove: Array<{ start: number; end: number }> = []
|
||||
for (let i = 0; i < headings.length; i += 1) {
|
||||
const current = headings[i]
|
||||
if (!shouldStripReleaseSection(current.headingText)) continue
|
||||
|
||||
const nextStart = i + 1 < headings.length ? headings[i + 1].start : input.length
|
||||
rangesToRemove.push({ start: current.start, end: nextStart })
|
||||
}
|
||||
|
||||
if (rangesToRemove.length === 0) return input
|
||||
|
||||
let output = ''
|
||||
let cursor = 0
|
||||
for (const range of rangesToRemove) {
|
||||
output += input.slice(cursor, range.start)
|
||||
cursor = range.end
|
||||
}
|
||||
output += input.slice(cursor)
|
||||
return output
|
||||
}
|
||||
|
||||
// 兼容 Markdown 场景(Action 最终 release note 模板)
|
||||
@@ -195,6 +235,8 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => {
|
||||
}
|
||||
|
||||
const cleaned = removeDownloadSectionFromMarkdown(removeDownloadSectionFromHtml(merged))
|
||||
// 兜底:即使没有匹配到标题,也不在弹窗展示 macOS 隔离标记清理命令
|
||||
.replace(/^[ \t>*-]*`?\s*xattr\s+-[a-z]*d[a-z]*\s+com\.apple\.quarantine[^\n]*`?\s*$/gim, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
@@ -1200,8 +1242,7 @@ function registerIpcHandlers() {
|
||||
return {
|
||||
hasUpdate: true,
|
||||
version: latestVersion,
|
||||
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes),
|
||||
minimumVersion: (result.updateInfo as any).minimumVersion
|
||||
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2662,8 +2703,7 @@ function checkForUpdatesOnStartup() {
|
||||
// 通知渲染进程有新版本
|
||||
mainWindow.webContents.send('app:updateAvailable', {
|
||||
version: latestVersion,
|
||||
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes),
|
||||
minimumVersion: (result.updateInfo as any).minimumVersion
|
||||
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,27 +93,39 @@ export class DbPathService {
|
||||
const possiblePaths: string[] = []
|
||||
const home = homedir()
|
||||
|
||||
// macOS 微信路径(固定)
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS 微信 4.0.5+ 新路径(优先检测)
|
||||
const appSupportBase = join(home, 'Library', 'Containers', 'com.tencent.xinWeChat', 'Data', 'Library', 'Application Support', 'com.tencent.xinWeChat')
|
||||
if (existsSync(appSupportBase)) {
|
||||
try {
|
||||
const entries = readdirSync(appSupportBase)
|
||||
for (const entry of entries) {
|
||||
// 匹配形如 2.0b4.0.9 的版本目录
|
||||
if (/^\d+\.\d+b\d+\.\d+/.test(entry) || /^\d+\.\d+\.\d+/.test(entry)) {
|
||||
possiblePaths.push(join(appSupportBase, entry))
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
// macOS 旧路径兜底
|
||||
possiblePaths.push(join(home, 'Library', 'Containers', 'com.tencent.xinWeChat', 'Data', 'Documents', 'xwechat_files'))
|
||||
} else {
|
||||
// Windows 微信4.x 数据目录
|
||||
possiblePaths.push(join(home, 'Documents', 'xwechat_files'))
|
||||
}
|
||||
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
if (existsSync(path)) {
|
||||
const rootName = path.split(/[/\\]/).pop()?.toLowerCase()
|
||||
if (rootName !== 'xwechat_files' && rootName !== 'wechat files') {
|
||||
continue
|
||||
}
|
||||
if (!existsSync(path)) continue
|
||||
|
||||
// 检查是否有有效的账号目录
|
||||
const accounts = this.findAccountDirs(path)
|
||||
if (accounts.length > 0) {
|
||||
return { success: true, path }
|
||||
}
|
||||
// 检查是否有有效的账号目录,或本身就是账号目录
|
||||
const accounts = this.findAccountDirs(path)
|
||||
if (accounts.length > 0) {
|
||||
return { success: true, path }
|
||||
}
|
||||
|
||||
// 如果该目录本身就是账号目录(直接包含 db_storage 等)
|
||||
if (this.isAccountDir(path)) {
|
||||
return { success: true, path }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +307,20 @@ export class DbPathService {
|
||||
getDefaultPath(): string {
|
||||
const home = homedir()
|
||||
if (process.platform === 'darwin') {
|
||||
// 优先返回 4.0.5+ 新路径
|
||||
const appSupportBase = join(home, 'Library', 'Containers', 'com.tencent.xinWeChat', 'Data', 'Library', 'Application Support', 'com.tencent.xinWeChat')
|
||||
if (existsSync(appSupportBase)) {
|
||||
try {
|
||||
const entries = readdirSync(appSupportBase)
|
||||
for (const entry of entries) {
|
||||
if (/^\d+\.\d+b\d+\.\d+/.test(entry) || /^\d+\.\d+\.\d+/.test(entry)) {
|
||||
const candidate = join(appSupportBase, entry)
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
// 旧版本路径兜底
|
||||
return join(home, 'Library', 'Containers', 'com.tencent.xinWeChat', 'Data', 'Documents', 'xwechat_files')
|
||||
}
|
||||
return join(home, 'Documents', 'xwechat_files')
|
||||
|
||||
@@ -935,10 +935,17 @@ export class KeyServiceMac {
|
||||
private resolveXwechatRootFromPath(accountPath?: string): string | null {
|
||||
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
if (!normalized) return null
|
||||
|
||||
// 旧路径:xwechat_files
|
||||
const marker = '/xwechat_files'
|
||||
const markerIdx = normalized.indexOf(marker)
|
||||
if (markerIdx < 0) return null
|
||||
return normalized.slice(0, markerIdx + marker.length)
|
||||
if (markerIdx >= 0) return normalized.slice(0, markerIdx + marker.length)
|
||||
|
||||
// 新路径(微信 4.0.5+):Application Support/com.tencent.xinWeChat/2.0b4.0.9
|
||||
const newMarkerMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/)
|
||||
if (newMarkerMatch) return newMarkerMatch[1]
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private pushAccountIdCandidates(candidates: string[], value?: string): void {
|
||||
@@ -1096,6 +1103,16 @@ export class KeyServiceMac {
|
||||
candidates.add(`${base}/app_data/net/kvcomm`)
|
||||
}
|
||||
|
||||
// 微信 4.0.5+ 新路径推导:版本目录同级的 net/kvcomm
|
||||
const newMarkerMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/)
|
||||
if (newMarkerMatch) {
|
||||
const versionBase = newMarkerMatch[1]
|
||||
candidates.add(`${versionBase}/net/kvcomm`)
|
||||
// 上级目录也尝试
|
||||
const parentBase = versionBase.replace(/\/[^\/]+$/, '')
|
||||
candidates.add(`${parentBase}/net/kvcomm`)
|
||||
}
|
||||
|
||||
let cursor = accountPath
|
||||
for (let i = 0; i < 6; i++) {
|
||||
candidates.add(join(cursor, 'net', 'kvcomm'))
|
||||
|
||||
@@ -304,7 +304,17 @@ export class WcdbCore {
|
||||
}
|
||||
|
||||
private formatInitProtectionError(code: number): string {
|
||||
return `错误码: ${code}`
|
||||
const messages: Record<number, string> = {
|
||||
'-3001': '未找到数据库目录 (db_storage),请确认已选择正确的微信数据目录(应包含以 wxid_ 开头的子文件夹)',
|
||||
'-3002': '未找到 session.db 文件,请确认微信已登录并且数据目录完整',
|
||||
'-3003': '数据库句柄无效,请重试',
|
||||
'-3004': '恢复数据库连接失败,请重试',
|
||||
'-2301': '动态库加载失败,请检查安装是否完整',
|
||||
'-2302': 'WCDB 初始化异常,请重试',
|
||||
'-2303': 'WCDB 未能成功初始化',
|
||||
}
|
||||
const msg = messages[String(code) as keyof typeof messages]
|
||||
return msg ? `${msg} (错误码: ${code})` : `操作失败,错误码: ${code}`
|
||||
}
|
||||
|
||||
private isLogEnabled(): boolean {
|
||||
@@ -480,6 +490,49 @@ export class WcdbCore {
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
// 兜底:向上查找 db_storage(最多 2 级),处理用户选择了子目录的情况
|
||||
try {
|
||||
let parent = normalized
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const up = join(parent, '..')
|
||||
if (up === parent) break
|
||||
parent = up
|
||||
const candidateUp = join(parent, 'db_storage')
|
||||
if (existsSync(candidateUp)) return candidateUp
|
||||
if (wxid) {
|
||||
const viaWxidUp = join(parent, wxid, 'db_storage')
|
||||
if (existsSync(viaWxidUp)) return viaWxidUp
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
// 兜底:递归搜索 basePath 下的 db_storage 目录(最多 3 层深)
|
||||
try {
|
||||
const found = this.findDbStorageRecursive(normalized, 3)
|
||||
if (found) return found
|
||||
} catch { }
|
||||
return null
|
||||
}
|
||||
|
||||
private findDbStorageRecursive(dir: string, maxDepth: number): string | null {
|
||||
if (maxDepth <= 0) return null
|
||||
try {
|
||||
const entries = readdirSync(dir)
|
||||
for (const entry of entries) {
|
||||
if (entry.toLowerCase() === 'db_storage') {
|
||||
const candidate = join(dir, entry)
|
||||
try { if (statSync(candidate).isDirectory()) return candidate } catch { }
|
||||
}
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(dir, entry)
|
||||
try {
|
||||
if (statSync(entryPath).isDirectory()) {
|
||||
const found = this.findDbStorageRecursive(entryPath, maxDepth - 1)
|
||||
if (found) return found
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
} catch { }
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "weflow",
|
||||
"version": "4.3.0",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "weflow",
|
||||
"version": "4.3.0",
|
||||
"version": "2.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"echarts": "^5.5.1",
|
||||
@@ -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.1.1",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sherpa-onnx-node": "^1.10.38",
|
||||
@@ -8867,9 +8867,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -9073,9 +9073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.13.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.13.2.tgz",
|
||||
"integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.11.0.tgz",
|
||||
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
@@ -9095,12 +9095,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.13.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.13.2.tgz",
|
||||
"integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.11.0.tgz",
|
||||
"integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.2"
|
||||
"react-router": "7.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -10236,9 +10236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -10786,9 +10786,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "weflow",
|
||||
"version": "4.3.0",
|
||||
"version": "2.1.0",
|
||||
"description": "WeFlow",
|
||||
"main": "dist-electron/main.js",
|
||||
"author": {
|
||||
@@ -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.1.1",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sherpa-onnx-node": "^1.10.38",
|
||||
@@ -61,17 +61,6 @@
|
||||
"vite-plugin-electron": "^0.28.8",
|
||||
"vite-plugin-electron-renderer": "^0.14.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"tar": ">=6.2.1",
|
||||
"minimatch": ">=3.1.2",
|
||||
"rollup": ">=4.0.0",
|
||||
"immutable": ">=4.0.0",
|
||||
"lodash": ">=4.17.21",
|
||||
"brace-expansion": ">=1.1.11",
|
||||
"picomatch": ">=2.3.2"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.WeFlow.app",
|
||||
"publish": {
|
||||
@@ -189,4 +178,4 @@
|
||||
],
|
||||
"icon": "resources/icon.icns"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/icon.ico
BIN
public/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 212 KiB |
BIN
public/icon.png
BIN
public/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 54 KiB |
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 1.0 MiB |
15
src/App.tsx
15
src/App.tsx
@@ -312,14 +312,10 @@ function App() {
|
||||
const removeUpdateListener = window.electronAPI?.app?.onUpdateAvailable?.((info: any) => {
|
||||
// 发现新版本时保存更新信息,锁定状态下不弹窗,解锁后再显示
|
||||
if (info) {
|
||||
window.electronAPI.app.getVersion().then((currentVersion: string) => {
|
||||
const isMandatory = !!(info.minimumVersion && currentVersion &&
|
||||
currentVersion.localeCompare(info.minimumVersion, undefined, { numeric: true, sensitivity: 'base' }) <= 0)
|
||||
setUpdateInfo({ ...info, hasUpdate: true, isMandatory })
|
||||
if (!useAppStore.getState().isLocked) {
|
||||
setShowUpdateDialog(true)
|
||||
}
|
||||
})
|
||||
setUpdateInfo({ ...info, hasUpdate: true })
|
||||
if (!useAppStore.getState().isLocked) {
|
||||
setShowUpdateDialog(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
const removeProgressListener = window.electronAPI?.app?.onDownloadProgress?.((progress: any) => {
|
||||
@@ -689,11 +685,10 @@ function App() {
|
||||
<UpdateDialog
|
||||
open={showUpdateDialog}
|
||||
updateInfo={updateInfo}
|
||||
onClose={() => { if (!(updateInfo as any)?.isMandatory) setShowUpdateDialog(false) }}
|
||||
onClose={() => setShowUpdateDialog(false)}
|
||||
onUpdate={handleUpdateNow}
|
||||
onIgnore={handleIgnoreUpdate}
|
||||
isDownloading={isDownloading}
|
||||
isMandatory={!!(updateInfo as any)?.isMandatory}
|
||||
progress={downloadProgress}
|
||||
/>
|
||||
|
||||
|
||||
@@ -282,13 +282,4 @@
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.mandatory-tip {
|
||||
color: #e53e3e;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(229, 62, 62, 0.08);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ interface UpdateDialogProps {
|
||||
onUpdate: () => void
|
||||
onIgnore?: () => void
|
||||
isDownloading: boolean
|
||||
isMandatory?: boolean
|
||||
progress: number | {
|
||||
percent: number
|
||||
bytesPerSecond?: number
|
||||
@@ -31,7 +30,6 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
onUpdate,
|
||||
onIgnore,
|
||||
isDownloading,
|
||||
isMandatory,
|
||||
progress
|
||||
}) => {
|
||||
if (!open || !updateInfo) return null
|
||||
@@ -71,7 +69,7 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
return (
|
||||
<div className="update-dialog-overlay">
|
||||
<div className="update-dialog">
|
||||
{!isDownloading && !isMandatory && (
|
||||
{!isDownloading && (
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -121,14 +119,11 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="actions">
|
||||
{onIgnore && !isMandatory && (
|
||||
{onIgnore && (
|
||||
<button className="btn-ignore" onClick={onIgnore}>
|
||||
忽略本次更新
|
||||
</button>
|
||||
)}
|
||||
{isMandatory && (
|
||||
<p className="mandatory-tip">此版本存在安全风险,必须更新后才能继续使用</p>
|
||||
)}
|
||||
<button className="btn-update" onClick={onUpdate}>
|
||||
开启新旅程
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user