From 7870cd54fcf669191150ccfa37b1849b25a5f603 Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Thu, 16 Apr 2026 01:19:09 +0800
Subject: [PATCH] =?UTF-8?q?improvement(sns):=20=E7=B2=BE=E7=AE=80=E6=9C=8B?=
=?UTF-8?q?=E5=8F=8B=E5=9C=88=E5=AA=92=E4=BD=93=E9=93=BE=E8=B7=AF=E5=B9=B6?=
=?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=89=8B=E5=8A=A8=E6=8C=91=E5=9B=BE=E5=85=A5?=
=?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除 Server-Timing/X-SNS 阶段透出与前端媒体来源标记
- 移除本地候选图与手动匹配接口及预览交互
- /api/sns/media 默认仅保留远程下载解密路径,未命中直接返回 404
- 将导出所需的本地缓存匹配逻辑下沉到 sns_export_service
---
frontend/composables/useApi.js | 26 -
frontend/pages/sns.vue | 397 +---------
src/wechat_decrypt_tool/api.py | 23 -
src/wechat_decrypt_tool/routers/sns.py | 732 +-----------------
src/wechat_decrypt_tool/sns_export_service.py | 290 ++++++-
src/wechat_decrypt_tool/sns_stage_timing.py | 63 --
tests/test_sns_media_route_weflow_default.py | 39 +
tests/test_sns_stage_server_timing.py | 40 -
8 files changed, 343 insertions(+), 1267 deletions(-)
delete mode 100644 src/wechat_decrypt_tool/sns_stage_timing.py
create mode 100644 tests/test_sns_media_route_weflow_default.py
delete mode 100644 tests/test_sns_stage_server_timing.py
diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js
index fab4e7a..470ee02 100644
--- a/frontend/composables/useApi.js
+++ b/frontend/composables/useApi.js
@@ -357,30 +357,6 @@ export const useApi = () => {
return await request(url)
}
- // 朋友圈图片本地缓存候选(用于错图时手动选择)
- const listSnsMediaCandidates = async (params = {}) => {
- const query = new URLSearchParams()
- if (params && params.account) query.set('account', params.account)
- if (params && params.create_time != null) query.set('create_time', String(params.create_time))
- if (params && params.width != null) query.set('width', String(params.width))
- if (params && params.height != null) query.set('height', String(params.height))
- if (params && params.limit != null) query.set('limit', String(params.limit))
- if (params && params.offset != null) query.set('offset', String(params.offset))
- const url = '/sns/media_candidates' + (query.toString() ? `?${query.toString()}` : '')
- return await request(url)
- }
-
- // 保存朋友圈图片手动匹配结果(本机)
- const saveSnsMediaPicks = async (data = {}) => {
- return await request('/sns/media_picks', {
- method: 'POST',
- body: {
- account: data.account || null,
- picks: (data && data.picks && typeof data.picks === 'object' && !Array.isArray(data.picks)) ? data.picks : {}
- }
- })
- }
-
const openChatMediaFolder = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
@@ -667,8 +643,6 @@ export const useApi = () => {
resolveAppMsg,
listSnsTimeline,
listSnsUsers,
- listSnsMediaCandidates,
- saveSnsMediaPicks,
openChatMediaFolder,
downloadChatEmoji,
saveMediaKeys,
diff --git a/frontend/pages/sns.vue b/frontend/pages/sns.vue
index c257211..da1136a 100644
--- a/frontend/pages/sns.vue
+++ b/frontend/pages/sns.vue
@@ -114,20 +114,7 @@
:src="getSnsMediaUrl(activeCover, activeCover.media[0], 0, activeCover.media[0].url)"
class="w-full h-full object-cover"
alt="朋友圈封面"
- @load="onCoverMediaLoaded(activeCover, $event)"
/>
-
-
- {{ snsMediaStageLabel(snsCoverStageKey(activeCover)) || '识别中' }}
-
-
@@ -361,7 +348,6 @@
loop
:muted="livePhotoHoverMuted"
playsinline
- @loadeddata="onSnsMediaLoaded(post, post.media[0], 0)"
@error="onLivePhotoVideoError(post.id, 0)"
>
@@ -372,22 +358,8 @@
alt=""
loading="lazy"
referrerpolicy="no-referrer"
- @load="onSnsMediaLoaded(post, post.media[0], 0, $event)"
@error="onMediaError(post.id, 0)"
/>
-
-
-
- {{ snsMediaStageLabel(snsMediaStageKey(post.id, 0, 'thumb')) || '识别中' }}
-
-
![]()
-
-
-
- {{ snsMediaStageLabel(snsMediaStageKey(post.id, idx, 'thumb')) || '识别中' }}
-
-
图片失败
@@ -630,7 +587,7 @@
-
+
` requests may not produce
-// entries, making Server-Timing based stage detection always fall back to "unknown".
-if (process.client) {
- try {
- if (typeof performance !== 'undefined' && performance?.setResourceTimingBufferSize) {
- performance.setResourceTimingBufferSize(5000)
- }
- } catch {}
-}
-
const api = useApi()
const chatAccounts = useChatAccountsStore()
@@ -937,186 +883,6 @@ const onMediaError = (postId, idx) => {
mediaErrors.value[mediaErrorKey(postId, idx)] = true
}
-// Hover badge: show which SNS media pipeline stage produced the image.
-// Backend provides `X-SNS-Source` (and optional `X-SNS-Hit-Type`, `X-SNS-X-Enc`) on `/api/sns/media` responses.
-const snsMediaStage = ref({}) // stageKey -> { source, hitType, xEnc }
-const snsMediaStageLoading = ref({}) // stageKey -> boolean
-const snsMediaStageInFlight = new Set()
-
-const isSnsMediaApiUrl = (url) => {
- const u = String(url || '').trim()
- return !!u && u.includes('/api/sns/media')
-}
-
-const snsMediaStageKey = (postId, idx, kind = 'thumb') => {
- const acc = String(selectedAccount.value || '').trim()
- const pid = String(postId || '').trim()
- return `sns:${acc}:${pid}:${String(Number(idx) || 0)}:${String(kind || 'thumb')}`
-}
-
-const snsCoverStageKey = (cover) => {
- const acc = String(selectedAccount.value || '').trim()
- const cid = String(cover?.id || cover?.tid || cover?.createTime || '').trim()
- return `sns:${acc}:cover:${cid || '0'}`
-}
-
-const snsMediaStageLabel = (key) => {
- const k = String(key || '').trim()
- if (!k) return ''
- const info = snsMediaStage.value[k]
- if (!info || typeof info !== 'object') return ''
-
- const source = String(info?.source || '').trim()
- const hitType = String(info?.hitType || '').trim()
-
- if (source === 'remote-cache') return '远程缓存'
- if (source === 'remote-decrypt') return '远程解密'
- if (source === 'remote') return '远程直出'
- if (source === 'deterministic-hash') return hitType ? `本地命中(${hitType})` : '本地命中'
- if (source === 'manual-pick') return '手动匹配'
- if (source === 'local-heuristic') return '本地兜底'
- if (source === 'local-heuristic-next') return '本地兜底(跳过)'
- if (source === 'browser-cache') return '浏览器缓存'
- if (source === 'bkg-cover') return '封面缓存'
- if (source === 'proxy') return '远程代理'
- if (source === 'unknown') return '未知'
- if (source === 'error') return '获取失败'
- return source || '未知'
-}
-
-const snsMediaStageBadgeColorClass = (key) => {
- const k = String(key || '').trim()
- const source = String(snsMediaStage.value?.[k]?.source || '').trim()
-
- if (source.startsWith('remote')) return 'bg-emerald-600/85 text-white'
- if (source === 'deterministic-hash') return 'bg-sky-600/85 text-white'
- if (source.startsWith('local')) return 'bg-blue-600/85 text-white'
- if (source === 'manual-pick') return 'bg-amber-600/90 text-white'
- if (source === 'browser-cache') return 'bg-slate-600/85 text-white'
- if (source === 'proxy') return 'bg-fuchsia-600/85 text-white'
- if (source === 'bkg-cover') return 'bg-indigo-600/85 text-white'
- if (source === 'error') return 'bg-red-600/85 text-white'
- return 'bg-black/50 text-white'
-}
-
-const snsMediaStageBadgeTitle = (key) => {
- const k = String(key || '').trim()
- const info = snsMediaStage.value?.[k]
- if (!info || typeof info !== 'object') return ''
- const source = String(info?.source || '').trim()
- const hitType = String(info?.hitType || '').trim()
- const xEnc = String(info?.xEnc || '').trim()
-
- const parts = []
- if (source) parts.push(`source=${source}`)
- if (hitType) parts.push(`hit=${hitType}`)
- if (xEnc) parts.push(`x-enc=${xEnc}`)
- return parts.join(' · ')
-}
-
-const readSnsStageFromResourceTiming = (url) => {
- try {
- if (!process.client) return null
- if (typeof performance === 'undefined' || !performance?.getEntriesByName) return null
- const u = String(url || '').trim()
- if (!u) return null
- const entries = performance.getEntriesByName(u) || []
- const latest = [...entries].reverse().find((e) => String(e?.entryType || '') === 'resource')
- if (!latest) return null
-
- // Prefer backend-injected stage info from `Server-Timing`.
- const st = latest?.serverTiming
- if (Array.isArray(st) && st.length > 0) {
- let source = ''
- let hitType = ''
- let xEnc = ''
- for (const item of st) {
- const name = String(item?.name || '').trim()
- const desc = String(item?.description || '').trim()
- if (name === 'sns_source' && desc) source = desc
- else if (name.startsWith('sns_source_')) source = name.slice('sns_source_'.length) || desc
- else if (name === 'sns_hit' && desc) hitType = desc
- else if (name.startsWith('sns_hit_')) hitType = name.slice('sns_hit_'.length) || desc
- else if (name === 'sns_xenc' && desc) xEnc = desc
- else if (name.startsWith('sns_xenc_')) xEnc = name.slice('sns_xenc_'.length) || desc
- }
- if (source) return { source, hitType, xEnc }
- }
-
- // When DevTools shows "(from disk cache)", browsers may not expose `serverTiming` at all.
- // Best-effort: infer a browser cache hit from ResourceTiming sizes.
- const transferSize = Number(latest?.transferSize)
- if (Number.isFinite(transferSize) && transferSize === 0) {
- return { source: 'browser-cache', hitType: 'transfer=0', xEnc: '' }
- }
-
- return null
- } catch {
- return null
- }
-}
-
-const ensureSnsMediaStage = async (key, url) => {
- if (!process.client) return
- const k = String(key || '').trim()
- const u = String(url || '').trim()
- if (!k || !u) return
- if (!isSnsMediaApiUrl(u)) return
-
- const existingSource = String(snsMediaStage.value?.[k]?.source || '').trim()
- if (existingSource && existingSource !== 'unknown') return
- if (snsMediaStageLoading.value[k]) return
- if (snsMediaStageInFlight.has(k)) return
-
- snsMediaStageInFlight.add(k)
- snsMediaStageLoading.value[k] = true
-
- try {
- // Prefer stage info from the *same* request that loaded the
![]()
/