规范化资源文件;修复消息气泡宽度异常的问题;优化资源管理页面性能
This commit is contained in:
1 parent
0acad9927a
commit
b356814ebb
40 files changed
+469
-128
No files matched your search
+3
-1
@@ -56,6 +56,8 @@ Thumbs.db
|
||||
*.aps
|
||||
|
||||
wcdb/
|
||||
!resources/wcdb/
|
||||
!resources/wcdb/**
|
||||
xkey/
|
||||
server/
|
||||
*info
|
||||
@@ -73,4 +75,4 @@ pnpm-lock.yaml
|
||||
wechat-research-site
|
||||
.codex
|
||||
weflow-web-offical
|
||||
Insight
|
||||
Insight
|
||||
+11
-4
@@ -2558,7 +2558,13 @@ function registerIpcHandlers() {
|
||||
ipcMain.handle('image:decrypt', async (_, payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => {
|
||||
return imageDecryptService.decryptImage(payload)
|
||||
})
|
||||
ipcMain.handle('image:resolveCache', async (_, payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; disableUpdateCheck?: boolean }) => {
|
||||
ipcMain.handle('image:resolveCache', async (_, payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
disableUpdateCheck?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}) => {
|
||||
return imageDecryptService.resolveCachedImage(payload)
|
||||
})
|
||||
ipcMain.handle(
|
||||
@@ -2566,13 +2572,14 @@ function registerIpcHandlers() {
|
||||
async (
|
||||
_,
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { disableUpdateCheck?: boolean }
|
||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
||||
) => {
|
||||
const list = Array.isArray(payloads) ? payloads : []
|
||||
const rows = await Promise.all(list.map(async (payload) => {
|
||||
return imageDecryptService.resolveCachedImage({
|
||||
...payload,
|
||||
disableUpdateCheck: options?.disableUpdateCheck === true
|
||||
disableUpdateCheck: options?.disableUpdateCheck === true,
|
||||
allowCacheIndex: options?.allowCacheIndex !== false
|
||||
})
|
||||
}))
|
||||
return { success: true, rows }
|
||||
@@ -2583,7 +2590,7 @@ function registerIpcHandlers() {
|
||||
async (
|
||||
_,
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { allowDecrypt?: boolean }
|
||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||
) => {
|
||||
imagePreloadService.enqueue(payloads || [], options)
|
||||
return true
|
||||
|
||||
+9
-3
@@ -266,15 +266,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) =>
|
||||
ipcRenderer.invoke('image:decrypt', payload),
|
||||
resolveCache: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; disableUpdateCheck?: boolean }) =>
|
||||
resolveCache: (payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
disableUpdateCheck?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}) =>
|
||||
ipcRenderer.invoke('image:resolveCache', payload),
|
||||
resolveCacheBatch: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { disableUpdateCheck?: boolean }
|
||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
||||
) => ipcRenderer.invoke('image:resolveCacheBatch', payloads, options),
|
||||
preload: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { allowDecrypt?: boolean }
|
||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||
) => ipcRenderer.invoke('image:preload', payloads, options),
|
||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => {
|
||||
const listener = (_: unknown, payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => callback(payload)
|
||||
|
||||
@@ -63,6 +63,7 @@ type CachedImagePayload = {
|
||||
imageDatName?: string
|
||||
preferFilePath?: boolean
|
||||
disableUpdateCheck?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}
|
||||
|
||||
type DecryptImagePayload = CachedImagePayload & {
|
||||
@@ -116,7 +117,9 @@ export class ImageDecryptService {
|
||||
}
|
||||
|
||||
async resolveCachedImage(payload: CachedImagePayload): Promise<DecryptResult & { hasUpdate?: boolean }> {
|
||||
await this.ensureCacheIndexed()
|
||||
if (payload.allowCacheIndex !== false) {
|
||||
await this.ensureCacheIndexed()
|
||||
}
|
||||
const cacheKeys = this.getCacheKeys(payload)
|
||||
const cacheKey = cacheKeys[0]
|
||||
if (!cacheKey) {
|
||||
|
||||
@@ -8,11 +8,13 @@ type PreloadImagePayload = {
|
||||
|
||||
type PreloadOptions = {
|
||||
allowDecrypt?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}
|
||||
|
||||
type PreloadTask = PreloadImagePayload & {
|
||||
key: string
|
||||
allowDecrypt: boolean
|
||||
allowCacheIndex: boolean
|
||||
}
|
||||
|
||||
export class ImagePreloadService {
|
||||
@@ -27,6 +29,7 @@ export class ImagePreloadService {
|
||||
enqueue(payloads: PreloadImagePayload[], options?: PreloadOptions): void {
|
||||
if (!Array.isArray(payloads) || payloads.length === 0) return
|
||||
const allowDecrypt = options?.allowDecrypt !== false
|
||||
const allowCacheIndex = options?.allowCacheIndex !== false
|
||||
for (const payload of payloads) {
|
||||
if (!allowDecrypt && this.queue.length >= this.maxQueueSize) break
|
||||
const cacheKey = payload.imageMd5 || payload.imageDatName
|
||||
@@ -34,7 +37,7 @@ export class ImagePreloadService {
|
||||
const key = `${payload.sessionId || 'unknown'}|${cacheKey}`
|
||||
if (this.pending.has(key)) continue
|
||||
this.pending.add(key)
|
||||
this.queue.push({ ...payload, key, allowDecrypt })
|
||||
this.queue.push({ ...payload, key, allowDecrypt, allowCacheIndex })
|
||||
}
|
||||
this.processQueue()
|
||||
}
|
||||
@@ -71,7 +74,8 @@ export class ImagePreloadService {
|
||||
sessionId: task.sessionId,
|
||||
imageMd5: task.imageMd5,
|
||||
imageDatName: task.imageDatName,
|
||||
disableUpdateCheck: !task.allowDecrypt
|
||||
disableUpdateCheck: !task.allowDecrypt,
|
||||
allowCacheIndex: task.allowCacheIndex
|
||||
})
|
||||
if (cached.success) return
|
||||
if (!task.allowDecrypt) return
|
||||
|
||||
@@ -61,6 +61,7 @@ export class KeyService {
|
||||
|
||||
private getDllPath(): string {
|
||||
const isPackaged = typeof app !== 'undefined' && app ? app.isPackaged : process.env.NODE_ENV === 'production'
|
||||
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const candidates: string[] = []
|
||||
|
||||
if (process.env.WX_KEY_DLL_PATH) {
|
||||
@@ -68,11 +69,20 @@ export class KeyService {
|
||||
}
|
||||
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'win32', archDir, 'wx_key.dll'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'win32', 'x64', 'wx_key.dll'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'win32', 'wx_key.dll'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'wx_key.dll'))
|
||||
candidates.push(join(process.resourcesPath, 'wx_key.dll'))
|
||||
} else {
|
||||
const cwd = process.cwd()
|
||||
candidates.push(join(cwd, 'resources', 'key', 'win32', archDir, 'wx_key.dll'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'win32', 'x64', 'wx_key.dll'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'win32', 'wx_key.dll'))
|
||||
candidates.push(join(cwd, 'resources', 'wx_key.dll'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'win32', archDir, 'wx_key.dll'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'win32', 'x64', 'wx_key.dll'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'win32', 'wx_key.dll'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'wx_key.dll'))
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,23 @@ export class KeyServiceLinux {
|
||||
|
||||
private getHelperPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const candidates: string[] = []
|
||||
if (process.env.WX_KEY_HELPER_PATH) candidates.push(process.env.WX_KEY_HELPER_PATH)
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'linux', archDir, 'xkey_helper_linux'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'linux', 'x64', 'xkey_helper_linux'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'linux', 'xkey_helper_linux'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'xkey_helper_linux'))
|
||||
candidates.push(join(process.resourcesPath, 'xkey_helper_linux'))
|
||||
} else {
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'linux', archDir, 'xkey_helper_linux'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'linux', 'x64', 'xkey_helper_linux'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'linux', 'xkey_helper_linux'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'xkey_helper_linux'))
|
||||
candidates.push(join(process.cwd(), 'resources', 'key', 'linux', archDir, 'xkey_helper_linux'))
|
||||
candidates.push(join(process.cwd(), 'resources', 'key', 'linux', 'x64', 'xkey_helper_linux'))
|
||||
candidates.push(join(process.cwd(), 'resources', 'key', 'linux', 'xkey_helper_linux'))
|
||||
candidates.push(join(app.getAppPath(), '..', 'Xkey', 'build', 'xkey_helper_linux'))
|
||||
}
|
||||
for (const p of candidates) {
|
||||
|
||||
@@ -27,6 +27,7 @@ export class KeyServiceMac {
|
||||
|
||||
private getHelperPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const candidates: string[] = []
|
||||
|
||||
if (process.env.WX_KEY_HELPER_PATH) {
|
||||
@@ -34,12 +35,21 @@ export class KeyServiceMac {
|
||||
}
|
||||
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', archDir, 'xkey_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'universal', 'xkey_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'xkey_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'xkey_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'xkey_helper'))
|
||||
} else {
|
||||
const cwd = process.cwd()
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', archDir, 'xkey_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'universal', 'xkey_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'xkey_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'xkey_helper'))
|
||||
candidates.push(join(cwd, 'Xkey', 'build', 'xkey_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', archDir, 'xkey_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'universal', 'xkey_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'xkey_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'xkey_helper'))
|
||||
}
|
||||
|
||||
@@ -52,14 +62,24 @@ export class KeyServiceMac {
|
||||
|
||||
private getImageScanHelperPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const candidates: string[] = []
|
||||
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', archDir, 'image_scan_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'universal', 'image_scan_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'image_scan_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'image_scan_helper'))
|
||||
candidates.push(join(process.resourcesPath, 'image_scan_helper'))
|
||||
} else {
|
||||
const cwd = process.cwd()
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', archDir, 'image_scan_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'universal', 'image_scan_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'image_scan_helper'))
|
||||
candidates.push(join(cwd, 'resources', 'image_scan_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', archDir, 'image_scan_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'universal', 'image_scan_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'image_scan_helper'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'image_scan_helper'))
|
||||
}
|
||||
|
||||
@@ -72,6 +92,7 @@ export class KeyServiceMac {
|
||||
|
||||
private getDylibPath(): string {
|
||||
const isPackaged = app.isPackaged
|
||||
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const candidates: string[] = []
|
||||
|
||||
if (process.env.WX_KEY_DYLIB_PATH) {
|
||||
@@ -79,11 +100,20 @@ export class KeyServiceMac {
|
||||
}
|
||||
|
||||
if (isPackaged) {
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', archDir, 'libwx_key.dylib'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'universal', 'libwx_key.dylib'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'key', 'macos', 'libwx_key.dylib'))
|
||||
candidates.push(join(process.resourcesPath, 'resources', 'libwx_key.dylib'))
|
||||
candidates.push(join(process.resourcesPath, 'libwx_key.dylib'))
|
||||
} else {
|
||||
const cwd = process.cwd()
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', archDir, 'libwx_key.dylib'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'universal', 'libwx_key.dylib'))
|
||||
candidates.push(join(cwd, 'resources', 'key', 'macos', 'libwx_key.dylib'))
|
||||
candidates.push(join(cwd, 'resources', 'libwx_key.dylib'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', archDir, 'libwx_key.dylib'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'universal', 'libwx_key.dylib'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'key', 'macos', 'libwx_key.dylib'))
|
||||
candidates.push(join(app.getAppPath(), 'resources', 'libwx_key.dylib'))
|
||||
}
|
||||
|
||||
|
||||
+126
-55
@@ -121,6 +121,9 @@ export class WcdbCore {
|
||||
private videoHardlinkCache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }> = new Map()
|
||||
private readonly hardlinkCacheTtlMs = 10 * 60 * 1000
|
||||
private readonly hardlinkCacheMaxEntries = 20000
|
||||
private mediaStreamSessionCache: Array<{ sessionId: string; displayName: string; sortTimestamp: number }> | null = null
|
||||
private mediaStreamSessionCacheAt = 0
|
||||
private readonly mediaStreamSessionCacheTtlMs = 12 * 1000
|
||||
private logTimer: NodeJS.Timeout | null = null
|
||||
private lastLogTail: string | null = null
|
||||
private lastResolvedLogPath: string | null = null
|
||||
@@ -277,7 +280,9 @@ export class WcdbCore {
|
||||
const isLinux = process.platform === 'linux'
|
||||
const isArm64 = process.arch === 'arm64'
|
||||
const libName = isMac ? 'libwcdb_api.dylib' : isLinux ? 'libwcdb_api.so' : 'wcdb_api.dll'
|
||||
const subDir = isMac ? 'macos' : isLinux ? 'linux' : (isArm64 ? 'arm64' : '')
|
||||
const legacySubDir = isMac ? 'macos' : isLinux ? 'linux' : (isArm64 ? 'arm64' : '')
|
||||
const platformDir = isMac ? 'macos' : (isLinux ? 'linux' : 'win32')
|
||||
const archDir = isMac ? 'universal' : (isArm64 ? 'arm64' : 'x64')
|
||||
|
||||
const envDllPath = process.env.WCDB_DLL_PATH
|
||||
if (envDllPath && envDllPath.length > 0) {
|
||||
@@ -287,20 +292,33 @@ export class WcdbCore {
|
||||
// 基础路径探测
|
||||
const isPackaged = typeof process['resourcesPath'] !== 'undefined'
|
||||
const resourcesPath = isPackaged ? process.resourcesPath : join(process.cwd(), 'resources')
|
||||
|
||||
const candidates = [
|
||||
// 环境变量指定 resource 目录
|
||||
process.env.WCDB_RESOURCES_PATH ? join(process.env.WCDB_RESOURCES_PATH, subDir, libName) : null,
|
||||
// 显式 setPaths 设置的路径
|
||||
this.resourcesPath ? join(this.resourcesPath, subDir, libName) : null,
|
||||
// resources/macos/libwcdb_api.dylib 或 resources/wcdb_api.dll
|
||||
join(resourcesPath, 'resources', subDir, libName),
|
||||
// resources/libwcdb_api.dylib 或 resources/wcdb_api.dll (扁平结构)
|
||||
join(resourcesPath, subDir, libName),
|
||||
// CWD fallback
|
||||
join(process.cwd(), 'resources', subDir, libName)
|
||||
const roots = [
|
||||
process.env.WCDB_RESOURCES_PATH || null,
|
||||
this.resourcesPath || null,
|
||||
join(resourcesPath, 'resources'),
|
||||
resourcesPath,
|
||||
join(process.cwd(), 'resources')
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
const normalizedArch = process.arch === 'arm64' ? 'arm64' : 'x64'
|
||||
const relativeCandidates = [
|
||||
join('wcdb', platformDir, archDir, libName),
|
||||
join('wcdb', platformDir, normalizedArch, libName),
|
||||
join('wcdb', platformDir, 'x64', libName),
|
||||
join('wcdb', platformDir, 'universal', libName),
|
||||
join('wcdb', platformDir, libName)
|
||||
]
|
||||
|
||||
const candidates: string[] = []
|
||||
for (const root of roots) {
|
||||
for (const relativePath of relativeCandidates) {
|
||||
candidates.push(join(root, relativePath))
|
||||
}
|
||||
// 兼容旧目录:resources/macos/libwcdb_api.dylib 或 resources/wcdb_api.dll
|
||||
candidates.push(join(root, legacySubDir, libName))
|
||||
candidates.push(join(root, libName))
|
||||
}
|
||||
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path
|
||||
}
|
||||
@@ -1465,6 +1483,11 @@ export class WcdbCore {
|
||||
this.videoHardlinkCache.clear()
|
||||
}
|
||||
|
||||
private clearMediaStreamSessionCache(): void {
|
||||
this.mediaStreamSessionCache = null
|
||||
this.mediaStreamSessionCacheAt = 0
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.ensureReady()
|
||||
}
|
||||
@@ -1580,6 +1603,7 @@ export class WcdbCore {
|
||||
this.currentDbStoragePath = null
|
||||
this.initialized = false
|
||||
this.clearHardlinkCaches()
|
||||
this.clearMediaStreamSessionCache()
|
||||
this.stopLogPolling()
|
||||
}
|
||||
}
|
||||
@@ -1957,7 +1981,7 @@ export class WcdbCore {
|
||||
error?: string
|
||||
}> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbScanMediaStream) return { success: false, error: '当前数据服务版本不支持媒体流扫描,请先更新 wcdb 数据服务' }
|
||||
if (!this.wcdbScanMediaStream) return { success: false, error: '当前数据服务版本不支持资源扫描,请先更新 wcdb 数据服务' }
|
||||
try {
|
||||
const toInt = (value: unknown): number => {
|
||||
const n = Number(value || 0)
|
||||
@@ -2168,37 +2192,64 @@ export class WcdbCore {
|
||||
const offset = Math.max(0, toInt(options?.offset))
|
||||
const limit = Math.min(1200, Math.max(40, toInt(options?.limit) || 240))
|
||||
|
||||
const sessionsRes = await this.getSessions()
|
||||
if (!sessionsRes.success || !Array.isArray(sessionsRes.sessions)) {
|
||||
return { success: false, error: sessionsRes.error || '读取会话失败' }
|
||||
const getSessionRows = async (): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; displayName: string; sortTimestamp: number }>
|
||||
error?: string
|
||||
}> => {
|
||||
const now = Date.now()
|
||||
const cachedRows = this.mediaStreamSessionCache
|
||||
if (
|
||||
cachedRows &&
|
||||
now - this.mediaStreamSessionCacheAt <= this.mediaStreamSessionCacheTtlMs
|
||||
) {
|
||||
return { success: true, rows: cachedRows }
|
||||
}
|
||||
|
||||
const sessionsRes = await this.getSessions()
|
||||
if (!sessionsRes.success || !Array.isArray(sessionsRes.sessions)) {
|
||||
return { success: false, error: sessionsRes.error || '读取会话失败' }
|
||||
}
|
||||
|
||||
const rows = (sessionsRes.sessions || [])
|
||||
.map((row: any) => ({
|
||||
sessionId: String(
|
||||
row.username ||
|
||||
row.user_name ||
|
||||
row.userName ||
|
||||
row.usrName ||
|
||||
row.UsrName ||
|
||||
row.talker ||
|
||||
''
|
||||
).trim(),
|
||||
displayName: String(row.displayName || row.display_name || row.remark || '').trim(),
|
||||
sortTimestamp: toInt(
|
||||
row.sort_timestamp ||
|
||||
row.sortTimestamp ||
|
||||
row.last_timestamp ||
|
||||
row.lastTimestamp ||
|
||||
0
|
||||
)
|
||||
}))
|
||||
.filter((row) => Boolean(row.sessionId))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
||||
|
||||
this.mediaStreamSessionCache = rows
|
||||
this.mediaStreamSessionCacheAt = now
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
const sessions = (sessionsRes.sessions || [])
|
||||
.map((row: any) => ({
|
||||
sessionId: String(
|
||||
row.username ||
|
||||
row.user_name ||
|
||||
row.userName ||
|
||||
row.usrName ||
|
||||
row.UsrName ||
|
||||
row.talker ||
|
||||
''
|
||||
).trim(),
|
||||
displayName: String(row.displayName || row.display_name || row.remark || '').trim(),
|
||||
sortTimestamp: toInt(
|
||||
row.sort_timestamp ||
|
||||
row.sortTimestamp ||
|
||||
row.last_timestamp ||
|
||||
row.lastTimestamp ||
|
||||
0
|
||||
)
|
||||
}))
|
||||
.filter((row) => Boolean(row.sessionId))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
||||
let sessionRows: Array<{ sessionId: string; displayName: string; sortTimestamp: number }> = []
|
||||
if (requestedSessionId) {
|
||||
sessionRows = [{ sessionId: requestedSessionId, displayName: requestedSessionId, sortTimestamp: 0 }]
|
||||
} else {
|
||||
const sessionsRowsRes = await getSessionRows()
|
||||
if (!sessionsRowsRes.success || !Array.isArray(sessionsRowsRes.rows)) {
|
||||
return { success: false, error: sessionsRowsRes.error || '读取会话失败' }
|
||||
}
|
||||
sessionRows = sessionsRowsRes.rows
|
||||
}
|
||||
|
||||
const sessionRows = requestedSessionId
|
||||
? sessions.filter((row) => row.sessionId === requestedSessionId)
|
||||
: sessions
|
||||
if (sessionRows.length === 0) {
|
||||
return { success: true, items: [], hasMore: false, nextOffset: offset }
|
||||
}
|
||||
@@ -2219,10 +2270,10 @@ export class WcdbCore {
|
||||
outHasMore
|
||||
)
|
||||
if (result !== 0 || !outPtr[0]) {
|
||||
return { success: false, error: `扫描媒体流失败: ${result}` }
|
||||
return { success: false, error: `扫描资源失败: ${result}` }
|
||||
}
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析媒体流失败' }
|
||||
if (!jsonStr) return { success: false, error: '解析资源失败' }
|
||||
const rows = JSON.parse(jsonStr)
|
||||
const list = Array.isArray(rows) ? rows as Array<Record<string, any>> : []
|
||||
|
||||
@@ -2254,19 +2305,39 @@ export class WcdbCore {
|
||||
rawMessageContent &&
|
||||
(rawMessageContent.includes('<') || rawMessageContent.includes('md5') || rawMessageContent.includes('videomsg'))
|
||||
)
|
||||
const content = useRawMessageContent
|
||||
? rawMessageContent
|
||||
: decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
const decodeContentIfNeeded = (): string => {
|
||||
if (useRawMessageContent) return rawMessageContent
|
||||
if (!rawMessageContent && !rawCompressContent) return ''
|
||||
return decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
}
|
||||
const packedPayload = extractPackedPayload(row)
|
||||
const imageMd5ByColumn = pickString(row, ['image_md5', 'imageMd5'])
|
||||
const imageMd5 = localType === 3
|
||||
? (imageMd5ByColumn || extractImageMd5(content) || extractHexMd5(packedPayload) || undefined)
|
||||
: undefined
|
||||
const imageDatName = localType === 3 ? (extractImageDatName(row, content) || undefined) : undefined
|
||||
const videoMd5ByColumn = pickString(row, ['video_md5', 'videoMd5', 'raw_md5', 'rawMd5'])
|
||||
const videoMd5 = localType === 43
|
||||
? (videoMd5ByColumn || extractVideoMd5(content) || extractHexMd5(packedPayload) || undefined)
|
||||
: undefined
|
||||
|
||||
let content = ''
|
||||
let imageMd5: string | undefined
|
||||
let imageDatName: string | undefined
|
||||
let videoMd5: string | undefined
|
||||
|
||||
if (localType === 3) {
|
||||
imageMd5 = imageMd5ByColumn || extractHexMd5(packedPayload) || undefined
|
||||
imageDatName = extractImageDatName(row, '') || undefined
|
||||
if (!imageMd5 || !imageDatName) {
|
||||
content = decodeContentIfNeeded()
|
||||
if (!imageMd5) imageMd5 = extractImageMd5(content) || extractHexMd5(packedPayload) || undefined
|
||||
if (!imageDatName) imageDatName = extractImageDatName(row, content) || undefined
|
||||
}
|
||||
} else if (localType === 43) {
|
||||
videoMd5 = videoMd5ByColumn || extractHexMd5(packedPayload) || undefined
|
||||
if (!videoMd5) {
|
||||
content = decodeContentIfNeeded()
|
||||
videoMd5 = extractVideoMd5(content) || extractHexMd5(packedPayload) || undefined
|
||||
} else if (useRawMessageContent) {
|
||||
// 占位态标题只依赖简单 XML,已带 md5 时不做额外解压
|
||||
content = rawMessageContent
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
sessionDisplayName: sessionNameMap.get(sessionId) || sessionId,
|
||||
@@ -2280,7 +2351,7 @@ export class WcdbCore {
|
||||
imageMd5,
|
||||
imageDatName,
|
||||
videoMd5,
|
||||
content: content || undefined
|
||||
content: localType === 43 ? (content || undefined) : undefined
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+7
-7
@@ -98,7 +98,7 @@
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": "electron/entitlements.mac.plist",
|
||||
"entitlementsInherit": "electron/entitlements.mac.plist",
|
||||
"icon": "resources/icon.icns"
|
||||
"icon": "resources/icons/macos/icon.icns"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
@@ -107,19 +107,19 @@
|
||||
"icon": "public/icon.ico",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "resources/msvcp140.dll",
|
||||
"from": "resources/runtime/win32/msvcp140.dll",
|
||||
"to": "."
|
||||
},
|
||||
{
|
||||
"from": "resources/msvcp140_1.dll",
|
||||
"from": "resources/runtime/win32/msvcp140_1.dll",
|
||||
"to": "."
|
||||
},
|
||||
{
|
||||
"from": "resources/vcruntime140.dll",
|
||||
"from": "resources/runtime/win32/vcruntime140.dll",
|
||||
"to": "."
|
||||
},
|
||||
{
|
||||
"from": "resources/vcruntime140_1.dll",
|
||||
"from": "resources/runtime/win32/vcruntime140_1.dll",
|
||||
"to": "."
|
||||
}
|
||||
]
|
||||
@@ -135,7 +135,7 @@
|
||||
"synopsis": "WeFlow for Linux",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "resources/linux/install.sh",
|
||||
"from": "resources/installer/linux/install.sh",
|
||||
"to": "install.sh"
|
||||
}
|
||||
]
|
||||
@@ -190,7 +190,7 @@
|
||||
"node_modules/sherpa-onnx-*/**/*",
|
||||
"node_modules/ffmpeg-static/**/*"
|
||||
],
|
||||
"icon": "resources/icon.icns"
|
||||
"icon": "resources/icons/macos/icon.icns"
|
||||
},
|
||||
"overrides": {
|
||||
"picomatch": "^4.0.4",
|
||||
|
||||
Binary file not shown.
File renamed without changes.
File renamed without changes.
Executable → Regular
File renamed without changes.
File renamed without changes.
File renamed without changes.
Executable → Regular
File renamed without changes.
Executable → Regular
File renamed without changes.
Executable → Regular
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
File renamed without changes.
Binary file not shown.
File renamed without changes.
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1965,6 +1965,10 @@
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// 对方发送的消息 - 左侧白色
|
||||
@@ -1974,6 +1978,10 @@
|
||||
color: var(--text-primary);
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.system {
|
||||
@@ -2038,6 +2046,12 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
// 让文字气泡按内容收缩,不被群昵称行宽度牵连
|
||||
.message-bubble:not(.system) .bubble-content {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 表情包消息
|
||||
.message-bubble.emoji {
|
||||
.bubble-content {
|
||||
|
||||
+86
-21
@@ -1,6 +1,7 @@
|
||||
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState, type HTMLAttributes } from 'react'
|
||||
import { Calendar, Image as ImageIcon, Loader2, PlayCircle, RefreshCw, Trash2, UserRound } from 'lucide-react'
|
||||
import { VirtuosoGrid } from 'react-virtuoso'
|
||||
import { finishBackgroundTask, registerBackgroundTask, updateBackgroundTask } from '../services/backgroundTaskMonitor'
|
||||
import './ResourcesPage.scss'
|
||||
|
||||
type MediaTab = 'image' | 'video'
|
||||
@@ -35,10 +36,14 @@ type DialogState = {
|
||||
onConfirm?: (() => void) | null
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 120
|
||||
const MAX_IMAGE_CACHE_RESOLVE_PER_TICK = 18
|
||||
const MAX_IMAGE_CACHE_PRELOAD_PER_TICK = 36
|
||||
const MAX_VIDEO_POSTER_RESOLVE_PER_TICK = 4
|
||||
const PAGE_SIZE = 96
|
||||
const MAX_IMAGE_CACHE_RESOLVE_PER_TICK = 12
|
||||
const MAX_IMAGE_CACHE_PRELOAD_PER_TICK = 24
|
||||
const MAX_VIDEO_POSTER_RESOLVE_PER_TICK = 3
|
||||
const INITIAL_IMAGE_PRELOAD_END = 48
|
||||
const INITIAL_IMAGE_RESOLVE_END = 12
|
||||
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
|
||||
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
|
||||
|
||||
const GridList = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function GridList(props, ref) {
|
||||
const { className = '', ...rest } = props
|
||||
@@ -409,7 +414,13 @@ function ResourcesPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
await window.electronAPI.chat.connect()
|
||||
if (reset) {
|
||||
const connectResult = await window.electronAPI.chat.connect()
|
||||
if (!connectResult.success) {
|
||||
setError(connectResult.error || '连接数据库失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
const requestOffset = reset ? 0 : nextOffset
|
||||
const streamResult = await window.electronAPI.chat.getMediaStream({
|
||||
sessionId: selectedContact === 'all' ? undefined : selectedContact,
|
||||
@@ -524,7 +535,6 @@ function ResourcesPage() {
|
||||
let cancelled = false
|
||||
const run = async () => {
|
||||
try {
|
||||
await window.electronAPI.chat.connect()
|
||||
const sessionResult = await window.electronAPI.chat.getSessions()
|
||||
if (!cancelled && sessionResult.success && Array.isArray(sessionResult.sessions)) {
|
||||
const initialNameMap: Record<string, string> = {}
|
||||
@@ -674,7 +684,10 @@ function ResourcesPage() {
|
||||
resolvingImageCacheBatchRef.current = true
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.image.resolveCacheBatch(payloads, { disableUpdateCheck: true })
|
||||
const result = await window.electronAPI.image.resolveCacheBatch(payloads, {
|
||||
disableUpdateCheck: true,
|
||||
allowCacheIndex: false
|
||||
})
|
||||
const rows = Array.isArray(result?.rows) ? result.rows : []
|
||||
const pathPatch: Record<string, string> = {}
|
||||
const updatePatch: Record<string, boolean> = {}
|
||||
@@ -741,7 +754,10 @@ function ResourcesPage() {
|
||||
if (payloads.length >= MAX_IMAGE_CACHE_PRELOAD_PER_TICK) break
|
||||
}
|
||||
if (payloads.length === 0) return
|
||||
void window.electronAPI.image.preload(payloads, { allowDecrypt: false })
|
||||
void window.electronAPI.image.preload(payloads, {
|
||||
allowDecrypt: false,
|
||||
allowCacheIndex: false
|
||||
})
|
||||
}, [displayItems])
|
||||
|
||||
const resolveItemVideoMd5 = useCallback(async (item: MediaStreamItem): Promise<string> => {
|
||||
@@ -813,14 +829,18 @@ function ResourcesPage() {
|
||||
if (!pending) return
|
||||
pendingRangeRef.current = null
|
||||
if (tab === 'image') {
|
||||
preloadImageCacheRange(pending.start - 8, pending.end + 32)
|
||||
resolveImageCacheRange(pending.start - 2, pending.end + 8)
|
||||
preloadImageCacheRange(pending.start - 4, pending.end + 20)
|
||||
resolveImageCacheRange(pending.start - 1, pending.end + 6)
|
||||
return
|
||||
}
|
||||
resolvePosterRange(pending.start, pending.end)
|
||||
}, [preloadImageCacheRange, resolveImageCacheRange, resolvePosterRange, tab])
|
||||
|
||||
const scheduleRangeResolve = useCallback((start: number, end: number) => {
|
||||
const previous = pendingRangeRef.current
|
||||
if (previous && start >= previous.start && end <= previous.end) {
|
||||
return
|
||||
}
|
||||
pendingRangeRef.current = { start, end }
|
||||
if (rangeTimerRef.current !== null) {
|
||||
window.clearTimeout(rangeTimerRef.current)
|
||||
@@ -832,8 +852,8 @@ function ResourcesPage() {
|
||||
useEffect(() => {
|
||||
if (displayItems.length === 0) return
|
||||
if (tab === 'image') {
|
||||
preloadImageCacheRange(0, Math.min(displayItems.length - 1, 80))
|
||||
resolveImageCacheRange(0, Math.min(displayItems.length - 1, 20))
|
||||
preloadImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_PRELOAD_END))
|
||||
resolveImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_RESOLVE_END))
|
||||
return
|
||||
}
|
||||
resolvePosterRange(0, Math.min(displayItems.length - 1, 12))
|
||||
@@ -1057,25 +1077,61 @@ function ResourcesPage() {
|
||||
|
||||
setBatchBusy(true)
|
||||
let success = 0
|
||||
let failed = 0
|
||||
const previewPatch: Record<string, string> = {}
|
||||
const updatePatch: Record<string, boolean> = {}
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'other',
|
||||
title: '资源页图片批量解密',
|
||||
detail: `正在解密图片(0/${imageItems.length})`,
|
||||
progressText: `0 / ${imageItems.length}`,
|
||||
cancelable: false
|
||||
})
|
||||
try {
|
||||
let completed = 0
|
||||
const progressStep = Math.max(1, Math.floor(imageItems.length / TASK_PROGRESS_UPDATE_MAX_STEPS))
|
||||
let lastProgressBucket = 0
|
||||
let lastProgressUpdateAt = Date.now()
|
||||
const updateTaskProgress = (force: boolean = false) => {
|
||||
const now = Date.now()
|
||||
const bucket = Math.floor(completed / progressStep)
|
||||
const crossedBucket = bucket !== lastProgressBucket
|
||||
const intervalReached = now - lastProgressUpdateAt >= TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS
|
||||
if (!force && !crossedBucket && !intervalReached) return
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: `正在解密图片(${completed}/${imageItems.length})`,
|
||||
progressText: `${completed} / ${imageItems.length}`
|
||||
})
|
||||
lastProgressBucket = bucket
|
||||
lastProgressUpdateAt = now
|
||||
}
|
||||
for (const item of imageItems) {
|
||||
if (!item.imageMd5 && !item.imageDatName) continue
|
||||
if (!item.imageMd5 && !item.imageDatName) {
|
||||
failed += 1
|
||||
completed += 1
|
||||
updateTaskProgress()
|
||||
continue
|
||||
}
|
||||
const result = await window.electronAPI.image.decrypt({
|
||||
sessionId: item.sessionId,
|
||||
imageMd5: item.imageMd5 || undefined,
|
||||
imageDatName: item.imageDatName || undefined,
|
||||
force: true
|
||||
})
|
||||
if (!result?.success) continue
|
||||
success += 1
|
||||
if (result.localPath) {
|
||||
const key = getItemKey(item)
|
||||
previewPatch[key] = result.localPath
|
||||
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
|
||||
if (!result?.success) {
|
||||
failed += 1
|
||||
} else {
|
||||
success += 1
|
||||
if (result.localPath) {
|
||||
const key = getItemKey(item)
|
||||
previewPatch[key] = result.localPath
|
||||
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
|
||||
}
|
||||
}
|
||||
completed += 1
|
||||
updateTaskProgress()
|
||||
}
|
||||
updateTaskProgress(true)
|
||||
|
||||
if (Object.keys(previewPatch).length > 0) {
|
||||
setPreviewPathMap((prev) => ({ ...prev, ...previewPatch }))
|
||||
@@ -1083,8 +1139,17 @@ function ResourcesPage() {
|
||||
if (Object.keys(updatePatch).length > 0) {
|
||||
setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
|
||||
}
|
||||
setActionMessage(`批量解密完成:成功 ${success},失败 ${imageItems.length - success}`)
|
||||
showAlert(`批量解密完成:成功 ${success},失败 ${imageItems.length - success}`, '批量解密完成')
|
||||
setActionMessage(`批量解密完成:成功 ${success},失败 ${failed}`)
|
||||
showAlert(`批量解密完成:成功 ${success},失败 ${failed}`, '批量解密完成')
|
||||
finishBackgroundTask(taskId, success > 0 || failed === 0 ? 'completed' : 'failed', {
|
||||
detail: `资源页图片批量解密完成:成功 ${success},失败 ${failed}`,
|
||||
progressText: `成功 ${success} / 失败 ${failed}`
|
||||
})
|
||||
} catch (e) {
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: `资源页图片批量解密失败:${String(e)}`
|
||||
})
|
||||
showAlert(`批量解密失败:${String(e)}`, '批量解密失败')
|
||||
} finally {
|
||||
setBatchBusy(false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { create } from 'zustand'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import type { BackgroundTaskSourcePage } from '../types/backgroundTask'
|
||||
|
||||
export interface BatchImageDecryptState {
|
||||
isBatchDecrypting: boolean
|
||||
@@ -8,8 +14,9 @@ export interface BatchImageDecryptState {
|
||||
result: { success: number; fail: number }
|
||||
startTime: number
|
||||
sessionName: string
|
||||
taskId: string | null
|
||||
|
||||
startDecrypt: (total: number, sessionName: string) => void
|
||||
startDecrypt: (total: number, sessionName: string, sourcePage?: BackgroundTaskSourcePage) => void
|
||||
updateProgress: (current: number, total: number) => void
|
||||
finishDecrypt: (success: number, fail: number) => void
|
||||
setShowToast: (show: boolean) => void
|
||||
@@ -17,7 +24,26 @@ export interface BatchImageDecryptState {
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set) => ({
|
||||
const clampProgress = (current: number, total: number): { current: number; total: number } => {
|
||||
const normalizedTotal = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0
|
||||
const normalizedCurrentRaw = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : 0
|
||||
const normalizedCurrent = normalizedTotal > 0
|
||||
? Math.min(normalizedCurrentRaw, normalizedTotal)
|
||||
: normalizedCurrentRaw
|
||||
return { current: normalizedCurrent, total: normalizedTotal }
|
||||
}
|
||||
|
||||
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
|
||||
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
|
||||
|
||||
const taskProgressUpdateMeta = new Map<string, { lastAt: number; lastBucket: number; step: number }>()
|
||||
|
||||
const calcProgressStep = (total: number): number => {
|
||||
if (total <= 0) return 1
|
||||
return Math.max(1, Math.floor(total / TASK_PROGRESS_UPDATE_MAX_STEPS))
|
||||
}
|
||||
|
||||
export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set, get) => ({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
@@ -25,40 +51,127 @@ export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set) =>
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: '',
|
||||
taskId: null,
|
||||
|
||||
startDecrypt: (total, sessionName) => set({
|
||||
isBatchDecrypting: true,
|
||||
progress: { current: 0, total },
|
||||
showToast: true,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: Date.now(),
|
||||
sessionName
|
||||
}),
|
||||
startDecrypt: (total, sessionName, sourcePage = 'chat') => {
|
||||
const previousTaskId = get().taskId
|
||||
if (previousTaskId) {
|
||||
taskProgressUpdateMeta.delete(previousTaskId)
|
||||
finishBackgroundTask(previousTaskId, 'canceled', {
|
||||
detail: '已被新的批量解密任务替换',
|
||||
progressText: '已替换'
|
||||
})
|
||||
}
|
||||
|
||||
updateProgress: (current, total) => set({
|
||||
progress: { current, total }
|
||||
}),
|
||||
const normalizedProgress = clampProgress(0, total)
|
||||
const normalizedSessionName = String(sessionName || '').trim()
|
||||
const title = normalizedSessionName
|
||||
? `图片批量解密(${normalizedSessionName})`
|
||||
: '图片批量解密'
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage,
|
||||
title,
|
||||
detail: `正在解密图片(${normalizedProgress.current}/${normalizedProgress.total})`,
|
||||
progressText: `${normalizedProgress.current} / ${normalizedProgress.total}`,
|
||||
cancelable: false
|
||||
})
|
||||
taskProgressUpdateMeta.set(taskId, {
|
||||
lastAt: Date.now(),
|
||||
lastBucket: 0,
|
||||
step: calcProgressStep(normalizedProgress.total)
|
||||
})
|
||||
|
||||
finishDecrypt: (success, fail) => set({
|
||||
isBatchDecrypting: false,
|
||||
showToast: false,
|
||||
showResultToast: true,
|
||||
result: { success, fail },
|
||||
startTime: 0
|
||||
}),
|
||||
set({
|
||||
isBatchDecrypting: true,
|
||||
progress: normalizedProgress,
|
||||
showToast: true,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: Date.now(),
|
||||
sessionName: normalizedSessionName,
|
||||
taskId
|
||||
})
|
||||
},
|
||||
|
||||
updateProgress: (current, total) => {
|
||||
const previousProgress = get().progress
|
||||
const normalizedProgress = clampProgress(current, total)
|
||||
const taskId = get().taskId
|
||||
if (taskId) {
|
||||
const now = Date.now()
|
||||
const meta = taskProgressUpdateMeta.get(taskId)
|
||||
const step = meta?.step || calcProgressStep(normalizedProgress.total)
|
||||
const bucket = Math.floor(normalizedProgress.current / step)
|
||||
const intervalReached = !meta || (now - meta.lastAt >= TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS)
|
||||
const crossedBucket = !meta || bucket !== meta.lastBucket
|
||||
const isFinal = normalizedProgress.total > 0 && normalizedProgress.current >= normalizedProgress.total
|
||||
if (crossedBucket || intervalReached || isFinal) {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: `正在解密图片(${normalizedProgress.current}/${normalizedProgress.total})`,
|
||||
progressText: `${normalizedProgress.current} / ${normalizedProgress.total}`
|
||||
})
|
||||
taskProgressUpdateMeta.set(taskId, {
|
||||
lastAt: now,
|
||||
lastBucket: bucket,
|
||||
step
|
||||
})
|
||||
}
|
||||
}
|
||||
if (
|
||||
previousProgress.current !== normalizedProgress.current ||
|
||||
previousProgress.total !== normalizedProgress.total
|
||||
) {
|
||||
set({
|
||||
progress: normalizedProgress
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
finishDecrypt: (success, fail) => {
|
||||
const taskId = get().taskId
|
||||
const normalizedSuccess = Number.isFinite(success) ? Math.max(0, Math.floor(success)) : 0
|
||||
const normalizedFail = Number.isFinite(fail) ? Math.max(0, Math.floor(fail)) : 0
|
||||
if (taskId) {
|
||||
taskProgressUpdateMeta.delete(taskId)
|
||||
const status = normalizedSuccess > 0 || normalizedFail === 0 ? 'completed' : 'failed'
|
||||
finishBackgroundTask(taskId, status, {
|
||||
detail: `图片批量解密完成:成功 ${normalizedSuccess},失败 ${normalizedFail}`,
|
||||
progressText: `成功 ${normalizedSuccess} / 失败 ${normalizedFail}`
|
||||
})
|
||||
}
|
||||
|
||||
set({
|
||||
isBatchDecrypting: false,
|
||||
showToast: false,
|
||||
showResultToast: true,
|
||||
result: { success: normalizedSuccess, fail: normalizedFail },
|
||||
startTime: 0,
|
||||
taskId: null
|
||||
})
|
||||
},
|
||||
|
||||
setShowToast: (show) => set({ showToast: show }),
|
||||
setShowResultToast: (show) => set({ showResultToast: show }),
|
||||
|
||||
reset: () => set({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: ''
|
||||
})
|
||||
}))
|
||||
reset: () => {
|
||||
const taskId = get().taskId
|
||||
if (taskId) {
|
||||
taskProgressUpdateMeta.delete(taskId)
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '批量解密任务已重置',
|
||||
progressText: '已停止'
|
||||
})
|
||||
}
|
||||
|
||||
set({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: '',
|
||||
taskId: null
|
||||
})
|
||||
}
|
||||
}))
|
||||
Vendored
+9
-3
@@ -403,10 +403,16 @@ export interface ElectronAPI {
|
||||
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||
resolveCache: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; disableUpdateCheck?: boolean }) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||
resolveCache: (payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
disableUpdateCheck?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||
resolveCacheBatch: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { disableUpdateCheck?: boolean }
|
||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
||||
) => Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
||||
@@ -414,7 +420,7 @@ export interface ElectronAPI {
|
||||
}>
|
||||
preload: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { allowDecrypt?: boolean }
|
||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||
) => Promise<boolean>
|
||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
||||
onCacheResolved: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string; localPath: string }) => void) => () => void
|
||||
|
||||
Reference in new issue
Block a user