Skip to content

Commit 5b2a81d

Browse files
committed
feat: support finder cards in chat view
1 parent c3e20cc commit 5b2a81d

7 files changed

Lines changed: 300 additions & 4 deletions

File tree

frontend/assets/css/chat.css

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,132 @@
11391139
flex-shrink: 0;
11401140
}
11411141

1142+
.wechat-link-card-finder {
1143+
width: 135px;
1144+
min-width: 135px;
1145+
max-width: 135px;
1146+
border: none;
1147+
box-shadow: none;
1148+
outline: none;
1149+
cursor: pointer;
1150+
text-decoration: none;
1151+
}
1152+
1153+
.wechat-link-card-finder.wechat-link-card--disabled {
1154+
cursor: default;
1155+
}
1156+
1157+
.wechat-link-finder-cover {
1158+
width: 135px;
1159+
height: 185px;
1160+
position: relative;
1161+
overflow: hidden;
1162+
border-radius: 4px;
1163+
background: var(--app-surface-muted);
1164+
}
1165+
1166+
.wechat-link-finder-cover--empty {
1167+
background: linear-gradient(180deg, #37cc6a 0%, #118f42 100%);
1168+
}
1169+
1170+
.wechat-link-finder-cover-img {
1171+
width: 100%;
1172+
height: 100%;
1173+
object-fit: cover;
1174+
object-position: center;
1175+
display: block;
1176+
}
1177+
1178+
.wechat-link-finder-cover-placeholder {
1179+
position: absolute;
1180+
inset: 0;
1181+
display: flex;
1182+
align-items: center;
1183+
justify-content: center;
1184+
color: rgba(255, 255, 255, 0.92);
1185+
}
1186+
1187+
.wechat-link-finder-cover-placeholder svg {
1188+
width: 34px;
1189+
height: 34px;
1190+
}
1191+
1192+
.wechat-link-finder-cover-shade {
1193+
position: absolute;
1194+
inset: 0;
1195+
background: linear-gradient(180deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.12) 42%, rgba(0, 0, 0, 0.68) 100%);
1196+
}
1197+
1198+
.wechat-link-finder-play {
1199+
position: absolute;
1200+
left: 50%;
1201+
top: 50%;
1202+
transform: translate(-50%, -66%);
1203+
width: 40px;
1204+
height: 40px;
1205+
border-radius: 50%;
1206+
background: rgba(0, 0, 0, 0.42);
1207+
display: flex;
1208+
align-items: center;
1209+
justify-content: center;
1210+
color: #fff;
1211+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
1212+
}
1213+
1214+
.wechat-link-finder-play svg {
1215+
width: 20px;
1216+
height: 20px;
1217+
margin-left: 2px;
1218+
}
1219+
1220+
.wechat-link-finder-meta {
1221+
position: absolute;
1222+
left: 8px;
1223+
right: 8px;
1224+
bottom: 8px;
1225+
display: flex;
1226+
flex-direction: column;
1227+
gap: 0;
1228+
}
1229+
1230+
.wechat-link-finder-author {
1231+
display: flex;
1232+
align-items: center;
1233+
gap: 5px;
1234+
min-width: 0;
1235+
padding: 5px 7px;
1236+
border-radius: 999px;
1237+
background: rgba(0, 0, 0, 0.28);
1238+
backdrop-filter: blur(6px);
1239+
}
1240+
1241+
.wechat-link-finder-author-avatar {
1242+
width: 18px;
1243+
height: 18px;
1244+
flex-shrink: 0;
1245+
display: flex;
1246+
align-items: center;
1247+
justify-content: center;
1248+
}
1249+
1250+
.wechat-link-finder-author-avatar-img {
1251+
width: 100%;
1252+
height: 100%;
1253+
object-fit: contain;
1254+
display: block;
1255+
}
1256+
1257+
.wechat-link-finder-author-name {
1258+
min-width: 0;
1259+
flex: 1 1 auto;
1260+
font-size: 10px;
1261+
color: rgba(255, 255, 255, 0.96);
1262+
overflow: hidden;
1263+
text-overflow: ellipsis;
1264+
white-space: nowrap;
1265+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
1266+
}
1267+
11421268
/* 隐私模式模糊效果 */
11431269
.privacy-blur {
11441270
filter: blur(9px);

frontend/assets/css/tailwind.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,14 +1437,20 @@
14371437

14381438
.session-list-item-name {
14391439
color: var(--session-list-name);
1440+
font-weight: 400;
1441+
font-synthesis: none;
14401442
}
14411443

14421444
.session-list-item-time {
14431445
color: var(--session-list-meta);
1446+
font-weight: 400;
1447+
font-synthesis: none;
14441448
}
14451449

14461450
.session-list-item-preview {
14471451
color: var(--session-list-preview);
1452+
font-weight: 400;
1453+
font-synthesis: none;
14481454
}
14491455

14501456
.contact-search-wrapper {

frontend/components/chat/LinkCard.vue

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { defineComponent, h, ref, watch } from 'vue'
33
import miniProgramIconUrl from '~/assets/images/wechat/mini-program.svg'
44
5+
const finderLogoUrl = '/assets/images/wechat/channels-logo.svg'
6+
57
export default defineComponent({
68
name: 'LinkCard',
79
props: {
@@ -51,7 +53,11 @@ export default defineComponent({
5153
return text ? (Array.from(text)[0] || '') : ''
5254
})()
5355
const fromAvatarUrl = String(props.fromAvatar || '').trim()
56+
const headingText = String(props.heading || href || '').trim()
57+
let abstractText = String(props.abstract || '').trim()
58+
if (abstractText && headingText && abstractText === headingText) abstractText = ''
5459
const isMiniProgram = String(props.linkType || '').trim() === 'mini_program'
60+
const isFinder = String(props.linkType || '').trim() === 'finder'
5561
const isCoverVariant = !isMiniProgram && String(props.variant || '').trim() === 'cover'
5662
const Tag = canNavigate ? 'a' : 'div'
5763
@@ -140,9 +146,68 @@ export default defineComponent({
140146
)
141147
}
142148
143-
const headingText = String(props.heading || href || '').trim()
144-
let abstractText = String(props.abstract || '').trim()
145-
if (abstractText && headingText && abstractText === headingText) abstractText = ''
149+
if (isFinder) {
150+
return h(
151+
Tag,
152+
{
153+
...(canNavigate ? { href, target: '_blank', rel: 'noreferrer' } : { role: 'group', 'aria-disabled': 'true' }),
154+
class: [
155+
'wechat-link-card-finder',
156+
!canNavigate ? 'wechat-link-card--disabled' : '',
157+
'wechat-special-card',
158+
'msg-radius',
159+
props.isSent ? 'wechat-special-sent-side' : ''
160+
].filter(Boolean).join(' '),
161+
style: {
162+
width: '135px',
163+
minWidth: '135px',
164+
maxWidth: '135px',
165+
display: 'flex',
166+
flexDirection: 'column',
167+
boxSizing: 'border-box',
168+
flex: '0 0 auto',
169+
border: 'none',
170+
boxShadow: 'none',
171+
textDecoration: 'none',
172+
outline: 'none'
173+
}
174+
},
175+
[
176+
h('div', { class: ['wechat-link-finder-cover', !props.preview ? 'wechat-link-finder-cover--empty' : ''].filter(Boolean).join(' ') }, [
177+
props.preview
178+
? h('img', {
179+
src: props.preview,
180+
alt: props.heading || '视频号封面',
181+
class: 'wechat-link-finder-cover-img',
182+
referrerpolicy: 'no-referrer'
183+
})
184+
: h('div', { class: 'wechat-link-finder-cover-placeholder', 'aria-hidden': 'true' }, [
185+
h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [
186+
h('path', { d: 'M8 5v14l11-7z' })
187+
])
188+
]),
189+
h('div', { class: 'wechat-link-finder-cover-shade', 'aria-hidden': 'true' }),
190+
h('div', { class: 'wechat-link-finder-play', 'aria-hidden': 'true' }, [
191+
h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [
192+
h('path', { d: 'M8 5v14l11-7z' })
193+
])
194+
]),
195+
h('div', { class: 'wechat-link-finder-meta' }, [
196+
h('div', { class: 'wechat-link-finder-author' }, [
197+
h('div', { class: 'wechat-link-finder-author-avatar', 'aria-hidden': 'true' }, [
198+
h('img', {
199+
src: finderLogoUrl,
200+
alt: '',
201+
class: 'wechat-link-finder-author-avatar-img'
202+
})
203+
]),
204+
h('div', { class: 'wechat-link-finder-author-name' }, fromText || '视频号')
205+
])
206+
])
207+
])
208+
]
209+
)
210+
}
146211
147212
if (isMiniProgram) {
148213
return h(

frontend/components/chat/SessionListPanel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
<!-- 联系人信息 -->
9999
<div class="flex-1 min-w-0">
100100
<div class="flex items-center justify-between">
101-
<h3 class="session-list-item-name text-sm font-medium truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
101+
<h3 class="session-list-item-name text-sm truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
102102
<div class="flex items-center flex-shrink-0 ml-2">
103103
<span class="session-list-item-time text-xs">{{ contact.lastMessageTime }}</span>
104104
</div>
Lines changed: 5 additions & 0 deletions
Loading

src/wechat_decrypt_tool/chat_helpers.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,70 @@ def _extract_appmsg_type(xml_text: str) -> int:
12201220
"linkStyle": link_style,
12211221
}
12221222

1223+
if app_type == 51:
1224+
# 视频号分享(Finder / Channels)
1225+
# 常见特征:
1226+
# - title 是「当前版本不支持展示该内容,请升级至最新版本。」
1227+
# - 真正标题在 <finderFeed><desc> 或其它 finder 节点里
1228+
finder_feed = _extract_xml_tag_text(text, "finderFeed")
1229+
finder_desc = (
1230+
(_extract_xml_tag_text(finder_feed, "desc") if finder_feed else "")
1231+
or _extract_xml_tag_text(text, "finderdesc")
1232+
or des
1233+
)
1234+
finder_nickname = (
1235+
_extract_xml_tag_text(text, "findernickname")
1236+
or _extract_xml_tag_text(text, "finder_nickname")
1237+
or (_extract_xml_tag_text(finder_feed, "nickname") if finder_feed else "")
1238+
or (_extract_xml_tag_text(finder_feed, "findernickname") if finder_feed else "")
1239+
)
1240+
finder_username = (
1241+
_extract_xml_tag_text(text, "finderusername")
1242+
or _extract_xml_tag_text(text, "finder_username")
1243+
or (_extract_xml_tag_text(finder_feed, "username") if finder_feed else "")
1244+
or (_extract_xml_tag_text(finder_feed, "finderusername") if finder_feed else "")
1245+
)
1246+
1247+
thumb_url = _normalize_xml_url(
1248+
_extract_xml_tag_or_attr(text, "thumburl")
1249+
or _extract_xml_tag_or_attr(text, "cdnthumburl")
1250+
or _extract_xml_tag_or_attr(text, "coverurl")
1251+
or _extract_xml_tag_or_attr(text, "cover")
1252+
or (_extract_xml_tag_or_attr(finder_feed, "thumbUrl") if finder_feed else "")
1253+
or (_extract_xml_tag_or_attr(finder_feed, "thumburl") if finder_feed else "")
1254+
or (_extract_xml_tag_or_attr(finder_feed, "coverUrl") if finder_feed else "")
1255+
or (_extract_xml_tag_or_attr(finder_feed, "coverurl") if finder_feed else "")
1256+
)
1257+
1258+
finder_url = url or _normalize_xml_url(
1259+
(_extract_xml_tag_text(finder_feed, "url") if finder_feed else "")
1260+
or (_extract_xml_tag_text(text, "playurl"))
1261+
or (_extract_xml_tag_text(text, "dataurl"))
1262+
)
1263+
1264+
display_title = str(title or "").strip()
1265+
if (not display_title) or ("不支持" in display_title):
1266+
display_title = str(finder_desc or "").strip()
1267+
if not display_title:
1268+
display_title = str(des or "").strip()
1269+
display_title = display_title or "[视频号]"
1270+
1271+
summary_text = str(finder_desc or "").strip() or display_title
1272+
from_display = str(finder_nickname or source_display_name or "").strip() or "视频号"
1273+
from_u = str(finder_username or source_username or "").strip()
1274+
1275+
return {
1276+
"renderType": "link",
1277+
"content": summary_text,
1278+
"title": display_title,
1279+
"url": finder_url or "",
1280+
"thumbUrl": thumb_url or "",
1281+
"from": from_display,
1282+
"fromUsername": from_u,
1283+
"linkType": "finder",
1284+
"linkStyle": "finder",
1285+
}
1286+
12231287
if app_type in (33, 36):
12241288
# 小程序分享(WeChat v4 常见:local_type = 49 + (33<<32) / 49 + (36<<32))
12251289
# 注:部分 payload 的 <url> 为空;前端会按需渲染为不可点击卡片。

tests/test_parse_app_message.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,36 @@ def test_public_account_link_exposes_link_type_and_style(self):
118118
self.assertEqual(parsed.get("linkType"), "official_article")
119119
self.assertEqual(parsed.get("linkStyle"), "cover")
120120

121+
def test_finder_type_51_uses_nested_desc_and_cover(self):
122+
raw_text = (
123+
'<msg><appmsg appid="" sdkver="0">'
124+
'<title>当前版本不支持展示该内容,请升级至最新版本。</title>'
125+
'<des></des>'
126+
'<type>51</type>'
127+
'<url></url>'
128+
'<finderFeed>'
129+
'<nickname><![CDATA[央视新闻]]></nickname>'
130+
'<username><![CDATA[finder_cctv_news]]></username>'
131+
'<desc><![CDATA[微信视频号全金融行业今公布发布]]></desc>'
132+
'<mediaList><media>'
133+
'<coverUrl><![CDATA[https://finder.video.qq.com/cover.jpg]]></coverUrl>'
134+
'<url><![CDATA[https://channels.weixin.qq.com/web/pages/feed?feedid=abc]]></url>'
135+
'</media></mediaList>'
136+
'</finderFeed>'
137+
'</appmsg></msg>'
138+
)
139+
140+
parsed = _parse_app_message(raw_text)
141+
142+
self.assertEqual(parsed.get("renderType"), "link")
143+
self.assertEqual(parsed.get("linkType"), "finder")
144+
self.assertEqual(parsed.get("title"), "微信视频号全金融行业今公布发布")
145+
self.assertEqual(parsed.get("content"), "微信视频号全金融行业今公布发布")
146+
self.assertEqual(parsed.get("from"), "央视新闻")
147+
self.assertEqual(parsed.get("fromUsername"), "finder_cctv_news")
148+
self.assertEqual(parsed.get("thumbUrl"), "https://finder.video.qq.com/cover.jpg")
149+
self.assertEqual(parsed.get("url"), "https://channels.weixin.qq.com/web/pages/feed?feedid=abc")
150+
121151
def test_quote_type_5_nested_xml_refermsg_uses_inner_title(self):
122152
raw_text = (
123153
'<msg><appmsg appid="" sdkver="0">'

0 commit comments

Comments
 (0)