// ==UserScript== // @name 豆包图片/视频一键无水印下载 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 豆包图片/视频一键无水印下载 // @author hydrachs // @match https://www.doubao.com/* // @grant GM_download // @run-at document-start // ==/UserScript== (function() { 'use strict'; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; const originalFetch = window.fetch; const processedUrls = new Set(); const MAX_DEDUP_SIZE = 100; const videoCache = new Map(); async function callGetPlayInfo(videoKey) { const baseUrl = 'https://www.doubao.com/samantha/media/get_play_info'; const params = new URLSearchParams({ aid: '497858', device_platform: 'web', samantha_web: '1', 'use-olympus-account': '1', version_code: '20800', pkg_type: 'release_version', web_tab_id: crypto.randomUUID() }); const url = `${baseUrl}?${params.toString()}`; const response = await fetch(url, { method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json', 'agw-js-conv': 'str', 'origin': location.origin, 'referer': location.href }, credentials: 'include', body: JSON.stringify({ key: videoKey, type: 'video' }) }); const json = await response.json(); if (json.code !== 0) { throw new Error(`get_play_info error: code=${json.code}`); } const data = json.data; if (!data) throw new Error('no data'); const originalMedia = data.original_media_info; if (originalMedia && originalMedia.main_url) { let mainUrl = originalMedia.main_url.replace(/lr=[^&]+/g, 'lr=video_gen_no_watermark'); let backupUrl = originalMedia.backup_url ? originalMedia.backup_url.replace(/lr=[^&]+/g, 'lr=video_gen_no_watermark') : null; return { success: true, mainUrl: mainUrl, backupUrl: backupUrl, width: originalMedia.width, height: originalMedia.height }; } const playInfos = data.play_infos || (data.play_info ? [data.play_info] : []); const playInfo = playInfos[0]; if (!playInfo || !playInfo.main) throw new Error('no video url'); const mainUrl = playInfo.main.replace(/lr=[^&]+/g, 'lr=video_gen_no_watermark'); return { success: true, mainUrl: mainUrl }; } function findVideoAndMessageId() { const routerData = window._ROUTER_DATA; if (!routerData) return null; const cells = routerData?.loaderData?.chat_layout?.trimmedChainRecentConvCells || []; for (const cell of cells) { const messages = cell?.conversation?.messages || []; for (const msg of messages) { const msgId = String(msg.message_id || "").trim(); if (!msgId || msgId === "0") continue; const vid = findVidInObject(msg); if (vid) return { vid, messageId: msgId }; } } return null; } function findVidByMessageId(messageId) { const cached = videoCache.get(messageId); if (cached) return { vid: cached, messageId }; const routerData = window._ROUTER_DATA; if (!routerData) return null; const cells = routerData?.loaderData?.chat_layout?.trimmedChainRecentConvCells || []; for (const cell of cells) { const messages = cell?.conversation?.messages || []; for (const msg of messages) { const msgId = String(msg.message_id || "").trim(); if (msgId === messageId) { const vid = findVidInObject(msg); if (vid) { videoCache.set(messageId, vid); return { vid, messageId }; } } } } return null; } function findVidInObject(obj, depth = 0) { if (depth > 10 || !obj) return null; if (Array.isArray(obj)) { for (const item of obj) { const found = findVidInObject(item, depth + 1); if (found) return found; } } else if (typeof obj === "object") { const vid = obj.vid || obj.video_id; if (vid && typeof vid === "string" && vid.startsWith("v0")) return vid; for (const val of Object.values(obj)) { const found = findVidInObject(val, depth + 1); if (found) return found; } } return null; } async function callDoubaoShareSave(messageId) { return new Promise((resolve) => { function handler(ev) { if (ev.data?.type === "doubaoShareSaveResult") { window.removeEventListener("message", handler); resolve(ev.data.data); } } window.postMessage({ type: "doubaoShareSave", messageId }, "*"); window.addEventListener("message", handler); setTimeout(() => { window.removeEventListener("message", handler); resolve(null); }, 15000); }); } async function callGetVideoShareInfo(shareId, vid) { const url = "https://www.doubao.com/creativity/share/get_video_share_info?version_code=20800&language=zh&device_platform=web&aid=497858&real_aid=497858&pkg_type=release_version&device_id=7550681679050343936&pc_version=3.14.6®ion=CN&sys_region=CN&samantha_web=1&use-olympus-account=1&web_tab_id=" + crypto.randomUUID(); try { const resp = await fetch(url, { method: "POST", headers: { "accept": "application/json", "content-type": "application/json", "agw-js-conv": "str" }, credentials: 'include', body: JSON.stringify({ share_id: shareId, vid, creation_id: "" }) }); const json = await resp.json(); if (json.code === 0 && json.data) return json.data; return null; } catch (e) { return null; } } function extractNoWatermarkVideoUrl(data) { const playInfo = data?.play_infos?.[0] || data?.play_info || (data?.main ? data : null); if (!playInfo?.main) return null; const replaceLr = (url) => url?.replace(/lr=video_gen_watermark_dyn/, "lr=video_gen_no_watermark") .replace(/lr=video_gen_watermark/, "lr=video_gen_no_watermark"); return { mainUrl: replaceLr(playInfo.main), backupUrl: replaceLr(playInfo.backup), width: playInfo.width, height: playInfo.height }; } async function startVideoDownload() { const info = findVideoAndMessageId(); if (!info) return { success: false, error: "no video" }; try { const res = await callGetPlayInfo(info.vid); if (res.mainUrl) { return { success: true, videoUrl: res.mainUrl, messageId: info.messageId }; } } catch (err) {} const share = await callDoubaoShareSave(info.messageId); if (!share?.share_id) return { success: false }; const videoData = await callGetVideoShareInfo(share.share_id, info.vid); if (!videoData) return { success: false }; const extracted = extractNoWatermarkVideoUrl(videoData); if (!extracted) return { success: false }; return { success: true, videoUrl: extracted.mainUrl, messageId: info.messageId }; } async function startVideoDownloadByMessageId(messageId) { const info = findVidByMessageId(messageId); if (!info) return { success: false, messageId }; try { const res = await callGetPlayInfo(info.vid); if (res.mainUrl) { return { success: true, videoUrl: res.mainUrl, messageId }; } } catch (err) {} const share = await callDoubaoShareSave(messageId); if (!share?.share_id) return { success: false, messageId }; const videoData = await callGetVideoShareInfo(share.share_id, info.vid); if (!videoData) return { success: false, messageId }; const extracted = extractNoWatermarkVideoUrl(videoData); if (!extracted) return { success: false, messageId }; return { success: true, videoUrl: extracted.mainUrl, messageId }; } function scanInitialVideoData() { const routerData = window._ROUTER_DATA; if (!routerData) return; const cells = routerData?.loaderData?.chat_layout?.trimmedChainRecentConvCells || []; const videos = []; for (const cell of cells) { const messages = cell?.conversation?.messages || []; for (const msg of messages) { const msgId = String(msg.message_id || "").trim(); if (!msgId || msgId === "0") continue; const vid = findVidInObject(msg); if (vid) { videoCache.set(msgId, vid); videos.push({ vid, messageId: msgId }); } } } if (videos.length) { window.postMessage({ type: "videoDataExtracted", data: videos }, "*"); } } function extractFromCreations(creations) { const images = []; for (const cr of creations) { const img = cr?.image; const raw = img?.image_ori_raw; if (raw?.url) { images.push({ watermark_url: img.image_thumb?.url, no_watermark_url: raw.url, width: raw.width, height: raw.height }); } } return images; } function extractFromPatchOps(patchOps) { let images = []; for (const op of patchOps) { const blocks = op?.patch_value?.content_block; if (blocks) { for (const block of blocks) { const creations = block?.content?.creation_block?.creations; if (creations) images.push(...extractFromCreations(creations)); } } } return images; } function extractFromMessages(messages) { let images = []; for (const msg of messages) { for (const block of msg?.content_block || []) { const creations = block?.content?.creation_block?.creations; if (creations) images.push(...extractFromCreations(creations)); } } return images; } function extractVideoFromMessages(messages) { const videos = []; for (const msg of messages) { const msgId = String(msg.message_id || "").trim(); if (!msgId || msgId === "0") continue; const vid = findVidInObject(msg); if (vid) { videoCache.set(msgId, vid); videos.push({ vid, messageId: msgId }); } } return videos; } function markProcessed(url) { if (url) { processedUrls.add(url); if (processedUrls.size > MAX_DEDUP_SIZE) { const first = processedUrls.values().next().value; processedUrls.delete(first); } } } function publishImages(images) { if (images.length) { window.postMessage({ type: "imageDataExtracted", data: images }, "*"); } } function publishVideos(videos) { if (videos.length) { window.postMessage({ type: "videoDataExtracted", data: videos }, "*"); } } async function readSSEStream(stream) { const reader = stream.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) return; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() || ""; for (const part of parts) { const match = part.match(/^data: (.+)$/m); if (match) { try { const data = JSON.parse(match[1]); const patchOps = data?.patch_op; if (patchOps) { const images = extractFromPatchOps(patchOps); if (images.length) { window.postMessage({ type: "imageDataExtracted", data: images }, "*"); } const msgId = String(data.message_id || "").trim(); for (const op of patchOps) { const pv = op.patch_value; if (!pv) continue; const id = String(pv.message_id || msgId || "").trim(); if (!id || id === "0") continue; const vid = findVidInObject(pv); if (vid) { videoCache.set(id, vid); window.postMessage({ type: "videoDataExtracted", data: [{ vid, messageId: id }] }, "*"); } } } } catch (e) {} } } } } function extractAndPublishFromXHR(response, url) { if (url && processedUrls.has(url)) return; const messages = response?.downlink_body?.pull_singe_chain_downlink_body?.messages; if (!messages) return; publishImages(extractFromMessages(messages)); publishVideos(extractVideoFromMessages(messages)); markProcessed(url); } window.addEventListener("message", async (ev) => { const msg = ev.data; if (msg?.type === "startVideoDownload") { const result = await startVideoDownload(); window.postMessage({ type: "videoDownloadResult", data: result }, "*"); } else if (msg?.type === "startVideoDownloadByMessageId") { const result = await startVideoDownloadByMessageId(msg.messageId); window.postMessage({ type: "videoDownloadResult", data: result }, "*"); } else if (msg?.type === "scanInitialVideos") { scanInitialVideoData(); } }); window.fetch = function(...args) { const url = typeof args[0] === "string" ? args[0] : args[0]?.url; if (typeof url === "string" && url.includes("chat/completion")) { if (processedUrls.has(url)) { return originalFetch.apply(this, args); } markProcessed(url); return originalFetch.apply(this, args).then(async (resp) => { const ct = resp.headers.get("content-type") || ""; if (!ct.includes("text/event-stream")) return resp; const [s1, s2] = resp.body.tee(); const newResp = new Response(s1, { status: resp.status, statusText: resp.statusText, headers: resp.headers }); readSSEStream(s2); return newResp; }); } return originalFetch.apply(this, args); }; XMLHttpRequest.prototype.open = function(method, url, ...rest) { this._url = url; return originalXHROpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function(...args) { this.addEventListener("load", () => { if (typeof this._url === "string" && this._url.includes("chain/single")) { try { extractAndPublishFromXHR(JSON.parse(this.responseText), this._url); } catch (e) {} } }); return originalXHRSend.apply(this, args); }; const imageDataMap = new Map(); const videoButtonMap = new Map(); let domObserverActive = false; function showToast(message, type = "error") { const toast = document.createElement("div"); toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: ${type === "success" ? "#10b981" : "#ef4444"}; color: white; padding: 10px 16px; border-radius: 8px; font-size: 13px; z-index: 100001; animation: fadeInOut 2.5s ease forwards; backdrop-filter: blur(8px); `; toast.textContent = type === "success" ? "✓ " + message : "⚠️ " + message; document.body.appendChild(toast); const style = document.createElement("style"); style.textContent = ` @keyframes fadeInOut { 0% { opacity: 0; transform: translateY(10px); } 15% { opacity: 1; transform: translateY(0); } 85% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } } `; document.head.appendChild(style); setTimeout(() => { if (toast.parentNode) toast.remove(); }, 2500); } function extractFileKey(e) { if (!e) return null; const t = e.match(/rc_gen_image\/([^?~]+)/); return t ? t[1] : null; } function registerImageData(e) { const t = extractFileKey(e.watermark_url || e.no_watermark_url); if (t) imageDataMap.set(t, e); } function injectStyles() { if (document.getElementById("doubao-dl-styles")) return; const e = document.createElement("style"); e.id = "doubao-dl-styles"; e.textContent = ` .doubao-dl-btn { position: absolute; bottom: 10px; right: 10px; z-index: 9999; display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px; background: rgba(0,0,0,0.62); color: #fff; border: none; border-radius: 8px; font-size: 12px; font-weight: 500; cursor: pointer; backdrop-filter: blur(6px); transition: all 0.2s; } .doubao-dl-btn:hover { background: rgba(0,0,0,0.82); } .doubao-dl-btn:active { transform: scale(0.97); } .doubao-dl-btn:disabled { opacity: 0.75; cursor: not-allowed; } .doubao-dl-btn.doubao-dl-success { background: rgba(16,185,129,0.85); } .doubao-dl-btn.doubao-dl-error { background: rgba(239,68,68,0.82); } `; document.head.appendChild(e); } const DOWNLOAD_ICON = ``; function downloadFile(url, name) { fetch(url) .then(resp => resp.blob()) .then(blob => { const blobUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = name; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(blobUrl); }) .catch(err => { const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); }); } function injectDownloadButton(img, data) { if (img.dataset.doubaoInjected) return; img.dataset.doubaoInjected = "1"; let p = img.parentElement; for (let i = 0; i < 6 && p && p !== document.body; i++) { const r = p.getBoundingClientRect(); if (r.width >= 100 && r.height >= 80) break; p = p.parentElement; } if (!p) p = img.parentElement; if (getComputedStyle(p).position === "static") p.style.position = "relative"; const btn = document.createElement("button"); btn.className = "doubao-dl-btn"; btn.innerHTML = DOWNLOAD_ICON + " 下载无水印原图"; btn.onclick = function(e) { e.preventDefault(); e.stopPropagation(); btn.disabled = true; btn.innerHTML = "下载中..."; try { downloadFile(data.no_watermark_url, `doubao_${data.width || 0}x${data.height || 0}.png`); btn.innerHTML = "✓ 已下载"; btn.classList.add("doubao-dl-success"); setTimeout(() => { btn.disabled = false; btn.innerHTML = DOWNLOAD_ICON + " 下载无水印原图"; btn.classList.remove("doubao-dl-success"); }, 2000); } catch (e) { btn.innerHTML = "失败重试"; btn.classList.add("doubao-dl-error"); btn.disabled = false; setTimeout(() => { btn.innerHTML = DOWNLOAD_ICON + " 下载无水印原图"; btn.classList.remove("doubao-dl-error"); }, 2000); } }; p.appendChild(btn); } function findMessageId(el) { let t = el; for (let i = 0; i < 20 && t && t !== document.body; i++) { if (t.dataset) { if (t.dataset.messageId) return t.dataset.messageId; if (t.dataset.message_id) return t.dataset.message_id; } t = t.parentElement; } return null; } function injectVideoDownloadButton(el, mid) { if (el.dataset.doubaoVideoInjected) return; el.dataset.doubaoVideoInjected = "1"; if (getComputedStyle(el).position === "static") el.style.position = "relative"; const btn = document.createElement("button"); btn.className = "doubao-dl-btn"; btn.innerHTML = DOWNLOAD_ICON + " 下载无水印视频"; btn.onclick = function(e) { e.preventDefault(); e.stopPropagation(); btn.disabled = true; btn.innerHTML = "获取链接中..."; videoButtonMap.set(mid, btn); window.postMessage({ type: "startVideoDownloadByMessageId", messageId: mid }, "*"); }; el.appendChild(btn); } function tryInjectForImg(img) { if (!img.src || img.dataset.doubaoInjected) return; const key = extractFileKey(img.src); if (!key) return; const data = imageDataMap.get(key); if (data) injectDownloadButton(img, data); } function tryInjectForVideo(el) { if (!el.className || !el.className.includes("block-video")) return; const mid = findMessageId(el); if (mid) injectVideoDownloadButton(el, mid); } function scanAndInject() { document.querySelectorAll("img").forEach(tryInjectForImg); document.querySelectorAll('[class*="block-video"]').forEach(tryInjectForVideo); } function startDOMObserver() { if (domObserverActive) return; domObserverActive = true; injectStyles(); scanAndInject(); new MutationObserver(() => { scanAndInject(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["src"] }); } let floatPanel = null; let isDragging = false; let dragStartX = 0, dragStartY = 0; let panelStartX = 0, panelStartY = 0; function updateFloatPanel() { if (!floatPanel) return; const ic = imageDataMap.size; const vc = videoCache.size; floatPanel.querySelector("#ic").innerText = ic; floatPanel.querySelector("#vc").innerText = vc; } function createFloatPanel() { if (floatPanel) return; floatPanel = document.createElement("div"); floatPanel.id = "doubao-float-panel"; floatPanel.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 120px; /* 更窄 */ background: rgba(255, 255, 255, 0.3); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); z-index: 999999; font-family: -apple-system; overflow: hidden; color: #333; `; const header = document.createElement("div"); header.style.cssText = ` padding: 10px 12px; background: rgba(255,255,255,0.4); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); display: flex; justify-content: space-between; align-items: center; cursor: grab; font-size: 12px; font-weight: 500; `; header.innerHTML = `
已发现
`; const body = document.createElement("div"); body.style.padding = "12px"; body.innerHTML = `
0
图片
0
视频
`; floatPanel.appendChild(header); floatPanel.appendChild(body); document.body.appendChild(floatPanel); header.onmousedown = function(e) { if (e.target.tagName === "SPAN") return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; const r = floatPanel.getBoundingClientRect(); panelStartX = r.left; panelStartY = r.top; }; document.onmousemove = function(e) { if (!isDragging) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; floatPanel.style.left = panelStartX + dx + "px"; floatPanel.style.top = panelStartY + dy + "px"; floatPanel.style.right = "auto"; floatPanel.style.bottom = "auto"; }; document.onmouseup = function() { isDragging = false; }; floatPanel.querySelector("#close").onclick = () => floatPanel.style.display = "none"; floatPanel.querySelector("#min").onclick = () => { body.style.display = body.style.display === "none" ? "block" : "none"; }; floatPanel.querySelector("#ref").onclick = updateFloatPanel; setInterval(updateFloatPanel, 3000); } window.addEventListener("message", (ev) => { const d = ev.data; if (d.type === "imageDataExtracted") { d.data.forEach(registerImageData); scanAndInject(); } else if (d.type === "videoDataExtracted") { scanAndInject(); } else if (d.type === "videoDownloadResult") { const r = d.data; const btn = videoButtonMap.get(r.messageId); if (!btn) return; if (r.success && r.videoUrl) { downloadFile(r.videoUrl, `doubao_video_${Date.now()}.mp4`); btn.innerHTML = "✓ 已开始下载"; btn.classList.add("doubao-dl-success"); } else { btn.innerHTML = "失败重试"; btn.classList.add("doubao-dl-error"); } btn.disabled = false; setTimeout(() => { btn.innerHTML = DOWNLOAD_ICON + " 下载无水印视频"; btn.classList.remove("doubao-dl-success", "doubao-dl-error"); }, 2000); videoButtonMap.delete(r.messageId); } }); window.addEventListener("load", function() { startDOMObserver(); createFloatPanel(); window.postMessage({ type: "scanInitialVideos" }, "*"); }); })();