// ==UserScript== // @name 豆包无水印下载助手(轻量版) // @namespace https://greasyfork.org/zh-CN/users/1572936-16dongdong // @version 3.1.0 // @description 轻量优化豆包图片下载:优先使用原始图链接下载,自动生成 iOS 风格文件名,尽量减少页面性能开销。 // @icon https://lf-flow-web-cdn.doubao.com/obj/flow-doubao/doubao/chat/favicon.png // @author Xmy // @license MIT // @match https://www.doubao.com/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; if (window.__DB_SIGNED_RAW_LITE_V310__) return; window.__DB_SIGNED_RAW_LITE_V310__ = true; const rawByPath = new Map(); // /.../xxx.jpeg -> signed raw URL const filenameByPath = new Map(); // stable iOS-like filename per image path let lastImageReqUrl = ''; let lastBlobAt = 0; const norm = (s) => typeof s === 'string' ? s.replace(/\\u002F/g, '/').replace(/\\\//g, '/').replace(/&/g, '&') : s; const pathKey = (u) => { try { const x = new URL(norm(u), location.href); const i = x.pathname.indexOf('~tplv-'); return i >= 0 ? x.pathname.slice(0, i) : x.pathname; } catch { return ''; } }; const iosFileName = (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 p = new URL(url || '', location.href).pathname.toLowerCase(); if (p.includes('.jpeg') || p.includes('.jpg')) ext = 'JPG'; else if (p.includes('.webp')) ext = 'WEBP'; else if (p.includes('.heic')) ext = 'HEIC'; else if (p.endsWith('.png')) ext = 'PNG'; } catch {} return `IMG_${date}_${time}.${ext}`; }; const stableFileNameByUrl = (url) => { const k = pathKey(url); if (!k) return iosFileName(url); if (!filenameByPath.has(k)) filenameByPath.set(k, iosFileName(url)); return filenameByPath.get(k); }; const rememberRaw = (raw, ...aliases) => { raw = norm(raw); if (!raw || !/~tplv-/.test(raw) || !/(image_raw_b|image_raw|ori_raw)/i.test(raw)) return; const keys = new Set(); const pushKey = (u) => { const k = pathKey(u); if (k) keys.add(k); }; pushKey(raw); aliases.forEach(pushKey); for (const k of keys) { rawByPath.set(k, raw); if (!filenameByPath.has(k)) filenameByPath.set(k, iosFileName(raw)); } }; const parseText = (text) => { if (!text || typeof text !== 'string') return; const t = norm(text); // Fast regex parsing for image_ori_raw + common aliases let m; const pairRe = /"image"\s*:\s*\{[\s\S]{0,5000}?"image_ori_raw"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"[\s\S]{0,5000}?(?:"image_thumb"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"|"image_preview"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"|"image_preview_resize"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)")/g; while ((m = pairRe.exec(t)) !== null) { rememberRaw(m[1], m[2], m[3], m[4]); } const rawOnlyRe = /"image_ori_raw"\s*:\s*\{\s*"url"\s*:\s*"([^"]+)"/g; while ((m = rawOnlyRe.exec(t)) !== null) { rememberRaw(m[1], m[1]); } }; const rewriteForDownload = (url) => { if (typeof url !== 'string' || !url.includes('~tplv-')) return url; return rawByPath.get(pathKey(url)) || url; }; const rewriteDetachedImageUrl = (url) => { if (typeof url !== 'string') return url; if (!url.includes('~tplv-')) return url; return rewriteForDownload(url); }; const maybeTrackImageReq = (url) => { if (typeof url !== 'string') return; if (!url.includes('~tplv-')) return; lastImageReqUrl = rewriteForDownload(url); }; // Learn mappings from API responses + rewrite image requests (no DOM sweep) const nativeFetch = window.fetch; if (typeof nativeFetch === 'function') { window.fetch = function patchedFetch(resource, init) { let reqUrl = ''; let nextResource = resource; try { if (typeof resource === 'string') { reqUrl = resource; const rewritten = rewriteForDownload(resource); maybeTrackImageReq(rewritten || resource); nextResource = rewritten; } else if (resource instanceof Request) { reqUrl = resource.url; const rewritten = rewriteForDownload(resource.url); maybeTrackImageReq(rewritten || resource.url); if (rewritten !== resource.url) nextResource = new Request(rewritten, resource); } } catch {} const p = nativeFetch.call(this, nextResource, init); p.then((resp) => { try { const ct = (resp.headers.get('content-type') || '').toLowerCase(); if ((ct.startsWith('image/') || ct.includes('octet-stream')) && reqUrl.includes('~tplv-')) { maybeTrackImageReq(reqUrl); } if (ct.includes('json') || ct.includes('text') || ct.includes('event-stream')) { resp.clone().text().then(parseText).catch(() => {}); } } catch {} }).catch(() => {}); return p; }; } const xopen = XMLHttpRequest.prototype.open; const xsend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) { let next = String(url || ''); try { next = rewriteForDownload(next); maybeTrackImageReq(next); this.__db_req_url = next; } catch {} return xopen.call(this, method, next, ...rest); }; XMLHttpRequest.prototype.send = function patchedSend(...args) { this.addEventListener('readystatechange', function () { try { if (this.readyState !== 4) return; const ct = (this.getResponseHeader('content-type') || '').toLowerCase(); if ((ct.startsWith('image/') || ct.includes('octet-stream')) && this.__db_req_url) { maybeTrackImageReq(this.__db_req_url); } if (ct.includes('json') || ct.includes('text') || ct.includes('event-stream')) { parseText(this.responseText || ''); } } catch {} }); return xsend.apply(this, args); }; // Rewrite URLs for detached Image() downloads; keep in-page visible previews untouched. const imgSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src'); if (imgSrcDesc?.set) { Object.defineProperty(HTMLImageElement.prototype, 'src', { configurable: true, enumerable: imgSrcDesc.enumerable, get: imgSrcDesc.get, set(v) { const raw = String(v || ''); const next = this.isConnected ? raw : rewriteDetachedImageUrl(raw); maybeTrackImageReq(next); return imgSrcDesc.set.call(this, next); } }); } // Only on download click: rewrite href and filename const forceDownloadName = (a, urlForName = '') => { if (!(a instanceof HTMLAnchorElement)) return; const base = urlForName || a.getAttribute('href') || a.href || lastImageReqUrl || ''; const name = stableFileNameByUrl(base); if (name) a.setAttribute('download', name); }; const patchAnchorForDownload = (a) => { if (!(a instanceof HTMLAnchorElement)) return; const href = a.getAttribute('href') || a.href || ''; const next = rewriteForDownload(href); if (next && next !== href) a.setAttribute('href', next); forceDownloadName(a, next || href || lastImageReqUrl || ''); }; // Capture phase: run before site handlers document.addEventListener('click', (ev) => { const t = ev.target; if (!(t instanceof Element)) return; const a = t.closest('a[href]'); if (!a) return; // Restrict to likely image download links to avoid touching unrelated links const href = a.getAttribute('href') || ''; const dataUrl = a.getAttribute('data-download-url') || ''; const candidate = href || dataUrl; if (!candidate.includes('~tplv-')) return; patchAnchorForDownload(a); }, true); // Blob download path used by site JS const nativeCreateObjectURL = URL.createObjectURL; URL.createObjectURL = function patchedCreateObjectURL(obj) { const u = nativeCreateObjectURL.call(this, obj); if (typeof u === 'string' && u.startsWith('blob:')) lastBlobAt = Date.now(); return u; }; const nativeSetAttr = Element.prototype.setAttribute; Element.prototype.setAttribute = function patchedSetAttribute(name, value) { const n = String(name || '').toLowerCase(); if (this instanceof HTMLAnchorElement && n === 'download') { const href = this.getAttribute('href') || this.href || ''; if (href.includes('~tplv-') || href.startsWith('blob:')) { return nativeSetAttr.call(this, name, stableFileNameByUrl(href.startsWith('blob:') ? (lastImageReqUrl || href) : href)); } } return nativeSetAttr.call(this, name, value); }; const dDesc = Object.getOwnPropertyDescriptor(HTMLAnchorElement.prototype, 'download'); if (dDesc?.set) { Object.defineProperty(HTMLAnchorElement.prototype, 'download', { configurable: true, enumerable: dDesc.enumerable, get: dDesc.get, set(v) { const href = this.getAttribute('href') || this.href || ''; const useBlobFallback = href.startsWith('blob:') && Date.now() - lastBlobAt < 15000; const forced = stableFileNameByUrl(useBlobFallback ? (lastImageReqUrl || href) : (href || String(v || ''))); return dDesc.set.call(this, forced); } }); } // Also patch programmatic a.click() const nativeAClick = HTMLAnchorElement.prototype.click; HTMLAnchorElement.prototype.click = function patchedAClick(...args) { try { const href = this.getAttribute('href') || this.href || ''; if (href.includes('~tplv-')) patchAnchorForDownload(this); else if (href.startsWith('blob:')) forceDownloadName(this, lastImageReqUrl || href); } catch {} return nativeAClick.apply(this, args); }; window.__DB_RAW_LITE_DEBUG__ = { rawByPath, filenameByPath, rewriteForDownload, stableFileNameByUrl, getLastImageReqUrl: () => lastImageReqUrl }; })();