mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
计划优化 P3/5
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { parentPort, workerData } from 'worker_threads'
|
||||
import { wcdbService } from './services/wcdbService'
|
||||
import { exportService, ExportOptions } from './services/exportService'
|
||||
import type { ExportOptions } from './services/exportService'
|
||||
|
||||
interface ExportWorkerConfig {
|
||||
sessionIds: string[]
|
||||
@@ -16,11 +15,21 @@ process.env.WEFLOW_WORKER = '1'
|
||||
if (config.resourcesPath) {
|
||||
process.env.WCDB_RESOURCES_PATH = config.resourcesPath
|
||||
}
|
||||
|
||||
wcdbService.setPaths(config.resourcesPath || '', config.userDataPath || '')
|
||||
wcdbService.setLogEnabled(config.logEnabled === true)
|
||||
if (config.userDataPath) {
|
||||
process.env.WEFLOW_USER_DATA_PATH = config.userDataPath
|
||||
process.env.WEFLOW_CONFIG_CWD = config.userDataPath
|
||||
}
|
||||
process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow'
|
||||
|
||||
async function run() {
|
||||
const [{ wcdbService }, { exportService }] = await Promise.all([
|
||||
import('./services/wcdbService'),
|
||||
import('./services/exportService')
|
||||
])
|
||||
|
||||
wcdbService.setPaths(config.resourcesPath || '', config.userDataPath || '')
|
||||
wcdbService.setLogEnabled(config.logEnabled === true)
|
||||
|
||||
const result = await exportService.exportSessions(
|
||||
Array.isArray(config.sessionIds) ? config.sessionIds : [],
|
||||
String(config.outputDir || ''),
|
||||
|
||||
@@ -84,45 +84,71 @@ export class ConfigService {
|
||||
return ConfigService.instance
|
||||
}
|
||||
ConfigService.instance = this
|
||||
this.store = new Store<ConfigSchema>({
|
||||
const defaults: ConfigSchema = {
|
||||
dbPath: '',
|
||||
decryptKey: '',
|
||||
myWxid: '',
|
||||
onboardingDone: false,
|
||||
imageXorKey: 0,
|
||||
imageAesKey: '',
|
||||
wxidConfigs: {},
|
||||
cachePath: '',
|
||||
lastOpenedDb: '',
|
||||
lastSession: '',
|
||||
theme: 'system',
|
||||
themeId: 'cloud-dancer',
|
||||
language: 'zh-CN',
|
||||
logEnabled: false,
|
||||
llmModelPath: '',
|
||||
whisperModelName: 'base',
|
||||
whisperModelDir: '',
|
||||
whisperDownloadSource: 'tsinghua',
|
||||
autoTranscribeVoice: false,
|
||||
transcribeLanguages: ['zh'],
|
||||
exportDefaultConcurrency: 4,
|
||||
analyticsExcludedUsernames: [],
|
||||
authEnabled: false,
|
||||
authPassword: '',
|
||||
authUseHello: false,
|
||||
authHelloSecret: '',
|
||||
ignoredUpdateVersion: '',
|
||||
notificationEnabled: true,
|
||||
notificationPosition: 'top-right',
|
||||
notificationFilterMode: 'all',
|
||||
notificationFilterList: [],
|
||||
messagePushEnabled: false,
|
||||
windowCloseBehavior: 'ask',
|
||||
wordCloudExcludeWords: []
|
||||
}
|
||||
|
||||
const storeOptions: any = {
|
||||
name: 'WeFlow-config',
|
||||
defaults: {
|
||||
dbPath: '',
|
||||
decryptKey: '',
|
||||
myWxid: '',
|
||||
onboardingDone: false,
|
||||
imageXorKey: 0,
|
||||
imageAesKey: '',
|
||||
wxidConfigs: {},
|
||||
cachePath: '',
|
||||
lastOpenedDb: '',
|
||||
lastSession: '',
|
||||
theme: 'system',
|
||||
themeId: 'cloud-dancer',
|
||||
language: 'zh-CN',
|
||||
logEnabled: false,
|
||||
llmModelPath: '',
|
||||
whisperModelName: 'base',
|
||||
whisperModelDir: '',
|
||||
whisperDownloadSource: 'tsinghua',
|
||||
autoTranscribeVoice: false,
|
||||
transcribeLanguages: ['zh'],
|
||||
exportDefaultConcurrency: 4,
|
||||
analyticsExcludedUsernames: [],
|
||||
authEnabled: false,
|
||||
authPassword: '',
|
||||
authUseHello: false,
|
||||
authHelloSecret: '',
|
||||
ignoredUpdateVersion: '',
|
||||
notificationEnabled: true,
|
||||
notificationPosition: 'top-right',
|
||||
notificationFilterMode: 'all',
|
||||
notificationFilterList: [],
|
||||
messagePushEnabled: false,
|
||||
windowCloseBehavior: 'ask',
|
||||
wordCloudExcludeWords: []
|
||||
defaults
|
||||
}
|
||||
const runningInWorker = process.env.WEFLOW_WORKER === '1'
|
||||
if (runningInWorker) {
|
||||
const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim()
|
||||
if (cwd) {
|
||||
storeOptions.cwd = cwd
|
||||
}
|
||||
})
|
||||
storeOptions.projectName = String(process.env.WEFLOW_PROJECT_NAME || 'WeFlow').trim() || 'WeFlow'
|
||||
}
|
||||
|
||||
try {
|
||||
this.store = new Store<ConfigSchema>(storeOptions)
|
||||
} catch (error) {
|
||||
const message = String((error as Error)?.message || error || '')
|
||||
if (message.includes('projectName')) {
|
||||
const fallbackOptions = {
|
||||
...storeOptions,
|
||||
projectName: 'WeFlow',
|
||||
cwd: storeOptions.cwd || process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || process.cwd()
|
||||
}
|
||||
this.store = new Store<ConfigSchema>(fallbackOptions)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
this.migrateAuthFields()
|
||||
}
|
||||
|
||||
|
||||
@@ -421,6 +421,34 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private isCloneUnsupportedError(code: string | undefined): boolean {
|
||||
return code === 'ENOTSUP' || code === 'ENOSYS' || code === 'EINVAL' || code === 'EXDEV' || code === 'ENOTTY'
|
||||
}
|
||||
|
||||
private async copyFileOptimized(sourcePath: string, destPath: string): Promise<{ success: boolean; code?: string }> {
|
||||
const cloneFlag = typeof fs.constants.COPYFILE_FICLONE === 'number' ? fs.constants.COPYFILE_FICLONE : 0
|
||||
try {
|
||||
if (cloneFlag) {
|
||||
await fs.promises.copyFile(sourcePath, destPath, cloneFlag)
|
||||
} else {
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
}
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
const code = (e as NodeJS.ErrnoException | undefined)?.code
|
||||
if (!this.isCloneUnsupportedError(code)) {
|
||||
return { success: false, code }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, code: (e as NodeJS.ErrnoException | undefined)?.code }
|
||||
}
|
||||
}
|
||||
|
||||
private isMediaExportEnabled(options: ExportOptions): boolean {
|
||||
return options.exportMedia === true &&
|
||||
Boolean(options.exportImages || options.exportVoices || options.exportVideos || options.exportEmojis)
|
||||
@@ -2387,14 +2415,18 @@ class ExportService {
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
if (!(await this.pathExists(sourcePath))) {
|
||||
console.log(`[Export] 源图片文件不存在 (localId=${msg.localId}): ${sourcePath} → 将显示 [图片] 占位符`)
|
||||
return null
|
||||
}
|
||||
const ext = path.extname(sourcePath) || '.jpg'
|
||||
const fileName = `${messageId}_${imageKey}${ext}`
|
||||
const destPath = path.join(imagesDir, fileName)
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
const copied = await this.copyFileOptimized(sourcePath, destPath)
|
||||
if (!copied.success) {
|
||||
if (copied.code === 'ENOENT') {
|
||||
console.log(`[Export] 源图片文件不存在 (localId=${msg.localId}): ${sourcePath} → 将显示 [图片] 占位符`)
|
||||
} else {
|
||||
console.log(`[Export] 复制图片失败 (localId=${msg.localId}): ${sourcePath}, code=${copied.code || 'UNKNOWN'} → 将显示 [图片] 占位符`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
relativePath: path.posix.join(mediaRelativePrefix, 'images', fileName),
|
||||
@@ -2598,7 +2630,7 @@ class ExportService {
|
||||
// 使用 chatService 下载表情包 (利用其重试和 fallback 逻辑)
|
||||
const localPath = await chatService.downloadEmojiFile(msg)
|
||||
|
||||
if (!localPath || !(await this.pathExists(localPath))) {
|
||||
if (!localPath) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2607,8 +2639,8 @@ class ExportService {
|
||||
const key = msg.emojiMd5 || String(msg.localId)
|
||||
const fileName = `${key}${ext}`
|
||||
const destPath = path.join(emojisDir, fileName)
|
||||
|
||||
await fs.promises.copyFile(localPath, destPath)
|
||||
const copied = await this.copyFileOptimized(localPath, destPath)
|
||||
if (!copied.success) return null
|
||||
|
||||
return {
|
||||
relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName),
|
||||
@@ -2649,7 +2681,8 @@ class ExportService {
|
||||
const fileName = path.basename(sourcePath)
|
||||
const destPath = path.join(videosDir, fileName)
|
||||
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
const copied = await this.copyFileOptimized(sourcePath, destPath)
|
||||
if (!copied.success) return null
|
||||
|
||||
return {
|
||||
relativePath: path.posix.join(mediaRelativePrefix, 'videos', fileName),
|
||||
|
||||
@@ -162,21 +162,22 @@ class VideoService {
|
||||
new Set((md5List || []).map((item) => String(item || '').trim().toLowerCase()).filter(Boolean))
|
||||
)
|
||||
const resolvedMap = new Map<string, string>()
|
||||
let unresolved = [...normalizedList]
|
||||
const unresolvedSet = new Set(normalizedList)
|
||||
|
||||
for (const md5 of normalizedList) {
|
||||
const cacheKey = `${scopeKey}|${md5}`
|
||||
const cached = this.readTimedCache(this.hardlinkResolveCache, cacheKey)
|
||||
if (cached === undefined) continue
|
||||
if (cached) resolvedMap.set(md5, cached)
|
||||
unresolved = unresolved.filter((item) => item !== md5)
|
||||
unresolvedSet.delete(md5)
|
||||
}
|
||||
|
||||
if (unresolved.length === 0) return resolvedMap
|
||||
if (unresolvedSet.size === 0) return resolvedMap
|
||||
|
||||
const encryptedDbPaths = this.getHardlinkDbPaths(dbPath, wxid, cleanedWxid)
|
||||
for (const p of encryptedDbPaths) {
|
||||
if (!existsSync(p) || unresolved.length === 0) continue
|
||||
if (!existsSync(p) || unresolvedSet.size === 0) continue
|
||||
const unresolved = Array.from(unresolvedSet)
|
||||
const requests = unresolved.map((md5) => ({ md5, dbPath: p }))
|
||||
try {
|
||||
const batchResult = await wcdbService.resolveVideoHardlinkMd5Batch(requests)
|
||||
@@ -194,6 +195,7 @@ class VideoService {
|
||||
const cacheKey = `${scopeKey}|${inputMd5}`
|
||||
this.writeTimedCache(this.hardlinkResolveCache, cacheKey, resolvedMd5, this.hardlinkCacheTtlMs, this.maxCacheEntries)
|
||||
resolvedMap.set(inputMd5, resolvedMd5)
|
||||
unresolvedSet.delete(inputMd5)
|
||||
}
|
||||
} else {
|
||||
// 兼容不支持批量接口的版本,回退单条请求。
|
||||
@@ -207,17 +209,16 @@ class VideoService {
|
||||
const cacheKey = `${scopeKey}|${req.md5}`
|
||||
this.writeTimedCache(this.hardlinkResolveCache, cacheKey, resolvedMd5, this.hardlinkCacheTtlMs, this.maxCacheEntries)
|
||||
resolvedMap.set(req.md5, resolvedMd5)
|
||||
unresolvedSet.delete(req.md5)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.log('resolveVideoHardlinks 批量查询失败', { path: p, error: String(e) })
|
||||
}
|
||||
|
||||
unresolved = unresolved.filter((md5) => !resolvedMap.has(md5))
|
||||
}
|
||||
|
||||
for (const md5 of unresolved) {
|
||||
for (const md5 of unresolvedSet) {
|
||||
const cacheKey = `${scopeKey}|${md5}`
|
||||
this.writeTimedCache(this.hardlinkResolveCache, cacheKey, null, this.hardlinkCacheTtlMs, this.maxCacheEntries)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,9 @@ export class WcdbCore {
|
||||
private wcdbGetMessageTableColumns: any = null
|
||||
private wcdbGetMessageTableTimeRange: any = null
|
||||
private wcdbResolveImageHardlink: any = null
|
||||
private wcdbResolveImageHardlinkBatch: any = null
|
||||
private wcdbResolveVideoHardlinkMd5: any = null
|
||||
private wcdbResolveVideoHardlinkMd5Batch: any = null
|
||||
private wcdbInstallSnsBlockDeleteTrigger: any = null
|
||||
private wcdbUninstallSnsBlockDeleteTrigger: any = null
|
||||
private wcdbCheckSnsBlockDeleteTrigger: any = null
|
||||
@@ -111,6 +113,7 @@ export class WcdbCore {
|
||||
private imageHardlinkCache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }> = new Map()
|
||||
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 logTimer: NodeJS.Timeout | null = null
|
||||
private lastLogTail: string | null = null
|
||||
private lastResolvedLogPath: string | null = null
|
||||
@@ -962,11 +965,21 @@ export class WcdbCore {
|
||||
} catch {
|
||||
this.wcdbResolveImageHardlink = null
|
||||
}
|
||||
try {
|
||||
this.wcdbResolveImageHardlinkBatch = this.lib.func('int32 wcdb_resolve_image_hardlink_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbResolveImageHardlinkBatch = null
|
||||
}
|
||||
try {
|
||||
this.wcdbResolveVideoHardlinkMd5 = this.lib.func('int32 wcdb_resolve_video_hardlink_md5(int64 handle, const char* md5, const char* dbPath, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbResolveVideoHardlinkMd5 = null
|
||||
}
|
||||
try {
|
||||
this.wcdbResolveVideoHardlinkMd5Batch = this.lib.func('int32 wcdb_resolve_video_hardlink_md5_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbResolveVideoHardlinkMd5Batch = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_install_sns_block_delete_trigger(wcdb_handle handle, char** out_error)
|
||||
try {
|
||||
@@ -1312,6 +1325,20 @@ export class WcdbCore {
|
||||
result: this.cloneHardlinkResult(result),
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
if (cache.size <= this.hardlinkCacheMaxEntries) return
|
||||
|
||||
const now = Date.now()
|
||||
for (const [cacheKey, entry] of cache) {
|
||||
if (now - entry.updatedAt > this.hardlinkCacheTtlMs) {
|
||||
cache.delete(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
while (cache.size > this.hardlinkCacheMaxEntries) {
|
||||
const oldestKey = cache.keys().next().value as string | undefined
|
||||
if (!oldestKey) break
|
||||
cache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
private cloneHardlinkResult(result: { success: boolean; data?: any; error?: string }): { success: boolean; data?: any; error?: string } {
|
||||
@@ -2853,22 +2880,98 @@ export class WcdbCore {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!Array.isArray(requests)) return { success: false, error: '参数错误: requests 必须是数组' }
|
||||
try {
|
||||
const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = []
|
||||
for (let i = 0; i < requests.length; i += 1) {
|
||||
const req = requests[i] || { md5: '' }
|
||||
const normalizedMd5 = String(req.md5 || '').trim().toLowerCase()
|
||||
if (!normalizedMd5) {
|
||||
rows.push({ index: i, md5: '', success: false, error: 'md5 为空' })
|
||||
const normalizedRequests = requests.map((req) => ({
|
||||
md5: String(req?.md5 || '').trim().toLowerCase(),
|
||||
accountDir: String(req?.accountDir || '').trim()
|
||||
}))
|
||||
const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = new Array(normalizedRequests.length)
|
||||
const unresolved: Array<{ index: number; md5: string; accountDir: string }> = []
|
||||
|
||||
for (let i = 0; i < normalizedRequests.length; i += 1) {
|
||||
const req = normalizedRequests[i]
|
||||
if (!req.md5) {
|
||||
rows[i] = { index: i, md5: '', success: false, error: 'md5 为空' }
|
||||
continue
|
||||
}
|
||||
const result = await this.resolveImageHardlink(normalizedMd5, req.accountDir)
|
||||
rows.push({
|
||||
index: i,
|
||||
md5: normalizedMd5,
|
||||
const cacheKey = this.makeHardlinkCacheKey(req.md5, req.accountDir)
|
||||
const cached = this.readHardlinkCache(this.imageHardlinkCache, cacheKey)
|
||||
if (cached) {
|
||||
rows[i] = {
|
||||
index: i,
|
||||
md5: req.md5,
|
||||
success: cached.success === true,
|
||||
data: cached.data,
|
||||
error: cached.error
|
||||
}
|
||||
} else {
|
||||
unresolved.push({ index: i, md5: req.md5, accountDir: req.accountDir })
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length === 0) {
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
if (this.wcdbResolveImageHardlinkBatch) {
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const payload = JSON.stringify(unresolved.map((req) => ({
|
||||
md5: req.md5,
|
||||
account_dir: req.accountDir || undefined
|
||||
})))
|
||||
const result = this.wcdbResolveImageHardlinkBatch(this.handle, payload, outPtr)
|
||||
if (result === 0 && outPtr[0]) {
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (jsonStr) {
|
||||
const nativeRows = JSON.parse(jsonStr)
|
||||
const mappedRows = Array.isArray(nativeRows) ? nativeRows.map((row: any, index: number) => {
|
||||
const rowIndexRaw = Number(row?.index)
|
||||
const rowIndex = Number.isFinite(rowIndexRaw) ? Math.floor(rowIndexRaw) : index
|
||||
const fallbackReq = rowIndex >= 0 && rowIndex < unresolved.length ? unresolved[rowIndex] : { md5: '', accountDir: '', index: -1 }
|
||||
const rowMd5 = String(row?.md5 || fallbackReq.md5 || '').trim().toLowerCase()
|
||||
const success = row?.success === true || row?.success === 1 || row?.success === '1'
|
||||
const data = row?.data && typeof row.data === 'object' ? row.data : {}
|
||||
const error = row?.error ? String(row.error) : undefined
|
||||
if (success && rowMd5) {
|
||||
const cacheKey = this.makeHardlinkCacheKey(rowMd5, fallbackReq.accountDir)
|
||||
this.writeHardlinkCache(this.imageHardlinkCache, cacheKey, { success: true, data })
|
||||
}
|
||||
return {
|
||||
index: rowIndex,
|
||||
md5: rowMd5,
|
||||
success,
|
||||
data,
|
||||
error
|
||||
}
|
||||
}) : []
|
||||
for (const row of mappedRows) {
|
||||
const fallbackReq = row.index >= 0 && row.index < unresolved.length ? unresolved[row.index] : null
|
||||
if (!fallbackReq) continue
|
||||
rows[fallbackReq.index] = {
|
||||
index: fallbackReq.index,
|
||||
md5: row.md5 || fallbackReq.md5,
|
||||
success: row.success,
|
||||
data: row.data,
|
||||
error: row.error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 回退到单条循环实现
|
||||
}
|
||||
}
|
||||
|
||||
for (const req of unresolved) {
|
||||
if (rows[req.index]) continue
|
||||
const result = await this.resolveImageHardlink(req.md5, req.accountDir)
|
||||
rows[req.index] = {
|
||||
index: req.index,
|
||||
md5: req.md5,
|
||||
success: result.success === true,
|
||||
data: result.data,
|
||||
error: result.error
|
||||
})
|
||||
}
|
||||
}
|
||||
return { success: true, rows }
|
||||
} catch (e) {
|
||||
@@ -2882,22 +2985,98 @@ export class WcdbCore {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!Array.isArray(requests)) return { success: false, error: '参数错误: requests 必须是数组' }
|
||||
try {
|
||||
const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = []
|
||||
for (let i = 0; i < requests.length; i += 1) {
|
||||
const req = requests[i] || { md5: '' }
|
||||
const normalizedMd5 = String(req.md5 || '').trim().toLowerCase()
|
||||
if (!normalizedMd5) {
|
||||
rows.push({ index: i, md5: '', success: false, error: 'md5 为空' })
|
||||
const normalizedRequests = requests.map((req) => ({
|
||||
md5: String(req?.md5 || '').trim().toLowerCase(),
|
||||
dbPath: String(req?.dbPath || '').trim()
|
||||
}))
|
||||
const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = new Array(normalizedRequests.length)
|
||||
const unresolved: Array<{ index: number; md5: string; dbPath: string }> = []
|
||||
|
||||
for (let i = 0; i < normalizedRequests.length; i += 1) {
|
||||
const req = normalizedRequests[i]
|
||||
if (!req.md5) {
|
||||
rows[i] = { index: i, md5: '', success: false, error: 'md5 为空' }
|
||||
continue
|
||||
}
|
||||
const result = await this.resolveVideoHardlinkMd5(normalizedMd5, req.dbPath)
|
||||
rows.push({
|
||||
index: i,
|
||||
md5: normalizedMd5,
|
||||
const cacheKey = this.makeHardlinkCacheKey(req.md5, req.dbPath)
|
||||
const cached = this.readHardlinkCache(this.videoHardlinkCache, cacheKey)
|
||||
if (cached) {
|
||||
rows[i] = {
|
||||
index: i,
|
||||
md5: req.md5,
|
||||
success: cached.success === true,
|
||||
data: cached.data,
|
||||
error: cached.error
|
||||
}
|
||||
} else {
|
||||
unresolved.push({ index: i, md5: req.md5, dbPath: req.dbPath })
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length === 0) {
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
if (this.wcdbResolveVideoHardlinkMd5Batch) {
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const payload = JSON.stringify(unresolved.map((req) => ({
|
||||
md5: req.md5,
|
||||
db_path: req.dbPath || undefined
|
||||
})))
|
||||
const result = this.wcdbResolveVideoHardlinkMd5Batch(this.handle, payload, outPtr)
|
||||
if (result === 0 && outPtr[0]) {
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (jsonStr) {
|
||||
const nativeRows = JSON.parse(jsonStr)
|
||||
const mappedRows = Array.isArray(nativeRows) ? nativeRows.map((row: any, index: number) => {
|
||||
const rowIndexRaw = Number(row?.index)
|
||||
const rowIndex = Number.isFinite(rowIndexRaw) ? Math.floor(rowIndexRaw) : index
|
||||
const fallbackReq = rowIndex >= 0 && rowIndex < unresolved.length ? unresolved[rowIndex] : { md5: '', dbPath: '', index: -1 }
|
||||
const rowMd5 = String(row?.md5 || fallbackReq.md5 || '').trim().toLowerCase()
|
||||
const success = row?.success === true || row?.success === 1 || row?.success === '1'
|
||||
const data = row?.data && typeof row.data === 'object' ? row.data : {}
|
||||
const error = row?.error ? String(row.error) : undefined
|
||||
if (success && rowMd5) {
|
||||
const cacheKey = this.makeHardlinkCacheKey(rowMd5, fallbackReq.dbPath)
|
||||
this.writeHardlinkCache(this.videoHardlinkCache, cacheKey, { success: true, data })
|
||||
}
|
||||
return {
|
||||
index: rowIndex,
|
||||
md5: rowMd5,
|
||||
success,
|
||||
data,
|
||||
error
|
||||
}
|
||||
}) : []
|
||||
for (const row of mappedRows) {
|
||||
const fallbackReq = row.index >= 0 && row.index < unresolved.length ? unresolved[row.index] : null
|
||||
if (!fallbackReq) continue
|
||||
rows[fallbackReq.index] = {
|
||||
index: fallbackReq.index,
|
||||
md5: row.md5 || fallbackReq.md5,
|
||||
success: row.success,
|
||||
data: row.data,
|
||||
error: row.error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 回退到单条循环实现
|
||||
}
|
||||
}
|
||||
|
||||
for (const req of unresolved) {
|
||||
if (rows[req.index]) continue
|
||||
const result = await this.resolveVideoHardlinkMd5(req.md5, req.dbPath)
|
||||
rows[req.index] = {
|
||||
index: req.index,
|
||||
md5: req.md5,
|
||||
success: result.success === true,
|
||||
data: result.data,
|
||||
error: result.error
|
||||
})
|
||||
}
|
||||
}
|
||||
return { success: true, rows }
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user