fix(imageDecryptService): 优化 ffmpeg 路径检测(多方案 fallback 并验证有效性)与错误处理(捕获 stderr 并输出详细日志),新增ffmpeg-static依赖。

This commit is contained in:
Forrest
2026-01-17 02:05:41 +08:00
parent 6707be2200
commit 11969ea2d4
3 changed files with 214 additions and 62 deletions

View File

@@ -11,10 +11,29 @@ import { wcdbService } from './wcdbService'
// 获取 ffmpeg-static 的路径 // 获取 ffmpeg-static 的路径
function getStaticFfmpegPath(): string | null { function getStaticFfmpegPath(): string | null {
try { try {
// 方法1: 直接 require ffmpeg-static
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ffmpegStatic = require('ffmpeg-static') const ffmpegStatic = require('ffmpeg-static')
if (typeof ffmpegStatic === 'string') {
if (typeof ffmpegStatic === 'string' && existsSync(ffmpegStatic)) {
return 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 return null
} catch { } catch {
return null return null
@@ -1543,15 +1562,13 @@ export class ImageDecryptService {
*/ */
private getFfmpegPath(): string { private getFfmpegPath(): string {
const staticPath = getStaticFfmpegPath() const staticPath = getStaticFfmpegPath()
this.logInfo('ffmpeg 路径检测', { staticPath, exists: staticPath ? existsSync(staticPath) : false })
if (staticPath) { 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' return 'ffmpeg'
} }
@@ -1560,10 +1577,12 @@ export class ImageDecryptService {
*/ */
private convertHevcToJpg(hevcData: Buffer): Promise<Buffer | null> { private convertHevcToJpg(hevcData: Buffer): Promise<Buffer | null> {
const ffmpeg = this.getFfmpegPath() const ffmpeg = this.getFfmpegPath()
this.logInfo('ffmpeg 转换开始', { ffmpegPath: ffmpeg, hevcSize: hevcData.length })
return new Promise((resolve) => { return new Promise((resolve) => {
const { spawn } = require('child_process') const { spawn } = require('child_process')
const chunks: Buffer[] = [] const chunks: Buffer[] = []
const errChunks: Buffer[] = []
const proc = spawn(ffmpeg, [ const proc = spawn(ffmpeg, [
'-hide_banner', '-hide_banner',
@@ -1580,16 +1599,23 @@ export class ImageDecryptService {
}) })
proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk)) proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk))
proc.stderr.on('data', (chunk: Buffer) => errChunks.push(chunk))
proc.on('close', (code: number) => { proc.on('close', (code: number) => {
if (code === 0 && chunks.length > 0) { if (code === 0 && chunks.length > 0) {
this.logInfo('ffmpeg 转换成功', { outputSize: Buffer.concat(chunks).length })
resolve(Buffer.concat(chunks)) resolve(Buffer.concat(chunks))
} else { } else {
const errMsg = Buffer.concat(errChunks).toString()
this.logInfo('ffmpeg 转换失败', { code, error: errMsg })
resolve(null) 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.write(hevcData)
proc.stdin.end() proc.stdin.end()

111
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "weflow", "name": "weflow",
"version": "1.0.4", "version": "1.1.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "weflow", "name": "weflow",
"version": "1.0.4", "version": "1.1.2",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"better-sqlite3": "^12.5.0", "better-sqlite3": "^12.5.0",
@@ -14,6 +14,7 @@
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"electron-store": "^10.0.0", "electron-store": "^10.0.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"ffmpeg-static": "^5.3.0",
"fzstd": "^0.1.1", "fzstd": "^0.1.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jieba-wasm": "^2.2.0", "jieba-wasm": "^2.2.0",
@@ -344,6 +345,21 @@
"node": ">=6.9.0" "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": { "node_modules/@develar/schema-utils": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", "resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -3929,7 +3945,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/builder-util": { "node_modules/builder-util": {
@@ -4188,6 +4203,12 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -4433,6 +4454,21 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/conf": {
"version": "14.0.0", "version": "14.0.0",
"resolved": "https://registry.npmmirror.com/conf/-/conf-14.0.0.tgz", "resolved": "https://registry.npmmirror.com/conf/-/conf-14.0.0.tgz",
@@ -5355,7 +5391,6 @@
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -5578,6 +5613,47 @@
"pend": "~1.2.0" "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": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -6109,6 +6185,21 @@
"node": ">= 14" "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": { "node_modules/http2-wrapper": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -7406,6 +7497,11 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)" "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": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -7595,7 +7691,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -8801,6 +8896,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",

View File

@@ -1306,6 +1306,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
const [imageClicked, setImageClicked] = useState(false) const [imageClicked, setImageClicked] = useState(false)
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 imageAutoDecryptTriggered = useRef(false)
const [voiceError, setVoiceError] = useState(false) const [voiceError, setVoiceError] = useState(false)
const [voiceLoading, setVoiceLoading] = useState(false) const [voiceLoading, setVoiceLoading] = useState(false)
const [isVoicePlaying, setIsVoicePlaying] = useState(false) const [isVoicePlaying, setIsVoicePlaying] = useState(false)
@@ -1555,6 +1557,31 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
} }
}, [isImage, imageCacheKey, message.imageDatName, message.imageMd5]) }, [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(() => { useEffect(() => {
if (!isVoice) return if (!isVoice) return
@@ -1637,15 +1664,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
// 渲染消息内容 // 渲染消息内容
const renderContent = () => { const renderContent = () => {
if (isImage) { if (isImage) {
if (imageLoading) {
return ( return (
<div ref={imageContainerRef}>
{imageLoading ? (
<div className="image-loading"> <div className="image-loading">
<Loader2 size={20} className="spin" /> <Loader2 size={20} className="spin" />
</div> </div>
) ) : imageError || !imageLocalPath ? (
}
if (imageError || !imageLocalPath) {
return (
<button <button
className={`image-unavailable ${imageClicked ? 'clicked' : ''}`} className={`image-unavailable ${imageClicked ? 'clicked' : ''}`}
onClick={handleImageClick} onClick={handleImageClick}
@@ -1656,9 +1681,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
<span></span> <span></span>
<span className="image-action">{imageClicked ? '已点击…' : '点击解密'}</span> <span className="image-action">{imageClicked ? '已点击…' : '点击解密'}</span>
</button> </button>
) ) : (
}
return (
<> <>
<div className="image-message-wrapper"> <div className="image-message-wrapper">
<img <img
@@ -1687,6 +1710,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
<ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} /> <ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} />
)} )}
</> </>
)}
</div>
) )
} }