mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f7d673abb | ||
|
|
4bba8bd871 | ||
|
|
30c47af1f9 | ||
|
|
77aa74dd92 | ||
|
|
f96ada906c | ||
|
|
a50e903e55 |
@@ -50,8 +50,7 @@ MAC系统 获取微信聊天记录 AI一键生成群聊总结
|
|||||||
### 使用前置要求
|
### 使用前置要求
|
||||||
|
|
||||||
- **微信版本**:
|
- **微信版本**:
|
||||||
- 微信 <= 4.0: 可正常使用,获取数据库密码方式参考:[Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
|
- 微信 4.0+: 已支持部分能力,仍在持续迭代与兼容性验证中;如需更成熟的完整方案,推荐使用 [WeFlow](https://github.com/hicccc77/WeFlow) [Chatlog](https://github.com/sjzar/chatlog)
|
||||||
- 微信 >= 4.0: 已支持部分能力,仍在持续迭代与兼容性验证中;如需更成熟的完整方案,推荐使用 [WeFlow](https://github.com/hicccc77/WeFlow) [Chatlog](https://github.com/sjzar/chatlog)
|
|
||||||
- 如无法获取本地数据库密码,则无法使用当前项目
|
- 如无法获取本地数据库密码,则无法使用当前项目
|
||||||
- Node.js (推荐 v16+)
|
- Node.js (推荐 v16+)
|
||||||
- pnpm@7
|
- pnpm@7
|
||||||
|
|||||||
+14
-9
@@ -1,5 +1,6 @@
|
|||||||
appId: com.electron.app
|
appId: com.wechatexplorer.app
|
||||||
productName: wechatexplorer
|
productName: WechatExplorer
|
||||||
|
afterPack: scripts/after-pack.cjs
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
@@ -29,13 +30,17 @@ nsis:
|
|||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
# The bundled WCDB bridge accepts Electron as its internal host name. The
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
# public app name and bundle identifier remain WechatExplorer-specific.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
CFBundleName: Electron
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
CFBundleDisplayName: WechatExplorer
|
||||||
|
NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: false
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-${arch}.${ext}
|
||||||
linux:
|
linux:
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
@@ -47,5 +52,5 @@ appImage:
|
|||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: github
|
||||||
url: https://example.com/auto-updates
|
releaseType: draft
|
||||||
|
|||||||
+7
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wechatexplorer",
|
"name": "wechatexplorer",
|
||||||
"version": "1.1.0",
|
"version": "2.0.1",
|
||||||
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"wechat",
|
"wechat",
|
||||||
@@ -21,17 +21,16 @@
|
|||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir",
|
||||||
"build:win": "npm run build && electron-builder --win",
|
"build:win": "npm run build && electron-builder --config electron-builder.yml --win",
|
||||||
"build:mac:x64": "electron-vite build && electron-builder --mac --x64",
|
"build:mac:x64": "electron-vite build && electron-builder --config electron-builder.yml --mac --x64",
|
||||||
"build:mac:arm64": "electron-vite build && electron-builder --mac --arm64",
|
"build:mac:arm64": "electron-vite build && electron-builder --config electron-builder.yml --mac --arm64",
|
||||||
"release:mac": "electron-vite build && electron-builder --mac --x64 --arm64 --publish always",
|
"release:mac": "electron-vite build && electron-builder --config electron-builder.yml --mac --x64 --arm64 --publish always",
|
||||||
"build:linux": "electron-vite build && electron-builder --linux"
|
"build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"better-sqlite3-multiple-ciphers": "^12.5.0",
|
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"fzstd": "^0.1.1",
|
"fzstd": "^0.1.1",
|
||||||
"koffi": "^2.9.0",
|
"koffi": "^2.9.0",
|
||||||
@@ -66,14 +65,5 @@
|
|||||||
"electron",
|
"electron",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
|
||||||
"mac": {
|
|
||||||
"target": [
|
|
||||||
"dmg",
|
|
||||||
"zip"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-126
@@ -16,7 +16,6 @@ specifiers:
|
|||||||
'@types/react': ^19.2.7
|
'@types/react': ^19.2.7
|
||||||
'@types/react-dom': ^19.2.3
|
'@types/react-dom': ^19.2.3
|
||||||
'@vitejs/plugin-react': ^5.1.1
|
'@vitejs/plugin-react': ^5.1.1
|
||||||
better-sqlite3-multiple-ciphers: ^12.5.0
|
|
||||||
electron: ^39.2.6
|
electron: ^39.2.6
|
||||||
electron-builder: ^26.0.12
|
electron-builder: ^26.0.12
|
||||||
electron-vite: ^5.0.0
|
electron-vite: ^5.0.0
|
||||||
@@ -38,7 +37,6 @@ specifiers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@electron-toolkit/preload': 3.0.2_electron@39.2.6
|
'@electron-toolkit/preload': 3.0.2_electron@39.2.6
|
||||||
'@electron-toolkit/utils': 4.0.0_electron@39.2.6
|
'@electron-toolkit/utils': 4.0.0_electron@39.2.6
|
||||||
better-sqlite3-multiple-ciphers: 12.5.0
|
|
||||||
fs-extra: 11.3.2
|
fs-extra: 11.3.2
|
||||||
fzstd: 0.1.1
|
fzstd: 0.1.1
|
||||||
koffi: 2.16.2
|
koffi: 2.16.2
|
||||||
@@ -1578,32 +1576,20 @@ packages:
|
|||||||
|
|
||||||
/base64-js/1.5.1:
|
/base64-js/1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/baseline-browser-mapping/2.9.5:
|
/baseline-browser-mapping/2.9.5:
|
||||||
resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==}
|
resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/better-sqlite3-multiple-ciphers/12.5.0:
|
|
||||||
resolution: {integrity: sha512-bYbiV0TdCIQFqlI122+wIFnc3yXWHyz8A/026Y/YfiA3vKcC0wLnHqqociNehpFmbRSowuY3Ko8SyUhBXI9+4A==}
|
|
||||||
engines: {node: 20.x || 22.x || 23.x || 24.x}
|
|
||||||
dependencies:
|
|
||||||
bindings: 1.5.0
|
|
||||||
prebuild-install: 7.1.3
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/bindings/1.5.0:
|
|
||||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
|
||||||
dependencies:
|
|
||||||
file-uri-to-path: 1.0.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/bl/4.1.0:
|
/bl/4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/boolean/3.2.0:
|
/boolean/3.2.0:
|
||||||
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
|
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
|
||||||
@@ -1647,6 +1633,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/builder-util-runtime/9.3.1:
|
/builder-util-runtime/9.3.1:
|
||||||
resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==}
|
resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==}
|
||||||
@@ -1772,10 +1759,6 @@ packages:
|
|||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/chownr/1.1.4:
|
|
||||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/chownr/2.0.0:
|
/chownr/2.0.0:
|
||||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1947,11 +1930,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
/deep-extend/0.6.0:
|
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
|
||||||
engines: {node: '>=4.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/deep-is/0.1.4:
|
/deep-is/0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -1990,6 +1968,7 @@ packages:
|
|||||||
/detect-libc/2.1.2:
|
/detect-libc/2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/detect-node/2.1.0:
|
/detect-node/2.1.0:
|
||||||
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
||||||
@@ -2528,11 +2507,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/expand-template/2.0.3:
|
|
||||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/exponential-backoff/3.1.3:
|
/exponential-backoff/3.1.3:
|
||||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2596,10 +2570,6 @@ packages:
|
|||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/file-uri-to-path/1.0.0:
|
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/filelist/1.0.4:
|
/filelist/1.0.4:
|
||||||
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2652,10 +2622,6 @@ packages:
|
|||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/fs-constants/1.0.0:
|
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/fs-extra/10.1.0:
|
/fs-extra/10.1.0:
|
||||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2787,10 +2753,6 @@ packages:
|
|||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/github-from-package/0.0.0:
|
|
||||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/glob-parent/6.0.2:
|
/glob-parent/6.0.2:
|
||||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -3020,6 +2982,7 @@ packages:
|
|||||||
|
|
||||||
/ieee754/1.2.1:
|
/ieee754/1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ignore/5.3.2:
|
/ignore/5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
@@ -3063,10 +3026,7 @@ packages:
|
|||||||
|
|
||||||
/inherits/2.0.4:
|
/inherits/2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
dev: true
|
||||||
/ini/1.3.8:
|
|
||||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/internal-slot/1.1.0:
|
/internal-slot/1.1.0:
|
||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
@@ -3574,6 +3534,7 @@ packages:
|
|||||||
|
|
||||||
/minimist/1.2.8:
|
/minimist/1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/minipass-collect/1.0.2:
|
/minipass-collect/1.0.2:
|
||||||
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
||||||
@@ -3639,10 +3600,6 @@ packages:
|
|||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/mkdirp-classic/0.5.3:
|
|
||||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mkdirp/1.0.4:
|
/mkdirp/1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3658,10 +3615,6 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/napi-build-utils/2.0.0:
|
|
||||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/natural-compare/1.4.0:
|
/natural-compare/1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3676,6 +3629,7 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/node-addon-api/1.7.2:
|
/node-addon-api/1.7.2:
|
||||||
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
||||||
@@ -3924,25 +3878,6 @@ packages:
|
|||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prebuild-install/7.1.3:
|
|
||||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
expand-template: 2.0.3
|
|
||||||
github-from-package: 0.0.0
|
|
||||||
minimist: 1.2.8
|
|
||||||
mkdirp-classic: 0.5.3
|
|
||||||
napi-build-utils: 2.0.0
|
|
||||||
node-abi: 3.85.0
|
|
||||||
pump: 3.0.3
|
|
||||||
rc: 1.2.8
|
|
||||||
simple-get: 4.0.1
|
|
||||||
tar-fs: 2.1.4
|
|
||||||
tunnel-agent: 0.6.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/prelude-ls/1.2.1:
|
/prelude-ls/1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4010,16 +3945,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
/rc/1.2.8:
|
|
||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
deep-extend: 0.6.0
|
|
||||||
ini: 1.3.8
|
|
||||||
minimist: 1.2.8
|
|
||||||
strip-json-comments: 2.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react-dom/19.2.1_react@19.2.1:
|
/react-dom/19.2.1_react@19.2.1:
|
||||||
resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
|
resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4059,6 +3984,7 @@ packages:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/reflect.getprototypeof/1.0.10:
|
/reflect.getprototypeof/1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
@@ -4198,6 +4124,7 @@ packages:
|
|||||||
|
|
||||||
/safe-buffer/5.2.1:
|
/safe-buffer/5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/safe-push-apply/1.0.0:
|
/safe-push-apply/1.0.0:
|
||||||
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
||||||
@@ -4356,18 +4283,6 @@ packages:
|
|||||||
engines: {node: '>=16.11.0'}
|
engines: {node: '>=16.11.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/simple-concat/1.0.1:
|
|
||||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/simple-get/4.0.1:
|
|
||||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
|
||||||
dependencies:
|
|
||||||
decompress-response: 6.0.0
|
|
||||||
once: 1.4.0
|
|
||||||
simple-concat: 1.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/simple-update-notifier/2.0.0:
|
/simple-update-notifier/2.0.0:
|
||||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4530,6 +4445,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/strip-ansi/6.0.1:
|
/strip-ansi/6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
@@ -4545,11 +4461,6 @@ packages:
|
|||||||
ansi-regex: 6.2.2
|
ansi-regex: 6.2.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/strip-json-comments/2.0.1:
|
|
||||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/strip-json-comments/3.1.1:
|
/strip-json-comments/3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4582,26 +4493,6 @@ packages:
|
|||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/tar-fs/2.1.4:
|
|
||||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
|
||||||
dependencies:
|
|
||||||
chownr: 1.1.4
|
|
||||||
mkdirp-classic: 0.5.3
|
|
||||||
pump: 3.0.3
|
|
||||||
tar-stream: 2.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tar-stream/2.2.0:
|
|
||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
dependencies:
|
|
||||||
bl: 4.1.0
|
|
||||||
end-of-stream: 1.4.5
|
|
||||||
fs-constants: 1.0.0
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tar/6.2.1:
|
/tar/6.2.1:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4661,12 +4552,6 @@ packages:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/tunnel-agent/0.6.0:
|
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/type-check/0.4.0:
|
/type-check/0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4805,6 +4690,7 @@ packages:
|
|||||||
|
|
||||||
/util-deprecate/1.0.2:
|
/util-deprecate/1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/verror/1.10.1:
|
/verror/1.10.1:
|
||||||
resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
|
resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
const { existsSync, renameSync } = require('node:fs')
|
||||||
|
const { execFileSync } = require('node:child_process')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const COMPATIBILITY_NAME = 'Electron'
|
||||||
|
const HELPER_SUFFIXES = ['', ' (Plugin)', ' (Renderer)', ' (GPU)']
|
||||||
|
|
||||||
|
function setPlistValue(plistPath, key, value) {
|
||||||
|
execFileSync('/usr/libexec/PlistBuddy', ['-c', `Set :${key} ${value}`, plistPath])
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.default = async function afterPack(context) {
|
||||||
|
if (context.electronPlatformName !== 'darwin') return
|
||||||
|
|
||||||
|
const productName = context.packager.appInfo.productFilename
|
||||||
|
const appPath = path.join(context.appOutDir, `${productName}.app`)
|
||||||
|
const contentsPath = path.join(appPath, 'Contents')
|
||||||
|
const sourceExecutable = path.join(contentsPath, 'MacOS', productName)
|
||||||
|
const targetExecutable = path.join(contentsPath, 'MacOS', COMPATIBILITY_NAME)
|
||||||
|
|
||||||
|
if (existsSync(sourceExecutable)) renameSync(sourceExecutable, targetExecutable)
|
||||||
|
if (!existsSync(targetExecutable)) {
|
||||||
|
throw new Error(`Missing Electron main executable: ${sourceExecutable}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPlistPath = path.join(contentsPath, 'Info.plist')
|
||||||
|
setPlistValue(appPlistPath, 'CFBundleExecutable', COMPATIBILITY_NAME)
|
||||||
|
setPlistValue(appPlistPath, 'CFBundleName', COMPATIBILITY_NAME)
|
||||||
|
|
||||||
|
const frameworksPath = path.join(contentsPath, 'Frameworks')
|
||||||
|
|
||||||
|
for (const suffix of HELPER_SUFFIXES) {
|
||||||
|
const sourceName = `${productName} Helper${suffix}`
|
||||||
|
const targetName = `${COMPATIBILITY_NAME} Helper${suffix}`
|
||||||
|
const sourceBundle = path.join(frameworksPath, `${sourceName}.app`)
|
||||||
|
const targetBundle = path.join(frameworksPath, `${targetName}.app`)
|
||||||
|
|
||||||
|
if (existsSync(sourceBundle)) renameSync(sourceBundle, targetBundle)
|
||||||
|
if (!existsSync(targetBundle)) {
|
||||||
|
throw new Error(`Missing Electron helper bundle: ${sourceBundle}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceExecutable = path.join(targetBundle, 'Contents', 'MacOS', sourceName)
|
||||||
|
const targetExecutable = path.join(targetBundle, 'Contents', 'MacOS', targetName)
|
||||||
|
if (existsSync(sourceExecutable)) renameSync(sourceExecutable, targetExecutable)
|
||||||
|
if (!existsSync(targetExecutable)) {
|
||||||
|
throw new Error(`Missing Electron helper executable: ${sourceExecutable}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const plistPath = path.join(targetBundle, 'Contents', 'Info.plist')
|
||||||
|
setPlistValue(plistPath, 'CFBundleExecutable', targetName)
|
||||||
|
setPlistValue(plistPath, 'CFBundleName', targetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -241,15 +241,11 @@ export class ImageDecryptService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let decrypted: Buffer
|
let decrypted: Buffer
|
||||||
if (version === 0) {
|
if (version === 1) {
|
||||||
console.log('[ImageDecrypt] using V3 (XOR only)')
|
|
||||||
decrypted = this.decryptDatV3(datPath)
|
|
||||||
} else if (version === 1) {
|
|
||||||
console.log('[ImageDecrypt] using V1 (default AES key)')
|
console.log('[ImageDecrypt] using V1 (default AES key)')
|
||||||
const key = Buffer.from(this.defaultV1AesKey, 'ascii')
|
const key = Buffer.from(this.defaultV1AesKey, 'ascii')
|
||||||
decrypted = this.decryptDatV4(datPath, key)
|
decrypted = this.decryptDatV4(datPath, key)
|
||||||
} else {
|
} else if (version === 2) {
|
||||||
// version === 2
|
|
||||||
console.log('[ImageDecrypt] using V2 (user AES key)')
|
console.log('[ImageDecrypt] using V2 (user AES key)')
|
||||||
if (!this.aesKey) {
|
if (!this.aesKey) {
|
||||||
console.log('[ImageDecrypt] no AES key configured')
|
console.log('[ImageDecrypt] no AES key configured')
|
||||||
@@ -257,6 +253,9 @@ export class ImageDecryptService {
|
|||||||
}
|
}
|
||||||
const key = Buffer.from(this.aesKey, 'ascii').slice(0, 16)
|
const key = Buffer.from(this.aesKey, 'ascii').slice(0, 16)
|
||||||
decrypted = this.decryptDatV4(datPath, key)
|
decrypted = this.decryptDatV4(datPath, key)
|
||||||
|
} else {
|
||||||
|
console.log('[ImageDecrypt] unsupported dat version:', version)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return decrypted
|
return decrypted
|
||||||
@@ -310,18 +309,6 @@ export class ImageDecryptService {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* V3 解密 - 仅 XOR
|
|
||||||
*/
|
|
||||||
private decryptDatV3(inputPath: string): Buffer {
|
|
||||||
const data = readFileSync(inputPath)
|
|
||||||
const out = Buffer.alloc(data.length)
|
|
||||||
for (let i = 0; i < data.length; i += 1) {
|
|
||||||
out[i] = data[i] ^ this.xorKey
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V4 解密 - AES + XOR
|
* V4 解密 - AES + XOR
|
||||||
*/
|
*/
|
||||||
|
|||||||
+35
-13
@@ -113,16 +113,13 @@ app.whenReady().then(() => {
|
|||||||
wechatDb?.close()
|
wechatDb?.close()
|
||||||
wechatDb = nextWechatDb
|
wechatDb = nextWechatDb
|
||||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||||
let monitoring = false
|
|
||||||
if (wcdb4Client) {
|
|
||||||
voiceService = new VoiceService(wcdb4Client)
|
voiceService = new VoiceService(wcdb4Client)
|
||||||
stickerService = new StickerService(wcdb4Client)
|
stickerService = new StickerService(wcdb4Client)
|
||||||
monitoring = wcdb4Client.startMonitor((type, json) => {
|
const monitoring = wcdb4Client.startMonitor((type, json) => {
|
||||||
for (const window of BrowserWindow.getAllWindows()) {
|
for (const window of BrowserWindow.getAllWindows()) {
|
||||||
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json })
|
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
imageDecryptService = null
|
imageDecryptService = null
|
||||||
return { success: true, monitoring }
|
return { success: true, monitoring }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -206,12 +203,11 @@ app.whenReady().then(() => {
|
|||||||
ipcMain.handle('db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number) => {
|
ipcMain.handle('db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number) => {
|
||||||
if (!wechatDb) return []
|
if (!wechatDb) return []
|
||||||
const wcdb4Client = wechatDb.getWcdb4Client()
|
const wcdb4Client = wechatDb.getWcdb4Client()
|
||||||
const username = wcdb4Client?.getUsernameByMd5(userMd5)
|
const username = wcdb4Client.getUsernameByMd5(userMd5)
|
||||||
const rawMessages = wechatDb.getUserMessages(userMd5, startTime, endTime)
|
const rawMessages = wechatDb.getUserMessages(userMd5, startTime, endTime)
|
||||||
const groupMembers = wechatDb.getGroupMembersForChat(userMd5)
|
const groupMembers = wechatDb.getGroupMembersForChat(userMd5)
|
||||||
const myAvatar = wechatDb.getMyAvatarUrl()
|
const myAvatar = wechatDb.getMyAvatarUrl()
|
||||||
const myGroupNickname =
|
const myGroupNickname = username?.endsWith('@chatroom')
|
||||||
username?.endsWith('@chatroom') && wcdb4Client
|
|
||||||
? wcdb4Client.getMyGroupNickname(username)
|
? wcdb4Client.getMyGroupNickname(username)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
@@ -265,20 +261,23 @@ app.whenReady().then(() => {
|
|||||||
/<appmsg\b|<refermsg\b|<appmsg\b|<refermsg\b/i.test(content)
|
/<appmsg\b|<refermsg\b|<appmsg\b|<refermsg\b/i.test(content)
|
||||||
? 49
|
? 49
|
||||||
: msgType
|
: msgType
|
||||||
if ([3, 42, 47, 48, 49, 50].includes(inferredMsgType)) {
|
if ([3, 42, 47, 48, 49, 50, 10000, 10002].includes(inferredMsgType)) {
|
||||||
try {
|
try {
|
||||||
const parsed =
|
const parsed =
|
||||||
inferredMsgType === 47
|
inferredMsgType === 47
|
||||||
? parseStickerMessageFromRow(msg, content)
|
? parseStickerMessageFromRow(msg, content)
|
||||||
: parseMessageContent(content, inferredMsgType)
|
: parseMessageContent(content, inferredMsgType)
|
||||||
if (parsed.type !== 'unknown') {
|
if (parsed.type === 'system') {
|
||||||
|
content = parsed.content
|
||||||
|
contentData = parsed
|
||||||
|
} else if (parsed.type !== 'unknown') {
|
||||||
content = ''
|
content = ''
|
||||||
}
|
}
|
||||||
if (parsed.type === 'image') {
|
if (parsed.type === 'image') {
|
||||||
const imageDatName = parseImageDatNameFromRow(msg)
|
const imageDatName = parseImageDatNameFromRow(msg)
|
||||||
contentData = { ...parsed, datName: parsed.datName || imageDatName }
|
contentData = { ...parsed, datName: parsed.datName || imageDatName }
|
||||||
} else {
|
} else if (parsed.type !== 'system') {
|
||||||
if (parsed.type === 'sticker' && !parsed.url && parsed.md5 && wcdb4Client) {
|
if (parsed.type === 'sticker' && !parsed.url && parsed.md5) {
|
||||||
parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5)
|
parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5)
|
||||||
}
|
}
|
||||||
contentData = parsed
|
contentData = parsed
|
||||||
@@ -298,7 +297,7 @@ app.whenReady().then(() => {
|
|||||||
) {
|
) {
|
||||||
const parsed = parseStickerMessageFromRow(msg, content)
|
const parsed = parseStickerMessageFromRow(msg, content)
|
||||||
if (parsed.type === 'sticker') {
|
if (parsed.type === 'sticker') {
|
||||||
if (!parsed.url && parsed.md5 && wcdb4Client) {
|
if (!parsed.url && parsed.md5) {
|
||||||
parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5)
|
parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5)
|
||||||
}
|
}
|
||||||
content = ''
|
content = ''
|
||||||
@@ -313,7 +312,7 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: msg.mesLocalID || Math.random().toString(),
|
id: msg.mesLocalID || Math.random().toString(),
|
||||||
from: isMine ? 'assistant' : 'user',
|
from: contentData?.type === 'system' ? 'system' : isMine ? 'assistant' : 'user',
|
||||||
type: displayType,
|
type: displayType,
|
||||||
datetime: date.toLocaleString('zh-CN', { hour12: false }),
|
datetime: date.toLocaleString('zh-CN', { hour12: false }),
|
||||||
content: content,
|
content: content,
|
||||||
@@ -327,6 +326,29 @@ app.whenReady().then(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('db:getGroupSnapshot', (_, userMd5: string) => {
|
||||||
|
if (!wechatDb) return null
|
||||||
|
const wcdb4Client = wechatDb.getWcdb4Client()
|
||||||
|
|
||||||
|
const roomId = wcdb4Client.getUsernameByMd5(userMd5)
|
||||||
|
if (!roomId || !roomId.endsWith('@chatroom')) return null
|
||||||
|
|
||||||
|
const members = wcdb4Client
|
||||||
|
.getGroupMembers(roomId)
|
||||||
|
.filter((member) => member?.m_nsUsrName)
|
||||||
|
.map((member) => ({
|
||||||
|
wxid: member.m_nsUsrName,
|
||||||
|
nickname: member.nickname || '',
|
||||||
|
avatar: member.m_nsHeadImgUrl || ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId,
|
||||||
|
memberCount: members.length,
|
||||||
|
members
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('db:search', (_, keyword: string) => {
|
ipcMain.handle('db:search', (_, keyword: string) => {
|
||||||
if (!wechatDb) return null
|
if (!wechatDb) return null
|
||||||
return wechatDb.searchAllMessages(keyword)
|
return wechatDb.searchAllMessages(keyword)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type QuoteContent = {
|
|||||||
quotedSender?: string
|
quotedSender?: string
|
||||||
quotedType?: string
|
quotedType?: string
|
||||||
}
|
}
|
||||||
type SystemContent = { type: 'system'; content: string }
|
type SystemContent = { type: 'system'; content: string; raw?: string }
|
||||||
type UnknownContent = { type: 'unknown'; raw: string }
|
type UnknownContent = { type: 'unknown'; raw: string }
|
||||||
|
|
||||||
export type ParsedContent =
|
export type ParsedContent =
|
||||||
@@ -79,12 +79,40 @@ export function parseMessageContent(content: string, messageType: number): Parse
|
|||||||
return parseVoipMessage(normalized)
|
return parseVoipMessage(normalized)
|
||||||
case 10000:
|
case 10000:
|
||||||
case 10002:
|
case 10002:
|
||||||
return { type: 'system', content: normalized }
|
return parseSystemMessage(normalized)
|
||||||
default:
|
default:
|
||||||
return { type: 'text', content: normalized }
|
return { type: 'text', content: normalized }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSystemMessage(content: string): ParsedContent {
|
||||||
|
const stripped = stripChatroomPrefix(content)
|
||||||
|
const decoded = decodeXmlEntities(stripped)
|
||||||
|
const delChatroomMemberText = extractDelChatroomMemberText(decoded)
|
||||||
|
if (delChatroomMemberText) {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
content: normalizeSystemText(delChatroomMemberText),
|
||||||
|
raw: content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const plainText =
|
||||||
|
extractXmlNodeText(decoded, 'plain') ||
|
||||||
|
extractXmlNodeText(decoded, 'text') ||
|
||||||
|
extractXmlNodeText(decoded, 'title') ||
|
||||||
|
extractXmlValue(decoded, 'plain') ||
|
||||||
|
extractXmlValue(decoded, 'text') ||
|
||||||
|
extractXmlValue(decoded, 'title') ||
|
||||||
|
''
|
||||||
|
|
||||||
|
const normalized = normalizeSystemText(plainText || fallbackSystemText(decoded))
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
content: normalized || '[系统消息]',
|
||||||
|
raw: content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseImageMessage(content: string): ParsedContent {
|
function parseImageMessage(content: string): ParsedContent {
|
||||||
// 尝试 XML 格式: <img md5="..." aeskey="..."/>
|
// 尝试 XML 格式: <img md5="..." aeskey="..."/>
|
||||||
let md5 = extractXmlAttribute(content, 'img', 'md5') || extractXmlValue(content, 'md5') || ''
|
let md5 = extractXmlAttribute(content, 'img', 'md5') || extractXmlValue(content, 'md5') || ''
|
||||||
@@ -356,6 +384,20 @@ function sanitizeQuotedContent(content: string): string {
|
|||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripChatroomPrefix(content: string): string {
|
||||||
|
return String(content || '')
|
||||||
|
.replace(/^[0-9a-z_-]+@chatroom:\s*/i, '')
|
||||||
|
.replace(/^wxid_[^:\n]+:\s*/i, '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSystemText(content: string): string {
|
||||||
|
return String(content || '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.replace(/\s+([,.;!?])/g, '$1')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
function parseVoipMessage(content: string): ParsedContent {
|
function parseVoipMessage(content: string): ParsedContent {
|
||||||
const roomTypeStr = extractXmlValue(content, 'room_type')
|
const roomTypeStr = extractXmlValue(content, 'room_type')
|
||||||
const msg = extractXmlValue(content, 'msg') || ''
|
const msg = extractXmlValue(content, 'msg') || ''
|
||||||
@@ -416,6 +458,44 @@ function decodeXmlUrl(value: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeXmlEntities(value: string): string {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractXmlNodeText(xml: string, tagName: string): string {
|
||||||
|
const match = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i').exec(xml)
|
||||||
|
if (!match?.[1]) return ''
|
||||||
|
return normalizeSystemText(
|
||||||
|
decodeXmlEntities(
|
||||||
|
match[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').replace(/<[^>]+>/g, ' ')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackSystemText(xml: string): string {
|
||||||
|
return normalizeSystemText(
|
||||||
|
decodeXmlEntities(
|
||||||
|
String(xml || '')
|
||||||
|
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDelChatroomMemberText(xml: string): string {
|
||||||
|
if (!/<sysmsg[^>]+delchatroommember/i.test(xml)) return ''
|
||||||
|
const plainMatch = /<plain[^>]*><!\[CDATA\[([\s\S]*?)\]\]><\/plain>/i.exec(xml)
|
||||||
|
if (plainMatch?.[1]) return plainMatch[1].trim()
|
||||||
|
const textMatch = /<text[^>]*><!\[CDATA\[([\s\S]*?)\]\]><\/text>/i.exec(xml)
|
||||||
|
if (textMatch?.[1]) return textMatch[1].trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMd5(value: unknown): string | undefined {
|
function normalizeMd5(value: unknown): string | undefined {
|
||||||
const md5 = String(value || '')
|
const md5 = String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
+9
-338
@@ -1,12 +1,5 @@
|
|||||||
import DatabaseConstructor from 'better-sqlite3-multiple-ciphers'
|
|
||||||
import fs from 'fs-extra'
|
|
||||||
import path from 'path'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
import os from 'os'
|
|
||||||
import { Wcdb4Client } from './wcdb4-client'
|
import { Wcdb4Client } from './wcdb4-client'
|
||||||
|
|
||||||
type Database = import('better-sqlite3-multiple-ciphers').Database
|
|
||||||
|
|
||||||
export interface UserContact {
|
export interface UserContact {
|
||||||
m_nsUsrName: string
|
m_nsUsrName: string
|
||||||
nickname: string
|
nickname: string
|
||||||
@@ -38,166 +31,22 @@ export interface GroupMemberInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WechatDb {
|
export class WechatDb {
|
||||||
private static WECHAT_DIR = path.join(
|
private wcdb4Client: Wcdb4Client
|
||||||
os.homedir(),
|
|
||||||
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9'
|
|
||||||
)
|
|
||||||
|
|
||||||
private rawKey: string
|
|
||||||
private correctUserId: string | null = null
|
|
||||||
private chatDb: { name: string; db_number: string }[] | null = null
|
|
||||||
private groupMemberCache = new Map<string, GroupMemberInfo | null>()
|
|
||||||
private wcdb4Client: Wcdb4Client | null = null
|
|
||||||
private chatMd5ToUsername = new Map<string, string>()
|
private chatMd5ToUsername = new Map<string, string>()
|
||||||
private wechat4OpenError: string | null = null
|
|
||||||
|
|
||||||
constructor(rawKey: string) {
|
constructor(rawKey: string) {
|
||||||
this.rawKey = rawKey
|
|
||||||
console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`)
|
console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`)
|
||||||
|
const client = new Wcdb4Client(rawKey)
|
||||||
if (this.tryOpenWechat4()) {
|
|
||||||
this.chatDb = this.getChatDbNumber()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(WechatDb.WECHAT_DIR)) {
|
|
||||||
throw new Error(`WeChat directory not found at ${WechatDb.WECHAT_DIR}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getUser()) {
|
|
||||||
console.log('User found, getting chat DB number')
|
|
||||||
this.chatDb = this.getChatDbNumber()
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`No valid user found or invalid key${this.wechat4OpenError ? `; WeChat 4.0 error: ${this.wechat4OpenError}` : ''}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryOpenWechat4(): boolean {
|
|
||||||
try {
|
|
||||||
const client = new Wcdb4Client(this.rawKey)
|
|
||||||
client.open()
|
client.open()
|
||||||
this.wcdb4Client = client
|
this.wcdb4Client = client
|
||||||
console.log('Opened WeChat 4.0 database with WechatExplorer WCDB native adapter')
|
for (const table of client.getChatTables()) {
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
this.wechat4OpenError = error instanceof Error ? error.message : String(error)
|
|
||||||
console.warn('WeChat 4.0 open failed, fallback to 3.0 SQLCipher mode:', error)
|
|
||||||
this.wcdb4Client = null
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private connectDb(dbPath: string): Database | null {
|
|
||||||
const targetPath = path.resolve(dbPath)
|
|
||||||
if (!fs.existsSync(targetPath)) {
|
|
||||||
console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const db = new DatabaseConstructor(targetPath, {
|
|
||||||
// verbose: console.log
|
|
||||||
})
|
|
||||||
|
|
||||||
// 配置加密方案 (Cipher Scheme)
|
|
||||||
db.pragma("cipher='sqlcipher'")
|
|
||||||
db.pragma('cipher_page_size = 1024')
|
|
||||||
db.pragma('legacy=3')
|
|
||||||
db.pragma('kdf_iter = 64000')
|
|
||||||
db.pragma('cipher_hmac_algorithm = HMAC_SHA1')
|
|
||||||
db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1')
|
|
||||||
|
|
||||||
const processedKey = this.processRawKey(this.rawKey)
|
|
||||||
// console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`)
|
|
||||||
|
|
||||||
db.pragma(`key = "x'${processedKey}'"`)
|
|
||||||
|
|
||||||
// 验证连接
|
|
||||||
db.prepare('SELECT count(*) as count FROM sqlite_master').get()
|
|
||||||
|
|
||||||
return db
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ 连接失败:`, error)
|
|
||||||
|
|
||||||
const errString = String(error)
|
|
||||||
if (errString.includes('file is not a database')) {
|
|
||||||
console.error(
|
|
||||||
"💡 诊断建议:\n1. 请确认是否已安装 'better-sqlite3-multiple-ciphers'。\n2. 检查密钥是否正确(WeChat密钥与设备强绑定)。\n3. 尝试将页大小改为 4096 (db.pragma('cipher_page_size = 4096'))。"
|
|
||||||
)
|
|
||||||
} else if (errString.includes('HMAC')) {
|
|
||||||
console.error(
|
|
||||||
'💡 诊断建议:密钥可能正确,但HMAC算法或页大小不匹配。尝试调整 cipher_hmac_algorithm。'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private processRawKey(key: string): string {
|
|
||||||
// 移除可能的 '0x' 前缀并修剪空格
|
|
||||||
return key.replace(/^0x/i, '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUser(): boolean {
|
|
||||||
const keyValuePath = path.join(WechatDb.WECHAT_DIR, 'KeyValue')
|
|
||||||
if (!fs.existsSync(keyValuePath)) return false
|
|
||||||
|
|
||||||
const contents = fs.readdirSync(keyValuePath)
|
|
||||||
const potentialUsers = contents.filter((name) => !name.startsWith('.'))
|
|
||||||
|
|
||||||
for (const user of potentialUsers) {
|
|
||||||
const dbPath = path.join(keyValuePath, user, 'KeyValue.db')
|
|
||||||
if (fs.existsSync(dbPath)) {
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (db) {
|
|
||||||
this.correctUserId = user
|
|
||||||
console.log(`Valid user found: ${user}`)
|
|
||||||
db.close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChatDbNumber(): { name: string; db_number: string }[] {
|
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const chatDb = this.wcdb4Client.getChatTables()
|
|
||||||
this.chatMd5ToUsername.clear()
|
|
||||||
for (const table of chatDb) {
|
|
||||||
if (table.name.startsWith('Chat_')) {
|
if (table.name.startsWith('Chat_')) {
|
||||||
this.chatMd5ToUsername.set(table.name.substring(5), table.db_number)
|
this.chatMd5ToUsername.set(table.name.substring(5), table.db_number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chatDb
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatDb: { name: string; db_number: string }[] = []
|
|
||||||
if (!this.correctUserId) return []
|
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', `msg_${i}.db`)
|
|
||||||
if (!fs.existsSync(dbPath)) continue
|
|
||||||
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (db) {
|
|
||||||
const tables = db
|
|
||||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
|
|
||||||
.all() as { name: string }[]
|
|
||||||
for (const table of tables) {
|
|
||||||
chatDb.push({ name: table.name, db_number: `msg_${i}.db` })
|
|
||||||
}
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chatDb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserList(nicknameFilter?: string): UserContact[] {
|
public getUserList(nicknameFilter?: string): UserContact[] {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const keyword = (nicknameFilter || '').trim().toLowerCase()
|
const keyword = (nicknameFilter || '').trim().toLowerCase()
|
||||||
return this.wcdb4Client
|
return this.wcdb4Client
|
||||||
.getSessions()
|
.getSessions()
|
||||||
@@ -215,23 +64,7 @@ export class WechatDb {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.correctUserId) return []
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Contact/wccontact_new2.db')
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) return []
|
|
||||||
|
|
||||||
let query = "SELECT m_nsUsrName, nickname FROM WCContact WHERE m_nsUsrName NOT LIKE 'gh_%'"
|
|
||||||
if (nicknameFilter) {
|
|
||||||
query += ` AND nickname LIKE '%${nicknameFilter}%'`
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = db.prepare(query).all() as unknown as UserContact[]
|
|
||||||
db.close()
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllGroupContacts(): Record<string, string> {
|
public getAllGroupContacts(): Record<string, string> {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const groupContacts: Record<string, string> = {}
|
const groupContacts: Record<string, string> = {}
|
||||||
for (const session of this.wcdb4Client.getSessions()) {
|
for (const session of this.wcdb4Client.getSessions()) {
|
||||||
if (session.username.endsWith('@chatroom')) {
|
if (session.username.endsWith('@chatroom')) {
|
||||||
@@ -241,29 +74,7 @@ export class WechatDb {
|
|||||||
return groupContacts
|
return groupContacts
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.correctUserId) return {}
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) return {}
|
|
||||||
|
|
||||||
const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupContact').all() as {
|
|
||||||
m_nsUsrName: string
|
|
||||||
nickname: string
|
|
||||||
}[]
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
const groupContacts: Record<string, string> = {}
|
|
||||||
for (const row of results) {
|
|
||||||
if (row.m_nsUsrName && row.nickname) {
|
|
||||||
const md5 = this.md5(row.m_nsUsrName)
|
|
||||||
groupContacts[md5] = row.nickname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groupContacts
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllGroupMembers(): Record<string, string> {
|
public getAllGroupMembers(): Record<string, string> {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const members: Record<string, string> = {}
|
const members: Record<string, string> = {}
|
||||||
for (const session of this.wcdb4Client.getSessions()) {
|
for (const session of this.wcdb4Client.getSessions()) {
|
||||||
if (!session.username.endsWith('@chatroom')) continue
|
if (!session.username.endsWith('@chatroom')) continue
|
||||||
@@ -276,30 +87,7 @@ export class WechatDb {
|
|||||||
return members
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.correctUserId) return {}
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) return {}
|
|
||||||
|
|
||||||
// 查询 GroupMember 表
|
|
||||||
const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupMember').all() as {
|
|
||||||
m_nsUsrName: string
|
|
||||||
nickname: string
|
|
||||||
}[]
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
const groupMembers: Record<string, string> = {}
|
|
||||||
for (const row of results) {
|
|
||||||
if (row.m_nsUsrName && row.nickname) {
|
|
||||||
// 将 wxid (m_nsUsrName) 映射到昵称
|
|
||||||
groupMembers[row.m_nsUsrName] = row.nickname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groupMembers
|
|
||||||
}
|
|
||||||
|
|
||||||
public getGroupMembersForChat(userMd5: string): Record<string, string> {
|
public getGroupMembersForChat(userMd5: string): Record<string, string> {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const username = this.chatMd5ToUsername.get(userMd5)
|
const username = this.chatMd5ToUsername.get(userMd5)
|
||||||
if (!username || !username.endsWith('@chatroom')) return {}
|
if (!username || !username.endsWith('@chatroom')) return {}
|
||||||
|
|
||||||
@@ -312,11 +100,8 @@ export class WechatDb {
|
|||||||
return members
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getAllGroupMembers()
|
|
||||||
}
|
|
||||||
|
|
||||||
public getGroupMember(wxid: string, chatroomId?: string): GroupMemberInfo | null {
|
public getGroupMember(wxid: string, chatroomId?: string): GroupMemberInfo | null {
|
||||||
if (this.wcdb4Client && chatroomId) {
|
if (!chatroomId) return null
|
||||||
return (
|
return (
|
||||||
this.wcdb4Client
|
this.wcdb4Client
|
||||||
.getGroupMembers(chatroomId)
|
.getGroupMembers(chatroomId)
|
||||||
@@ -324,53 +109,23 @@ export class WechatDb {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.correctUserId) return null
|
|
||||||
|
|
||||||
// 检查缓存
|
|
||||||
if (this.groupMemberCache.has(wxid)) {
|
|
||||||
return this.groupMemberCache.get(wxid) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) {
|
|
||||||
this.groupMemberCache.set(wxid, null)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = db
|
|
||||||
.prepare(
|
|
||||||
'SELECT m_nsUsrName, nickname, m_nsHeadImgUrl FROM GroupMember WHERE m_nsUsrName = ?'
|
|
||||||
)
|
|
||||||
.get(wxid) as GroupMemberInfo
|
|
||||||
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
// 写入缓存
|
|
||||||
this.groupMemberCache.set(wxid, result || null)
|
|
||||||
|
|
||||||
return result || null
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllChatTables(): { name: string; db_number: string }[] {
|
public getAllChatTables(): { name: string; db_number: string }[] {
|
||||||
return this.chatDb || []
|
return this.wcdb4Client.getChatTables()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMyAvatarUrl(): string | undefined {
|
public getMyAvatarUrl(): string | undefined {
|
||||||
return this.wcdb4Client?.getMyAvatarUrl()
|
return this.wcdb4Client.getMyAvatarUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWcdb4Client(): Wcdb4Client | null {
|
public getWcdb4Client(): Wcdb4Client {
|
||||||
return this.wcdb4Client
|
return this.wcdb4Client
|
||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(): void {
|
||||||
this.wcdb4Client?.close()
|
this.wcdb4Client.close()
|
||||||
this.wcdb4Client = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] {
|
public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const username = this.chatMd5ToUsername.get(userMd5)
|
const username = this.chatMd5ToUsername.get(userMd5)
|
||||||
if (!username) return []
|
if (!username) return []
|
||||||
return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({
|
return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({
|
||||||
@@ -379,57 +134,7 @@ export class WechatDb {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.chatDb || !this.correctUserId) return []
|
|
||||||
|
|
||||||
const tableName = `Chat_${userMd5}`
|
|
||||||
const tableInfo = this.chatDb.find((t) => t.name === tableName)
|
|
||||||
|
|
||||||
if (!tableInfo) {
|
|
||||||
console.error(`No table found for ${tableName}`)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbPath = path.join(
|
|
||||||
WechatDb.WECHAT_DIR,
|
|
||||||
this.correctUserId,
|
|
||||||
'Message',
|
|
||||||
tableInfo.db_number
|
|
||||||
)
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) return []
|
|
||||||
|
|
||||||
try {
|
|
||||||
let query = `SELECT * FROM ${tableName}`
|
|
||||||
const conditions: string[] = []
|
|
||||||
|
|
||||||
// console.log(startTime, endTime, 'time range')
|
|
||||||
|
|
||||||
if (startTime) {
|
|
||||||
conditions.push(`msgCreateTime >= ${startTime}`)
|
|
||||||
}
|
|
||||||
if (endTime) {
|
|
||||||
conditions.push(`msgCreateTime <= ${endTime}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conditions.length > 0) {
|
|
||||||
query += ` WHERE ${conditions.join(' AND ')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加排序以确保存储正确的顺序
|
|
||||||
query += ` ORDER BY msgCreateTime ASC`
|
|
||||||
|
|
||||||
const results = db.prepare(query).all() as unknown as WechatMessage[]
|
|
||||||
db.close()
|
|
||||||
return results
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error querying messages for ${tableName}:`, e)
|
|
||||||
db.close()
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public searchAllMessages(keyword: string): string | null {
|
public searchAllMessages(keyword: string): string | null {
|
||||||
if (this.wcdb4Client) {
|
|
||||||
const lowerKeyword = keyword.trim().toLowerCase()
|
const lowerKeyword = keyword.trim().toLowerCase()
|
||||||
if (!lowerKeyword) return null
|
if (!lowerKeyword) return null
|
||||||
for (const session of this.wcdb4Client.getSessions()) {
|
for (const session of this.wcdb4Client.getSessions()) {
|
||||||
@@ -441,41 +146,7 @@ export class WechatDb {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.correctUserId) return null
|
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const dbName = `msg_${i}.db`
|
|
||||||
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', dbName)
|
|
||||||
if (!fs.existsSync(dbPath)) continue
|
|
||||||
|
|
||||||
const db = this.connectDb(dbPath)
|
|
||||||
if (!db) continue
|
|
||||||
|
|
||||||
const tables = db
|
|
||||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
|
|
||||||
.all() as { name: string }[]
|
|
||||||
for (const table of tables) {
|
|
||||||
try {
|
|
||||||
// 首先检查 msgContent 列是否存在以避免错误?
|
|
||||||
// 或者像 Swift 代码中那样使用 try/catch "抑制错误"
|
|
||||||
const results = db
|
|
||||||
.prepare(`SELECT 1 FROM ${table.name} WHERE msgContent LIKE '%${keyword}%' LIMIT 1`)
|
|
||||||
.get()
|
|
||||||
if (results) {
|
|
||||||
console.log(`Found keyword in ${table.name} (${dbName})`)
|
|
||||||
db.close()
|
|
||||||
return table.name
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 忽略没有 msgContent 的表
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
public md5(str: string): string {
|
public md5(str: string): string {
|
||||||
return crypto.createHash('md5').update(str).digest('hex')
|
return this.wcdb4Client.md5(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -39,6 +39,11 @@ declare global {
|
|||||||
) => Promise<boolean | { success: boolean; error?: string; monitoring?: boolean }>
|
) => Promise<boolean | { success: boolean; error?: string; monitoring?: boolean }>
|
||||||
getContacts: (filter?: string) => Promise<Contact[]>
|
getContacts: (filter?: string) => Promise<Contact[]>
|
||||||
getMessages: (userMd5: string, startTime?: number, endTime?: number) => Promise<Message[]>
|
getMessages: (userMd5: string, startTime?: number, endTime?: number) => Promise<Message[]>
|
||||||
|
getGroupSnapshot: (userMd5: string) => Promise<{
|
||||||
|
roomId: string
|
||||||
|
memberCount: number
|
||||||
|
members: { wxid: string; nickname: string; avatar: string }[]
|
||||||
|
} | null>
|
||||||
search: (keyword: string) => Promise<string | null>
|
search: (keyword: string) => Promise<string | null>
|
||||||
aiChat: (
|
aiChat: (
|
||||||
messages: { role: string; content: string }[],
|
messages: { role: string; content: string }[],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const api = {
|
|||||||
getContacts: (filter?: string) => ipcRenderer.invoke('db:getContacts', filter),
|
getContacts: (filter?: string) => ipcRenderer.invoke('db:getContacts', filter),
|
||||||
getMessages: (userMd5: string, startTime?: number, endTime?: number) =>
|
getMessages: (userMd5: string, startTime?: number, endTime?: number) =>
|
||||||
ipcRenderer.invoke('db:getMessages', userMd5, startTime, endTime),
|
ipcRenderer.invoke('db:getMessages', userMd5, startTime, endTime),
|
||||||
|
getGroupSnapshot: (userMd5: string) => ipcRenderer.invoke('db:getGroupSnapshot', userMd5),
|
||||||
search: (keyword: string) => ipcRenderer.invoke('db:search', keyword),
|
search: (keyword: string) => ipcRenderer.invoke('db:search', keyword),
|
||||||
aiChat: (
|
aiChat: (
|
||||||
messages: { role: string; content: string }[],
|
messages: { role: string; content: string }[],
|
||||||
|
|||||||
+144
-4
@@ -21,6 +21,67 @@ const areMessagesEquivalent = (left: Message[], right: Message[]): boolean => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GroupSnapshot = {
|
||||||
|
roomId: string
|
||||||
|
memberCount: number
|
||||||
|
members: { wxid: string; nickname: string; avatar: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatGroupMemberName = (member: GroupSnapshot['members'][number]): string =>
|
||||||
|
member.nickname || member.wxid
|
||||||
|
|
||||||
|
const buildSyntheticGroupMessages = (
|
||||||
|
previous: GroupSnapshot | null,
|
||||||
|
next: GroupSnapshot | null,
|
||||||
|
referenceMessages: Message[]
|
||||||
|
): Message[] => {
|
||||||
|
if (!previous || !next || previous.roomId !== next.roomId) return []
|
||||||
|
|
||||||
|
const previousMap = new Map(previous.members.map((member) => [member.wxid, member]))
|
||||||
|
const nextMap = new Map(next.members.map((member) => [member.wxid, member]))
|
||||||
|
const latestMessageTime = referenceMessages.reduce(
|
||||||
|
(max, message) => Math.max(max, message.createTime || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const fallbackNow = Math.floor(Date.now() / 1000)
|
||||||
|
const events: Message[] = []
|
||||||
|
|
||||||
|
let offset = 1
|
||||||
|
for (const [wxid, member] of previousMap.entries()) {
|
||||||
|
if (nextMap.has(wxid)) continue
|
||||||
|
const name = formatGroupMemberName(member)
|
||||||
|
const eventTime = Math.max(latestMessageTime + offset, fallbackNow)
|
||||||
|
const eventDate = new Date(eventTime * 1000)
|
||||||
|
offset += 1
|
||||||
|
events.push({
|
||||||
|
id: `synthetic-leave:${next.roomId}:${wxid}:${eventTime}`,
|
||||||
|
from: 'system',
|
||||||
|
type: '系统消息',
|
||||||
|
datetime: eventDate.toLocaleString('zh-CN', { hour12: false }),
|
||||||
|
content: `${name} 退出了群聊`,
|
||||||
|
isSender: false,
|
||||||
|
createTime: eventTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
console.log(
|
||||||
|
`[GroupMonitor] synthetic leave detected roomId=${next.roomId} events=${events
|
||||||
|
.map((event) => event.content)
|
||||||
|
.join(' | ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortMessagesChronologically = (items: Message[]): Message[] =>
|
||||||
|
[...items].sort((left, right) => {
|
||||||
|
const timeDelta = (left.createTime || 0) - (right.createTime || 0)
|
||||||
|
if (timeDelta !== 0) return timeDelta
|
||||||
|
return getMessageIdentity(left).localeCompare(getMessageIdentity(right))
|
||||||
|
})
|
||||||
|
|
||||||
function App(): React.ReactElement {
|
function App(): React.ReactElement {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
const [dbKey, setDbKey] = useState(import.meta.env.VITE_DB_KEY || '')
|
const [dbKey, setDbKey] = useState(import.meta.env.VITE_DB_KEY || '')
|
||||||
@@ -36,6 +97,8 @@ function App(): React.ReactElement {
|
|||||||
const [showDbKey, setShowDbKey] = useState(false)
|
const [showDbKey, setShowDbKey] = useState(false)
|
||||||
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
||||||
const [isNativeMonitorActive, setIsNativeMonitorActive] = useState(false)
|
const [isNativeMonitorActive, setIsNativeMonitorActive] = useState(false)
|
||||||
|
const currentGroupSnapshotRef = React.useRef<GroupSnapshot | null>(null)
|
||||||
|
const syntheticGroupMessagesRef = React.useRef<Record<string, Message[]>>({})
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let active = true
|
let active = true
|
||||||
@@ -84,6 +147,36 @@ function App(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logGroupSnapshot = React.useCallback(
|
||||||
|
async (contact: Contact | null, reason: string): Promise<GroupSnapshot | null> => {
|
||||||
|
if (!contact || contact.type !== 'group') return null
|
||||||
|
try {
|
||||||
|
const snapshot = (await window.api.getGroupSnapshot(contact.md5)) as GroupSnapshot | null
|
||||||
|
if (!snapshot) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return snapshot
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[GroupSnapshot] reason=${reason} name=${contact.m_nsNickName} failed:`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeSyntheticMessages = React.useCallback(
|
||||||
|
(contact: Contact | null, baseMessages: Message[], roomId?: string): Message[] => {
|
||||||
|
if (!contact || contact.type !== 'group') return baseMessages
|
||||||
|
const resolvedRoomId = roomId || currentGroupSnapshotRef.current?.roomId
|
||||||
|
if (!resolvedRoomId) return baseMessages
|
||||||
|
const synthetic = syntheticGroupMessagesRef.current[resolvedRoomId] || []
|
||||||
|
return synthetic.length
|
||||||
|
? sortMessagesChronologically([...baseMessages, ...synthetic])
|
||||||
|
: baseMessages
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const handleAutoGetDbKey = async (): Promise<void> => {
|
const handleAutoGetDbKey = async (): Promise<void> => {
|
||||||
if (isFetchingDbKey) return
|
if (isFetchingDbKey) return
|
||||||
setIsFetchingDbKey(true)
|
setIsFetchingDbKey(true)
|
||||||
@@ -175,14 +268,18 @@ function App(): React.ReactElement {
|
|||||||
setSelectedContact(contact)
|
setSelectedContact(contact)
|
||||||
const { startTime, endTime } = getDateRangeParams(dateRange)
|
const { startTime, endTime } = getDateRangeParams(dateRange)
|
||||||
const msgs = await window.api.getMessages(contact.md5, startTime, endTime)
|
const msgs = await window.api.getMessages(contact.md5, startTime, endTime)
|
||||||
setMessages(msgs)
|
const snapshot = await logGroupSnapshot(contact, 'select-contact')
|
||||||
|
currentGroupSnapshotRef.current = snapshot
|
||||||
|
setMessages(mergeSyntheticMessages(contact, msgs, snapshot?.roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDateRangeChange = (range: string): void => {
|
const handleDateRangeChange = (range: string): void => {
|
||||||
setDateRange(range)
|
setDateRange(range)
|
||||||
if (selectedContact) {
|
if (selectedContact) {
|
||||||
const { startTime, endTime } = getDateRangeParams(range)
|
const { startTime, endTime } = getDateRangeParams(range)
|
||||||
window.api.getMessages(selectedContact.md5, startTime, endTime).then(setMessages)
|
window.api.getMessages(selectedContact.md5, startTime, endTime).then((nextMessages) => {
|
||||||
|
setMessages(mergeSyntheticMessages(selectedContact, nextMessages))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,9 +298,45 @@ function App(): React.ReactElement {
|
|||||||
range.startTime,
|
range.startTime,
|
||||||
range.endTime
|
range.endTime
|
||||||
)
|
)
|
||||||
|
const latestSnapshot = await logGroupSnapshot(selectedContact, 'wcdb-change')
|
||||||
|
const syntheticEvents = buildSyntheticGroupMessages(
|
||||||
|
currentGroupSnapshotRef.current,
|
||||||
|
latestSnapshot,
|
||||||
|
latestMessages
|
||||||
|
)
|
||||||
|
if (latestSnapshot) {
|
||||||
|
currentGroupSnapshotRef.current = latestSnapshot
|
||||||
|
if (syntheticEvents.length) {
|
||||||
|
const existing = syntheticGroupMessagesRef.current[latestSnapshot.roomId] || []
|
||||||
|
const existingIds = new Set(existing.map((message) => message.id))
|
||||||
|
const appended = syntheticEvents.filter((message) => !existingIds.has(message.id))
|
||||||
|
if (appended.length) {
|
||||||
|
syntheticGroupMessagesRef.current[latestSnapshot.roomId] = [...existing, ...appended]
|
||||||
|
console.log(
|
||||||
|
`[GroupMonitor] merged synthetic messages roomId=${latestSnapshot.roomId} total=${syntheticGroupMessagesRef.current[latestSnapshot.roomId].length}`
|
||||||
|
)
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setMessages((current) =>
|
setMessages((current) =>
|
||||||
areMessagesEquivalent(current, latestMessages) ? current : latestMessages
|
sortMessagesChronologically([
|
||||||
|
...current,
|
||||||
|
...appended.filter(
|
||||||
|
(message) =>
|
||||||
|
!current.some((existingMessage) => existingMessage.id === message.id)
|
||||||
|
)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextMessages = mergeSyntheticMessages(
|
||||||
|
selectedContact,
|
||||||
|
latestMessages,
|
||||||
|
latestSnapshot?.roomId
|
||||||
|
)
|
||||||
|
if (!disposed) {
|
||||||
|
setMessages((current) =>
|
||||||
|
areMessagesEquivalent(current, nextMessages) ? current : nextMessages
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -224,7 +357,14 @@ function App(): React.ReactElement {
|
|||||||
if (refreshTimer) window.clearTimeout(refreshTimer)
|
if (refreshTimer) window.clearTimeout(refreshTimer)
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
}
|
}
|
||||||
}, [dateRange, isAuthenticated, isNativeMonitorActive, selectedContact])
|
}, [
|
||||||
|
dateRange,
|
||||||
|
isAuthenticated,
|
||||||
|
isNativeMonitorActive,
|
||||||
|
selectedContact,
|
||||||
|
logGroupSnapshot,
|
||||||
|
mergeSyntheticMessages
|
||||||
|
])
|
||||||
|
|
||||||
const handleSearchContacts = (keyword: string): void => {
|
const handleSearchContacts = (keyword: string): void => {
|
||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
|
|||||||
@@ -233,7 +233,6 @@ body {
|
|||||||
|
|
||||||
.wechat-message-list {
|
.wechat-message-list {
|
||||||
padding: 18px 28px;
|
padding: 18px 28px;
|
||||||
gap: 14px;
|
|
||||||
background-color: #edf1f2;
|
background-color: #edf1f2;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 20px 20px, rgba(0, 0, 0, 0.025) 1px, transparent 1px),
|
radial-gradient(circle at 20px 20px, rgba(0, 0, 0, 0.025) 1px, transparent 1px),
|
||||||
@@ -241,6 +240,10 @@ body {
|
|||||||
background-size: 120px 120px;
|
background-size: 120px 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wechat-message-row {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.wechat-message-row {
|
.wechat-message-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -252,6 +255,31 @@ body {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wechat-system-message-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 2px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-system-message {
|
||||||
|
max-width: min(78%, 760px);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(227, 86, 86, 0.1);
|
||||||
|
color: #c84a4a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-system-message-meta {
|
||||||
|
color: #a0a7ab;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.message-avatar {
|
.message-avatar {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
@@ -297,7 +325,6 @@ body {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-message-row.mine .message-bubble {
|
.wechat-message-row.mine .message-bubble {
|
||||||
@@ -865,9 +892,43 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-modal {
|
.image-preview-modal {
|
||||||
background: transparent;
|
width: min(520px, 92vw);
|
||||||
box-shadow: none;
|
max-height: 88vh;
|
||||||
padding: 0;
|
background: #f7f8fa;
|
||||||
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-preview-frame {
|
||||||
|
border: 1px solid #d8dde3;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #eef1f4;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-preview-scroller {
|
||||||
|
max-height: calc(88vh - 120px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-preview-image {
|
||||||
|
display: block;
|
||||||
|
width: min(430px, 100%);
|
||||||
|
height: auto;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ccd3d9;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-preview-actions {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-viewer-overlay {
|
.image-viewer-overlay {
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
<div className="message-list wechat-message-list">
|
<div className="message-list wechat-message-list">
|
||||||
{filteredMessages.map((msg) => {
|
{filteredMessages.map((msg) => {
|
||||||
const isMine = msg.from === 'assistant'
|
const isMine = msg.from === 'assistant'
|
||||||
|
const isSystem = msg.from === 'system' || msg.type === '系统消息'
|
||||||
const displayName = isMine
|
const displayName = isMine
|
||||||
? '我'
|
? '我'
|
||||||
: isGroupChat
|
: isGroupChat
|
||||||
@@ -312,7 +313,18 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
const avatarSrc = isMine ? msg.img : msg.img || contact.avatar
|
const avatarSrc = isMine ? msg.img : msg.img || contact.avatar
|
||||||
const isVoice = msg.type === '语音'
|
const isVoice = msg.type === '语音'
|
||||||
const isImage = msg.type === '图片'
|
const isImage = msg.type === '图片'
|
||||||
const isRichMedia = ['名片', '位置', '分享消息', '通话', '表情包'].includes(msg.type)
|
const isRichMedia = ['名片', '位置', '分享消息', '通话', '表情包', '系统消息'].includes(
|
||||||
|
msg.type
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSystem) {
|
||||||
|
return (
|
||||||
|
<div key={msg.id} className="wechat-system-message-row">
|
||||||
|
<div className="wechat-system-message">{msg.content}</div>
|
||||||
|
<div className="wechat-system-message-meta">{msg.datetime}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={msg.id} className={`wechat-message-row ${isMine ? 'mine' : 'other'}`}>
|
<div key={msg.id} className={`wechat-message-row ${isMine ? 'mine' : 'other'}`}>
|
||||||
@@ -420,19 +432,16 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
{generatedImage && (
|
{generatedImage && (
|
||||||
<div className="modal-overlay" onClick={() => setGeneratedImage(null)}>
|
<div className="modal-overlay" onClick={() => setGeneratedImage(null)}>
|
||||||
<div className="modal-content image-preview-modal" onClick={(e) => e.stopPropagation()}>
|
<div className="modal-content image-preview-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="report-preview-frame">
|
||||||
|
<div className="report-preview-scroller">
|
||||||
<img
|
<img
|
||||||
src={generatedImage}
|
src={generatedImage}
|
||||||
alt="Generated Summary"
|
alt="Generated Summary"
|
||||||
style={{ maxWidth: '100%', maxHeight: '80vh', border: '1px solid #ccc' }}
|
className="report-preview-image"
|
||||||
/>
|
/>
|
||||||
<div
|
</div>
|
||||||
style={{
|
</div>
|
||||||
marginTop: '10px',
|
<div className="report-preview-actions">
|
||||||
display: 'flex',
|
|
||||||
// justifyContent: 'flex-end',
|
|
||||||
gap: '10px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyImage}
|
onClick={handleCopyImage}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export function RichMessageBubble({ contentData }: RichMessageBubbleProps): JSX.
|
|||||||
return <StickerBubble data={contentData} />
|
return <StickerBubble data={contentData} />
|
||||||
case 'quote':
|
case 'quote':
|
||||||
return <QuoteBubble data={contentData} />
|
return <QuoteBubble data={contentData} />
|
||||||
|
case 'system':
|
||||||
|
return <SystemBubble data={contentData} />
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
return (
|
return (
|
||||||
<div className="message-text">{(contentData as { raw?: string }).raw || '[未知消息]'}</div>
|
<div className="message-text">{(contentData as { raw?: string }).raw || '[未知消息]'}</div>
|
||||||
@@ -212,6 +214,10 @@ function QuoteBubble({ data }: { data: Extract<ParsedContent, { type: 'quote' }>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SystemBubble({ data }: { data: Extract<ParsedContent, { type: 'system' }> }): JSX.Element {
|
||||||
|
return <div className="message-text">{data.content || '[系统消息]'}</div>
|
||||||
|
}
|
||||||
|
|
||||||
function getUrlHost(url?: string): string {
|
function getUrlHost(url?: string): string {
|
||||||
if (!url) return ''
|
if (!url) return ''
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ type QuoteContent = {
|
|||||||
quotedSender?: string
|
quotedSender?: string
|
||||||
quotedType?: string
|
quotedType?: string
|
||||||
}
|
}
|
||||||
type SystemContent = { type: 'system'; content: string }
|
type SystemContent = { type: 'system'; content: string; raw?: string }
|
||||||
type UnknownContent = { type: 'unknown'; raw: string }
|
type UnknownContent = { type: 'unknown'; raw: string }
|
||||||
|
|
||||||
export type ParsedContent =
|
export type ParsedContent =
|
||||||
|
|||||||
Reference in New Issue
Block a user