mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
fix(imageDecryptService): 优化 ffmpeg 路径检测(多方案 fallback 并验证有效性)与错误处理(捕获 stderr 并输出详细日志),新增ffmpeg-static依赖。
This commit is contained in:
@@ -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<Buffer | null> {
|
||||
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()
|
||||
|
||||
111
package-lock.json
generated
111
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1306,6 +1306,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
const [imageClicked, setImageClicked] = useState(false)
|
||||
const imageUpdateCheckedRef = useRef<string | null>(null)
|
||||
const imageClickTimerRef = useRef<number | null>(null)
|
||||
const imageContainerRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="image-loading">
|
||||
<Loader2 size={20} className="spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (imageError || !imageLocalPath) {
|
||||
return (
|
||||
<button
|
||||
className={`image-unavailable ${imageClicked ? 'clicked' : ''}`}
|
||||
onClick={handleImageClick}
|
||||
disabled={imageLoading}
|
||||
type="button"
|
||||
>
|
||||
<ImageIcon size={24} />
|
||||
<span>图片未解密</span>
|
||||
<span className="image-action">{imageClicked ? '已点击…' : '点击解密'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="image-message-wrapper">
|
||||
<img
|
||||
src={imageLocalPath}
|
||||
alt="图片"
|
||||
className="image-message"
|
||||
onClick={() => setShowImagePreview(true)}
|
||||
onLoad={() => setImageError(false)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
{imageHasUpdate && (
|
||||
<button
|
||||
className="image-update-button"
|
||||
type="button"
|
||||
title="发现更高清图片,点击更新"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
void requestImageDecrypt(true)
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showImagePreview && (
|
||||
<ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} />
|
||||
<div ref={imageContainerRef}>
|
||||
{imageLoading ? (
|
||||
<div className="image-loading">
|
||||
<Loader2 size={20} className="spin" />
|
||||
</div>
|
||||
) : imageError || !imageLocalPath ? (
|
||||
<button
|
||||
className={`image-unavailable ${imageClicked ? 'clicked' : ''}`}
|
||||
onClick={handleImageClick}
|
||||
disabled={imageLoading}
|
||||
type="button"
|
||||
>
|
||||
<ImageIcon size={24} />
|
||||
<span>图片未解密</span>
|
||||
<span className="image-action">{imageClicked ? '已点击…' : '点击解密'}</span>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div className="image-message-wrapper">
|
||||
<img
|
||||
src={imageLocalPath}
|
||||
alt="图片"
|
||||
className="image-message"
|
||||
onClick={() => setShowImagePreview(true)}
|
||||
onLoad={() => setImageError(false)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
{imageHasUpdate && (
|
||||
<button
|
||||
className="image-update-button"
|
||||
type="button"
|
||||
title="发现更高清图片,点击更新"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
void requestImageDecrypt(true)
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showImagePreview && (
|
||||
<ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user