// ==UserScript==
// @name 小红书无水印下载【路径可设置·UI美化版】
// @namespace http://tampermonkey.net/
// @version 2026.5.5
// @description 支持面板设置下载路径;美化UI;无水印视频/封面/原图一键下载;标题实时监控自动同步;小于20K图片自动过滤
// @author 修复版
// @match *://*.xiaohongshu.com/*
// @match https://www.xiaohongshu.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @connect xhscdn.com
// @connect xiaohongshu.com
// @connect sns-video-bd.xhscdn.com
// @connect sns-webpic-qc.xhscdn.com
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
panelLeft: '20px',
panelTop: '120px',
panelWidth: '340px',
downloadDelayMs: 500,
clipboardPollIntervalMs: 800,
loadWaitMs: 1500,
qualityPriority: ['4K', '2K', '1080P', '720P', '480P', '360P', '流畅'],
retryTimes: 3,
debugMode: true,
minImageSizeKB: 20,
titleChangeDebounceMs: 300,
titleForceOverride: true
};
let rootDirHandle = null;
let currentFolderName = '';
let isDownloading = false;
let lastProcessedClipboard = '';
let panel = null;
let skuInput = null;
let productInput = null;
let statusDiv = null;
let pathDiv = null;
let choosePathBtn = null;
let lastPageTitle = '';
let titleObserver = null;
let clipboardTimer = null;
let routeObserver = null;
function log(...args) {
if (CONFIG.debugMode) console.log('【小红书下载器】', ...args);
}
function errorLog(...args) {
console.error('【小红书下载器-错误】', ...args);
}
function generateRandomSuffix() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function sanitizeFileName(name) {
return String(name || '')
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, ' ')
.trim();
}
function getPageTitle() {
let title = document.title;
if (!title) return '';
title = title.replace(/\s*[-–]\s*小红书\s*$/i, '');
title = title.replace(/\s*[-–]\s*xiaohongshu\s*$/i, '');
return title.trim();
}
function setStatus(text, type = 'normal') {
if (!statusDiv) return;
statusDiv.textContent = text;
statusDiv.className = `status-text status-${type}`;
log('状态:', text);
}
function updateStatus(text) {
setStatus(text, 'normal');
}
function updatePathDisplay(text, type = 'normal') {
if (!pathDiv) return;
pathDiv.textContent = text || '未设置下载路径';
pathDiv.className = `path-display path-${type}`;
}
function autoFillProductFromTitle() {
if (productInput && !productInput.value.trim()) {
const pageTitle = getPageTitle();
if (pageTitle) {
productInput.value = pageTitle;
lastPageTitle = pageTitle;
log('自动填充页面标题:', pageTitle);
setStatus(`📄 自动使用标题: ${pageTitle.substring(0, 40)}${pageTitle.length > 40 ? '…' : ''}`, 'success');
}
}
}
function cleanMediaUrl(url, type = 'image') {
if (!url) return '';
let cleanUrl = url
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/\\u002F/g, '/')
.replace(/\\\//g, '/')
.replace(/^\/\//, 'https://')
.trim();
if (type === 'video') {
cleanUrl = cleanUrl.replace(/!.*$/, '');
if (cleanUrl.startsWith('sns-video')) cleanUrl = 'https://' + cleanUrl;
} else {
cleanUrl = cleanUrl
.replace(/!(webp|jpe?g|png|avif)\?.*$/, '')
.replace(/!q\d+\.(jpg|webp|png)$/, '!q100.jpg')
.replace(/\/s\d+x\d+\//, '/')
.replace(/\/format\/webp|\/format\/avif/, '')
.replace(/\/image_process\/.*$/, '');
if (cleanUrl.startsWith('sns-webpic')) cleanUrl = 'https://' + cleanUrl;
}
return cleanUrl;
}
function handleTitleChange() {
const currentTitle = getPageTitle();
if (!currentTitle || currentTitle === lastPageTitle || !productInput) return;
lastPageTitle = currentTitle;
if (CONFIG.titleForceOverride || !productInput.value.trim()) {
productInput.value = currentTitle;
log('检测到标题变化,已自动更新商品名:', currentTitle);
setStatus(`📄 标题已更新: ${currentTitle.substring(0, 40)}${currentTitle.length > 40 ? '…' : ''}`, 'success');
} else {
log('检测到标题变化,但输入框已有内容,未覆盖');
}
}
function startTitleMonitor() {
stopTitleMonitor();
lastPageTitle = getPageTitle();
const titleElement = document.querySelector('head title');
if (!titleElement) {
log('未找到 title 元素,降级为轮询监控');
titleObserver = setInterval(handleTitleChange, CONFIG.titleChangeDebounceMs);
return;
}
titleObserver = new MutationObserver(() => {
clearTimeout(window.titleChangeTimeout);
window.titleChangeTimeout = setTimeout(handleTitleChange, CONFIG.titleChangeDebounceMs);
});
titleObserver.observe(titleElement, {
childList: true,
subtree: true,
characterData: true,
attributes: true
});
log('页面标题实时监控已启动');
}
function stopTitleMonitor() {
if (titleObserver) {
if (titleObserver instanceof MutationObserver) {
titleObserver.disconnect();
} else {
clearInterval(titleObserver);
}
titleObserver = null;
}
clearTimeout(window.titleChangeTimeout);
log('页面标题监控已停止');
}
function getNoteData() {
let noteData = null;
const dataSources = [
() => window.__INITIAL_STATE__?.note?.note,
() => window.__INITIAL_STATE__?.feed?.note,
() => window.__INITIAL_STATE__?.discovery?.item?.note,
() => window.__REDUX_STATE__?.note?.currentNote,
() => window._SSR_DATA?.data?.noteDetail?.note,
() => window.__INITIAL_STATE__?.search?.result?.notes?.[0],
() => window.__INITIAL_STATE__?.user?.notes?.[0],
() => {
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
if (script.innerHTML.includes('window.__INITIAL_STATE__')) {
try {
const match = script.innerHTML.match(/window\.__INITIAL_STATE__\s*=\s*(\{.*?\});?\s*<\/script>/s);
if (match) {
const state = JSON.parse(match[1]);
return state?.note?.note || state?.feed?.note;
}
} catch (e) {}
}
}
return null;
}
];
for (const getSource of dataSources) {
try {
const data = getSource();
if (data && (data.video || data.imageList)) {
noteData = data;
break;
}
} catch (e) {}
}
return noteData;
}
function getVideoMedia() {
let videoInfo = {
videoUrl: '',
coverUrl: '',
quality: ''
};
try {
const noteData = getNoteData();
if (noteData?.video) {
const video = noteData.video;
const media = video.media || video;
const streamList = media.stream?.h264 || media.videoStream?.h264 || [];
for (const quality of CONFIG.qualityPriority) {
const targetStream = streamList.find(item =>
(item.qualityName && item.qualityName.includes(quality)) ||
(item.resolutionName && item.resolutionName.includes(quality)) ||
item.videoQuality === quality
);
if (targetStream) {
videoInfo.videoUrl = cleanMediaUrl(
targetStream.masterUrl || targetStream.url || targetStream.playUrl,
'video'
);
videoInfo.quality = quality;
break;
}
}
if (!videoInfo.videoUrl && streamList.length) {
const firstStream = streamList[0];
videoInfo.videoUrl = cleanMediaUrl(
firstStream.masterUrl || firstStream.url || firstStream.playUrl,
'video'
);
videoInfo.quality = firstStream.qualityName || '默认';
}
if (!videoInfo.videoUrl && (media.originVideoKey || media.videoId)) {
const videoKey = media.originVideoKey || media.videoId;
videoInfo.videoUrl = cleanMediaUrl(`https://sns-video-bd.xhscdn.com/${videoKey}`, 'video');
videoInfo.quality = '原始无水印';
}
if (noteData.imageList?.length) {
const firstImage = noteData.imageList[0];
videoInfo.coverUrl = cleanMediaUrl(
firstImage.urlDefault || firstImage.url || firstImage.originUrl || firstImage.originalUrl,
'image'
);
}
}
} catch (err) {}
if (!videoInfo.videoUrl) {
try {
const playerInstances = [
window.player,
window.xgplayer,
window.videoPlayer,
document.querySelector('.xgplayer')?.player,
document.querySelector('#video-track')?.player
];
for (const player of playerInstances) {
if (player) {
const src = player.currentSrc || player.src || player.config?.url || player.playUrl;
if (src && src.includes('xhscdn.com')) {
videoInfo.videoUrl = cleanMediaUrl(src, 'video');
const poster = player.poster || player.config?.poster;
if (poster) videoInfo.coverUrl = cleanMediaUrl(poster, 'image');
break;
}
}
}
} catch (err) {}
}
if (!videoInfo.videoUrl) {
try {
const videoEls = document.querySelectorAll('video');
for (const video of videoEls) {
const src = video.src || video.currentSrc;
if (src && src.includes('xhscdn.com') && !src.includes('blob:')) {
videoInfo.videoUrl = cleanMediaUrl(src, 'video');
if (video.poster) {
videoInfo.coverUrl = cleanMediaUrl(video.poster, 'image');
}
break;
}
}
} catch (err) {}
}
if (!videoInfo.videoUrl) {
try {
const performanceEntries = performance.getEntriesByType('resource');
for (const entry of performanceEntries) {
const url = entry.name;
if (
url.includes('xhscdn.com') &&
(url.includes('.mp4') || url.includes('m3u8')) &&
!url.includes('watermark')
) {
videoInfo.videoUrl = cleanMediaUrl(url, 'video');
break;
}
}
} catch (err) {}
}
if (!videoInfo.coverUrl) {
try {
const posterSelectors = [
'.xgplayer-poster',
'.note-first-image',
'.video-cover',
'.note-content img:first-child',
'.swiper-slide img:first-child'
];
for (const selector of posterSelectors) {
const el = document.querySelector(selector);
if (el) {
const bgMatch = el.style.backgroundImage?.match(/url\(["']?(.*?)["']?\)/);
const src = bgMatch?.[1] || el.src || el.dataset.src;
if (src) {
videoInfo.coverUrl = cleanMediaUrl(src, 'image');
break;
}
}
}
} catch (err) {}
}
return videoInfo.videoUrl ? videoInfo : null;
}
function getImageList() {
const imageSet = new Set();
try {
const noteData = getNoteData();
if (noteData?.imageList?.length) {
noteData.imageList.forEach(img => {
const originUrl =
img.originUrl ||
img.originalUrl ||
img.urlDefault ||
img.url;
if (originUrl) {
const cleanUrl = cleanMediaUrl(originUrl, 'image');
if (cleanUrl) imageSet.add(cleanUrl);
}
});
}
} catch (err) {
errorLog('获取笔记图片列表失败', err);
}
return Array.from(imageSet);
}
function getFullMediaList() {
const mediaList = [];
const usedCoverUrls = new Set();
const videoInfo = getVideoMedia();
if (videoInfo) {
mediaList.push({
type: 'video',
url: videoInfo.videoUrl,
quality: videoInfo.quality,
coverUrl: videoInfo.coverUrl
});
if (videoInfo.coverUrl) {
usedCoverUrls.add(videoInfo.coverUrl);
}
} else {
const imageList = getImageList();
imageList.forEach(url => {
if (!usedCoverUrls.has(url)) {
mediaList.push({
type: 'image',
url: url
});
}
});
}
return mediaList;
}
async function fetchMediaWithRetry(url, retryLeft = CONFIG.retryTimes) {
try {
return await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
timeout: 300000,
headers: {
Referer: 'https://www.xiaohongshu.com/',
Origin: 'https://www.xiaohongshu.com',
'User-Agent': navigator.userAgent,
Accept: '*/*'
},
anonymous: true,
onload: res => {
if (res.status >= 200 && res.status < 300) {
resolve(res.response);
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
onerror: err => reject(err),
ontimeout: () => reject(new Error('下载超时'))
});
});
} catch (err) {
if (retryLeft > 0) {
await new Promise(r => setTimeout(r, 1500));
return fetchMediaWithRetry(url, retryLeft - 1);
}
throw err;
}
}
function getFileExtension(url, type) {
if (type === 'video') return 'mp4';
const match = url.match(/\.(jpg|jpeg|png|webp|avif|bmp)(?:[?#!]|$)/i);
return match ? match[1].toLowerCase() : 'jpg';
}
const DB_CONFIG = {
name: 'XHS_Downloader_DB',
version: 1,
store: 'root_directory'
};
function saveRootHandle(handle) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);
request.onupgradeneeded = e => {
const db = e.target.result;
if (!db.objectStoreNames.contains(DB_CONFIG.store)) {
db.createObjectStore(DB_CONFIG.store);
}
};
request.onsuccess = e => {
const db = e.target.result;
const tx = db.transaction(DB_CONFIG.store, 'readwrite');
const store = tx.objectStore(DB_CONFIG.store);
const putReq = store.put(handle, 'root');
putReq.onsuccess = () => {
db.close();
resolve();
};
putReq.onerror = () => {
db.close();
reject(putReq.error);
};
};
request.onerror = () => reject(request.error);
});
}
function loadRootHandle() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);
request.onupgradeneeded = e => {
const db = e.target.result;
if (!db.objectStoreNames.contains(DB_CONFIG.store)) {
db.createObjectStore(DB_CONFIG.store);
}
};
request.onsuccess = e => {
const db = e.target.result;
if (!db.objectStoreNames.contains(DB_CONFIG.store)) {
db.close();
resolve(null);
return;
}
const tx = db.transaction(DB_CONFIG.store, 'readonly');
const store = tx.objectStore(DB_CONFIG.store);
const getReq = store.get('root');
getReq.onsuccess = () => {
const handle = getReq.result;
db.close();
resolve(handle || null);
};
getReq.onerror = () => {
db.close();
reject(getReq.error);
};
};
request.onerror = () => reject(request.error);
});
}
async function chooseRootDirectory() {
if (!window.showDirectoryPicker) {
throw new Error('当前浏览器不支持设置下载路径,请使用 Chrome 或 Edge 最新版');
}
const handle = await window.showDirectoryPicker({
mode: 'readwrite',
startIn: 'downloads'
});
await saveRootHandle(handle);
rootDirHandle = handle;
currentFolderName = handle.name || '已选择目录';
updatePathDisplay(`📂 ${currentFolderName}`, 'success');
setStatus('✅ 下载路径设置成功', 'success');
return handle;
}
async function restoreRootDirectory() {
try {
const savedHandle = await loadRootHandle();
if (!savedHandle) {
updatePathDisplay('未设置下载路径', 'warning');
return null;
}
const permission = await savedHandle.queryPermission({
mode: 'readwrite'
});
rootDirHandle = savedHandle;
currentFolderName = savedHandle.name || '已保存目录';
if (permission === 'granted') {
updatePathDisplay(`📂 ${currentFolderName}`, 'success');
return savedHandle;
}
updatePathDisplay(`📂 ${currentFolderName},需重新授权`, 'warning');
return savedHandle;
} catch (err) {
errorLog('读取下载路径失败', err);
updatePathDisplay('下载路径读取失败,请重新设置', 'error');
return null;
}
}
async function getRootDirectory() {
if (rootDirHandle) {
const permission = await rootDirHandle.queryPermission({
mode: 'readwrite'
});
if (permission === 'granted') {
return rootDirHandle;
}
if (permission === 'prompt') {
const newPermission = await rootDirHandle.requestPermission({
mode: 'readwrite'
});
if (newPermission === 'granted') {
updatePathDisplay(`📂 ${rootDirHandle.name || '已授权目录'}`, 'success');
return rootDirHandle;
}
}
}
const savedHandle = await restoreRootDirectory();
if (savedHandle) {
const permission = await savedHandle.queryPermission({
mode: 'readwrite'
});
if (permission === 'granted') {
rootDirHandle = savedHandle;
return rootDirHandle;
}
if (permission === 'prompt') {
const newPermission = await savedHandle.requestPermission({
mode: 'readwrite'
});
if (newPermission === 'granted') {
rootDirHandle = savedHandle;
updatePathDisplay(`📂 ${savedHandle.name || '已授权目录'}`, 'success');
return rootDirHandle;
}
}
}
setStatus('⚠️ 请先设置下载路径', 'warning');
return await chooseRootDirectory();
}
async function downloadMediaToFolder(sku, productName, mediaList, onProgress) {
if (!productName) {
throw new Error('商品名不能为空');
}
if (!mediaList || mediaList.length === 0) {
throw new Error('未找到媒体文件');
}
const rootDir = await getRootDirectory();
const safeProductName = sanitizeFileName(productName);
const randomSuffix = generateRandomSuffix();
let folderName;
if (sku && sku.trim() !== '') {
const safeSku = sanitizeFileName(sku);
folderName = `${safeSku}----${safeProductName}${randomSuffix}`;
} else {
folderName = `${safeProductName}${randomSuffix}`;
}
let targetDir;
try {
targetDir = await rootDir.getDirectoryHandle(folderName, {
create: true
});
} catch (err) {
throw new Error(`创建文件夹失败: ${err.message}`);
}
try {
const titleFile = await targetDir.getFileHandle('skutitle.txt', {
create: true
});
const writable = await titleFile.createWritable();
await writable.write(productName);
await writable.close();
} catch (err) {
errorLog('创建说明文件失败', err);
}
let successCount = 0;
let failCount = 0;
let skipSmallCount = 0;
let index = 0;
for (const media of mediaList) {
index++;
if (onProgress) {
onProgress(index, mediaList.length, successCount, failCount);
}
try {
if (media.type === 'video') {
const videoBlob = await fetchMediaWithRetry(media.url);
const fileName = `${String(index).padStart(3, '0')}_video_${media.quality || 'default'}.mp4`;
const fileHandle = await targetDir.getFileHandle(fileName, {
create: true
});
const writable = await fileHandle.createWritable();
await writable.write(videoBlob);
await writable.close();
successCount++;
if (media.coverUrl) {
let coverBlob;
try {
coverBlob = await fetchMediaWithRetry(media.coverUrl);
} catch (err) {
failCount++;
continue;
}
const coverSizeKB = coverBlob.size / 1024;
if (coverSizeKB < CONFIG.minImageSizeKB) {
skipSmallCount++;
continue;
}
const ext = getFileExtension(media.coverUrl, 'image');
const coverFileName = `${String(index).padStart(3, '0')}_video_cover.${ext}`;
const coverHandle = await targetDir.getFileHandle(coverFileName, {
create: true
});
const coverWritable = await coverHandle.createWritable();
await coverWritable.write(coverBlob);
await coverWritable.close();
successCount++;
}
} else if (media.type === 'image') {
let imageBlob;
try {
imageBlob = await fetchMediaWithRetry(media.url);
} catch (err) {
failCount++;
continue;
}
const imgSizeKB = imageBlob.size / 1024;
if (imgSizeKB < CONFIG.minImageSizeKB) {
skipSmallCount++;
continue;
}
const ext = getFileExtension(media.url, 'image');
const randomStr = Math.random().toString(36).substring(2, 8);
const fileName = `${String(index).padStart(3, '0')}_${randomStr}.${ext}`;
const fileHandle = await targetDir.getFileHandle(fileName, {
create: true
});
const writable = await fileHandle.createWritable();
await writable.write(imageBlob);
await writable.close();
successCount++;
}
await new Promise(r => setTimeout(r, CONFIG.downloadDelayMs));
} catch (err) {
errorLog(`第 ${index} 个文件失败`, err);
failCount++;
}
}
return {
successCount,
failCount,
skipSmallCount,
folderName
};
}
async function startDownload(sku, productName) {
if (isDownloading) {
setStatus('⚠️ 正在下载中,请稍候...', 'warning');
return;
}
isDownloading = true;
setStatus('🔍 正在提取媒体文件...', 'normal');
await new Promise(r => setTimeout(r, CONFIG.loadWaitMs));
try {
const mediaList = getFullMediaList();
if (mediaList.length === 0) {
throw new Error('未找到视频或图片,请确认当前页面是笔记详情页,并等待加载完成');
}
const hasVideo = mediaList.some(m => m.type === 'video');
let effectiveSku = sku;
if (hasVideo) {
effectiveSku = '';
log('检测到视频笔记,忽略 SKU,仅用商品名作为文件夹名');
} else if (!effectiveSku || effectiveSku.trim() === '') {
throw new Error('纯图片笔记需要填写 SKU 编码');
}
setStatus(`📦 找到 ${mediaList.length} 个文件,准备下载...`, 'normal');
const result = await downloadMediaToFolder(
effectiveSku,
productName,
mediaList,
(cur, total, succ, fail) => {
setStatus(`⬇️ ${cur}/${total} | 成功:${succ} | 失败:${fail}`, 'normal');
}
);
let msg = `✅ 下载完成:成功 ${result.successCount} 个,失败 ${result.failCount} 个`;
if (result.skipSmallCount > 0) {
msg += `,过滤小图 ${result.skipSmallCount} 个`;
}
msg += ` | 文件夹:${result.folderName}`;
setStatus(msg, 'success');
} catch (err) {
errorLog('下载失败', err);
setStatus(`❌ 下载失败:${err.message}`, 'error');
} finally {
isDownloading = false;
}
}
function parseClipboardContent(text) {
if (!text) return null;
const trimmed = text.trim();
const match = trimmed.match(/^([^\n-]+?)----([^\n]+)$/);
if (match && match[1] && match[2]) {
return {
sku: match[1].trim(),
productName: match[2].trim()
};
}
return null;
}
async function readClipboard() {
try {
if (navigator.clipboard && navigator.clipboard.readText) {
return await navigator.clipboard.readText();
}
} catch (err) {}
return null;
}
function startClipboardMonitor() {
if (clipboardTimer) clearInterval(clipboardTimer);
clipboardTimer = setInterval(async () => {
if (isDownloading) return;
const text = await readClipboard();
if (!text || text === lastProcessedClipboard) return;
const parsed = parseClipboardContent(text);
if (parsed) {
lastProcessedClipboard = text;
if (skuInput) skuInput.value = parsed.sku;
if (productInput) productInput.value = parsed.productName;
setStatus(`📋 剪贴板触发:${parsed.sku} / ${parsed.productName}`, 'normal');
await startDownload(parsed.sku, parsed.productName);
}
}, CONFIG.clipboardPollIntervalMs);
}
function createPanel() {
if (panel) return;
GM_addStyle(`
#xhs-download-panel {
position: fixed;
left: ${CONFIG.panelLeft};
top: ${CONFIG.panelTop};
width: ${CONFIG.panelWidth};
z-index: 99999999;
padding: 0;
border-radius: 18px;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: rgba(255, 255, 255, 0.96);
box-shadow:
0 18px 50px rgba(15, 23, 42, 0.18),
0 2px 8px rgba(15, 23, 42, 0.08);
border: 1px solid rgba(255, 255, 255, 0.75);
backdrop-filter: blur(16px);
color: #111827;
}
#xhs-download-panel * {
box-sizing: border-box;
}
#xhs-download-panel .panel-top {
padding: 16px 16px 14px;
background:
radial-gradient(circle at 20% 20%, rgba(255, 36, 66, 0.18), transparent 32%),
linear-gradient(135deg, #ff2442 0%, #ff6b86 100%);
color: #fff;
}
#xhs-download-panel .panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
#xhs-download-panel .panel-title {
display: flex;
flex-direction: column;
gap: 2px;
}
#xhs-download-panel .panel-title-main {
font-size: 16px;
font-weight: 800;
letter-spacing: 0.2px;
}
#xhs-download-panel .panel-title-sub {
font-size: 12px;
opacity: 0.86;
}
#xhs-download-panel .close-btn {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
cursor: pointer;
font-size: 20px;
line-height: 1;
color: rgba(255,255,255,0.9);
background: rgba(255,255,255,0.16);
transition: all .2s ease;
}
#xhs-download-panel .close-btn:hover {
background: rgba(255,255,255,0.28);
transform: rotate(90deg);
}
#xhs-download-panel .panel-body {
padding: 16px;
}
#xhs-download-panel .input-group {
margin-bottom: 13px;
}
#xhs-download-panel .input-group label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
color: #374151;
font-size: 13px;
font-weight: 700;
}
#xhs-download-panel .input-hint {
color: #9ca3af;
font-size: 11px;
font-weight: 500;
}
#xhs-download-panel .input-group input {
width: 100%;
height: 38px;
padding: 8px 11px;
border: 1px solid #e5e7eb;
border-radius: 10px;
outline: none;
background: #f9fafb;
color: #111827;
font-size: 13px;
transition: all .2s ease;
}
#xhs-download-panel .input-group input:focus {
border-color: #ff2442;
background: #fff;
box-shadow: 0 0 0 4px rgba(255,36,66,0.10);
}
#xhs-download-panel .path-card {
margin: 4px 0 14px;
padding: 12px;
border-radius: 14px;
background: #f9fafb;
border: 1px solid #eef2f7;
}
#xhs-download-panel .path-card-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
font-weight: 800;
color: #374151;
}
#xhs-download-panel .path-display {
min-height: 32px;
display: flex;
align-items: center;
padding: 8px 10px;
margin-bottom: 10px;
border-radius: 10px;
font-size: 12px;
line-height: 1.35;
word-break: break-all;
background: #fff;
border: 1px dashed #d1d5db;
color: #6b7280;
}
#xhs-download-panel .path-success {
color: #047857;
border-color: #a7f3d0;
background: #ecfdf5;
}
#xhs-download-panel .path-warning {
color: #92400e;
border-color: #fde68a;
background: #fffbeb;
}
#xhs-download-panel .path-error {
color: #b91c1c;
border-color: #fecaca;
background: #fef2f2;
}
#xhs-download-panel .path-btn {
width: 100%;
height: 34px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 13px;
font-weight: 800;
color: #374151;
background: #e5e7eb;
transition: all .2s ease;
}
#xhs-download-panel .path-btn:hover {
background: #d1d5db;
transform: translateY(-1px);
}
#xhs-download-panel .main-btn {
width: 100%;
height: 42px;
margin-bottom: 9px;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 900;
color: #fff;
background: linear-gradient(135deg, #ff2442, #ff5d73);
box-shadow: 0 10px 20px rgba(255, 36, 66, 0.24);
transition: all .2s ease;
}
#xhs-download-panel .main-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 26px rgba(255, 36, 66, 0.28);
}
#xhs-download-panel .sub-btn-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
#xhs-download-panel .sub-btn {
height: 34px;
border: none;
border-radius: 10px;
cursor: pointer;
color: #4b5563;
background: #f3f4f6;
font-weight: 800;
font-size: 12px;
transition: all .2s ease;
}
#xhs-download-panel .sub-btn:hover {
background: #e5e7eb;
}
#xhs-download-panel .status-box {
padding: 10px 11px;
border-radius: 12px;
background: #f9fafb;
border: 1px solid #eef2f7;
}
#xhs-download-panel .status-label {
margin-bottom: 5px;
font-size: 12px;
font-weight: 800;
color: #6b7280;
}
#xhs-download-panel .status-text {
font-size: 12px;
line-height: 1.45;
word-break: break-all;
color: #4b5563;
}
#xhs-download-panel .status-success {
color: #047857;
}
#xhs-download-panel .status-warning {
color: #b45309;
}
#xhs-download-panel .status-error {
color: #dc2626;
}
@media (prefers-color-scheme: dark) {
#xhs-download-panel {
background: rgba(17, 24, 39, 0.94);
color: #f9fafb;
border-color: rgba(255,255,255,0.08);
}
#xhs-download-panel .panel-body,
#xhs-download-panel .path-card,
#xhs-download-panel .status-box {
background: rgba(31, 41, 55, 0.72);
border-color: rgba(255,255,255,0.08);
}
#xhs-download-panel .input-group label,
#xhs-download-panel .path-card-title {
color: #e5e7eb;
}
#xhs-download-panel .input-group input,
#xhs-download-panel .path-display {
background: rgba(17, 24, 39, 0.85);
border-color: rgba(255,255,255,0.1);
color: #f9fafb;
}
#xhs-download-panel .sub-btn,
#xhs-download-panel .path-btn {
background: rgba(75,85,99,0.7);
color: #f9fafb;
}
}
`);
panel = document.createElement('div');
panel.id = 'xhs-download-panel';
panel.innerHTML = `
<div class="panel-top">
<div class="panel-header">
<div class="panel-title">
<div class="panel-title-main">📥 小红书无水印下载</div>
<div class="panel-title-sub">路径可设置 · 视频 / 封面 / 原图</div>
</div>
<span class="close-btn" id="panel-close-btn">×</span>
</div>
</div>
<div class="panel-body">
<div class="input-group">
<label>
SKU 编码
<span class="input-hint">视频可留空</span>
</label>
<input type="text" id="xhs-sku-input" placeholder="例如:A001">
</div>
<div class="input-group">
<label>
商品名称
<span class="input-hint">自动读取标题</span>
</label>
<input type="text" id="xhs-product-input" placeholder="自动填充页面标题">
</div>
<div class="path-card">
<div class="path-card-title">
<span>下载路径</span>
</div>
<div class="path-display path-warning" id="xhs-path-display">未设置下载路径</div>
<button class="path-btn" id="xhs-choose-path-btn">📂 设置 / 修改下载路径</button>
</div>
<button class="main-btn" id="download-btn">🚀 开始下载</button>
<div class="sub-btn-row">
<button class="sub-btn" id="debug-btn">🔍 调试</button>
<button class="sub-btn" id="refill-title-btn">📄 读取标题</button>
</div>
<div class="status-box">
<div class="status-label">状态</div>
<div class="status-text" id="status-text">就绪</div>
</div>
</div>
`;
document.body.appendChild(panel);
skuInput = document.getElementById('xhs-sku-input');
productInput = document.getElementById('xhs-product-input');
statusDiv = document.getElementById('status-text');
pathDiv = document.getElementById('xhs-path-display');
choosePathBtn = document.getElementById('xhs-choose-path-btn');
const closeBtn = document.getElementById('panel-close-btn');
const downloadBtn = document.getElementById('download-btn');
const debugBtn = document.getElementById('debug-btn');
const refillTitleBtn = document.getElementById('refill-title-btn');
closeBtn.onclick = () => {
panel.style.display = 'none';
setStatus('面板已隐藏,可从油猴菜单重新显示', 'normal');
};
choosePathBtn.onclick = async () => {
try {
await chooseRootDirectory();
} catch (err) {
errorLog('设置路径失败', err);
setStatus(`❌ 设置路径失败:${err.message}`, 'error');
}
};
downloadBtn.onclick = async () => {
let sku = skuInput.value.trim();
let product = productInput.value.trim();
if (!product) {
const title = getPageTitle();
if (title) {
product = title;
productInput.value = title;
setStatus(`📄 自动填充标题:${title.substring(0, 40)}${title.length > 40 ? '…' : ''}`, 'success');
}
}
if (!product) {
setStatus('⚠️ 请填写商品名称', 'warning');
return;
}
await startDownload(sku, product);
};
debugBtn.onclick = () => {
setStatus('🔍 调试信息已输出到控制台', 'normal');
console.log('【调试信息】当前页面媒体列表', getFullMediaList());
console.log('【调试信息】当前页面笔记数据', getNoteData());
console.log('【调试信息】当前保存路径', rootDirHandle);
};
refillTitleBtn.onclick = () => {
const title = getPageTitle();
if (title) {
productInput.value = title;
lastPageTitle = title;
setStatus(`📄 已读取标题:${title.substring(0, 40)}${title.length > 40 ? '…' : ''}`, 'success');
} else {
setStatus('⚠️ 当前页面未读取到标题', 'warning');
}
};
skuInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
productInput.focus();
}
});
productInput.addEventListener('keydown', async e => {
if (e.key === 'Enter') {
let sku = skuInput.value.trim();
let product = productInput.value.trim();
if (!product) {
const title = getPageTitle();
if (title) {
product = title;
productInput.value = title;
}
}
if (product) {
await startDownload(sku, product);
}
}
});
autoFillProductFromTitle();
restoreRootDirectory();
GM_registerMenuCommand('显示下载面板', () => {
if (panel) panel.style.display = 'block';
setStatus('面板已显示', 'normal');
});
GM_registerMenuCommand('设置下载路径', async () => {
try {
await chooseRootDirectory();
} catch (err) {
setStatus(`❌ 设置路径失败:${err.message}`, 'error');
}
});
GM_registerMenuCommand('重新初始化', init);
}
function isSupportedPage() {
const url = window.location.href;
return (
/\/note\/[a-f0-9]{24}/i.test(url) ||
/\/discovery\/item\//.test(url) ||
/\/explore\/[a-f0-9]{24}/i.test(url) ||
/\/search_result/.test(url) ||
/\/user\/profile/.test(url) ||
/\/creator\/content\/note/.test(url)
);
}
function init() {
if (isSupportedPage()) {
createPanel();
startClipboardMonitor();
startTitleMonitor();
restoreRootDirectory().catch(err => {
errorLog('恢复下载路径失败', err);
});
} else {
if (panel) {
panel.remove();
}
panel = null;
if (clipboardTimer) {
clearInterval(clipboardTimer);
}
clipboardTimer = null;
stopTitleMonitor();
}
}
let lastUrl = location.href;
routeObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(init, 800);
}
});
routeObserver.observe(document.body, {
childList: true,
subtree: true
});
window.addEventListener('DOMContentLoaded', init);
window.addEventListener('load', init);
window.addEventListener('beforeunload', () => {
stopTitleMonitor();
if (clipboardTimer) {
clearInterval(clipboardTimer);
}
if (routeObserver) {
routeObserver.disconnect();
}
});
setTimeout(init, 500);
setTimeout(init, 2000);
})();
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。