mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
feat: 提交
This commit is contained in:
+58
-34
@@ -17,7 +17,7 @@ import { extname } from 'path'
|
|||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
import { WechatDb } from './wechat-db'
|
import { WechatDb } from './wechat-db'
|
||||||
import { bootstrapWcdbNative } from './wcdb4-client'
|
import { bootstrapWcdbNative, Wcdb4Client } from './wcdb4-client'
|
||||||
import { VoiceService } from './voice-service'
|
import { VoiceService } from './voice-service'
|
||||||
import { StickerService } from './sticker-service'
|
import { StickerService } from './sticker-service'
|
||||||
import { parseMessageContent } from './message-parser'
|
import { parseMessageContent } from './message-parser'
|
||||||
@@ -94,6 +94,51 @@ const keyServiceMac = new KeyServiceMac()
|
|||||||
const keyServiceWin = new KeyServiceWin()
|
const keyServiceWin = new KeyServiceWin()
|
||||||
let tray: Tray | null = null
|
let tray: Tray | null = null
|
||||||
let recallArchiveMonitor: RecallArchiveMonitor | null = null
|
let recallArchiveMonitor: RecallArchiveMonitor | null = null
|
||||||
|
let recallProtectionGeneration = 0
|
||||||
|
|
||||||
|
function configureRecallProtection(
|
||||||
|
wcdb4Client: Wcdb4Client,
|
||||||
|
accountRoot: string,
|
||||||
|
enabled: boolean
|
||||||
|
): void {
|
||||||
|
recallProtectionGeneration += 1
|
||||||
|
const generation = recallProtectionGeneration
|
||||||
|
recallArchiveMonitor?.stop()
|
||||||
|
recallArchiveMonitor = null
|
||||||
|
configureRecallArchive(enabled ? accountRoot : '')
|
||||||
|
if (!enabled) return
|
||||||
|
|
||||||
|
const monitor = new RecallArchiveMonitor(
|
||||||
|
() =>
|
||||||
|
wcdb4Client.getSessions().map((session) => ({
|
||||||
|
md5: wcdb4Client.md5(session.username),
|
||||||
|
m_nsUsrName: session.username,
|
||||||
|
type: session.username.endsWith('@chatroom') ? 'group' : 'user',
|
||||||
|
activityKey: [
|
||||||
|
session.raw['last_timestamp'],
|
||||||
|
session.raw['sort_timestamp'],
|
||||||
|
session.raw['last_msg_locald_id'],
|
||||||
|
session.raw['last_msg_type'],
|
||||||
|
session.raw['summary']
|
||||||
|
].join(':')
|
||||||
|
})),
|
||||||
|
(sessionMd5) =>
|
||||||
|
chat.listMessages(sessionMd5, Math.floor(Date.now() / 1000) - 10 * 60, undefined, {
|
||||||
|
limit: 500
|
||||||
|
})
|
||||||
|
)
|
||||||
|
recallArchiveMonitor = monitor
|
||||||
|
monitor.seedAll()
|
||||||
|
setTimeout(() => {
|
||||||
|
if (generation !== recallProtectionGeneration || recallArchiveMonitor !== monitor) return
|
||||||
|
const result = wcdb4Client.installRecallJournal(
|
||||||
|
wcdb4Client.getSessions().map((session) => session.username)
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
`[WCDB4] recall journal ready installed=${result.installed} failed=${result.failed}`
|
||||||
|
)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
const packagedIconPath = join(process.resourcesPath, 'resources', 'icon.png')
|
const packagedIconPath = join(process.resourcesPath, 'resources', 'icon.png')
|
||||||
const appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon
|
const appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon
|
||||||
@@ -316,36 +361,7 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
chat.setChatDb(nextWechatDb)
|
chat.setChatDb(nextWechatDb)
|
||||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||||
configureRecallArchive(resolvedRoot)
|
configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled)
|
||||||
recallArchiveMonitor?.stop()
|
|
||||||
recallArchiveMonitor = new RecallArchiveMonitor(
|
|
||||||
() =>
|
|
||||||
wcdb4Client.getSessions().map((session) => ({
|
|
||||||
md5: wcdb4Client.md5(session.username),
|
|
||||||
m_nsUsrName: session.username,
|
|
||||||
type: session.username.endsWith('@chatroom') ? 'group' : 'user',
|
|
||||||
activityKey: [
|
|
||||||
session.raw['last_timestamp'],
|
|
||||||
session.raw['sort_timestamp'],
|
|
||||||
session.raw['last_msg_locald_id'],
|
|
||||||
session.raw['last_msg_type'],
|
|
||||||
session.raw['summary']
|
|
||||||
].join(':')
|
|
||||||
})),
|
|
||||||
(sessionMd5) =>
|
|
||||||
chat.listMessages(sessionMd5, Math.floor(Date.now() / 1000) - 10 * 60, undefined, {
|
|
||||||
limit: 500
|
|
||||||
})
|
|
||||||
)
|
|
||||||
recallArchiveMonitor.seedAll()
|
|
||||||
setTimeout(() => {
|
|
||||||
const result = wcdb4Client.installRecallJournal(
|
|
||||||
wcdb4Client.getSessions().map((session) => session.username)
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
`[WCDB4] recall journal ready installed=${result.installed} failed=${result.failed}`
|
|
||||||
)
|
|
||||||
}, 0)
|
|
||||||
voiceService = new VoiceService(wcdb4Client)
|
voiceService = new VoiceService(wcdb4Client)
|
||||||
stickerService = new StickerService(wcdb4Client)
|
stickerService = new StickerService(wcdb4Client)
|
||||||
videoAssetService = new VideoAssetService(wcdb4Client)
|
videoAssetService = new VideoAssetService(wcdb4Client)
|
||||||
@@ -796,13 +812,21 @@ app.whenReady().then(async () => {
|
|||||||
const xorKey = patch.imageXorKey ?? current.xorKey ?? '0x40'
|
const xorKey = patch.imageXorKey ?? current.xorKey ?? '0x40'
|
||||||
const aesKey = patch.imageAesKey ?? current.aesKey ?? ''
|
const aesKey = patch.imageAesKey ?? current.aesKey ?? ''
|
||||||
const includesImageKey = 'imageXorKey' in patch || 'imageAesKey' in patch
|
const includesImageKey = 'imageXorKey' in patch || 'imageAesKey' in patch
|
||||||
|
const nextSettings = saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' })
|
||||||
if (includesImageKey) {
|
if (includesImageKey) {
|
||||||
saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' })
|
|
||||||
if (aesKey) imageKeyConfigService.save({ resourceRoot, xorKey, aesKey })
|
if (aesKey) imageKeyConfigService.save({ resourceRoot, xorKey, aesKey })
|
||||||
else imageKeyConfigService.clear()
|
else imageKeyConfigService.clear()
|
||||||
imageDecryptService = null
|
imageDecryptService = null
|
||||||
} else {
|
}
|
||||||
saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' })
|
if ('recallProtectionEnabled' in patch && chat.isReady()) {
|
||||||
|
const currentDb = chat.getChatDb()
|
||||||
|
if (currentDb) {
|
||||||
|
configureRecallProtection(
|
||||||
|
currentDb.getWcdb4Client(),
|
||||||
|
chat.getCurrentAccountRoot(),
|
||||||
|
nextSettings.recallProtectionEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
settings: imageKeyConfigService.getLegacySettingsView(),
|
settings: imageKeyConfigService.getLegacySettingsView(),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { app } from 'electron'
|
|||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import { discoverWindowsDbRoots } from '../windows-db-root-discovery'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把 V3 时代的 "...\\Documents\\WeChat Files" 路径重定向到
|
* 把 V3 时代的 "...\\Documents\\WeChat Files" 路径重定向到
|
||||||
@@ -29,6 +28,7 @@ export interface AppSettings {
|
|||||||
imageXorKey: string
|
imageXorKey: string
|
||||||
imageAesKey: string
|
imageAesKey: string
|
||||||
imageKeyFallbackDisabled: boolean
|
imageKeyFallbackDisabled: boolean
|
||||||
|
recallProtectionEnabled: boolean
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
autoLoginPreferenceSet: boolean
|
autoLoginPreferenceSet: boolean
|
||||||
}
|
}
|
||||||
@@ -55,8 +55,6 @@ function getDefaultDbRootCandidates(home: string): string[] {
|
|||||||
path.join(os.homedir(), 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
path.join(os.homedir(), 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
||||||
]
|
]
|
||||||
|
|
||||||
candidates.push(...discoverWindowsDbRoots())
|
|
||||||
|
|
||||||
return unique(candidates)
|
return unique(candidates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +104,7 @@ const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
imageXorKey: '',
|
imageXorKey: '',
|
||||||
imageAesKey: '',
|
imageAesKey: '',
|
||||||
imageKeyFallbackDisabled: false,
|
imageKeyFallbackDisabled: false,
|
||||||
|
recallProtectionEnabled: false,
|
||||||
autoLogin: ['1', 'true', 'yes', 'on'].includes(
|
autoLogin: ['1', 'true', 'yes', 'on'].includes(
|
||||||
String(import.meta.env.VITE_AUTO_LOGIN || '')
|
String(import.meta.env.VITE_AUTO_LOGIN || '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import { discoverWindowsDbRoots } from './windows-db-root-discovery'
|
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { createRequire } from 'module'
|
import { createRequire } from 'module'
|
||||||
import { createConnection, Socket } from 'net'
|
import { createConnection, Socket } from 'net'
|
||||||
@@ -319,7 +318,6 @@ export class Wcdb4Client {
|
|||||||
path.join(home, 'Documents', 'xwechat_files'),
|
path.join(home, 'Documents', 'xwechat_files'),
|
||||||
path.join(home, 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
path.join(home, 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
||||||
]
|
]
|
||||||
candidates.push(...discoverWindowsDbRoots())
|
|
||||||
return Array.from(new Set(candidates))
|
return Array.from(new Set(candidates))
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|||||||
Vendored
+4
@@ -208,6 +208,7 @@ declare global {
|
|||||||
apiHost: string
|
apiHost: string
|
||||||
apiPort: number
|
apiPort: number
|
||||||
imageKeyRoot: string
|
imageKeyRoot: string
|
||||||
|
recallProtectionEnabled: boolean
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
autoLoginPreferenceSet: boolean
|
autoLoginPreferenceSet: boolean
|
||||||
imageXorKey: string
|
imageXorKey: string
|
||||||
@@ -234,6 +235,7 @@ declare global {
|
|||||||
apiHost: string
|
apiHost: string
|
||||||
apiPort: number
|
apiPort: number
|
||||||
imageKeyRoot: string
|
imageKeyRoot: string
|
||||||
|
recallProtectionEnabled: boolean
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
autoLoginPreferenceSet: boolean
|
autoLoginPreferenceSet: boolean
|
||||||
imageXorKey: string
|
imageXorKey: string
|
||||||
@@ -248,6 +250,7 @@ declare global {
|
|||||||
apiHost: string
|
apiHost: string
|
||||||
apiPort: number
|
apiPort: number
|
||||||
imageKeyRoot: string
|
imageKeyRoot: string
|
||||||
|
recallProtectionEnabled: boolean
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
autoLoginPreferenceSet: boolean
|
autoLoginPreferenceSet: boolean
|
||||||
imageXorKey: string
|
imageXorKey: string
|
||||||
@@ -260,6 +263,7 @@ declare global {
|
|||||||
apiHost: string
|
apiHost: string
|
||||||
apiPort: number
|
apiPort: number
|
||||||
imageKeyRoot: string
|
imageKeyRoot: string
|
||||||
|
recallProtectionEnabled: boolean
|
||||||
autoLogin: boolean
|
autoLogin: boolean
|
||||||
autoLoginPreferenceSet: boolean
|
autoLoginPreferenceSet: boolean
|
||||||
imageXorKey: string
|
imageXorKey: string
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ function App(): React.ReactElement {
|
|||||||
usernames.length ? 55 : 90
|
usernames.length ? 55 : 90
|
||||||
)
|
)
|
||||||
let loadedCount = 0
|
let loadedCount = 0
|
||||||
const chunkSize = 32
|
const chunkSize = 128
|
||||||
for (let index = 0; index < usernames.length; index += chunkSize) {
|
for (let index = 0; index < usernames.length; index += chunkSize) {
|
||||||
if (runId !== contactAvatarHydrationRunRef.current) return
|
if (runId !== contactAvatarHydrationRunRef.current) return
|
||||||
const chunk = usernames.slice(index, index + chunkSize)
|
const chunk = usernames.slice(index, index + chunkSize)
|
||||||
@@ -348,7 +348,7 @@ function App(): React.ReactElement {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Contacts] avatar hydrate failed:', error)
|
console.warn('[Contacts] avatar hydrate failed:', error)
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 50))
|
await new Promise((resolve) => window.setTimeout(resolve, 20))
|
||||||
}
|
}
|
||||||
if (usernames.length) onProgress?.('头像加载完成', 90)
|
if (usernames.length) onProgress?.('头像加载完成', 90)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6239,6 +6239,15 @@ body {
|
|||||||
.settings-page-header {
|
.settings-page-header {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
}
|
}
|
||||||
|
.settings-recall-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.settings-recall-notice {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 0;
|
||||||
|
border-top: 2px solid #e5b45f;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
.settings-account-overview {
|
.settings-account-overview {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@@ -7788,6 +7797,55 @@ body {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
accent-color: var(--wxex-brand);
|
accent-color: var(--wxex-brand);
|
||||||
}
|
}
|
||||||
|
.settings-recall-card {
|
||||||
|
padding: 18px 20px;
|
||||||
|
}
|
||||||
|
.settings-recall-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(220px, 0.8fr);
|
||||||
|
gap: 18px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.settings-recall-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
min-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.settings-recall-option span {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.settings-recall-option b {
|
||||||
|
color: var(--wxex-text-primary);
|
||||||
|
font: 700 13px/19px var(--wxex-font);
|
||||||
|
}
|
||||||
|
.settings-recall-option small {
|
||||||
|
color: var(--wxex-text-secondary);
|
||||||
|
font: 12px/18px var(--wxex-font);
|
||||||
|
}
|
||||||
|
.settings-recall-option input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
accent-color: var(--wxex-brand);
|
||||||
|
}
|
||||||
|
.settings-recall-notice {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
padding-left: 14px;
|
||||||
|
border-left: 2px solid #e5b45f;
|
||||||
|
color: #765326;
|
||||||
|
font: 12px/18px var(--wxex-font);
|
||||||
|
}
|
||||||
|
.settings-recall-notice strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.settings-recall-notice span {
|
||||||
|
color: #8a6a3e;
|
||||||
|
}
|
||||||
.api-upload-test-row {
|
.api-upload-test-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ export function AccountDatabasePage({
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
||||||
const [autoLogin, setAutoLogin] = useState(false)
|
const [autoLogin, setAutoLogin] = useState(false)
|
||||||
|
const [recallProtectionEnabled, setRecallProtectionEnabled] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true
|
let active = true
|
||||||
void window.api.getSettings().then((result) => {
|
void window.api.getSettings().then((result) => {
|
||||||
if (active) setAutoLogin(result.settings.autoLogin)
|
if (!active) return
|
||||||
|
setAutoLogin(result.settings.autoLogin)
|
||||||
|
setRecallProtectionEnabled(result.settings.recallProtectionEnabled)
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
active = false
|
active = false
|
||||||
@@ -46,6 +49,12 @@ export function AccountDatabasePage({
|
|||||||
setAutoLogin(result.settings.autoLogin)
|
setAutoLogin(result.settings.autoLogin)
|
||||||
onNotice(checked ? '已开启启动时自动连接' : '已关闭启动时自动连接')
|
onNotice(checked ? '已开启启动时自动连接' : '已关闭启动时自动连接')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeRecallProtection = async (checked: boolean): Promise<void> => {
|
||||||
|
const result = await window.api.setSettings({ recallProtectionEnabled: checked })
|
||||||
|
setRecallProtectionEnabled(result.settings.recallProtectionEnabled)
|
||||||
|
onNotice(checked ? '已开启防撤回' : '已关闭防撤回')
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="settings-page">
|
<div className="settings-page">
|
||||||
<header className="settings-page-header">
|
<header className="settings-page-header">
|
||||||
@@ -93,6 +102,26 @@ export function AccountDatabasePage({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
<h2 className="settings-section-heading">数据读取</h2>
|
||||||
|
<section className="settings-card settings-recall-card">
|
||||||
|
<div className="settings-recall-grid">
|
||||||
|
<label className="settings-recall-option">
|
||||||
|
<span>
|
||||||
|
<b>开启防撤回</b>
|
||||||
|
<small>尽量保留已撤回的聊天内容,方便后续查看。</small>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={recallProtectionEnabled}
|
||||||
|
onChange={(event) => void changeRecallProtection(event.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<aside className="settings-recall-notice">
|
||||||
|
<strong>性能提示</strong>
|
||||||
|
<span>开启后会为消息表增加监听,数据库较大或磁盘较慢时可能让加载变慢。</span>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user