mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-20 06:14:33 +08:00
简单优化图片解密
This commit is contained in:
32
.agents/skills/weflow-overview-sync/SKILL.md
Normal file
32
.agents/skills/weflow-overview-sync/SKILL.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: weflow-overview-sync
|
||||
description: Keep the WeFlow architecture overview document synchronized with code and interface changes. Use when editing WeFlow source files, Electron services, IPC contracts, DB access logic, export and analytics flows, or related docs that affect architecture, fields, or data paths.
|
||||
---
|
||||
|
||||
# WeFlow Overview Sync
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the architecture overview markdown at repo root before any WeFlow edit.
|
||||
2. Identify touched files and impacted concepts (module, interface, data flow, field definition, export behavior).
|
||||
3. Update the overview document in the same task when affected items are already documented.
|
||||
4. Add a new subsection in the overview document when the requested change is not documented yet.
|
||||
5. Preserve the existing formatting style of the overview document before finalizing:
|
||||
- Keep heading hierarchy and numbering style consistent.
|
||||
- Keep concise wording and use `-` list markers.
|
||||
- Wrap file paths, APIs, and field names in backticks.
|
||||
- Place new content in the logically matching section.
|
||||
6. Re-check the overview document for format consistency and architecture accuracy before replying.
|
||||
|
||||
## Update Rules
|
||||
|
||||
- Update existing sections when they already cover the changed files or interfaces.
|
||||
- Add missing coverage when new modules, IPC methods, SQL fields, or service flows appear.
|
||||
- Avoid broad rewrites; apply focused edits that keep the document stable and scannable.
|
||||
- Reflect any renamed path, API, or field immediately to prevent architecture drift.
|
||||
|
||||
## Collaboration and UI Rules
|
||||
|
||||
- If unrelated additions from other collaborators appear in files you edit, leave them as-is and focus only on the current task scope.
|
||||
- For dropdown menu UI design, inspect and follow existing in-app dropdown patterns; do not use native browser dropdown styles.
|
||||
- Do not use native styles for frontend UI design; implement consistent custom-styled components aligned with the product's existing visual system.
|
||||
@@ -914,6 +914,9 @@ function registerIpcHandlers() {
|
||||
ipcMain.handle('chat:getAllVoiceMessages', async (_, sessionId: string) => {
|
||||
return chatService.getAllVoiceMessages(sessionId)
|
||||
})
|
||||
ipcMain.handle('chat:getAllImageMessages', async (_, sessionId: string) => {
|
||||
return chatService.getAllImageMessages(sessionId)
|
||||
})
|
||||
ipcMain.handle('chat:getMessageDates', async (_, sessionId: string) => {
|
||||
return chatService.getMessageDates(sessionId)
|
||||
})
|
||||
|
||||
@@ -154,6 +154,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
||||
ipcRenderer.invoke('chat:getVoiceData', sessionId, msgId, createTime, serverId),
|
||||
getAllVoiceMessages: (sessionId: string) => ipcRenderer.invoke('chat:getAllVoiceMessages', sessionId),
|
||||
getAllImageMessages: (sessionId: string) => ipcRenderer.invoke('chat:getAllImageMessages', sessionId),
|
||||
getMessageDates: (sessionId: string) => ipcRenderer.invoke('chat:getMessageDates', sessionId),
|
||||
resolveVoiceCache: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:resolveVoiceCache', sessionId, msgId),
|
||||
getVoiceTranscript: (sessionId: string, msgId: string, createTime?: number) => ipcRenderer.invoke('chat:getVoiceTranscript', sessionId, msgId, createTime),
|
||||
|
||||
@@ -155,6 +155,17 @@ export class ImageDecryptService {
|
||||
return { success: false, error: '缺少图片标识' }
|
||||
}
|
||||
|
||||
if (payload.force) {
|
||||
const hdCached = this.findCachedOutput(cacheKey, true, payload.sessionId)
|
||||
if (hdCached && existsSync(hdCached) && this.isImageFile(hdCached) && !this.isThumbnailPath(hdCached)) {
|
||||
const dataUrl = this.fileToDataUrl(hdCached)
|
||||
const localPath = dataUrl || this.filePathToUrl(hdCached)
|
||||
const liveVideoPath = this.checkLiveVideoCache(hdCached)
|
||||
this.emitCacheResolved(payload, cacheKey, localPath)
|
||||
return { success: true, localPath, isThumb: false, liveVideoPath }
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.force) {
|
||||
const cached = this.resolvedCache.get(cacheKey)
|
||||
if (cached && existsSync(cached) && this.isImageFile(cached)) {
|
||||
@@ -346,23 +357,37 @@ export class ImageDecryptService {
|
||||
* 获取解密后的缓存目录(用于查找 hardlink.db)
|
||||
*/
|
||||
private getDecryptedCacheDir(wxid: string): string | null {
|
||||
const cachePath = this.configService.get('cachePath')
|
||||
if (!cachePath) return null
|
||||
|
||||
const cleanedWxid = this.cleanAccountDirName(wxid)
|
||||
const cacheAccountDir = join(cachePath, cleanedWxid)
|
||||
const configured = this.configService.get('cachePath')
|
||||
const documentsPath = app.getPath('documents')
|
||||
const baseCandidates = Array.from(new Set([
|
||||
configured || '',
|
||||
join(documentsPath, 'WeFlow'),
|
||||
join(documentsPath, 'WeFlowData'),
|
||||
this.configService.getCacheBasePath()
|
||||
].filter(Boolean)))
|
||||
|
||||
// 检查缓存目录下是否有 hardlink.db
|
||||
if (existsSync(join(cacheAccountDir, 'hardlink.db'))) {
|
||||
return cacheAccountDir
|
||||
}
|
||||
if (existsSync(join(cachePath, 'hardlink.db'))) {
|
||||
return cachePath
|
||||
}
|
||||
const cacheHardlinkDir = join(cacheAccountDir, 'db_storage', 'hardlink')
|
||||
if (existsSync(join(cacheHardlinkDir, 'hardlink.db'))) {
|
||||
return cacheHardlinkDir
|
||||
for (const base of baseCandidates) {
|
||||
const accountCandidates = Array.from(new Set([
|
||||
join(base, wxid),
|
||||
join(base, cleanedWxid),
|
||||
join(base, 'databases', wxid),
|
||||
join(base, 'databases', cleanedWxid)
|
||||
]))
|
||||
for (const accountDir of accountCandidates) {
|
||||
if (existsSync(join(accountDir, 'hardlink.db'))) {
|
||||
return accountDir
|
||||
}
|
||||
const hardlinkSubdir = join(accountDir, 'db_storage', 'hardlink')
|
||||
if (existsSync(join(hardlinkSubdir, 'hardlink.db'))) {
|
||||
return hardlinkSubdir
|
||||
}
|
||||
}
|
||||
if (existsSync(join(base, 'hardlink.db'))) {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -371,7 +396,8 @@ export class ImageDecryptService {
|
||||
existsSync(join(dirPath, 'hardlink.db')) ||
|
||||
existsSync(join(dirPath, 'db_storage')) ||
|
||||
existsSync(join(dirPath, 'FileStorage', 'Image')) ||
|
||||
existsSync(join(dirPath, 'FileStorage', 'Image2'))
|
||||
existsSync(join(dirPath, 'FileStorage', 'Image2')) ||
|
||||
existsSync(join(dirPath, 'msg', 'attach'))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -437,6 +463,12 @@ export class ImageDecryptService {
|
||||
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hdPath)
|
||||
return hdPath
|
||||
}
|
||||
const hdInDir = await this.searchDatFileInDir(dirname(hardlinkPath), imageDatName || imageMd5 || '', false)
|
||||
if (hdInDir) {
|
||||
this.cacheDatPath(accountDir, imageMd5, hdInDir)
|
||||
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hdInDir)
|
||||
return hdInDir
|
||||
}
|
||||
// 没找到高清图,返回 null(不进行全局搜索)
|
||||
return null
|
||||
}
|
||||
@@ -454,9 +486,16 @@ export class ImageDecryptService {
|
||||
// 找到缩略图但要求高清图,尝试同目录查找高清图变体
|
||||
const hdPath = this.findHdVariantInSameDir(fallbackPath)
|
||||
if (hdPath) {
|
||||
this.cacheDatPath(accountDir, imageMd5, hdPath)
|
||||
this.cacheDatPath(accountDir, imageDatName, hdPath)
|
||||
return hdPath
|
||||
}
|
||||
const hdInDir = await this.searchDatFileInDir(dirname(fallbackPath), imageDatName || imageMd5 || '', false)
|
||||
if (hdInDir) {
|
||||
this.cacheDatPath(accountDir, imageMd5, hdInDir)
|
||||
this.cacheDatPath(accountDir, imageDatName, hdInDir)
|
||||
return hdInDir
|
||||
}
|
||||
return null
|
||||
}
|
||||
this.logInfo('[ImageDecrypt] hardlink miss (datName)', { imageDatName })
|
||||
@@ -479,15 +518,17 @@ export class ImageDecryptService {
|
||||
this.cacheDatPath(accountDir, imageDatName, hdPath)
|
||||
return hdPath
|
||||
}
|
||||
const hdInDir = await this.searchDatFileInDir(dirname(hardlinkPath), imageDatName || '', false)
|
||||
if (hdInDir) {
|
||||
this.cacheDatPath(accountDir, imageDatName, hdInDir)
|
||||
return hdInDir
|
||||
}
|
||||
return null
|
||||
}
|
||||
this.logInfo('[ImageDecrypt] hardlink miss (datName)', { imageDatName })
|
||||
}
|
||||
|
||||
// 如果要求高清图但 hardlink 没找到,也不要搜索了(搜索太慢)
|
||||
if (!allowThumbnail) {
|
||||
return null
|
||||
}
|
||||
// force 模式下也继续尝试缓存目录/文件系统搜索,避免 hardlink.db 缺行时只能拿到缩略图
|
||||
|
||||
if (!imageDatName) return null
|
||||
if (!skipResolvedCache) {
|
||||
@@ -497,6 +538,8 @@ export class ImageDecryptService {
|
||||
// 缓存的是缩略图,尝试找高清图
|
||||
const hdPath = this.findHdVariantInSameDir(cached)
|
||||
if (hdPath) return hdPath
|
||||
const hdInDir = await this.searchDatFileInDir(dirname(cached), imageDatName, false)
|
||||
if (hdInDir) return hdInDir
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1024,7 +1024,7 @@ export class WcdbCore {
|
||||
}
|
||||
try {
|
||||
// 1. 打开游标 (使用 Ascending=1 从指定时间往后查)
|
||||
const openRes = await this.openMessageCursorLite(sessionId, limit, true, minTime, 0)
|
||||
const openRes = await this.openMessageCursor(sessionId, limit, true, minTime, 0)
|
||||
if (!openRes.success || !openRes.cursor) {
|
||||
return { success: false, error: openRes.error }
|
||||
}
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -80,7 +80,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2910,7 +2909,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3057,7 +3055,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3997,7 +3994,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5107,7 +5103,6 @@
|
||||
"integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "25.1.8",
|
||||
"builder-util": "25.1.7",
|
||||
@@ -5295,7 +5290,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
|
||||
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.6.1"
|
||||
@@ -5382,6 +5376,7 @@
|
||||
"integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "25.1.8",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -5395,6 +5390,7 @@
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -5410,6 +5406,7 @@
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -5423,6 +5420,7 @@
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -9152,7 +9150,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9162,7 +9159,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -9597,7 +9593,6 @@
|
||||
"integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -9828,9 +9823,6 @@
|
||||
"sherpa-onnx-win-x64": "^1.12.23"
|
||||
}
|
||||
},
|
||||
"node_modules/sherpa-onnx-node/node_modules/sherpa-onnx-darwin-x64": {
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sherpa-onnx-win-ia32": {
|
||||
"version": "1.12.23",
|
||||
"resolved": "https://registry.npmmirror.com/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.23.tgz",
|
||||
@@ -10442,7 +10434,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -10890,7 +10881,6 @@
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -10980,8 +10970,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
|
||||
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
@@ -11007,7 +10996,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1114,6 +1114,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.appmsg-meta-badge {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--primary);
|
||||
background: rgba(127, 127, 127, 0.08);
|
||||
border: 1px solid rgba(127, 127, 127, 0.18);
|
||||
border-radius: 999px;
|
||||
padding: 3px 7px;
|
||||
align-self: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link-desc-block {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.appmsg-url-line {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.appmsg-rich-card {
|
||||
.link-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-thumb.theme-adaptive,
|
||||
.miniapp-thumb.theme-adaptive {
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
.link-thumb.theme-adaptive,
|
||||
.miniapp-thumb.theme-adaptive {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配发送出去的消息中的链接卡片
|
||||
@@ -2752,12 +2799,14 @@
|
||||
|
||||
.card-message,
|
||||
.chat-record-message,
|
||||
.miniapp-message {
|
||||
.miniapp-message,
|
||||
.appmsg-rich-card {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
|
||||
.card-name,
|
||||
.miniapp-title,
|
||||
.source-name {
|
||||
.source-name,
|
||||
.link-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -2765,7 +2814,9 @@
|
||||
.miniapp-label,
|
||||
.chat-record-item,
|
||||
.chat-record-meta-line,
|
||||
.chat-record-desc {
|
||||
.chat-record-desc,
|
||||
.link-desc,
|
||||
.appmsg-url-line {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@@ -2778,6 +2829,12 @@
|
||||
.chat-record-more {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.appmsg-meta-badge {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.call-message {
|
||||
@@ -3235,4 +3292,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.miniapp-message-rich {
|
||||
.miniapp-thumb {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3061,7 +3061,7 @@ function MessageBubble({
|
||||
setImageLocalPath(result.localPath)
|
||||
setImageHasUpdate(false)
|
||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||
return
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3072,7 +3072,7 @@ function MessageBubble({
|
||||
imageDataUrlCache.set(imageCacheKey, dataUrl)
|
||||
setImageLocalPath(dataUrl)
|
||||
setImageHasUpdate(false)
|
||||
return
|
||||
return { success: true, localPath: dataUrl } as any
|
||||
}
|
||||
if (!silent) setImageError(true)
|
||||
} catch {
|
||||
@@ -3080,6 +3080,7 @@ function MessageBubble({
|
||||
} finally {
|
||||
if (!silent) setImageLoading(false)
|
||||
}
|
||||
return { success: false } as any
|
||||
}, [isImage, imageLoading, message.imageMd5, message.imageDatName, message.localId, session.username, imageCacheKey, detectImageMimeFromBase64])
|
||||
|
||||
const triggerForceHd = useCallback(() => {
|
||||
@@ -3110,6 +3111,55 @@ function MessageBubble({
|
||||
void requestImageDecrypt()
|
||||
}, [message.imageDatName, message.imageMd5, message.localId, requestImageDecrypt, session.username])
|
||||
|
||||
const handleOpenImageViewer = useCallback(async () => {
|
||||
if (!imageLocalPath) return
|
||||
|
||||
let finalImagePath = imageLocalPath
|
||||
let finalLiveVideoPath = imageLiveVideoPath || undefined
|
||||
|
||||
// If current cache is a thumbnail, wait for a silent force-HD decrypt before opening viewer.
|
||||
if (imageHasUpdate) {
|
||||
try {
|
||||
const upgraded = await requestImageDecrypt(true, true)
|
||||
if (upgraded?.success && upgraded.localPath) {
|
||||
finalImagePath = upgraded.localPath
|
||||
finalLiveVideoPath = upgraded.liveVideoPath || finalLiveVideoPath
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// One more resolve helps when background/batch decrypt has produced a clearer image or live video
|
||||
// but local component state hasn't caught up yet.
|
||||
if (message.imageMd5 || message.imageDatName) {
|
||||
try {
|
||||
const resolved = await window.electronAPI.image.resolveCache({
|
||||
sessionId: session.username,
|
||||
imageMd5: message.imageMd5 || undefined,
|
||||
imageDatName: message.imageDatName
|
||||
})
|
||||
if (resolved?.success && resolved.localPath) {
|
||||
finalImagePath = resolved.localPath
|
||||
finalLiveVideoPath = resolved.liveVideoPath || finalLiveVideoPath
|
||||
imageDataUrlCache.set(imageCacheKey, resolved.localPath)
|
||||
setImageLocalPath(resolved.localPath)
|
||||
if (resolved.liveVideoPath) setImageLiveVideoPath(resolved.liveVideoPath)
|
||||
setImageHasUpdate(Boolean(resolved.hasUpdate))
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
void window.electronAPI.window.openImageViewerWindow(finalImagePath, finalLiveVideoPath)
|
||||
}, [
|
||||
imageHasUpdate,
|
||||
imageLiveVideoPath,
|
||||
imageLocalPath,
|
||||
imageCacheKey,
|
||||
message.imageDatName,
|
||||
message.imageMd5,
|
||||
requestImageDecrypt,
|
||||
session.username
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (imageClickTimerRef.current) {
|
||||
@@ -3631,10 +3681,7 @@ function MessageBubble({
|
||||
src={imageLocalPath}
|
||||
alt="图片"
|
||||
className="image-message"
|
||||
onClick={() => {
|
||||
if (imageHasUpdate) void requestImageDecrypt(true, true)
|
||||
void window.electronAPI.window.openImageViewerWindow(imageLocalPath!, imageLiveVideoPath || undefined)
|
||||
}}
|
||||
onClick={() => { void handleOpenImageViewer() }}
|
||||
onLoad={() => setImageError(false)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
|
||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
@@ -126,6 +126,11 @@ export interface ElectronAPI {
|
||||
getImageData: (sessionId: string, msgId: string) => Promise<{ success: boolean; data?: string; error?: string }>
|
||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) => Promise<{ success: boolean; data?: string; error?: string }>
|
||||
getAllVoiceMessages: (sessionId: string) => Promise<{ success: boolean; messages?: Message[]; error?: string }>
|
||||
getAllImageMessages: (sessionId: string) => Promise<{
|
||||
success: boolean
|
||||
images?: { imageMd5?: string; imageDatName?: string; createTime?: number }[]
|
||||
error?: string
|
||||
}>
|
||||
resolveVoiceCache: (sessionId: string, msgId: string) => Promise<{ success: boolean; hasCache: boolean; data?: string }>
|
||||
getVoiceTranscript: (sessionId: string, msgId: string, createTime?: number) => Promise<{ success: boolean; transcript?: string; error?: string }>
|
||||
onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => () => void
|
||||
|
||||
@@ -64,6 +64,17 @@ export interface Message {
|
||||
fileSize?: number // 文件大小
|
||||
fileExt?: string // 文件扩展名
|
||||
xmlType?: string // XML 中的 type 字段
|
||||
appMsgKind?: string // 归一化 appmsg 类型
|
||||
appMsgDesc?: string
|
||||
appMsgAppName?: string
|
||||
appMsgSourceName?: string
|
||||
appMsgSourceUsername?: string
|
||||
appMsgThumbUrl?: string
|
||||
appMsgMusicUrl?: string
|
||||
appMsgDataUrl?: string
|
||||
appMsgLocationLabel?: string
|
||||
finderNickname?: string
|
||||
finderUsername?: string
|
||||
// 转账消息
|
||||
transferPayerUsername?: string // 转账付款方 wxid
|
||||
transferReceiverUsername?: string // 转账收款方 wxid
|
||||
|
||||
Reference in New Issue
Block a user