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 icon from '../../resources/icon.png?asset'
|
||||
import { WechatDb } from './wechat-db'
|
||||
import { bootstrapWcdbNative } from './wcdb4-client'
|
||||
import { bootstrapWcdbNative, Wcdb4Client } from './wcdb4-client'
|
||||
import { VoiceService } from './voice-service'
|
||||
import { StickerService } from './sticker-service'
|
||||
import { parseMessageContent } from './message-parser'
|
||||
@@ -94,6 +94,51 @@ const keyServiceMac = new KeyServiceMac()
|
||||
const keyServiceWin = new KeyServiceWin()
|
||||
let tray: Tray | 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 appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon
|
||||
@@ -316,36 +361,7 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
chat.setChatDb(nextWechatDb)
|
||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||
configureRecallArchive(resolvedRoot)
|
||||
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)
|
||||
configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled)
|
||||
voiceService = new VoiceService(wcdb4Client)
|
||||
stickerService = new StickerService(wcdb4Client)
|
||||
videoAssetService = new VideoAssetService(wcdb4Client)
|
||||
@@ -796,13 +812,21 @@ app.whenReady().then(async () => {
|
||||
const xorKey = patch.imageXorKey ?? current.xorKey ?? '0x40'
|
||||
const aesKey = patch.imageAesKey ?? current.aesKey ?? ''
|
||||
const includesImageKey = 'imageXorKey' in patch || 'imageAesKey' in patch
|
||||
const nextSettings = saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' })
|
||||
if (includesImageKey) {
|
||||
saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' })
|
||||
if (aesKey) imageKeyConfigService.save({ resourceRoot, xorKey, aesKey })
|
||||
else imageKeyConfigService.clear()
|
||||
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 {
|
||||
settings: imageKeyConfigService.getLegacySettingsView(),
|
||||
|
||||
@@ -2,7 +2,6 @@ import { app } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { discoverWindowsDbRoots } from '../windows-db-root-discovery'
|
||||
|
||||
/**
|
||||
* 把 V3 时代的 "...\\Documents\\WeChat Files" 路径重定向到
|
||||
@@ -29,6 +28,7 @@ export interface AppSettings {
|
||||
imageXorKey: string
|
||||
imageAesKey: string
|
||||
imageKeyFallbackDisabled: boolean
|
||||
recallProtectionEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
}
|
||||
@@ -55,8 +55,6 @@ function getDefaultDbRootCandidates(home: string): string[] {
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
||||
]
|
||||
|
||||
candidates.push(...discoverWindowsDbRoots())
|
||||
|
||||
return unique(candidates)
|
||||
}
|
||||
|
||||
@@ -106,6 +104,7 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
imageXorKey: '',
|
||||
imageAesKey: '',
|
||||
imageKeyFallbackDisabled: false,
|
||||
recallProtectionEnabled: false,
|
||||
autoLogin: ['1', 'true', 'yes', 'on'].includes(
|
||||
String(import.meta.env.VITE_AUTO_LOGIN || '')
|
||||
.trim()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { discoverWindowsDbRoots } from './windows-db-root-discovery'
|
||||
import crypto from 'crypto'
|
||||
import { createRequire } from 'module'
|
||||
import { createConnection, Socket } from 'net'
|
||||
@@ -319,7 +318,6 @@ export class Wcdb4Client {
|
||||
path.join(home, 'Documents', 'xwechat_files'),
|
||||
path.join(home, 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
|
||||
]
|
||||
candidates.push(...discoverWindowsDbRoots())
|
||||
return Array.from(new Set(candidates))
|
||||
}
|
||||
return [
|
||||
|
||||
Vendored
+4
@@ -208,6 +208,7 @@ declare global {
|
||||
apiHost: string
|
||||
apiPort: number
|
||||
imageKeyRoot: string
|
||||
recallProtectionEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
imageXorKey: string
|
||||
@@ -234,6 +235,7 @@ declare global {
|
||||
apiHost: string
|
||||
apiPort: number
|
||||
imageKeyRoot: string
|
||||
recallProtectionEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
imageXorKey: string
|
||||
@@ -248,6 +250,7 @@ declare global {
|
||||
apiHost: string
|
||||
apiPort: number
|
||||
imageKeyRoot: string
|
||||
recallProtectionEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
imageXorKey: string
|
||||
@@ -260,6 +263,7 @@ declare global {
|
||||
apiHost: string
|
||||
apiPort: number
|
||||
imageKeyRoot: string
|
||||
recallProtectionEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
imageXorKey: string
|
||||
|
||||
@@ -317,7 +317,7 @@ function App(): React.ReactElement {
|
||||
usernames.length ? 55 : 90
|
||||
)
|
||||
let loadedCount = 0
|
||||
const chunkSize = 32
|
||||
const chunkSize = 128
|
||||
for (let index = 0; index < usernames.length; index += chunkSize) {
|
||||
if (runId !== contactAvatarHydrationRunRef.current) return
|
||||
const chunk = usernames.slice(index, index + chunkSize)
|
||||
@@ -348,7 +348,7 @@ function App(): React.ReactElement {
|
||||
} catch (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)
|
||||
}
|
||||
|
||||
@@ -6239,6 +6239,15 @@ body {
|
||||
.settings-page-header {
|
||||
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 {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 18px;
|
||||
@@ -7788,6 +7797,55 @@ body {
|
||||
height: 18px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -27,11 +27,14 @@ export function AccountDatabasePage({
|
||||
}): React.ReactElement {
|
||||
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
||||
const [autoLogin, setAutoLogin] = useState(false)
|
||||
const [recallProtectionEnabled, setRecallProtectionEnabled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void window.api.getSettings().then((result) => {
|
||||
if (active) setAutoLogin(result.settings.autoLogin)
|
||||
if (!active) return
|
||||
setAutoLogin(result.settings.autoLogin)
|
||||
setRecallProtectionEnabled(result.settings.recallProtectionEnabled)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
@@ -46,6 +49,12 @@ export function AccountDatabasePage({
|
||||
setAutoLogin(result.settings.autoLogin)
|
||||
onNotice(checked ? '已开启启动时自动连接' : '已关闭启动时自动连接')
|
||||
}
|
||||
|
||||
const changeRecallProtection = async (checked: boolean): Promise<void> => {
|
||||
const result = await window.api.setSettings({ recallProtectionEnabled: checked })
|
||||
setRecallProtectionEnabled(result.settings.recallProtectionEnabled)
|
||||
onNotice(checked ? '已开启防撤回' : '已关闭防撤回')
|
||||
}
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<header className="settings-page-header">
|
||||
@@ -93,6 +102,26 @@ export function AccountDatabasePage({
|
||||
/>
|
||||
</label>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user