From 3af62783dd53076dc7a7390403f8fab2eb425d65 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com>
Date: Fri, 7 Aug 2026 00:00:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=97=A5=E6=8A=A5?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=8E=E5=85=AC=E4=BC=97=E5=8F=B7=E6=B6=88?=
=?UTF-8?q?=E6=81=AF=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修复日报图片读取格式与模型不支持时的友好降级
恢复话题关键词回退,确保词云在缺少顶层关键词时仍可展示
支持公众号多文章消息在聊天、日报、检索和 HTML 导出中完整呈现
迁移账号身份缓存并优化首次连接的账号占位信息
补充图片、词云、账号缓存与多文章消息回归测试
---
src/main/export-html-template.ts | 29 ++++++++
src/main/index.ts | 22 +++---
.../knowledge/knowledge-search-service.ts | 5 +-
src/main/message-parser.ts | 71 +++++++++++++++++--
src/main/services/bootstrap-cache.ts | 36 +++++++++-
src/preload/index.d.ts | 7 +-
src/preload/index.ts | 7 +-
.../src/components/DatabaseConnectionPage.tsx | 8 ++-
.../src/components/RichMessageBubble.tsx | 26 ++++++-
.../reports/ReportTemplateSelector.tsx | 6 +-
src/renderer/src/styles/rich-message.scss | 61 ++++++++++++++++
src/renderer/src/utils/group-report-facts.ts | 40 ++++++++---
src/renderer/src/utils/group-report.ts | 11 ++-
src/shared/types.ts | 7 ++
tests/component/image-bubble.test.tsx | 31 ++++++++
tests/unit/bootstrap-cache.test.ts | 38 +++++++++-
tests/unit/group-report.test.ts | 46 ++++++++++++
tests/unit/message-parser.test.ts | 17 +++++
18 files changed, 432 insertions(+), 36 deletions(-)
create mode 100644 tests/unit/group-report.test.ts
diff --git a/src/main/export-html-template.ts b/src/main/export-html-template.ts
index 7666dfd..b0358df 100644
--- a/src/main/export-html-template.ts
+++ b/src/main/export-html-template.ts
@@ -579,6 +579,19 @@ body {
}
.structured-link { color: inherit; text-decoration: none; }
.structured-link:hover .structured-title { color: var(--accent); }
+.structured-share-articles { display: flex; flex-direction: column; }
+.structured-share-article {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ padding: 10px 0;
+ border-top: 1px solid var(--border);
+ color: inherit;
+ text-decoration: none;
+}
+.structured-share-article:first-child { border-top: 0; }
+.structured-share-article img { width: 54px; height: 54px; border-radius: 4px; object-fit: cover; }
.location-content {
gap: 8px;
padding-top: 8px;
@@ -990,6 +1003,22 @@ const renderExportScript = (name: string): string => `
}
const renderShareContent = (data) => {
const label = shareLabel(data.typeVal)
+ const articles = Array.isArray(data.articles) ? data.articles : []
+ if (articles.length) {
+ const articleMarkup = articles.map((article) => {
+ const href = externalUrl(article.url)
+ const cover = imageUrl(article.coverUrl)
+ const body = '' + displayText(article.title || '公众号文章') + '
' +
+ (article.description ? '' + displayText(article.description) + '
' : '') +
+ '' + (cover ? '
' : '')
+ return href
+ ? '' + body + ''
+ : '
' +
+ '
' + displayText(data.appname || label) + '
' +
+ '
' + articleMarkup + '
'
+ }
let title = decodeEntities(data.title || label)
let description = decodeEntities(data.des || '')
if (String(data.typeVal) === '51' && /当前微信版本不支持展示该内容/.test(title) && description) {
diff --git a/src/main/index.ts b/src/main/index.ts
index a49b6e1..7daec90 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -321,7 +321,7 @@ function getLocalMediaMimeType(filePath: string): string {
}
}
-function buildImageResponse(image: DecodedImage): {
+function buildImageResponse(image: DecodedImage, includeData = false): {
success: true
data: string
isThumb: boolean
@@ -330,7 +330,7 @@ function buildImageResponse(image: DecodedImage): {
} {
const mediaService = image.cacheFilePath ? getImageMediaService() : null
const data =
- mediaService && image.cacheFilePath && existsSync(image.cacheFilePath)
+ !includeData && mediaService && image.cacheFilePath && existsSync(image.cacheFilePath)
? mediaService.createLocalMediaUrl(image.cacheFilePath)
: image.data
return {
@@ -1126,7 +1126,12 @@ app.whenReady().then(async () => {
imageMd5?: string,
imageDatNameOrThumb?: string | boolean,
_sessionId?: string,
- options?: { force?: boolean; preferThumbnail?: boolean; priority?: number }
+ options?: {
+ force?: boolean
+ preferThumbnail?: boolean
+ priority?: number
+ includeData?: boolean
+ }
) => {
let service = imageDecryptService
if (!service) {
@@ -1146,6 +1151,7 @@ app.whenReady().then(async () => {
const imageDatName = typeof imageDatNameOrThumb === 'string' ? imageDatNameOrThumb : undefined
const force = options?.force === true
const preferThumbnail = options?.preferThumbnail === true
+ const includeData = options?.includeData === true
const priority = Number.isFinite(options?.priority) ? Number(options?.priority) : 0
const imageCacheKey = [
imageMd5 || '',
@@ -1154,10 +1160,10 @@ app.whenReady().then(async () => {
].join('|')
const mediaService = getImageMediaService()
const cachedImage = await service.getCachedDecodedImage(imageCacheKey, {
- includeData: !mediaService
+ includeData: includeData || !mediaService
})
if (cachedImage && (!force || !cachedImage.isThumbnail)) {
- return buildImageResponse(cachedImage)
+ return buildImageResponse(cachedImage, includeData)
}
return enqueueColdImageLoad(async () => {
@@ -1183,10 +1189,10 @@ app.whenReady().then(async () => {
// A previous queued request may have populated the cache while this one waited.
const queuedMediaService = getImageMediaService()
const queuedCacheHit = await coldService.getCachedDecodedImage(imageCacheKey, {
- includeData: !queuedMediaService
+ includeData: includeData || !queuedMediaService
})
if (queuedCacheHit && (!force || !queuedCacheHit.isThumbnail)) {
- return buildImageResponse(queuedCacheHit)
+ return buildImageResponse(queuedCacheHit, includeData)
}
let filePath = force
@@ -1220,7 +1226,7 @@ app.whenReady().then(async () => {
isThumbnail: coldService.isThumbnailFile(decrypted.filePath)
}
await coldService.cacheDecodedImage(imageCacheKey, decodedImage)
- return buildImageResponse(decodedImage)
+ return buildImageResponse(decodedImage, includeData)
}, priority)
}
)
diff --git a/src/main/knowledge/knowledge-search-service.ts b/src/main/knowledge/knowledge-search-service.ts
index 3997c20..912355a 100644
--- a/src/main/knowledge/knowledge-search-service.ts
+++ b/src/main/knowledge/knowledge-search-service.ts
@@ -89,8 +89,11 @@ function sourceTextAndAttachment(message: chat.FormattedMessage): {
if (content.type === 'share') {
const title = content.title?.trim() || ''
const description = content.des?.trim() || ''
+ const articles = (content.articles || []).flatMap((article) =>
+ [article.title, article.description].map((value) => value?.trim()).filter(Boolean)
+ )
return {
- text: [text, title, description].filter(Boolean).join('\n') || undefined,
+ text: [text, title, description, ...articles].filter(Boolean).join('\n') || undefined,
attachment:
title || content.url
? {
diff --git a/src/main/message-parser.ts b/src/main/message-parser.ts
index fde7bcf..fcb937a 100644
--- a/src/main/message-parser.ts
+++ b/src/main/message-parser.ts
@@ -8,6 +8,12 @@ type LocationContent = {
lng: number
}
type CardContent = { type: 'card'; username: string; nickname: string; avatarUrl?: string }
+type ShareArticle = {
+ title: string
+ description?: string
+ url: string
+ coverUrl?: string
+}
type ShareContent = {
type: 'share'
title: string
@@ -15,6 +21,7 @@ type ShareContent = {
url: string
appname?: string
typeVal?: string
+ articles?: ShareArticle[]
}
type ForwardedMessageItem = {
messageType: number
@@ -482,17 +489,71 @@ function parseShareMessage(content: string): ParsedContent {
}
}
- const title = decodeXmlEntities(extractXmlValue(content, 'title')) || ''
- const des = extractXmlValue(content, 'des') || extractXmlValue(content, 'desc') || ''
- const url = extractXmlValue(content, 'url') || ''
- const appname = extractXmlValue(content, 'appname') || extractXmlValue(content, 'appInfo') || ''
+ const articles = parseShareArticles(content)
+ const title =
+ articles[0]?.title || decodeXmlEntities(extractXmlValue(content, 'title')) || ''
+ const des =
+ articles[0]?.description ||
+ decodeXmlEntities(extractXmlValue(content, 'des') || extractXmlValue(content, 'desc')) ||
+ ''
+ const url = articles[0]?.url || decodeXmlUrl(extractXmlValue(content, 'url')) || ''
+ const appname =
+ decodeXmlEntities(
+ extractXmlValue(content, 'appname') ||
+ extractXmlValue(content, 'publisher') ||
+ extractXmlValue(content, 'appInfo')
+ ) || ''
const typeVal = extractXmlValue(content, 'type') || ''
if (!title && !url) {
return { type: 'unknown', raw: content }
}
- return { type: 'share', title, des, url, appname, typeVal }
+ return {
+ type: 'share',
+ title,
+ des,
+ url,
+ appname,
+ typeVal,
+ articles: articles.length > 1 ? articles : undefined
+ }
+}
+
+function parseShareArticles(content: string): ShareArticle[] {
+ if (!/