diff --git a/electron/services/imageDecryptService.ts b/electron/services/imageDecryptService.ts index 0188ec7..370431d 100644 --- a/electron/services/imageDecryptService.ts +++ b/electron/services/imageDecryptService.ts @@ -11,10 +11,29 @@ import { wcdbService } from './wcdbService' // 获取 ffmpeg-static 的路径 function getStaticFfmpegPath(): string | null { try { + // 方法1: 直接 require ffmpeg-static + // eslint-disable-next-line @typescript-eslint/no-var-requires const ffmpegStatic = require('ffmpeg-static') - if (typeof ffmpegStatic === 'string') { + + if (typeof ffmpegStatic === 'string' && existsSync(ffmpegStatic)) { return ffmpegStatic } + + // 方法2: 手动构建路径(开发环境) + const devPath = join(process.cwd(), 'node_modules', 'ffmpeg-static', 'ffmpeg.exe') + if (existsSync(devPath)) { + return devPath + } + + // 方法3: 打包后的路径 + if (app.isPackaged) { + const resourcesPath = process.resourcesPath + const packedPath = join(resourcesPath, 'app.asar.unpacked', 'node_modules', 'ffmpeg-static', 'ffmpeg.exe') + if (existsSync(packedPath)) { + return packedPath + } + } + return null } catch { return null @@ -1543,15 +1562,13 @@ export class ImageDecryptService { */ private getFfmpegPath(): string { const staticPath = getStaticFfmpegPath() + this.logInfo('ffmpeg 路径检测', { staticPath, exists: staticPath ? existsSync(staticPath) : false }) + if (staticPath) { - const unpackedPath = staticPath.replace('app.asar', 'app.asar.unpacked') - if (existsSync(unpackedPath)) { - return unpackedPath - } - if (existsSync(staticPath)) { - return staticPath - } + return staticPath } + + // 回退到系统 ffmpeg return 'ffmpeg' } @@ -1560,10 +1577,12 @@ export class ImageDecryptService { */ private convertHevcToJpg(hevcData: Buffer): Promise { const ffmpeg = this.getFfmpegPath() + this.logInfo('ffmpeg 转换开始', { ffmpegPath: ffmpeg, hevcSize: hevcData.length }) return new Promise((resolve) => { const { spawn } = require('child_process') const chunks: Buffer[] = [] + const errChunks: Buffer[] = [] const proc = spawn(ffmpeg, [ '-hide_banner', @@ -1580,16 +1599,23 @@ export class ImageDecryptService { }) proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk)) + proc.stderr.on('data', (chunk: Buffer) => errChunks.push(chunk)) proc.on('close', (code: number) => { if (code === 0 && chunks.length > 0) { + this.logInfo('ffmpeg 转换成功', { outputSize: Buffer.concat(chunks).length }) resolve(Buffer.concat(chunks)) } else { + const errMsg = Buffer.concat(errChunks).toString() + this.logInfo('ffmpeg 转换失败', { code, error: errMsg }) resolve(null) } }) - proc.on('error', () => resolve(null)) + proc.on('error', (err: Error) => { + this.logInfo('ffmpeg 进程错误', { error: err.message }) + resolve(null) + }) proc.stdin.write(hevcData) proc.stdin.end() diff --git a/package-lock.json b/package-lock.json index 6c3e749..d7e0928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weflow", - "version": "1.0.4", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "weflow", - "version": "1.0.4", + "version": "1.1.2", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.5.0", @@ -14,6 +14,7 @@ "echarts-for-react": "^3.0.2", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", + "ffmpeg-static": "^5.3.0", "fzstd": "^0.1.1", "html2canvas": "^1.4.1", "jieba-wasm": "^2.2.0", @@ -344,6 +345,21 @@ "node": ">=6.9.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmmirror.com/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3929,7 +3945,6 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/builder-util": { @@ -4188,6 +4203,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -4433,6 +4454,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/conf": { "version": "14.0.0", "resolved": "https://registry.npmmirror.com/conf/-/conf-14.0.0.tgz", @@ -5355,7 +5391,6 @@ "version": "2.2.1", "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5578,6 +5613,47 @@ "pend": "~1.2.0" } }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffmpeg-static/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ffmpeg-static/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -6109,6 +6185,21 @@ "node": ">= 14" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7406,6 +7497,11 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7595,7 +7691,6 @@ "version": "2.0.3", "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -8801,6 +8896,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 3de02ac..72dabe4 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1306,6 +1306,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }: const [imageClicked, setImageClicked] = useState(false) const imageUpdateCheckedRef = useRef(null) const imageClickTimerRef = useRef(null) + const imageContainerRef = useRef(null) + const imageAutoDecryptTriggered = useRef(false) const [voiceError, setVoiceError] = useState(false) const [voiceLoading, setVoiceLoading] = useState(false) const [isVoicePlaying, setIsVoicePlaying] = useState(false) @@ -1555,6 +1557,31 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }: } }, [isImage, imageCacheKey, message.imageDatName, message.imageMd5]) + // 图片进入视野前自动解密(懒加载) + useEffect(() => { + if (!isImage) return + if (imageLocalPath) return // 已有图片,不需要解密 + if (!message.imageMd5 && !message.imageDatName) return + + const container = imageContainerRef.current + if (!container) return + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0] + // rootMargin 设置为 200px,提前触发解密 + if (entry.isIntersecting && !imageAutoDecryptTriggered.current) { + imageAutoDecryptTriggered.current = true + void requestImageDecrypt() + } + }, + { rootMargin: '200px', threshold: 0 } + ) + + observer.observe(container) + return () => observer.disconnect() + }, [isImage, imageLocalPath, message.imageMd5, message.imageDatName, requestImageDecrypt]) + useEffect(() => { if (!isVoice) return @@ -1637,56 +1664,54 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }: // 渲染消息内容 const renderContent = () => { if (isImage) { - if (imageLoading) { - return ( -
- -
- ) - } - if (imageError || !imageLocalPath) { - return ( - - ) - } return ( - <> -
- 图片 setShowImagePreview(true)} - onLoad={() => setImageError(false)} - onError={() => setImageError(true)} - /> - {imageHasUpdate && ( - - )} -
- {showImagePreview && ( - setShowImagePreview(false)} /> +
+ {imageLoading ? ( +
+ +
+ ) : imageError || !imageLocalPath ? ( + + ) : ( + <> +
+ 图片 setShowImagePreview(true)} + onLoad={() => setImageError(false)} + onError={() => setImageError(true)} + /> + {imageHasUpdate && ( + + )} +
+ {showImagePreview && ( + setShowImagePreview(false)} /> + )} + )} - +
) }