// ==UserScript== // @name 豆包图片去水印 // @namespace local.doubao.watermark // @version 1.6.0 // @description 提取豆包当前页面的无水印图片,支持批量下载 // @match https://www.doubao.com/* // @run-at document-start // @grant GM_download // @grant unsafeWindow // @connect * // @license MIT // @icon https://lf-flow-web-cdn.doubao.com/obj/flow-doubao/doubao/chat/favicon.png // ==/UserScript== (function () { 'use strict'; const PAGE_WIN = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const LOCATION = PAGE_WIN.location; const FLOATING_POSITION_KEY = 'doubao-watermark-floating-position-v3'; const imageStore = { rawByPath: new Map(), filenameCache: new Map(), images: [], imageUrls: new Set(), pendingImages: new Map(), clear() { this.images.length = 0; this.imageUrls.clear(); this.rawByPath.clear(); this.filenameCache.clear(); this.pendingImages.clear(); }, addImage(record) { if (!record || this.imageUrls.has(record.url)) return false; this.imageUrls.add(record.url); this.images.push({ url: record.url, width: record.width, height: record.height }); return true; }, }; const uiState = { root: null, floatingButton: null, modal: null, previewModal: null, previewContent: null, modalVisible: false, suppressNextFloatingClick: false, drag: null, updateTimer: 0, }; let pageImageObserver = null; let indexedImageElements = new WeakMap(); const currentPageImageKeyCounts = new Map(); function normalize(str) { if (typeof str !== 'string') return str; return str.replace(/\\u002F/g, '/').replace(/\\\//g, '/').replace(/&/g, '&'); } const pathKeyCache = new Map(); function getPathKey(url) { if (typeof url !== 'string') return ''; if (pathKeyCache.has(url)) return pathKeyCache.get(url); try { const u = new URL(normalize(url), LOCATION.href); const idx = u.pathname.indexOf('~tplv-'); const key = idx >= 0 ? u.pathname.slice(0, idx) : u.pathname; pathKeyCache.set(url, key); return key; } catch { pathKeyCache.set(url, ''); return ''; } } function isValidRaw(url) { return typeof url === 'string' && url.includes('~tplv-') && /(image_raw_b|image_raw|ori_raw)/i.test(url); } function scheduleUpdateUI() { if (uiState.updateTimer) return; uiState.updateTimer = setTimeout(() => { uiState.updateTimer = 0; updateUI(); }, 50); } function notifyImageChange() { scheduleUpdateUI(); } function clearImageState() { imageStore.clear(); pathKeyCache.clear(); indexedImageElements = new WeakMap(); currentPageImageKeyCounts.clear(); } function adjustImageKeyCount(key, delta) { const count = (currentPageImageKeyCounts.get(key) || 0) + delta; if (count <= 0) currentPageImageKeyCounts.delete(key); else currentPageImageKeyCounts.set(key, count); } function getImageElementKeys(img) { const keys = new Set(); const values = [ img.currentSrc, img.src, img.getAttribute('src'), img.getAttribute('srcset'), img.getAttribute('data-src'), img.getAttribute('data-original'), ]; for (const value of values) { if (!value) continue; for (const part of String(value).split(',').map(v => v.trim().split(/\s+/)[0])) { const key = getPathKey(part); if (key) keys.add(key); } } return keys; } function indexImageElement(img) { if (!(img instanceof HTMLImageElement)) return; const nextKeys = getImageElementKeys(img); const previousKeys = indexedImageElements.get(img) || new Set(); for (const key of previousKeys) { if (!nextKeys.has(key)) adjustImageKeyCount(key, -1); } for (const key of nextKeys) { if (!previousKeys.has(key)) adjustImageKeyCount(key, 1); } indexedImageElements.set(img, nextKeys); } function removeImageElement(img) { if (!(img instanceof HTMLImageElement)) return; const keys = indexedImageElements.get(img); if (!keys) return; for (const key of keys) adjustImageKeyCount(key, -1); indexedImageElements.delete(img); } function scanCurrentPageImages() { indexedImageElements = new WeakMap(); currentPageImageKeyCounts.clear(); document.querySelectorAll('img').forEach(indexImageElement); } function indexNodeImages(node) { if (!(node instanceof Element)) return; if (node.matches('img')) indexImageElement(node); node.querySelectorAll('img').forEach(indexImageElement); } function removeNodeImages(node) { if (!(node instanceof Element)) return; if (node.matches('img')) removeImageElement(node); node.querySelectorAll('img').forEach(removeImageElement); } function queueCurrentPageImage(rawUrl, image, altUrls) { const urls = [rawUrl, ...(altUrls || [])].filter(Boolean).map(normalize); const keys = [...new Set(urls.map(getPathKey).filter(Boolean))]; if (!keys.length) return; const record = { url: normalize(rawUrl), width: image.image_ori?.width || image.image_ori_raw?.width || 0, height: image.image_ori?.height || image.image_ori_raw?.height || 0, keys, }; if (keys.some(key => currentPageImageKeyCounts.has(key))) { if (imageStore.addImage(record)) notifyImageChange(); } else { imageStore.pendingImages.set(record.url, record); } } function flushPendingImages() { if (!imageStore.pendingImages.size) return; let changed = false; for (const [url, record] of imageStore.pendingImages) { if (!record.keys.some(key => currentPageImageKeyCounts.has(key))) continue; imageStore.pendingImages.delete(url); changed = imageStore.addImage(record) || changed; } if (changed) notifyImageChange(); } function observeCurrentPageImages() { if (pageImageObserver || !document.body) return; pageImageObserver = new MutationObserver(records => { for (const record of records) { if (record.type === 'attributes') { if (record.target instanceof HTMLImageElement) indexImageElement(record.target); } for (const node of record.addedNodes) indexNodeImages(node); for (const node of record.removedNodes) removeNodeImages(node); } flushPendingImages(); }); pageImageObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'], }); document.addEventListener('load', event => { if (event.target instanceof HTMLImageElement) { indexImageElement(event.target); flushPendingImages(); } }, true); } function rewriteForDownload(url) { if (typeof url !== 'string' || !url.includes('~tplv-')) return url; const key = getPathKey(url); return key && imageStore.rawByPath.has(key) ? imageStore.rawByPath.get(key) : url; } function generateFileName(url) { const d = new Date(); const pad = n => String(n).padStart(2, '0'); const date = `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}`; const time = `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; let ext = 'PNG'; try { const path = new URL(url || '', LOCATION.href).pathname.toLowerCase(); if (path.includes('.jpeg') || path.includes('.jpg')) ext = 'JPG'; else if (path.includes('.webp')) ext = 'WEBP'; else if (path.includes('.heic')) ext = 'HEIC'; } catch {} return `IMG_${date}_${time}.${ext}`; } function getStableFileName(url) { const key = getPathKey(url); if (!key) return generateFileName(url); const cacheKey = key; if (!imageStore.filenameCache.has(cacheKey)) { imageStore.filenameCache.set(cacheKey, generateFileName(url)); } return imageStore.filenameCache.get(cacheKey); } function rememberRaw(rawUrl, ...altUrls) { rawUrl = normalize(rawUrl); if (!isValidRaw(rawUrl)) return; const rawKey = getPathKey(rawUrl); if (rawKey) imageStore.rawByPath.set(rawKey, rawUrl); for (const alt of altUrls) { const k = getPathKey(alt); if (k) imageStore.rawByPath.set(k, rawUrl); } } function parseTextForRaw(text) { if (typeof text !== 'string') return; if (!text.includes('image_ori_raw') && !text.includes('creations')) return; const t = normalize(text); const raws = [...t.matchAll(/"image_ori_raw"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"/g)]; const previews = [...t.matchAll(/"image_preview"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"/g)]; raws.forEach((match, index) => rememberRaw(match[1], previews[index]?.[1])); } function processImageItem(item) { if (!item || typeof item !== 'object') return; const image = item.image; if (image) { const raw = image.image_ori_raw?.url; if (raw && isValidRaw(raw)) { const normalizedRaw = normalize(raw); rememberRaw(normalizedRaw); const oldImageUrls = []; const fields = ['image_ori', 'image_preview', 'image_thumb', 'image_preview_resize']; for (const field of fields) { if (image[field]?.url) { const old = image[field].url; oldImageUrls.push(old); image[field].url = normalizedRaw; rememberRaw(normalizedRaw, old); } } queueCurrentPageImage(normalizedRaw, image, oldImageUrls); } } } function walkImagePayload(obj) { if (!obj || typeof obj !== 'object') return; processImageItem(obj); const values = Array.isArray(obj) ? obj : Object.values(obj); for (const value of values) walkImagePayload(value); } function containsImageData(text) { return typeof text === 'string' && ( text.includes('creations') || text.includes('image_ori_raw') ); } function inspectParsedData(text, data) { if (containsImageData(text)) walkImagePayload(data); } const nativeJSONParse = PAGE_WIN.JSON.parse; function installJSONHook() { PAGE_WIN.JSON.parse = function (text) { const data = nativeJSONParse(text); try { inspectParsedData(text, data); } catch {} return data; }; } function parseEventStream(text) { for (const line of text.split('\n')) { if (!line.startsWith('data: ')) continue; const payload = line.slice(6); try { const data = nativeJSONParse(payload); inspectParsedData(payload, data); } catch {} } } function inspectResponseText(text, contentType) { if (!containsImageData(text)) return; parseTextForRaw(text); if (contentType.includes('event-stream')) parseEventStream(text); } const nativeFetch = PAGE_WIN.fetch; function installFetchHook() { PAGE_WIN.fetch = async function (resource, init) { let nextResource = resource; try { if (typeof resource === 'string') { const rewritten = rewriteForDownload(resource); if (rewritten !== resource) nextResource = rewritten; } else if (resource instanceof Request) { const rewritten = rewriteForDownload(resource.url); if (rewritten !== resource.url) { nextResource = new Request(rewritten, resource); } } } catch {} const resp = await nativeFetch.call(this, nextResource, init); try { const ct = (resp.headers.get('content-type') || '').toLowerCase(); if (ct.includes('json') || ct.includes('text') || ct.includes('event-stream')) { const clone = resp.clone(); const text = await clone.text(); inspectResponseText(text, ct); } } catch {} return resp; }; } const xhrOpen = XMLHttpRequest.prototype.open; const xhrSend = XMLHttpRequest.prototype.send; function installXHRHook() { XMLHttpRequest.prototype.open = function (method, url, ...rest) { let next = String(url || ''); try { next = rewriteForDownload(next); } catch {} return xhrOpen.call(this, method, next, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { this.addEventListener('load', function () { try { const ct = (this.getResponseHeader('content-type') || '').toLowerCase(); if (ct.includes('json') || ct.includes('text')) inspectResponseText(this.responseText, ct); } catch {} }, { once: true }); return xhrSend.apply(this, args); }; } function installNetworkHooks() { installJSONHook(); installFetchHook(); installXHRHook(); } function downloadFile(url, filename) { return new Promise(resolve => { let settled = false; const finish = () => { if (settled) return; settled = true; resolve(); }; const fail = error => { console.error('[水印助手] 下载失败:', error); alert('下载失败,请重试'); finish(); }; try { if (typeof GM_download === 'function') { GM_download({ url, name: filename, saveAs: false, onload: finish, onerror: fail, ontimeout: fail, }); } else { const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.click(); setTimeout(finish, 0); } } catch (error) { fail(error); } }); } async function batchDownload() { const checks = uiState.root?.querySelectorAll('#db-media-grid .dbwh-media-check:checked') || []; if (!checks.length) return alert('请至少选择一项'); const items = []; for (const checkbox of checks) { const item = imageStore.images[Number(checkbox.dataset.index)]; if (item) items.push(item); } for (const item of items) { await downloadFile(item.url, getStableFileName(item.url)); } } function restoreFloatingPosition() { try { const saved = JSON.parse(localStorage.getItem(FLOATING_POSITION_KEY) || 'null'); if (!saved || !Number.isFinite(saved.left) || !Number.isFinite(saved.top)) return; uiState.floatingButton.style.left = `${saved.left}px`; uiState.floatingButton.style.top = `${saved.top}px`; uiState.floatingButton.style.right = 'auto'; uiState.floatingButton.style.bottom = 'auto'; } catch {} } function clampFloatingPosition(left, top) { const margin = 8; const maxLeft = Math.max(margin, window.innerWidth - uiState.floatingButton.offsetWidth - margin); const maxTop = Math.max(margin, window.innerHeight - uiState.floatingButton.offsetHeight - margin); return { left: Math.min(Math.max(margin, left), maxLeft), top: Math.min(Math.max(margin, top), maxTop), }; } function applyFloatingPosition(left, top) { const position = clampFloatingPosition(left, top); uiState.floatingButton.style.left = `${position.left}px`; uiState.floatingButton.style.top = `${position.top}px`; uiState.floatingButton.style.right = 'auto'; uiState.floatingButton.style.bottom = 'auto'; return position; } function startFloatingDrag(event) { if (event.button !== 0) return; const rect = uiState.floatingButton.getBoundingClientRect(); uiState.drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startLeft: rect.left, startTop: rect.top, nextLeft: rect.left, nextTop: rect.top, moved: false, frame: 0, }; uiState.floatingButton.classList.add('dragging'); uiState.floatingButton.setPointerCapture?.(event.pointerId); uiState.floatingButton.addEventListener('pointermove', moveFloatingDrag); uiState.floatingButton.addEventListener('pointerup', endFloatingDrag, { once: true }); uiState.floatingButton.addEventListener('pointercancel', endFloatingDrag, { once: true }); } function moveFloatingDrag(event) { if (!uiState.drag || event.pointerId !== uiState.drag.pointerId) return; const dx = event.clientX - uiState.drag.startX; const dy = event.clientY - uiState.drag.startY; if (Math.abs(dx) > 4 || Math.abs(dy) > 4) uiState.drag.moved = true; uiState.drag.nextLeft = uiState.drag.startLeft + dx; uiState.drag.nextTop = uiState.drag.startTop + dy; if (uiState.drag.frame) return; uiState.drag.frame = requestAnimationFrame(() => { if (!uiState.drag) return; uiState.drag.frame = 0; applyFloatingPosition(uiState.drag.nextLeft, uiState.drag.nextTop); }); } function endFloatingDrag(event) { if (!uiState.drag || event.pointerId !== uiState.drag.pointerId) return; const state = uiState.drag; if (state.frame) cancelAnimationFrame(state.frame); applyFloatingPosition(state.nextLeft, state.nextTop); uiState.floatingButton.releasePointerCapture?.(event.pointerId); uiState.floatingButton.removeEventListener('pointermove', moveFloatingDrag); uiState.floatingButton.classList.remove('dragging'); uiState.drag = null; if (state.moved) { uiState.suppressNextFloatingClick = true; try { const rect = uiState.floatingButton.getBoundingClientRect(); localStorage.setItem(FLOATING_POSITION_KEY, JSON.stringify({ left: Math.round(rect.left), top: Math.round(rect.top), })); } catch {} } } function closePreview() { if (!uiState.previewModal || !uiState.previewContent) return; uiState.previewModal.classList.remove('show'); uiState.previewContent.replaceChildren(); } function openPreview(item) { if (!uiState.previewModal || !uiState.previewContent || !item?.url) return; uiState.previewContent.replaceChildren(); const media = document.createElement('img'); media.id = 'dbwh-preview-media'; media.src = item.url; uiState.previewContent.appendChild(media); uiState.previewModal.classList.add('show'); } async function handleGridClick(event) { if (!(event.target instanceof Element)) return; const downloadButton = event.target.closest('.db-download-single'); if (downloadButton) { event.stopPropagation(); const item = imageStore.images[Number(downloadButton.dataset.index)]; if (item) await downloadFile(item.url, getStableFileName(item.url)); return; } const preview = event.target.closest('.dbwh-media-preview'); if (!preview) return; const item = imageStore.images[Number(preview.dataset.index)]; if (item) openPreview(item); } const UI_CSS = ` :host, :host * { box-sizing: border-box; } #db-watermark-float-btn { position: fixed; top: 72px; right: 24px; z-index: 9999; background: #1f2937; color: #fff; border: none; border-radius: 40px; padding: 12px 20px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 10px; cursor: pointer; touch-action: none; user-select: none; box-shadow: 0 6px 18px rgba(0,0,0,0.3); transition: all 0.2s; } #db-watermark-float-btn:hover { background: #111827; transform: translateY(-2px); } #db-watermark-float-btn.dragging { cursor: grabbing; transform: none; transition: none; } #db-watermark-float-btn .badge { background: #ef4444; color: #fff; border-radius: 999px; padding: 0 8px; font-size: 12px; line-height: 20px; min-width: 20px; text-align: center; } #db-watermark-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: none; align-items: center; justify-content: center; } #db-watermark-modal.show { display: flex; } .dbwh-modal-box { background: #fff; border-radius: 16px; width: 880px; max-width: 94vw; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,0.2); } .dbwh-modal-header { padding: 14px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } .dbwh-modal-title { font-size: 13px; font-weight: 700; color: #1f2937; } .dbwh-modal-actions { display: flex; gap: 6px; } .dbwh-modal-actions button { height: 32px; padding: 0 13px; border: 1px solid #d1d5db; border-radius: 8px; background: #fff; color: #1f2937; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s; } .dbwh-modal-actions button:hover { background: #f3f4f6; border-color: #9ca3af; } #db-batch-download { border-color: #111827; background: #111827; color: #fff; } #db-batch-download:hover { border-color: #000; background: #000; color: #fff; } .dbwh-modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #6b7280; padding: 0 6px; } .dbwh-modal-body { padding: 16px 20px; overflow-y: auto; flex: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; } .dbwh-media-card { border: 1px solid #e5e7eb; border-radius: 10px; overflow: hidden; background: #fafafa; transition: 0.15s; } .dbwh-media-card:hover { border-color: #1f2937; } .dbwh-media-preview { position: relative; height: 160px; background: #000; display: flex; align-items: center; justify-content: center; cursor: zoom-in; } .dbwh-media-preview img { width: 100%; height: 100%; max-width: 100%; max-height: 100%; object-fit: contain; } .dbwh-media-info { padding: 6px 10px; font-size: 12px; color: #4b5563; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #f3f4f6; } .dbwh-media-info .dbwh-actions { display: flex; gap: 4px; } .dbwh-media-info .dbwh-actions button { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; background: #111827; border: 1px solid #111827; cursor: pointer; color: #fff; padding: 0; border-radius: 8px; transition: background 0.15s, border-color 0.15s, transform 0.15s; } .dbwh-media-info .dbwh-actions button:hover { background: #000; border-color: #000; transform: translateY(-1px); } .dbwh-download-icon { width: 18px; height: 18px; display: block; } .dbwh-media-check { appearance: none; width: 28px; height: 28px; margin: 0; border: 1px solid #d1d5db; border-radius: 8px; background: #fff; cursor: pointer; transition: background 0.15s, border-color 0.15s, transform 0.15s; } .dbwh-media-check:hover { border-color: #111827; transform: translateY(-1px); } .dbwh-media-check:checked { border-color: #111827; background: #111827; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' d='m5 12 4 4L19 6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 17px 17px; } .dbwh-empty-state { grid-column: 1 / -1; text-align: center; color: #9ca3af; padding: 40px 0; } #dbwh-preview-modal { position: fixed; inset: 0; z-index: 10001; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(0, 0, 0, 0.82); } #dbwh-preview-modal.show { display: flex; } #dbwh-preview-media { display: block; max-width: 92vw; max-height: 88vh; object-fit: contain; border-radius: 6px; background: #000; } .dbwh-preview-close { position: absolute; top: 16px; right: 20px; width: 36px; height: 36px; border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 50%; background: rgba(0, 0, 0, 0.35); color: #fff; font-size: 22px; line-height: 1; cursor: pointer; } `; const UI_HTML = `
图片 0
`; function createUI() { if (document.getElementById('db-watermark-helper')) return false; const host = document.createElement('div'); host.id = 'db-watermark-helper'; uiState.root = host.attachShadow({ mode: 'open' }); uiState.root.innerHTML = `${UI_HTML}`; document.body.appendChild(host); uiState.floatingButton = uiState.root.getElementById('db-watermark-float-btn'); uiState.modal = uiState.root.getElementById('db-watermark-modal'); uiState.previewModal = uiState.root.getElementById('dbwh-preview-modal'); uiState.previewContent = uiState.root.getElementById('dbwh-preview-content'); restoreFloatingPosition(); updateBadge(); return true; } function bindUIEvents() { uiState.floatingButton.addEventListener('pointerdown', startFloatingDrag); uiState.floatingButton.addEventListener('click', event => { if (uiState.suppressNextFloatingClick) { uiState.suppressNextFloatingClick = false; event.preventDefault(); return; } uiState.modal.classList.add('show'); uiState.modalVisible = true; renderGrid(); }); uiState.modal.querySelector('.dbwh-modal-close').addEventListener('click', () => { uiState.modal.classList.remove('show'); uiState.modalVisible = false; }); uiState.modal.addEventListener('click', event => { if (event.target === uiState.modal) { uiState.modal.classList.remove('show'); uiState.modalVisible = false; } }); uiState.root.getElementById('db-select-all').addEventListener('click', () => { uiState.root.querySelectorAll('#db-media-grid .dbwh-media-check').forEach(cb => cb.checked = true); }); uiState.root.getElementById('db-clear-all').addEventListener('click', () => { uiState.root.querySelectorAll('#db-media-grid .dbwh-media-check').forEach(cb => cb.checked = false); }); uiState.root.getElementById('db-batch-download').addEventListener('click', batchDownload); uiState.root.getElementById('db-media-grid').addEventListener('click', handleGridClick); uiState.root.querySelector('.dbwh-preview-close').addEventListener('click', closePreview); uiState.previewModal.addEventListener('click', event => { if (event.target === uiState.previewModal) closePreview(); }); document.addEventListener('keydown', event => { if (event.key === 'Escape') closePreview(); }); } function updateBadge() { const total = imageStore.images.length; const badge = uiState.root?.getElementById('db-badge'); if (badge) badge.textContent = total; const imgCount = uiState.root?.getElementById('img-count'); if (imgCount) imgCount.textContent = imageStore.images.length; } function renderGrid() { const grid = uiState.root?.getElementById('db-media-grid'); if (!grid) return; const items = imageStore.images; if (items.length === 0) { grid.innerHTML = '
暂无图片
'; return; } grid.innerHTML = items.map((item, idx) => { const url = item.url; const info = `${item.width || 0}×${item.height || 0}`; return `
${info}
`; }).join(''); } function updateUI() { updateBadge(); if (uiState.modalVisible) renderGrid(); } const imgDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src'); function installImageRewriteHook() { if (!imgDesc?.set) return; Object.defineProperty(HTMLImageElement.prototype, 'src', { configurable: true, enumerable: imgDesc.enumerable, get: imgDesc.get, set(value) { const raw = String(value || ''); const next = this.isConnected ? raw : rewriteForDownload(raw); return imgDesc.set.call(this, next); }, }); } function prepareDownloadAnchor(anchor) { const href = anchor.getAttribute('href') || anchor.href || ''; if (href.includes('~tplv-') || href.startsWith('blob:')) { const rewritten = rewriteForDownload(href); if (rewritten !== href) anchor.setAttribute('href', rewritten); const name = getStableFileName(rewritten || href); if (name) anchor.setAttribute('download', name); } } const nativeAClick = HTMLAnchorElement.prototype.click; function installAnchorDownloadHooks() { document.addEventListener('click', event => { if (!(event.target instanceof Element)) return; const anchor = event.target.closest('a[href]'); if (anchor) prepareDownloadAnchor(anchor); }, true); HTMLAnchorElement.prototype.click = function () { prepareDownloadAnchor(this); return nativeAClick.call(this); }; } function installDownloadHooks() { installImageRewriteHook(); installAnchorDownloadHooks(); } function scanEmbeddedImageData() { for (const script of document.querySelectorAll('script[data-script-src]')) { const args = script.getAttribute('data-fn-args'); if (!args) continue; try { const data = nativeJSONParse(args.replace(/"/g, '"')); walkImagePayload(data); } catch {} } flushPendingImages(); updateUI(); } function installRouteWatcher() { let lastUrl = LOCATION.href; setInterval(() => { if (LOCATION.href === lastUrl) return; lastUrl = LOCATION.href; clearImageState(); updateUI(); setTimeout(() => { scanCurrentPageImages(); flushPendingImages(); }, 300); }, 2000); } function initializeEarly() { installNetworkHooks(); installDownloadHooks(); window.__DB_WATERMARK_HELPER = { rawByPath: imageStore.rawByPath, imageList: imageStore.images, rewriteForDownload, }; } function initializeDOM() { if (!createUI()) return; bindUIEvents(); scanCurrentPageImages(); observeCurrentPageImages(); installRouteWatcher(); setTimeout(scanEmbeddedImageData, 500); } initializeEarly(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeDOM, { once: true }); } else { initializeDOM(); } console.log('[豆包图片去水印] 已启动'); })();