mirror of
https://fastgit.cc/github.com/hicccc77/WeFlow
synced 2026-04-20 12:51:02 +08:00
4
.github/workflows/dev-daily-fixed.yml
vendored
4
.github/workflows/dev-daily-fixed.yml
vendored
@@ -93,7 +93,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -160,7 +159,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -208,7 +206,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -256,7 +253,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/preview-nightly-main.yml
vendored
4
.github/workflows/preview-nightly-main.yml
vendored
@@ -120,7 +120,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -190,7 +189,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -242,7 +240,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -294,7 +291,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -27,7 +27,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -84,7 +83,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -140,7 +138,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
@@ -191,7 +188,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -75,4 +75,4 @@ pnpm-lock.yaml
|
|||||||
wechat-research-site
|
wechat-research-site
|
||||||
.codex
|
.codex
|
||||||
weflow-web-offical
|
weflow-web-offical
|
||||||
Insight
|
/Wedecrypt
|
||||||
@@ -2636,13 +2636,24 @@ function registerIpcHandlers() {
|
|||||||
// 私聊克隆
|
// 私聊克隆
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('image:decrypt', async (_, payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => {
|
ipcMain.handle('image:decrypt', async (_, payload: {
|
||||||
|
sessionId?: string
|
||||||
|
imageMd5?: string
|
||||||
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
|
force?: boolean
|
||||||
|
preferFilePath?: boolean
|
||||||
|
hardlinkOnly?: boolean
|
||||||
|
}) => {
|
||||||
return imageDecryptService.decryptImage(payload)
|
return imageDecryptService.decryptImage(payload)
|
||||||
})
|
})
|
||||||
ipcMain.handle('image:resolveCache', async (_, payload: {
|
ipcMain.handle('image:resolveCache', async (_, payload: {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
imageMd5?: string
|
imageMd5?: string
|
||||||
imageDatName?: string
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
|
preferFilePath?: boolean
|
||||||
|
hardlinkOnly?: boolean
|
||||||
disableUpdateCheck?: boolean
|
disableUpdateCheck?: boolean
|
||||||
allowCacheIndex?: boolean
|
allowCacheIndex?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
@@ -2652,13 +2663,15 @@ function registerIpcHandlers() {
|
|||||||
'image:resolveCacheBatch',
|
'image:resolveCacheBatch',
|
||||||
async (
|
async (
|
||||||
_,
|
_,
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; preferFilePath?: boolean; hardlinkOnly?: boolean }>,
|
||||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean; preferFilePath?: boolean; hardlinkOnly?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const list = Array.isArray(payloads) ? payloads : []
|
const list = Array.isArray(payloads) ? payloads : []
|
||||||
const rows = await Promise.all(list.map(async (payload) => {
|
const rows = await Promise.all(list.map(async (payload) => {
|
||||||
return imageDecryptService.resolveCachedImage({
|
return imageDecryptService.resolveCachedImage({
|
||||||
...payload,
|
...payload,
|
||||||
|
preferFilePath: payload.preferFilePath ?? options?.preferFilePath === true,
|
||||||
|
hardlinkOnly: payload.hardlinkOnly ?? options?.hardlinkOnly === true,
|
||||||
disableUpdateCheck: options?.disableUpdateCheck === true,
|
disableUpdateCheck: options?.disableUpdateCheck === true,
|
||||||
allowCacheIndex: options?.allowCacheIndex !== false
|
allowCacheIndex: options?.allowCacheIndex !== false
|
||||||
})
|
})
|
||||||
@@ -2670,7 +2683,7 @@ function registerIpcHandlers() {
|
|||||||
'image:preload',
|
'image:preload',
|
||||||
async (
|
async (
|
||||||
_,
|
_,
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }>,
|
||||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||||
) => {
|
) => {
|
||||||
imagePreloadService.enqueue(payloads || [], options)
|
imagePreloadService.enqueue(payloads || [], options)
|
||||||
|
|||||||
@@ -286,22 +286,25 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
// 图片解密
|
// 图片解密
|
||||||
image: {
|
image: {
|
||||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) =>
|
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; force?: boolean; preferFilePath?: boolean; hardlinkOnly?: boolean }) =>
|
||||||
ipcRenderer.invoke('image:decrypt', payload),
|
ipcRenderer.invoke('image:decrypt', payload),
|
||||||
resolveCache: (payload: {
|
resolveCache: (payload: {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
imageMd5?: string
|
imageMd5?: string
|
||||||
imageDatName?: string
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
|
preferFilePath?: boolean
|
||||||
|
hardlinkOnly?: boolean
|
||||||
disableUpdateCheck?: boolean
|
disableUpdateCheck?: boolean
|
||||||
allowCacheIndex?: boolean
|
allowCacheIndex?: boolean
|
||||||
}) =>
|
}) =>
|
||||||
ipcRenderer.invoke('image:resolveCache', payload),
|
ipcRenderer.invoke('image:resolveCache', payload),
|
||||||
resolveCacheBatch: (
|
resolveCacheBatch: (
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; preferFilePath?: boolean; hardlinkOnly?: boolean }>,
|
||||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean; preferFilePath?: boolean; hardlinkOnly?: boolean }
|
||||||
) => ipcRenderer.invoke('image:resolveCacheBatch', payloads, options),
|
) => ipcRenderer.invoke('image:resolveCacheBatch', payloads, options),
|
||||||
preload: (
|
preload: (
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }>,
|
||||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||||
) => ipcRenderer.invoke('image:preload', payloads, options),
|
) => ipcRenderer.invoke('image:preload', payloads, options),
|
||||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => {
|
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => {
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ class ChatService {
|
|||||||
return Number.isFinite(parsed) ? parsed : null
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
}
|
}
|
||||||
|
|
||||||
private toCodeOnlyMessage(rawMessage?: string, fallbackCode = -3999): string {
|
private toCodeOnlyMessage(rawMessage?: string | null, fallbackCode = -3999): string {
|
||||||
const code = this.extractErrorCode(rawMessage) ?? fallbackCode
|
const code = this.extractErrorCode(rawMessage) ?? fallbackCode
|
||||||
return `错误码: ${code}`
|
return `错误码: ${code}`
|
||||||
}
|
}
|
||||||
@@ -7105,13 +7105,23 @@ class ChatService {
|
|||||||
return { success: false, error: '未找到消息' }
|
return { success: false, error: '未找到消息' }
|
||||||
}
|
}
|
||||||
const msg = msgResult.message
|
const msg = msgResult.message
|
||||||
|
const rawImageInfo = msg.rawContent ? this.parseImageInfo(msg.rawContent) : {}
|
||||||
|
const imageMd5 = msg.imageMd5 || rawImageInfo.md5
|
||||||
|
const imageDatName = msg.imageDatName
|
||||||
|
|
||||||
// 2. 使用 imageDecryptService 解密图片
|
if (!imageMd5 && !imageDatName) {
|
||||||
|
return { success: false, error: '图片缺少 md5/datName,无法定位原文件' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 使用 imageDecryptService 解密图片(仅使用真实图片标识)
|
||||||
const result = await this.imageDecryptService.decryptImage({
|
const result = await this.imageDecryptService.decryptImage({
|
||||||
sessionId,
|
sessionId,
|
||||||
imageMd5: msg.imageMd5,
|
imageMd5,
|
||||||
imageDatName: msg.imageDatName || String(msg.localId),
|
imageDatName,
|
||||||
force: false
|
createTime: msg.createTime,
|
||||||
|
force: false,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success || !result.localPath) {
|
if (!result.success || !result.localPath) {
|
||||||
@@ -8358,7 +8368,6 @@ class ChatService {
|
|||||||
if (normalized.length === 0) return []
|
if (normalized.length === 0) return []
|
||||||
|
|
||||||
// 规避 native options_json 可能存在的固定缓冲上限:按 payload 字节安全分块。
|
// 规避 native options_json 可能存在的固定缓冲上限:按 payload 字节安全分块。
|
||||||
// 这不是降级或裁剪范围,而是完整遍历所有群并做结果合并。
|
|
||||||
const maxBytesRaw = Number(process.env.WEFLOW_MY_FOOTPRINT_GROUP_OPTIONS_MAX_BYTES || 900)
|
const maxBytesRaw = Number(process.env.WEFLOW_MY_FOOTPRINT_GROUP_OPTIONS_MAX_BYTES || 900)
|
||||||
const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw >= 512
|
const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw >= 512
|
||||||
? Math.floor(maxBytesRaw)
|
? Math.floor(maxBytesRaw)
|
||||||
@@ -9325,7 +9334,7 @@ class ChatService {
|
|||||||
latest_ts: this.toSafeInt(item?.latest_ts, 0),
|
latest_ts: this.toSafeInt(item?.latest_ts, 0),
|
||||||
anchor_local_id: this.toSafeInt(item?.anchor_local_id, 0),
|
anchor_local_id: this.toSafeInt(item?.anchor_local_id, 0),
|
||||||
anchor_create_time: this.toSafeInt(item?.anchor_create_time, 0)
|
anchor_create_time: this.toSafeInt(item?.anchor_create_time, 0)
|
||||||
})).filter((item) => item.session_id)
|
})).filter((item: MyFootprintPrivateSession) => item.session_id)
|
||||||
|
|
||||||
const private_segments: MyFootprintPrivateSegment[] = privateSegmentsRaw.map((item: any) => ({
|
const private_segments: MyFootprintPrivateSegment[] = privateSegmentsRaw.map((item: any) => ({
|
||||||
session_id: String(item?.session_id || '').trim(),
|
session_id: String(item?.session_id || '').trim(),
|
||||||
@@ -9344,7 +9353,7 @@ class ChatService {
|
|||||||
anchor_create_time: this.toSafeInt(item?.anchor_create_time, 0),
|
anchor_create_time: this.toSafeInt(item?.anchor_create_time, 0),
|
||||||
displayName: String(item?.displayName || '').trim() || undefined,
|
displayName: String(item?.displayName || '').trim() || undefined,
|
||||||
avatarUrl: String(item?.avatarUrl || '').trim() || undefined
|
avatarUrl: String(item?.avatarUrl || '').trim() || undefined
|
||||||
})).filter((item) => item.session_id && item.start_ts > 0)
|
})).filter((item: MyFootprintPrivateSegment) => item.session_id && item.start_ts > 0)
|
||||||
|
|
||||||
const mentions: MyFootprintMentionItem[] = mentionsRaw.map((item: any) => ({
|
const mentions: MyFootprintMentionItem[] = mentionsRaw.map((item: any) => ({
|
||||||
session_id: String(item?.session_id || '').trim(),
|
session_id: String(item?.session_id || '').trim(),
|
||||||
@@ -9353,13 +9362,13 @@ class ChatService {
|
|||||||
sender_username: String(item?.sender_username || '').trim(),
|
sender_username: String(item?.sender_username || '').trim(),
|
||||||
message_content: String(item?.message_content || ''),
|
message_content: String(item?.message_content || ''),
|
||||||
source: String(item?.source || '')
|
source: String(item?.source || '')
|
||||||
})).filter((item) => item.session_id)
|
})).filter((item: MyFootprintMentionItem) => item.session_id)
|
||||||
|
|
||||||
const mention_groups: MyFootprintMentionGroup[] = mentionGroupsRaw.map((item: any) => ({
|
const mention_groups: MyFootprintMentionGroup[] = mentionGroupsRaw.map((item: any) => ({
|
||||||
session_id: String(item?.session_id || '').trim(),
|
session_id: String(item?.session_id || '').trim(),
|
||||||
count: this.toSafeInt(item?.count, 0),
|
count: this.toSafeInt(item?.count, 0),
|
||||||
latest_ts: this.toSafeInt(item?.latest_ts, 0)
|
latest_ts: this.toSafeInt(item?.latest_ts, 0)
|
||||||
})).filter((item) => item.session_id)
|
})).filter((item: MyFootprintMentionGroup) => item.session_id)
|
||||||
|
|
||||||
const diagnostics: MyFootprintDiagnostics = {
|
const diagnostics: MyFootprintDiagnostics = {
|
||||||
truncated: Boolean(diagnosticsRaw.truncated),
|
truncated: Boolean(diagnosticsRaw.truncated),
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ interface ConfigSchema {
|
|||||||
autoTranscribeVoice: boolean
|
autoTranscribeVoice: boolean
|
||||||
transcribeLanguages: string[]
|
transcribeLanguages: string[]
|
||||||
exportDefaultConcurrency: number
|
exportDefaultConcurrency: number
|
||||||
exportDefaultImageDeepSearchOnMiss: boolean
|
|
||||||
analyticsExcludedUsernames: string[]
|
analyticsExcludedUsernames: string[]
|
||||||
|
|
||||||
// 安全相关
|
// 安全相关
|
||||||
@@ -165,7 +164,6 @@ export class ConfigService {
|
|||||||
autoTranscribeVoice: false,
|
autoTranscribeVoice: false,
|
||||||
transcribeLanguages: ['zh'],
|
transcribeLanguages: ['zh'],
|
||||||
exportDefaultConcurrency: 4,
|
exportDefaultConcurrency: 4,
|
||||||
exportDefaultImageDeepSearchOnMiss: true,
|
|
||||||
analyticsExcludedUsernames: [],
|
analyticsExcludedUsernames: [],
|
||||||
authEnabled: false,
|
authEnabled: false,
|
||||||
authPassword: '',
|
authPassword: '',
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export interface ExportOptions {
|
|||||||
sessionNameWithTypePrefix?: boolean
|
sessionNameWithTypePrefix?: boolean
|
||||||
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
||||||
exportConcurrency?: number
|
exportConcurrency?: number
|
||||||
imageDeepSearchOnMiss?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
|
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
|
||||||
@@ -1092,8 +1091,7 @@ class ExportService {
|
|||||||
private getImageMissingRunCacheKey(
|
private getImageMissingRunCacheKey(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
imageMd5?: unknown,
|
imageMd5?: unknown,
|
||||||
imageDatName?: unknown,
|
imageDatName?: unknown
|
||||||
imageDeepSearchOnMiss = true
|
|
||||||
): string | null {
|
): string | null {
|
||||||
const normalizedSessionId = String(sessionId || '').trim()
|
const normalizedSessionId = String(sessionId || '').trim()
|
||||||
const normalizedImageMd5 = String(imageMd5 || '').trim().toLowerCase()
|
const normalizedImageMd5 = String(imageMd5 || '').trim().toLowerCase()
|
||||||
@@ -1105,8 +1103,7 @@ class ExportService {
|
|||||||
const secondaryToken = normalizedImageMd5 && normalizedImageDatName && normalizedImageDatName !== normalizedImageMd5
|
const secondaryToken = normalizedImageMd5 && normalizedImageDatName && normalizedImageDatName !== normalizedImageMd5
|
||||||
? normalizedImageDatName
|
? normalizedImageDatName
|
||||||
: ''
|
: ''
|
||||||
const lookupMode = imageDeepSearchOnMiss ? 'deep' : 'hardlink'
|
return `${normalizedSessionId}\u001f${primaryToken}\u001f${secondaryToken}`
|
||||||
return `${lookupMode}\u001f${normalizedSessionId}\u001f${primaryToken}\u001f${secondaryToken}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeEmojiMd5(value: unknown): string | undefined {
|
private normalizeEmojiMd5(value: unknown): string | undefined {
|
||||||
@@ -3583,7 +3580,6 @@ class ExportService {
|
|||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
includeVideoPoster?: boolean
|
includeVideoPoster?: boolean
|
||||||
includeVoiceWithTranscript?: boolean
|
includeVoiceWithTranscript?: boolean
|
||||||
imageDeepSearchOnMiss?: boolean
|
|
||||||
dirCache?: Set<string>
|
dirCache?: Set<string>
|
||||||
}
|
}
|
||||||
): Promise<MediaExportItem | null> {
|
): Promise<MediaExportItem | null> {
|
||||||
@@ -3596,8 +3592,7 @@ class ExportService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
mediaRootDir,
|
mediaRootDir,
|
||||||
mediaRelativePrefix,
|
mediaRelativePrefix,
|
||||||
options.dirCache,
|
options.dirCache
|
||||||
options.imageDeepSearchOnMiss !== false
|
|
||||||
)
|
)
|
||||||
if (result) {
|
if (result) {
|
||||||
}
|
}
|
||||||
@@ -3654,8 +3649,7 @@ class ExportService {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
mediaRootDir: string,
|
mediaRootDir: string,
|
||||||
mediaRelativePrefix: string,
|
mediaRelativePrefix: string,
|
||||||
dirCache?: Set<string>,
|
dirCache?: Set<string>
|
||||||
imageDeepSearchOnMiss = true
|
|
||||||
): Promise<MediaExportItem | null> {
|
): Promise<MediaExportItem | null> {
|
||||||
try {
|
try {
|
||||||
const imagesDir = path.join(mediaRootDir, mediaRelativePrefix, 'images')
|
const imagesDir = path.join(mediaRootDir, mediaRelativePrefix, 'images')
|
||||||
@@ -3675,8 +3669,7 @@ class ExportService {
|
|||||||
const missingRunCacheKey = this.getImageMissingRunCacheKey(
|
const missingRunCacheKey = this.getImageMissingRunCacheKey(
|
||||||
sessionId,
|
sessionId,
|
||||||
imageMd5,
|
imageMd5,
|
||||||
imageDatName,
|
imageDatName
|
||||||
imageDeepSearchOnMiss
|
|
||||||
)
|
)
|
||||||
if (missingRunCacheKey && this.mediaRunMissingImageKeys.has(missingRunCacheKey)) {
|
if (missingRunCacheKey && this.mediaRunMissingImageKeys.has(missingRunCacheKey)) {
|
||||||
return null
|
return null
|
||||||
@@ -3686,25 +3679,20 @@ class ExportService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
imageMd5,
|
imageMd5,
|
||||||
imageDatName,
|
imageDatName,
|
||||||
|
createTime: msg.createTime,
|
||||||
force: true, // 导出优先高清,失败再回退缩略图
|
force: true, // 导出优先高清,失败再回退缩略图
|
||||||
preferFilePath: true,
|
preferFilePath: true,
|
||||||
hardlinkOnly: !imageDeepSearchOnMiss
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success || !result.localPath) {
|
if (!result.success || !result.localPath) {
|
||||||
console.log(`[Export] 图片解密失败 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
|
console.log(`[Export] 图片解密失败 (localId=${msg.localId}): imageMd5=${imageMd5}, imageDatName=${imageDatName}, error=${result.error || '未知'}`)
|
||||||
if (!imageDeepSearchOnMiss) {
|
|
||||||
console.log(`[Export] 未命中 hardlink(已关闭缺图深度搜索)→ 将显示 [图片] 占位符`)
|
|
||||||
if (missingRunCacheKey) {
|
|
||||||
this.mediaRunMissingImageKeys.add(missingRunCacheKey)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// 尝试获取缩略图
|
// 尝试获取缩略图
|
||||||
const thumbResult = await imageDecryptService.resolveCachedImage({
|
const thumbResult = await imageDecryptService.resolveCachedImage({
|
||||||
sessionId,
|
sessionId,
|
||||||
imageMd5,
|
imageMd5,
|
||||||
imageDatName,
|
imageDatName,
|
||||||
|
createTime: msg.createTime,
|
||||||
preferFilePath: true
|
preferFilePath: true
|
||||||
})
|
})
|
||||||
if (thumbResult.success && thumbResult.localPath) {
|
if (thumbResult.success && thumbResult.localPath) {
|
||||||
@@ -5302,7 +5290,6 @@ class ExportService {
|
|||||||
maxFileSizeMb: options.maxFileSizeMb,
|
maxFileSizeMb: options.maxFileSizeMb,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
@@ -5813,7 +5800,6 @@ class ExportService {
|
|||||||
maxFileSizeMb: options.maxFileSizeMb,
|
maxFileSizeMb: options.maxFileSizeMb,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
@@ -6685,7 +6671,6 @@ class ExportService {
|
|||||||
maxFileSizeMb: options.maxFileSizeMb,
|
maxFileSizeMb: options.maxFileSizeMb,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
@@ -7436,7 +7421,6 @@ class ExportService {
|
|||||||
maxFileSizeMb: options.maxFileSizeMb,
|
maxFileSizeMb: options.maxFileSizeMb,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
@@ -7816,7 +7800,6 @@ class ExportService {
|
|||||||
maxFileSizeMb: options.maxFileSizeMb,
|
maxFileSizeMb: options.maxFileSizeMb,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
@@ -8240,7 +8223,6 @@ class ExportService {
|
|||||||
includeVideoPoster: options.format === 'html',
|
includeVideoPoster: options.format === 'html',
|
||||||
includeVoiceWithTranscript: true,
|
includeVoiceWithTranscript: true,
|
||||||
exportVideos: options.exportVideos,
|
exportVideos: options.exportVideos,
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
dirCache: mediaDirCache
|
dirCache: mediaDirCache
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
|
|||||||
@@ -1208,6 +1208,30 @@ class HttpService {
|
|||||||
const sessionDir = path.join(this.getApiMediaExportPath(), this.sanitizeFileName(talker, 'session'))
|
const sessionDir = path.join(this.getApiMediaExportPath(), this.sanitizeFileName(talker, 'session'))
|
||||||
this.ensureDir(sessionDir)
|
this.ensureDir(sessionDir)
|
||||||
|
|
||||||
|
// 预热图片 hardlink 索引,减少逐条导出时的查找开销
|
||||||
|
if (options.exportImages) {
|
||||||
|
const imageMd5Set = new Set<string>()
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.localType !== 3) continue
|
||||||
|
const imageMd5 = String(msg.imageMd5 || '').trim().toLowerCase()
|
||||||
|
if (imageMd5) {
|
||||||
|
imageMd5Set.add(imageMd5)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const imageDatName = String(msg.imageDatName || '').trim().toLowerCase()
|
||||||
|
if (/^[a-f0-9]{32}$/i.test(imageDatName)) {
|
||||||
|
imageMd5Set.add(imageDatName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageMd5Set.size > 0) {
|
||||||
|
try {
|
||||||
|
await imageDecryptService.preloadImageHardlinkMd5s(Array.from(imageMd5Set))
|
||||||
|
} catch {
|
||||||
|
// ignore preload failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const exported = await this.exportMediaForMessage(msg, talker, sessionDir, options)
|
const exported = await this.exportMediaForMessage(msg, talker, sessionDir, options)
|
||||||
if (exported) {
|
if (exported) {
|
||||||
@@ -1230,27 +1254,50 @@ class HttpService {
|
|||||||
sessionId: talker,
|
sessionId: talker,
|
||||||
imageMd5: msg.imageMd5,
|
imageMd5: msg.imageMd5,
|
||||||
imageDatName: msg.imageDatName,
|
imageDatName: msg.imageDatName,
|
||||||
force: true
|
createTime: msg.createTime,
|
||||||
|
force: true,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
if (result.success && result.localPath) {
|
|
||||||
let imagePath = result.localPath
|
let imagePath = result.success ? result.localPath : undefined
|
||||||
|
if (!imagePath) {
|
||||||
|
try {
|
||||||
|
const cached = await imageDecryptService.resolveCachedImage({
|
||||||
|
sessionId: talker,
|
||||||
|
imageMd5: msg.imageMd5,
|
||||||
|
imageDatName: msg.imageDatName,
|
||||||
|
createTime: msg.createTime,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
|
})
|
||||||
|
if (cached.success && cached.localPath) {
|
||||||
|
imagePath = cached.localPath
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore resolve failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
if (imagePath.startsWith('data:')) {
|
if (imagePath.startsWith('data:')) {
|
||||||
const base64Match = imagePath.match(/^data:[^;]+;base64,(.+)$/)
|
const base64Match = imagePath.match(/^data:[^;]+;base64,(.+)$/)
|
||||||
if (base64Match) {
|
if (!base64Match) return null
|
||||||
const imageBuffer = Buffer.from(base64Match[1], 'base64')
|
const imageBuffer = Buffer.from(base64Match[1], 'base64')
|
||||||
const ext = this.detectImageExt(imageBuffer)
|
const ext = this.detectImageExt(imageBuffer)
|
||||||
const fileBase = this.sanitizeFileName(msg.imageMd5 || msg.imageDatName || `image_${msg.localId}`, `image_${msg.localId}`)
|
const fileBase = this.sanitizeFileName(msg.imageMd5 || msg.imageDatName || `image_${msg.localId}`, `image_${msg.localId}`)
|
||||||
const fileName = `${fileBase}${ext}`
|
const fileName = `${fileBase}${ext}`
|
||||||
const targetDir = path.join(sessionDir, 'images')
|
const targetDir = path.join(sessionDir, 'images')
|
||||||
const fullPath = path.join(targetDir, fileName)
|
const fullPath = path.join(targetDir, fileName)
|
||||||
this.ensureDir(targetDir)
|
this.ensureDir(targetDir)
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!fs.existsSync(fullPath)) {
|
||||||
fs.writeFileSync(fullPath, imageBuffer)
|
fs.writeFileSync(fullPath, imageBuffer)
|
||||||
}
|
|
||||||
const relativePath = `${this.sanitizeFileName(talker, 'session')}/images/${fileName}`
|
|
||||||
return { kind: 'image', fileName, fullPath, relativePath }
|
|
||||||
}
|
}
|
||||||
} else if (fs.existsSync(imagePath)) {
|
const relativePath = `${this.sanitizeFileName(talker, 'session')}/images/${fileName}`
|
||||||
|
return { kind: 'image', fileName, fullPath, relativePath }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(imagePath)) {
|
||||||
const imageBuffer = fs.readFileSync(imagePath)
|
const imageBuffer = fs.readFileSync(imagePath)
|
||||||
const ext = this.detectImageExt(imageBuffer)
|
const ext = this.detectImageExt(imageBuffer)
|
||||||
const fileBase = this.sanitizeFileName(msg.imageMd5 || msg.imageDatName || `image_${msg.localId}`, `image_${msg.localId}`)
|
const fileBase = this.sanitizeFileName(msg.imageMd5 || msg.imageDatName || `image_${msg.localId}`, `image_${msg.localId}`)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ type PreloadImagePayload = {
|
|||||||
sessionId?: string
|
sessionId?: string
|
||||||
imageMd5?: string
|
imageMd5?: string
|
||||||
imageDatName?: string
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreloadOptions = {
|
type PreloadOptions = {
|
||||||
@@ -74,6 +75,9 @@ export class ImagePreloadService {
|
|||||||
sessionId: task.sessionId,
|
sessionId: task.sessionId,
|
||||||
imageMd5: task.imageMd5,
|
imageMd5: task.imageMd5,
|
||||||
imageDatName: task.imageDatName,
|
imageDatName: task.imageDatName,
|
||||||
|
createTime: task.createTime,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true,
|
||||||
disableUpdateCheck: !task.allowDecrypt,
|
disableUpdateCheck: !task.allowDecrypt,
|
||||||
allowCacheIndex: task.allowCacheIndex
|
allowCacheIndex: task.allowCacheIndex
|
||||||
})
|
})
|
||||||
@@ -82,7 +86,10 @@ export class ImagePreloadService {
|
|||||||
await imageDecryptService.decryptImage({
|
await imageDecryptService.decryptImage({
|
||||||
sessionId: task.sessionId,
|
sessionId: task.sessionId,
|
||||||
imageMd5: task.imageMd5,
|
imageMd5: task.imageMd5,
|
||||||
imageDatName: task.imageDatName
|
imageDatName: task.imageDatName,
|
||||||
|
createTime: task.createTime,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
// ignore preload failures
|
// ignore preload failures
|
||||||
|
|||||||
110
electron/services/nativeImageDecrypt.ts
Normal file
110
electron/services/nativeImageDecrypt.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
type NativeDecryptResult = {
|
||||||
|
data: Buffer
|
||||||
|
ext: string
|
||||||
|
isWxgf?: boolean
|
||||||
|
is_wxgf?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type NativeAddon = {
|
||||||
|
decryptDatNative: (inputPath: string, xorKey: number, aesKey?: string) => NativeDecryptResult
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedAddon: NativeAddon | null | undefined
|
||||||
|
|
||||||
|
function shouldEnableNative(): boolean {
|
||||||
|
return process.env.WEFLOW_IMAGE_NATIVE !== '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandAsarCandidates(filePath: string): string[] {
|
||||||
|
if (!filePath.includes('app.asar') || filePath.includes('app.asar.unpacked')) {
|
||||||
|
return [filePath]
|
||||||
|
}
|
||||||
|
return [filePath.replace('app.asar', 'app.asar.unpacked'), filePath]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformDir(): string {
|
||||||
|
if (process.platform === 'win32') return 'win32'
|
||||||
|
if (process.platform === 'darwin') return 'macos'
|
||||||
|
if (process.platform === 'linux') return 'linux'
|
||||||
|
return process.platform
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArchDir(): string {
|
||||||
|
if (process.arch === 'x64') return 'x64'
|
||||||
|
if (process.arch === 'arm64') return 'arm64'
|
||||||
|
return process.arch
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAddonCandidates(): string[] {
|
||||||
|
const platformDir = getPlatformDir()
|
||||||
|
const archDir = getArchDir()
|
||||||
|
const cwd = process.cwd()
|
||||||
|
const fileNames = [
|
||||||
|
`weflow-image-native-${platformDir}-${archDir}.node`
|
||||||
|
]
|
||||||
|
const roots = [
|
||||||
|
join(cwd, 'resources', 'wedecrypt', platformDir, archDir),
|
||||||
|
...(process.resourcesPath
|
||||||
|
? [
|
||||||
|
join(process.resourcesPath, 'resources', 'wedecrypt', platformDir, archDir),
|
||||||
|
join(process.resourcesPath, 'wedecrypt', platformDir, archDir)
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
const candidates = roots.flatMap((root) => fileNames.map((name) => join(root, name)))
|
||||||
|
return Array.from(new Set(candidates.flatMap(expandAsarCandidates)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAddon(): NativeAddon | null {
|
||||||
|
if (!shouldEnableNative()) return null
|
||||||
|
if (cachedAddon !== undefined) return cachedAddon
|
||||||
|
|
||||||
|
for (const candidate of getAddonCandidates()) {
|
||||||
|
if (!existsSync(candidate)) continue
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const addon = require(candidate) as NativeAddon
|
||||||
|
if (addon && typeof addon.decryptDatNative === 'function') {
|
||||||
|
cachedAddon = addon
|
||||||
|
return addon
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// try next candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedAddon = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nativeAddonLocation(): string | null {
|
||||||
|
for (const candidate of getAddonCandidates()) {
|
||||||
|
if (existsSync(candidate)) return candidate
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptDatViaNative(
|
||||||
|
inputPath: string,
|
||||||
|
xorKey: number,
|
||||||
|
aesKey?: string
|
||||||
|
): { data: Buffer; ext: string; isWxgf: boolean } | null {
|
||||||
|
const addon = loadAddon()
|
||||||
|
if (!addon) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = addon.decryptDatNative(inputPath, xorKey, aesKey)
|
||||||
|
const isWxgf = Boolean(result?.isWxgf ?? result?.is_wxgf)
|
||||||
|
if (!result || !Buffer.isBuffer(result.data)) return null
|
||||||
|
const rawExt = typeof result.ext === 'string' && result.ext.trim()
|
||||||
|
? result.ext.trim().toLowerCase()
|
||||||
|
: ''
|
||||||
|
const ext = rawExt ? (rawExt.startsWith('.') ? rawExt : `.${rawExt}`) : ''
|
||||||
|
return { data: result.data, ext, isWxgf }
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -9,7 +9,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
@@ -28,8 +27,9 @@
|
|||||||
"react-router-dom": "^7.14.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"react-virtuoso": "^4.18.1",
|
"react-virtuoso": "^4.18.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sherpa-onnx-node": "^1.12.35",
|
"sherpa-onnx-node": "^1.10.38",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
|
"sudo-prompt": "^9.2.1",
|
||||||
"wechat-emojis": "^1.0.2",
|
"wechat-emojis": "^1.0.2",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
@@ -40,11 +40,11 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"electron": "^41.1.1",
|
"electron": "^41.1.1",
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.98.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-electron": "^0.29.1",
|
"vite-plugin-electron": "^0.28.8",
|
||||||
"vite-plugin-electron-renderer": "^0.14.6"
|
"vite-plugin-electron-renderer": "^0.14.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3050,12 +3050,6 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vscode/sudo-prompt": {
|
|
||||||
"version": "9.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz",
|
|
||||||
"integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.12",
|
"version": "0.8.12",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
||||||
@@ -9462,6 +9456,13 @@
|
|||||||
"inline-style-parser": "0.2.7"
|
"inline-style-parser": "0.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sudo-prompt": {
|
||||||
|
"version": "9.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz",
|
||||||
|
"integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sumchecker": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
@@ -10140,9 +10141,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-electron": {
|
"node_modules/vite-plugin-electron": {
|
||||||
"version": "0.29.1",
|
"version": "0.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.8.tgz",
|
||||||
"integrity": "sha512-AejNed5BgHFnuw8h5puTa61C6vdP4ydbsbo/uVjH1fTdHAlCDz1+o6pDQ/scQj1udDrGvH01+vTbzQh/vMnR9w==",
|
"integrity": "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Jasonzhu1207/WeFlow"
|
"url": "https://github.com/hicccc77/WeFlow"
|
||||||
},
|
},
|
||||||
"//": "二改不应改变此处的作者与应用信息",
|
"//": "二改不应改变此处的作者与应用信息",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"appId": "com.WeFlow.app",
|
"appId": "com.WeFlow.app",
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "Jasonzhu1207",
|
"owner": "hicccc77",
|
||||||
"repo": "WeFlow",
|
"repo": "WeFlow",
|
||||||
"releaseType": "release"
|
"releaseType": "release"
|
||||||
},
|
},
|
||||||
@@ -186,7 +186,8 @@
|
|||||||
"node_modules/sherpa-onnx-node/**/*",
|
"node_modules/sherpa-onnx-node/**/*",
|
||||||
"node_modules/sherpa-onnx-*/*",
|
"node_modules/sherpa-onnx-*/*",
|
||||||
"node_modules/sherpa-onnx-*/**/*",
|
"node_modules/sherpa-onnx-*/**/*",
|
||||||
"node_modules/ffmpeg-static/**/*"
|
"node_modules/ffmpeg-static/**/*",
|
||||||
|
"resources/wedecrypt/**/*.node"
|
||||||
],
|
],
|
||||||
"icon": "resources/icon.icns"
|
"icon": "resources/icon.icns"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
resources/wedecrypt/linux/x64/weflow-image-native-linux-x64.node
Normal file
BIN
resources/wedecrypt/linux/x64/weflow-image-native-linux-x64.node
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
resources/wedecrypt/win32/x64/weflow-image-native-win32-x64.node
Normal file
BIN
resources/wedecrypt/win32/x64/weflow-image-native-win32-x64.node
Normal file
Binary file not shown.
@@ -154,6 +154,21 @@ function hasRenderableChatRecordName(value?: string): boolean {
|
|||||||
return value !== undefined && value !== null && String(value).length > 0
|
return value !== undefined && value !== null && String(value).length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toRenderableImageSrc(path?: string): string | undefined {
|
||||||
|
const raw = String(path || '').trim()
|
||||||
|
if (!raw) return undefined
|
||||||
|
if (/^(data:|blob:|https?:|file:)/i.test(raw)) return raw
|
||||||
|
|
||||||
|
const normalized = raw.replace(/\\/g, '/')
|
||||||
|
if (/^[a-zA-Z]:\//.test(normalized)) {
|
||||||
|
return encodeURI(`file:///${normalized}`)
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('/')) {
|
||||||
|
return encodeURI(`file://${normalized}`)
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
function getChatRecordPreviewText(item: ChatRecordItem): string {
|
function getChatRecordPreviewText(item: ChatRecordItem): string {
|
||||||
const text = normalizeChatRecordText(item.datadesc) || normalizeChatRecordText(item.datatitle)
|
const text = normalizeChatRecordText(item.datadesc) || normalizeChatRecordText(item.datatitle)
|
||||||
if (item.datatype === 17) {
|
if (item.datatype === 17) {
|
||||||
@@ -4853,7 +4868,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
const candidates = [...head, ...tail]
|
const candidates = [...head, ...tail]
|
||||||
const queued = preloadImageKeysRef.current
|
const queued = preloadImageKeysRef.current
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }> = []
|
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
|
||||||
for (const msg of candidates) {
|
for (const msg of candidates) {
|
||||||
if (payloads.length >= maxPreload) break
|
if (payloads.length >= maxPreload) break
|
||||||
if (msg.localType !== 3) continue
|
if (msg.localType !== 3) continue
|
||||||
@@ -4867,11 +4882,14 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
payloads.push({
|
payloads.push({
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
imageMd5: msg.imageMd5 || undefined,
|
imageMd5: msg.imageMd5 || undefined,
|
||||||
imageDatName: msg.imageDatName
|
imageDatName: msg.imageDatName,
|
||||||
|
createTime: msg.createTime
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (payloads.length > 0) {
|
if (payloads.length > 0) {
|
||||||
window.electronAPI.image.preload(payloads).catch(() => { })
|
window.electronAPI.image.preload(payloads, {
|
||||||
|
allowCacheIndex: false
|
||||||
|
}).catch(() => { })
|
||||||
}
|
}
|
||||||
}, [currentSessionId, messages])
|
}, [currentSessionId, messages])
|
||||||
|
|
||||||
@@ -5840,7 +5858,10 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
sessionId: session.username,
|
sessionId: session.username,
|
||||||
imageMd5: img.imageMd5,
|
imageMd5: img.imageMd5,
|
||||||
imageDatName: img.imageDatName,
|
imageDatName: img.imageDatName,
|
||||||
force: true
|
createTime: img.createTime,
|
||||||
|
force: true,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
if (r?.success) successCount++
|
if (r?.success) successCount++
|
||||||
else failCount++
|
else failCount++
|
||||||
@@ -7882,7 +7903,7 @@ function MessageBubble({
|
|||||||
)
|
)
|
||||||
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
|
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
|
||||||
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
|
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
|
||||||
() => imageDataUrlCache.get(imageCacheKey)
|
() => toRenderableImageSrc(imageDataUrlCache.get(imageCacheKey))
|
||||||
)
|
)
|
||||||
const voiceIdentityKey = buildVoiceCacheIdentity(session.username, message)
|
const voiceIdentityKey = buildVoiceCacheIdentity(session.username, message)
|
||||||
const voiceCacheKey = `voice:${voiceIdentityKey}`
|
const voiceCacheKey = `voice:${voiceIdentityKey}`
|
||||||
@@ -7904,6 +7925,7 @@ function MessageBubble({
|
|||||||
const imageUpdateCheckedRef = useRef<string | null>(null)
|
const imageUpdateCheckedRef = useRef<string | null>(null)
|
||||||
const imageClickTimerRef = useRef<number | null>(null)
|
const imageClickTimerRef = useRef<number | null>(null)
|
||||||
const imageContainerRef = useRef<HTMLDivElement>(null)
|
const imageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const imageElementRef = useRef<HTMLImageElement | null>(null)
|
||||||
const emojiContainerRef = useRef<HTMLDivElement>(null)
|
const emojiContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const imageResizeBaselineRef = useRef<number | null>(null)
|
const imageResizeBaselineRef = useRef<number | null>(null)
|
||||||
const emojiResizeBaselineRef = useRef<number | null>(null)
|
const emojiResizeBaselineRef = useRef<number | null>(null)
|
||||||
@@ -8260,19 +8282,27 @@ function MessageBubble({
|
|||||||
sessionId: session.username,
|
sessionId: session.username,
|
||||||
imageMd5: message.imageMd5 || undefined,
|
imageMd5: message.imageMd5 || undefined,
|
||||||
imageDatName: message.imageDatName,
|
imageDatName: message.imageDatName,
|
||||||
force: forceUpdate
|
createTime: message.createTime,
|
||||||
|
force: forceUpdate,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
}) as SharedImageDecryptResult
|
}) as SharedImageDecryptResult
|
||||||
})
|
})
|
||||||
if (result.success && result.localPath) {
|
if (result.success && result.localPath) {
|
||||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
const renderPath = toRenderableImageSrc(result.localPath)
|
||||||
if (imageLocalPath !== result.localPath) {
|
if (!renderPath) {
|
||||||
|
if (!silent) setImageError(true)
|
||||||
|
return { success: false }
|
||||||
|
}
|
||||||
|
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||||
|
if (imageLocalPath !== renderPath) {
|
||||||
captureImageResizeBaseline()
|
captureImageResizeBaseline()
|
||||||
lockImageStageHeight()
|
lockImageStageHeight()
|
||||||
}
|
}
|
||||||
setImageLocalPath(result.localPath)
|
setImageLocalPath(renderPath)
|
||||||
setImageHasUpdate(false)
|
setImageHasUpdate(false)
|
||||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||||
return result
|
return { ...result, localPath: renderPath }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8297,7 +8327,7 @@ function MessageBubble({
|
|||||||
imageDecryptPendingRef.current = false
|
imageDecryptPendingRef.current = false
|
||||||
}
|
}
|
||||||
return { success: false }
|
return { success: false }
|
||||||
}, [isImage, message.imageMd5, message.imageDatName, message.localId, session.username, imageCacheKey, detectImageMimeFromBase64, imageLocalPath, captureImageResizeBaseline, lockImageStageHeight])
|
}, [isImage, message.imageMd5, message.imageDatName, message.createTime, message.localId, session.username, imageCacheKey, detectImageMimeFromBase64, imageLocalPath, captureImageResizeBaseline, lockImageStageHeight])
|
||||||
|
|
||||||
const triggerForceHd = useCallback(() => {
|
const triggerForceHd = useCallback(() => {
|
||||||
if (!message.imageMd5 && !message.imageDatName) return
|
if (!message.imageMd5 && !message.imageDatName) return
|
||||||
@@ -8352,24 +8382,29 @@ function MessageBubble({
|
|||||||
const resolved = await window.electronAPI.image.resolveCache({
|
const resolved = await window.electronAPI.image.resolveCache({
|
||||||
sessionId: session.username,
|
sessionId: session.username,
|
||||||
imageMd5: message.imageMd5 || undefined,
|
imageMd5: message.imageMd5 || undefined,
|
||||||
imageDatName: message.imageDatName
|
imageDatName: message.imageDatName,
|
||||||
|
createTime: message.createTime,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true
|
||||||
})
|
})
|
||||||
if (resolved?.success && resolved.localPath) {
|
if (resolved?.success && resolved.localPath) {
|
||||||
finalImagePath = resolved.localPath
|
const renderPath = toRenderableImageSrc(resolved.localPath)
|
||||||
|
if (!renderPath) return
|
||||||
|
finalImagePath = renderPath
|
||||||
finalLiveVideoPath = resolved.liveVideoPath || finalLiveVideoPath
|
finalLiveVideoPath = resolved.liveVideoPath || finalLiveVideoPath
|
||||||
imageDataUrlCache.set(imageCacheKey, resolved.localPath)
|
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||||
if (imageLocalPath !== resolved.localPath) {
|
if (imageLocalPath !== renderPath) {
|
||||||
captureImageResizeBaseline()
|
captureImageResizeBaseline()
|
||||||
lockImageStageHeight()
|
lockImageStageHeight()
|
||||||
}
|
}
|
||||||
setImageLocalPath(resolved.localPath)
|
setImageLocalPath(renderPath)
|
||||||
if (resolved.liveVideoPath) setImageLiveVideoPath(resolved.liveVideoPath)
|
if (resolved.liveVideoPath) setImageLiveVideoPath(resolved.liveVideoPath)
|
||||||
setImageHasUpdate(Boolean(resolved.hasUpdate))
|
setImageHasUpdate(Boolean(resolved.hasUpdate))
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
void window.electronAPI.window.openImageViewerWindow(finalImagePath, finalLiveVideoPath)
|
void window.electronAPI.window.openImageViewerWindow(toRenderableImageSrc(finalImagePath) || finalImagePath, finalLiveVideoPath)
|
||||||
}, [
|
}, [
|
||||||
imageLiveVideoPath,
|
imageLiveVideoPath,
|
||||||
imageLocalPath,
|
imageLocalPath,
|
||||||
@@ -8378,6 +8413,7 @@ function MessageBubble({
|
|||||||
lockImageStageHeight,
|
lockImageStageHeight,
|
||||||
message.imageDatName,
|
message.imageDatName,
|
||||||
message.imageMd5,
|
message.imageMd5,
|
||||||
|
message.createTime,
|
||||||
requestImageDecrypt,
|
requestImageDecrypt,
|
||||||
session.username
|
session.username
|
||||||
])
|
])
|
||||||
@@ -8391,8 +8427,19 @@ function MessageBubble({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImageLoaded(false)
|
if (!isImage) return
|
||||||
}, [imageLocalPath])
|
if (!imageLocalPath) {
|
||||||
|
setImageLoaded(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 某些 file:// 缓存图在 src 切换时可能不会稳定触发 onLoad,
|
||||||
|
// 这里用 complete/naturalWidth 做一次兜底,避免图片进入 pending 隐身态。
|
||||||
|
const img = imageElementRef.current
|
||||||
|
if (img && img.complete && img.naturalWidth > 0) {
|
||||||
|
setImageLoaded(true)
|
||||||
|
}
|
||||||
|
}, [isImage, imageLocalPath])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imageLoading) return
|
if (imageLoading) return
|
||||||
@@ -8401,7 +8448,7 @@ function MessageBubble({
|
|||||||
}, [imageError, imageLoading, imageLocalPath])
|
}, [imageError, imageLoading, imageLocalPath])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isImage || imageLoading) return
|
if (!isImage || imageLoading || !imageInView) return
|
||||||
if (!message.imageMd5 && !message.imageDatName) return
|
if (!message.imageMd5 && !message.imageDatName) return
|
||||||
if (imageUpdateCheckedRef.current === imageCacheKey) return
|
if (imageUpdateCheckedRef.current === imageCacheKey) return
|
||||||
imageUpdateCheckedRef.current = imageCacheKey
|
imageUpdateCheckedRef.current = imageCacheKey
|
||||||
@@ -8409,15 +8456,21 @@ function MessageBubble({
|
|||||||
window.electronAPI.image.resolveCache({
|
window.electronAPI.image.resolveCache({
|
||||||
sessionId: session.username,
|
sessionId: session.username,
|
||||||
imageMd5: message.imageMd5 || undefined,
|
imageMd5: message.imageMd5 || undefined,
|
||||||
imageDatName: message.imageDatName
|
imageDatName: message.imageDatName,
|
||||||
|
createTime: message.createTime,
|
||||||
|
preferFilePath: true,
|
||||||
|
hardlinkOnly: true,
|
||||||
|
allowCacheIndex: false
|
||||||
}).then((result: { success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }) => {
|
}).then((result: { success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (result.success && result.localPath) {
|
if (result.success && result.localPath) {
|
||||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
const renderPath = toRenderableImageSrc(result.localPath)
|
||||||
if (!imageLocalPath || imageLocalPath !== result.localPath) {
|
if (!renderPath) return
|
||||||
|
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||||
|
if (!imageLocalPath || imageLocalPath !== renderPath) {
|
||||||
captureImageResizeBaseline()
|
captureImageResizeBaseline()
|
||||||
lockImageStageHeight()
|
lockImageStageHeight()
|
||||||
setImageLocalPath(result.localPath)
|
setImageLocalPath(renderPath)
|
||||||
setImageError(false)
|
setImageError(false)
|
||||||
}
|
}
|
||||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||||
@@ -8427,7 +8480,7 @@ function MessageBubble({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [isImage, imageLocalPath, imageLoading, message.imageMd5, message.imageDatName, imageCacheKey, session.username, captureImageResizeBaseline, lockImageStageHeight])
|
}, [isImage, imageInView, imageLocalPath, imageLoading, message.imageMd5, message.imageDatName, message.createTime, imageCacheKey, session.username, captureImageResizeBaseline, lockImageStageHeight])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isImage) return
|
if (!isImage) return
|
||||||
@@ -8455,15 +8508,17 @@ function MessageBubble({
|
|||||||
(payload.imageMd5 && payload.imageMd5 === message.imageMd5) ||
|
(payload.imageMd5 && payload.imageMd5 === message.imageMd5) ||
|
||||||
(payload.imageDatName && payload.imageDatName === message.imageDatName)
|
(payload.imageDatName && payload.imageDatName === message.imageDatName)
|
||||||
if (matchesCacheKey) {
|
if (matchesCacheKey) {
|
||||||
|
const renderPath = toRenderableImageSrc(payload.localPath)
|
||||||
|
if (!renderPath) return
|
||||||
const cachedPath = imageDataUrlCache.get(imageCacheKey)
|
const cachedPath = imageDataUrlCache.get(imageCacheKey)
|
||||||
if (cachedPath !== payload.localPath) {
|
if (cachedPath !== renderPath) {
|
||||||
imageDataUrlCache.set(imageCacheKey, payload.localPath)
|
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||||
}
|
}
|
||||||
if (imageLocalPath !== payload.localPath) {
|
if (imageLocalPath !== renderPath) {
|
||||||
captureImageResizeBaseline()
|
captureImageResizeBaseline()
|
||||||
lockImageStageHeight()
|
lockImageStageHeight()
|
||||||
}
|
}
|
||||||
setImageLocalPath((prev) => (prev === payload.localPath ? prev : payload.localPath))
|
setImageLocalPath((prev) => (prev === renderPath ? prev : renderPath))
|
||||||
setImageError(false)
|
setImageError(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -9093,6 +9148,7 @@ function MessageBubble({
|
|||||||
<>
|
<>
|
||||||
<div className="image-message-wrapper">
|
<div className="image-message-wrapper">
|
||||||
<img
|
<img
|
||||||
|
ref={imageElementRef}
|
||||||
src={imageLocalPath}
|
src={imageLocalPath}
|
||||||
alt="图片"
|
alt="图片"
|
||||||
className={`image-message ${imageLoaded ? 'ready' : 'pending'}`}
|
className={`image-message ${imageLoaded ? 'ready' : 'pending'}`}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ interface ExportOptions {
|
|||||||
txtColumns: string[]
|
txtColumns: string[]
|
||||||
displayNamePreference: DisplayNamePreference
|
displayNamePreference: DisplayNamePreference
|
||||||
exportConcurrency: number
|
exportConcurrency: number
|
||||||
imageDeepSearchOnMiss: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionRow extends AppChatSession {
|
interface SessionRow extends AppChatSession {
|
||||||
@@ -336,6 +335,15 @@ const isTextBatchTask = (task: ExportTask): boolean => (
|
|||||||
task.payload.scope === 'content' && task.payload.contentType === 'text'
|
task.payload.scope === 'content' && task.payload.contentType === 'text'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isImageExportTask = (task: ExportTask): boolean => {
|
||||||
|
if (task.payload.scope === 'sns') {
|
||||||
|
return Boolean(task.payload.snsOptions?.exportImages)
|
||||||
|
}
|
||||||
|
if (task.payload.scope !== 'content') return false
|
||||||
|
if (task.payload.contentType === 'image') return true
|
||||||
|
return Boolean(task.payload.options?.exportImages)
|
||||||
|
}
|
||||||
|
|
||||||
const resolvePerfStageByPhase = (phase?: ExportProgress['phase']): TaskPerfStage => {
|
const resolvePerfStageByPhase = (phase?: ExportProgress['phase']): TaskPerfStage => {
|
||||||
if (phase === 'preparing') return 'collect'
|
if (phase === 'preparing') return 'collect'
|
||||||
if (phase === 'writing') return 'write'
|
if (phase === 'writing') return 'write'
|
||||||
@@ -1705,6 +1713,24 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
const currentSessionRatio = task.progress.phaseTotal > 0
|
const currentSessionRatio = task.progress.phaseTotal > 0
|
||||||
? Math.max(0, Math.min(1, task.progress.phaseProgress / task.progress.phaseTotal))
|
? Math.max(0, Math.min(1, task.progress.phaseProgress / task.progress.phaseTotal))
|
||||||
: null
|
: null
|
||||||
|
const imageTask = isImageExportTask(task)
|
||||||
|
const imageTimingElapsedMs = imageTask
|
||||||
|
? Math.max(0, (
|
||||||
|
typeof task.finishedAt === 'number'
|
||||||
|
? task.finishedAt
|
||||||
|
: nowTick
|
||||||
|
) - (task.startedAt || task.createdAt))
|
||||||
|
: 0
|
||||||
|
const imageTimingAvgMs = imageTask && mediaDoneFiles > 0
|
||||||
|
? Math.floor(imageTimingElapsedMs / Math.max(1, mediaDoneFiles))
|
||||||
|
: 0
|
||||||
|
const imageTimingLabel = imageTask
|
||||||
|
? (
|
||||||
|
mediaDoneFiles > 0
|
||||||
|
? `图片耗时 ${formatDurationMs(imageTimingElapsedMs)} · 平均 ${imageTimingAvgMs}ms/张`
|
||||||
|
: `图片耗时 ${formatDurationMs(imageTimingElapsedMs)}`
|
||||||
|
)
|
||||||
|
: ''
|
||||||
return (
|
return (
|
||||||
<div key={task.id} className={`task-card ${task.status}`}>
|
<div key={task.id} className={`task-card ${task.status}`}>
|
||||||
<div className="task-main">
|
<div className="task-main">
|
||||||
@@ -1734,6 +1760,11 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{imageTimingLabel && task.status !== 'queued' && (
|
||||||
|
<div className="task-perf-summary">
|
||||||
|
<span>{imageTimingLabel}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{canShowPerfDetail && stageTotals && (
|
{canShowPerfDetail && stageTotals && (
|
||||||
<div className="task-perf-summary">
|
<div className="task-perf-summary">
|
||||||
<span>累计耗时 {formatDurationMs(stageTotalMs)}</span>
|
<span>累计耗时 {formatDurationMs(stageTotalMs)}</span>
|
||||||
@@ -1903,7 +1934,6 @@ function ExportPage() {
|
|||||||
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
|
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
|
||||||
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
||||||
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
|
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
|
||||||
const [exportDefaultImageDeepSearchOnMiss, setExportDefaultImageDeepSearchOnMiss] = useState(true)
|
|
||||||
|
|
||||||
const [options, setOptions] = useState<ExportOptions>({
|
const [options, setOptions] = useState<ExportOptions>({
|
||||||
format: 'json',
|
format: 'json',
|
||||||
@@ -1924,8 +1954,7 @@ function ExportPage() {
|
|||||||
excelCompactColumns: true,
|
excelCompactColumns: true,
|
||||||
txtColumns: defaultTxtColumns,
|
txtColumns: defaultTxtColumns,
|
||||||
displayNamePreference: 'remark',
|
displayNamePreference: 'remark',
|
||||||
exportConcurrency: 2,
|
exportConcurrency: 2
|
||||||
imageDeepSearchOnMiss: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [exportDialog, setExportDialog] = useState<ExportDialogState>({
|
const [exportDialog, setExportDialog] = useState<ExportDialogState>({
|
||||||
@@ -2622,7 +2651,7 @@ function ExportPage() {
|
|||||||
automationTasksReadyRef.current = false
|
automationTasksReadyRef.current = false
|
||||||
let isReady = true
|
let isReady = true
|
||||||
try {
|
try {
|
||||||
const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedImageDeepSearchOnMiss, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([
|
const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([
|
||||||
configService.getExportPath(),
|
configService.getExportPath(),
|
||||||
configService.getExportDefaultFormat(),
|
configService.getExportDefaultFormat(),
|
||||||
configService.getExportDefaultAvatars(),
|
configService.getExportDefaultAvatars(),
|
||||||
@@ -2631,7 +2660,6 @@ function ExportPage() {
|
|||||||
configService.getExportDefaultExcelCompactColumns(),
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
configService.getExportDefaultTxtColumns(),
|
configService.getExportDefaultTxtColumns(),
|
||||||
configService.getExportDefaultConcurrency(),
|
configService.getExportDefaultConcurrency(),
|
||||||
configService.getExportDefaultImageDeepSearchOnMiss(),
|
|
||||||
configService.getExportLastSessionRunMap(),
|
configService.getExportLastSessionRunMap(),
|
||||||
configService.getExportLastContentRunMap(),
|
configService.getExportLastContentRunMap(),
|
||||||
configService.getExportSessionRecordMap(),
|
configService.getExportSessionRecordMap(),
|
||||||
@@ -2671,7 +2699,6 @@ function ExportPage() {
|
|||||||
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
|
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
|
||||||
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
|
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
|
||||||
setExportDefaultConcurrency(savedConcurrency ?? 2)
|
setExportDefaultConcurrency(savedConcurrency ?? 2)
|
||||||
setExportDefaultImageDeepSearchOnMiss(savedImageDeepSearchOnMiss ?? true)
|
|
||||||
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
||||||
setAutomationTasks(automationTaskItem?.tasks || [])
|
setAutomationTasks(automationTaskItem?.tasks || [])
|
||||||
automationTasksReadyRef.current = true
|
automationTasksReadyRef.current = true
|
||||||
@@ -2709,8 +2736,7 @@ function ExportPage() {
|
|||||||
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
|
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
|
||||||
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
|
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
|
||||||
txtColumns,
|
txtColumns,
|
||||||
exportConcurrency: savedConcurrency ?? prev.exportConcurrency,
|
exportConcurrency: savedConcurrency ?? prev.exportConcurrency
|
||||||
imageDeepSearchOnMiss: savedImageDeepSearchOnMiss ?? prev.imageDeepSearchOnMiss
|
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
isReady = false
|
isReady = false
|
||||||
@@ -4491,8 +4517,7 @@ function ExportPage() {
|
|||||||
maxFileSizeMb: prev.maxFileSizeMb,
|
maxFileSizeMb: prev.maxFileSizeMb,
|
||||||
exportVoiceAsText: exportDefaultVoiceAsText,
|
exportVoiceAsText: exportDefaultVoiceAsText,
|
||||||
excelCompactColumns: exportDefaultExcelCompactColumns,
|
excelCompactColumns: exportDefaultExcelCompactColumns,
|
||||||
exportConcurrency: exportDefaultConcurrency,
|
exportConcurrency: exportDefaultConcurrency
|
||||||
imageDeepSearchOnMiss: exportDefaultImageDeepSearchOnMiss
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.scope === 'sns') {
|
if (payload.scope === 'sns') {
|
||||||
@@ -4527,8 +4552,7 @@ function ExportPage() {
|
|||||||
exportDefaultAvatars,
|
exportDefaultAvatars,
|
||||||
exportDefaultMedia,
|
exportDefaultMedia,
|
||||||
exportDefaultVoiceAsText,
|
exportDefaultVoiceAsText,
|
||||||
exportDefaultConcurrency,
|
exportDefaultConcurrency
|
||||||
exportDefaultImageDeepSearchOnMiss
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const closeExportDialog = useCallback(() => {
|
const closeExportDialog = useCallback(() => {
|
||||||
@@ -4755,7 +4779,6 @@ function ExportPage() {
|
|||||||
txtColumns: options.txtColumns,
|
txtColumns: options.txtColumns,
|
||||||
displayNamePreference: options.displayNamePreference,
|
displayNamePreference: options.displayNamePreference,
|
||||||
exportConcurrency: options.exportConcurrency,
|
exportConcurrency: options.exportConcurrency,
|
||||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
|
||||||
fileNamingMode: exportDefaultFileNamingMode,
|
fileNamingMode: exportDefaultFileNamingMode,
|
||||||
sessionLayout,
|
sessionLayout,
|
||||||
sessionNameWithTypePrefix,
|
sessionNameWithTypePrefix,
|
||||||
@@ -5691,8 +5714,6 @@ function ExportPage() {
|
|||||||
await configService.setExportDefaultExcelCompactColumns(options.excelCompactColumns)
|
await configService.setExportDefaultExcelCompactColumns(options.excelCompactColumns)
|
||||||
await configService.setExportDefaultTxtColumns(options.txtColumns)
|
await configService.setExportDefaultTxtColumns(options.txtColumns)
|
||||||
await configService.setExportDefaultConcurrency(options.exportConcurrency)
|
await configService.setExportDefaultConcurrency(options.exportConcurrency)
|
||||||
await configService.setExportDefaultImageDeepSearchOnMiss(options.imageDeepSearchOnMiss)
|
|
||||||
setExportDefaultImageDeepSearchOnMiss(options.imageDeepSearchOnMiss)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSingleExport = useCallback((session: SessionRow) => {
|
const openSingleExport = useCallback((session: SessionRow) => {
|
||||||
@@ -7393,14 +7414,6 @@ function ExportPage() {
|
|||||||
const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog
|
const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog
|
||||||
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
||||||
const shouldShowMediaSection = !isContentScopeDialog
|
const shouldShowMediaSection = !isContentScopeDialog
|
||||||
const shouldRenderImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
|
||||||
isSessionScopeDialog ||
|
|
||||||
(isContentScopeDialog && exportDialog.contentType === 'image')
|
|
||||||
)
|
|
||||||
const shouldShowImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
|
||||||
(isSessionScopeDialog && options.exportImages) ||
|
|
||||||
(isContentScopeDialog && exportDialog.contentType === 'image')
|
|
||||||
)
|
|
||||||
const avatarExportStatusLabel = options.exportAvatars ? '已开启聊天消息导出带头像' : '已关闭聊天消息导出带头像'
|
const avatarExportStatusLabel = options.exportAvatars ? '已开启聊天消息导出带头像' : '已关闭聊天消息导出带头像'
|
||||||
const contentTextDialogSummary = '此模式只导出聊天文本,不包含图片语音视频表情包等多媒体文件。'
|
const contentTextDialogSummary = '此模式只导出聊天文本,不包含图片语音视频表情包等多媒体文件。'
|
||||||
const activeDialogFormatLabel = exportDialog.scope === 'sns'
|
const activeDialogFormatLabel = exportDialog.scope === 'sns'
|
||||||
@@ -9710,30 +9723,6 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldRenderImageDeepSearchToggle && (
|
|
||||||
<div className={`dialog-collapse-slot ${shouldShowImageDeepSearchToggle ? 'open' : ''}`} aria-hidden={!shouldShowImageDeepSearchToggle}>
|
|
||||||
<div className="dialog-collapse-inner">
|
|
||||||
<div className="dialog-section">
|
|
||||||
<div className="dialog-switch-row">
|
|
||||||
<div className="dialog-switch-copy">
|
|
||||||
<h4>缺图时深度搜索</h4>
|
|
||||||
<div className="format-note">关闭后仅尝试 hardlink 命中,未命中将直接显示占位符,导出速度更快。</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dialog-switch ${options.imageDeepSearchOnMiss ? 'on' : ''}`}
|
|
||||||
aria-pressed={options.imageDeepSearchOnMiss}
|
|
||||||
aria-label="切换缺图时深度搜索"
|
|
||||||
onClick={() => setOptions(prev => ({ ...prev, imageDeepSearchOnMiss: !prev.imageDeepSearchOnMiss }))}
|
|
||||||
>
|
|
||||||
<span className="dialog-switch-thumb" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSessionScopeDialog && (
|
{isSessionScopeDialog && (
|
||||||
<div className="dialog-section">
|
<div className="dialog-section">
|
||||||
<div className="dialog-switch-row">
|
<div className="dialog-switch-row">
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
||||||
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
||||||
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
||||||
EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS: 'exportDefaultImageDeepSearchOnMiss',
|
|
||||||
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
||||||
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
||||||
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
||||||
@@ -548,18 +547,6 @@ export async function setExportDefaultConcurrency(concurrency: number): Promise<
|
|||||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
|
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取缺图时是否深度搜索(默认导出行为)
|
|
||||||
export async function getExportDefaultImageDeepSearchOnMiss(): Promise<boolean | null> {
|
|
||||||
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS)
|
|
||||||
if (typeof value === 'boolean') return value
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置缺图时是否深度搜索(默认导出行为)
|
|
||||||
export async function setExportDefaultImageDeepSearchOnMiss(enabled: boolean): Promise<void> {
|
|
||||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExportWriteLayout = 'A' | 'B' | 'C'
|
export type ExportWriteLayout = 'A' | 'B' | 'C'
|
||||||
|
|
||||||
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
||||||
|
|||||||
20
src/types/electron.d.ts
vendored
20
src/types/electron.d.ts
vendored
@@ -491,24 +491,35 @@ export interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
decrypt: (payload: {
|
||||||
|
sessionId?: string
|
||||||
|
imageMd5?: string
|
||||||
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
|
force?: boolean
|
||||||
|
preferFilePath?: boolean
|
||||||
|
hardlinkOnly?: boolean
|
||||||
|
}) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||||
resolveCache: (payload: {
|
resolveCache: (payload: {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
imageMd5?: string
|
imageMd5?: string
|
||||||
imageDatName?: string
|
imageDatName?: string
|
||||||
|
createTime?: number
|
||||||
|
preferFilePath?: boolean
|
||||||
|
hardlinkOnly?: boolean
|
||||||
disableUpdateCheck?: boolean
|
disableUpdateCheck?: boolean
|
||||||
allowCacheIndex?: boolean
|
allowCacheIndex?: boolean
|
||||||
}) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
}) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||||
resolveCacheBatch: (
|
resolveCacheBatch: (
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; preferFilePath?: boolean; hardlinkOnly?: boolean }>,
|
||||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean; preferFilePath?: boolean; hardlinkOnly?: boolean }
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
rows?: Array<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
rows?: Array<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
preload: (
|
preload: (
|
||||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }>,
|
||||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||||
) => Promise<boolean>
|
) => Promise<boolean>
|
||||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
||||||
@@ -1117,7 +1128,6 @@ export interface ExportOptions {
|
|||||||
sessionNameWithTypePrefix?: boolean
|
sessionNameWithTypePrefix?: boolean
|
||||||
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
||||||
exportConcurrency?: number
|
exportConcurrency?: number
|
||||||
imageDeepSearchOnMiss?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportProgress {
|
export interface ExportProgress {
|
||||||
|
|||||||
Reference in New Issue
Block a user