Compare commits

..

21 Commits

Author SHA1 Message Date
hicccc77
da15f829d3 ci: add security-events write permission for CodeQL 2026-03-27 19:12:20 +08:00
hicccc77
bb60694013 ci: fix pnpm install frozen-lockfile issue 2026-03-27 18:17:26 +08:00
hicccc77
c80115d0f7 ci: add daily security scan workflow for all branches 2026-03-27 17:56:35 +08:00
hicccc77
0cf8ea8166 chore: bump version to 4.2.0 2026-03-26 23:20:42 +08:00
hicccc77
74b830dd79 chore: update service files and xkey_helper
- xkey_helper: use mach exception port to intercept EXC_BREAKPOINT,
  fixes key capture failure on macOS 26.2
2026-03-26 23:19:08 +08:00
cc
8668c168a7 333 2026-03-26 22:43:59 +08:00
cc
8b8c5f33ce 333 2026-03-26 22:34:50 +08:00
cc
2fcbb026df 222 2026-03-26 22:32:33 +08:00
cc
66ee72380d 222 2026-03-26 22:30:21 +08:00
cc
4f16345351 111 2026-03-26 22:26:33 +08:00
cc
5110618996 再次修复 2026-03-26 22:19:30 +08:00
cc
bf51368cf4 修复密钥问题 2026-03-26 22:16:30 +08:00
cc
d6054745d6 修复macos打包错误 2026-03-26 22:00:42 +08:00
hicccc77
a4731f25f8 chore: update xkey_helper (macOS) with pure semantic scan mode
Always use pure semantic scan mode (KNOWN_RVA=0) regardless of
WeChat version, improving compatibility for versions < 4.1.8.
2026-03-26 21:16:14 +08:00
hicccc77
6c4507e495 fix(ci): remove invalid --no-fail-on-no-release flag from gh release edit 2026-03-26 20:33:18 +08:00
hicccc77
c8e0160d5c fix(ci): use bash shell for Windows packaging steps to avoid PowerShell variable expansion 2026-03-26 20:21:13 +08:00
hicccc77
ac40a81901 fix(ci): pre-release placeholder + fix latest.yml arm64 overwrite
- Add prepare-release job: immediately marks release as pre-release
  with "正在构建中,请勿下载" notice; all build jobs depend on it
- Fix arm64 job channel override: use -c.publish.channel=latest-arm64
  (correct syntax) instead of broken single-quoted CLI arg
- Fix artifactName quoting for both win x64 and arm64 jobs
- Add "Fix latest.yml" step in update-release-notes: downloads x64 exe,
  computes correct sha512/size, uploads latest.yml with --clobber
- Final step in update-release-notes: remove prerelease flag, mark as
  latest official release

Fixes #553
2026-03-26 19:57:45 +08:00
hicccc77
ca38a68a75 fix: 改善错误码 -3001 提示信息并增强 db_storage 路径解析兼容性\n\n- formatInitProtectionError 返回可读的中文错误说明,替代裸错误码\n- resolveDbStoragePath 新增向上查找(最多2级)兜底逻辑\n- 新增 findDbStorageRecursive 递归搜索(最多3层)兜底\n- 解决使用 wx_key 获取密钥后因路径层级不同导致 -3001 报错的问题\n\nFixes #552 2026-03-26 15:08:08 +08:00
hicccc77
64be2dd562 fix: 支持微信 4.0.5+ 新数据目录结构 (Application Support/com.tencent.xinWeChat/2.0b4.0.x)
- dbPathService.autoDetect: 自动枚举版本目录(如 2.0b4.0.9),优先检测新路径
- dbPathService.getDefaultPath: 同步返回新版本路径
- keyServiceMac.resolveXwechatRootFromPath: 兼容新路径标记
- keyServiceMac.getKvcommCandidates: 补充新路径下的 kvcomm 推导

Fixes #551
2026-03-26 12:08:47 +08:00
cc
ea2abb6c72 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-03-25 20:09:53 +08:00
cc
011e2ff37a 修复Action打包问题与渲染层过滤问题 2026-03-25 20:09:48 +08:00
21 changed files with 286 additions and 179 deletions

View File

@@ -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
@@ -42,31 +61,16 @@ jobs:
npx tsc
npx vite build
- name: Package and Publish macOS arm64 (unsigned DMG)
- name: Package and Publish macOS arm64 (unsigned DMG + ZIP)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"
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
npx electron-builder --mac --arm64 --publish always
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 --draft=false --prerelease=false

View File

@@ -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'''
]

View File

@@ -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(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;/gi, '\'')
.replace(/&#x27;/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)
}
}
}
@@ -1259,7 +1300,7 @@ function registerIpcHandlers() {
try {
console.log('[Update] 开始下载更新...')
await autoUpdater.downloadUpdate()
} catch (error) {
} catch (error: any) {
console.error('[Update] 下载更新失败:', error)
// 失败时清理状态和监听器
isDownloadInProgress = false
@@ -1271,7 +1312,10 @@ function registerIpcHandlers() {
autoUpdater.removeListener('update-downloaded', downloadedHandler)
downloadedHandler = null
}
throw error
// 统一错误提示格式,避免出现 [object Object] 的 JSON 字符串
const errorMessage = error.message || (typeof error === 'string' ? error : JSON.stringify(error))
throw new Error(errorMessage)
}
})
@@ -2595,19 +2639,19 @@ function registerIpcHandlers() {
// 密钥获取
ipcMain.handle('key:autoGetDbKey', async (event) => {
return keyService.autoGetDbKey(180_000, (message, level) => {
return keyService.autoGetDbKey(180_000, (message: string, level: number) => {
event.sender.send('key:dbKeyStatus', { message, level })
})
})
ipcMain.handle('key:autoGetImageKey', async (event, manualDir?: string, wxid?: string) => {
return keyService.autoGetImageKey(manualDir, (message) => {
return keyService.autoGetImageKey(manualDir, (message: string) => {
event.sender.send('key:imageKeyStatus', { message })
}, wxid)
})
ipcMain.handle('key:scanImageKeyFromMemory', async (event, userDir: string) => {
return keyService.autoGetImageKeyByMemoryScan(userDir, (message) => {
return keyService.autoGetImageKeyByMemoryScan(userDir, (message: string) => {
event.sender.send('key:imageKeyStatus', { message })
})
})
@@ -2662,8 +2706,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)
})
}
}

View File

@@ -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')

View File

@@ -389,7 +389,7 @@ export class KeyServiceMac {
`set timeoutSec to ${timeoutSec}`,
'try',
'with timeout of timeoutSec seconds',
'set outText to do shell script cmd with administrator privileges',
'set outText to do shell script (cmd & " 2>&1") with administrator privileges',
'end timeout',
'return "WF_OK::" & outText',
'on error errMsg number errNum partial result pr',
@@ -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'))

View File

@@ -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
}

28
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "weflow",
"version": "4.3.0",
"version": "4.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "weflow",
"version": "4.3.0",
"version": "4.2.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",
@@ -6633,9 +6633,9 @@
"license": "MIT"
},
"node_modules/immutable": {
"version": "5.1.5",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"version": "5.1.4",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"dev": true,
"license": "MIT"
},
@@ -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"
@@ -11062,4 +11062,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "weflow",
"version": "4.3.0",
"version": "4.2.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.1"
}
},
"build": {
"appId": "com.WeFlow.app",
"publish": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

BIN
resources/libwcdb_api.so Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/xkey_helper_macos Normal file

Binary file not shown.

View File

@@ -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}
/>

View File

@@ -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;
}
}

View File

@@ -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>

BIN
temp_assets.json Normal file

Binary file not shown.