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 /