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 /