小红书视频插件
// ==UserScript==
// @name 小红书无水印下载【2026.5标题实时监控修复版】
// @namespace http://tampermonkey.net/
// @version 2026.5.4
// @description 修复提取失效问题,适配小红书最新结构,新增标题实时监控自动同步;无水印视频/封面/原图一键下载;视频笔记自动以商品名作为文件夹名;小于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',
downloadDelayMs: 500,
clipboardPollIntervalMs: 800,
loadWaitMs: 1500,
qualityPriority: ['4K', '2K', '1080P', '720P', '480P', '360P', '流畅'],
retryTimes: 3,
debugMode: true,
minImageSizeKB: 20, // 小于该值的图片不下载
titleChangeDebounceMs: 300, // 标题变化防抖间隔
titleForceOverride: true, // 标题变化时是否强制覆盖输入框内容,false=仅空值填充,true=强制覆盖
};
// ========== 全局变量 ==========
let rootDirHandle = null;
let isDownloading = false;
let lastProcessedClipboard = '';
let panel = null;
let skuInput = null;
let productInput = null;
let statusDiv = 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 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 autoFillProductFromTitle() {
if (productInput && !productInput.value.trim()) {
const pageTitle = getPageTitle();
if (pageTitle) {
productInput.value = pageTitle;
lastPageTitle = pageTitle;
log('自动填充页面标题:', pageTitle);
updateStatus(`📄 自动使用标题: ${pageTitle.substring(0, 40)}${pageTitle.length > 40 ? '…' : ''}`);
}
}
}
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);
updateStatus(`📄 标题已更新,自动填充: ${currentTitle.substring(0, 40)}${currentTitle.length > 40 ? '…' : ''}`);
} else {
log('检测到标题变化,输入框已有内容,未执行覆盖', { 原标题: lastPageTitle, 新标题: currentTitle });
}
}
// 启动标题实时监控
function startTitleMonitor() {
// 先销毁已存在的监控,避免重复创建
stopTitleMonitor();
// 初始化标题缓存
lastPageTitle = getPageTitle();
const titleElement = document.querySelector('head title');
if (!titleElement) {
log('未找到title元素,降级为轮询监控');
// 降级方案:轮询检测标题变化
titleObserver = setInterval(handleTitleChange, CONFIG.titleChangeDebounceMs);
return;
}
// 创建MutationObserver监控title元素变化
titleObserver = new MutationObserver(() => {
// 防抖处理,避免频繁触发
clearTimeout(window.titleChangeTimeout);
window.titleChangeTimeout = setTimeout(handleTitleChange, CONFIG.titleChangeDebounceMs);
});
// 监控title元素的文本、子节点、属性全维度变化
titleObserver.observe(titleElement, {
childList: true,
subtree: true,
characterData: true,
attributes: true
});
log('页面标题实时监控已启动');
}
// 停止标题监控
function stopTitleMonitor() {
if (titleObserver) {
// 区分MutationObserver和轮询定时器的销毁
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();
// 🎯 只从接口数据里拿图片,不碰页面DOM里的杂图
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 {
// 🎯 图片笔记:只加 noteData 里的核心图片
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); }
else 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';
}
// ========== IndexedDB 存储 ==========
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 = () => 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 getRootDirectory() {
if (rootDirHandle) { const permission = await rootDirHandle.queryPermission({ mode: 'readwrite' }); if (permission === 'granted') return rootDirHandle; }
const savedHandle = await loadRootHandle();
if (savedHandle) {
const permission = await savedHandle.queryPermission({ mode: 'readwrite' });
if (permission === 'granted') { rootDirHandle = savedHandle; return rootDirHandle; }
else if (permission === 'prompt') { await savedHandle.requestPermission({ mode: 'readwrite' }); const newPermission = await savedHandle.queryPermission({ mode: 'readwrite' }); if (newPermission === 'granted') { rootDirHandle = savedHandle; return rootDirHandle; } }
}
if (!window.showDirectoryPicker) throw new Error('浏览器不支持,请使用Chrome/Edge最新版');
const newHandle = await window.showDirectoryPicker({ mode: 'readwrite', startIn: 'downloads' });
await saveRootHandle(newHandle); rootDirHandle = newHandle; updateStatus('✅ 根目录选择成功'); return rootDirHandle;
}
// ========== 批量下载逻辑 ==========
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 = productName.replace(/[\\/:*?"<>|]/g, '_').trim();
const randomSuffix = generateRandomSuffix();
// 文件夹命名:有SKU则加前缀,视频场景SKU为空,仅用商品名
let folderName;
if (sku && sku.trim() !== '') {
const safeSku = sku.replace(/[\\/:*?"<>|]/g, '_').trim();
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}`); }
// 创建 skutitle.txt
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, failCount = 0, skipSmallCount = 0, 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) { updateStatus('⚠️ 正在下载中...'); return; }
isDownloading = true;
updateStatus('🔍 提取媒体文件...');
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');
// 视频场景:忽略SKU,仅用商品名作为文件夹名;纯图片场景:校验SKU
let effectiveSku = sku;
if (hasVideo) {
effectiveSku = '';
log('检测到视频,忽略SKU,仅用商品名作为文件夹名');
} else if (!effectiveSku || effectiveSku.trim() === '') {
throw new Error('纯图片笔记需要填写SKU编码');
}
updateStatus(`📦 找到${mediaList.length}个文件 (${hasVideo ? '含视频' : '纯图片'}),准备下载...`);
const result = await downloadMediaToFolder(effectiveSku, productName, mediaList, (cur, total, succ, fail) => {
updateStatus(`⬇️ ${cur}/${total} | 成功:${succ} | 失败:${fail}`);
});
let msg = `✅ 完成!成功${result.successCount}个,失败${result.failCount}个`;
if (result.skipSmallCount > 0) msg += `,过滤小图片${result.skipSmallCount}个(<${CONFIG.minImageSizeKB}KB)`;
msg += ` | 文件夹: ${result.folderName}`;
updateStatus(msg);
setTimeout(() => { if (!isDownloading) updateStatus('就绪'); }, 8000);
} catch (err) {
errorLog('下载失败', err);
updateStatus(`❌ 失败: ${err.message}`);
} 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;
updateStatus(`📋 剪贴板触发: ${parsed.sku} / ${parsed.productName}`);
await startDownload(parsed.sku, parsed.productName);
}
}, CONFIG.clipboardPollIntervalMs);
}
// ========== UI面板 ==========
function updateStatus(text) { if (statusDiv) statusDiv.textContent = text; log('状态:', text); }
function createPanel() {
if (panel) return;
GM_addStyle(`
#xhs-download-panel {
position: fixed; left: ${CONFIG.panelLeft}; top: ${CONFIG.panelTop}; width: 280px; background: #fff;
border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); padding: 16px; z-index: 99999999;
font-family: system-ui, sans-serif; font-size: 14px; border: 1px solid #eee;
}
#xhs-download-panel .panel-header { display: flex; justify-content: space-between; margin-bottom: 16px; }
#xhs-download-panel .panel-title { font-weight: bold; color: #ff2442; display: flex; align-items: center; gap: 4px; }
#xhs-download-panel .close-btn { cursor: pointer; font-size: 20px; color: #999; }
#xhs-download-panel .close-btn:hover { color: #ff2442; }
#xhs-download-panel .input-group { margin-bottom: 12px; }
#xhs-download-panel .input-group label { display: block; margin-bottom: 4px; font-weight: 500; }
#xhs-download-panel .input-group input { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
#xhs-download-panel .input-group input:focus { border-color: #ff2442; outline: none; }
#xhs-download-panel .btn-group { display: flex; gap: 8px; margin-bottom: 8px; }
#xhs-download-panel .download-btn { flex: 1; background: #ff2442; color: #fff; border: none; padding: 10px; border-radius: 6px; font-weight: bold; cursor: pointer; }
#xhs-download-panel .download-btn:hover { background: #e01e37; }
#xhs-download-panel .debug-btn { width: 30%; background: #666; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
#xhs-download-panel .debug-btn:hover { background: #444; }
#xhs-download-panel .status-text { margin-top: 12px; font-size: 12px; color: #666; word-break: break-all; line-height: 1.4; }
`);
panel = document.createElement('div'); panel.id = 'xhs-download-panel';
panel.innerHTML = `
<div class="panel-header"><span class="panel-title">📥 小红书无水印下载</span><span class="close-btn" id="panel-close-btn">×</span></div>
<div class="input-group"><label>SKU编码</label><input type="text" id="xhs-sku-input" placeholder="视频笔记可留空"></div>
<div class="input-group"><label>商品名称</label><input type="text" id="xhs-product-input" placeholder="自动填充页面标题"></div>
<div class="btn-group"><button class="download-btn" id="download-btn">开始下载</button><button class="debug-btn" id="debug-btn">调试</button></div>
<div class="status-text" id="status-text">就绪</div>
`;
document.body.appendChild(panel);
skuInput = document.getElementById('xhs-sku-input');
productInput = document.getElementById('xhs-product-input');
statusDiv = document.getElementById('status-text');
const closeBtn = document.getElementById('panel-close-btn');
const downloadBtn = document.getElementById('download-btn');
const debugBtn = document.getElementById('debug-btn');
closeBtn.onclick = () => { panel.style.display = 'none'; updateStatus('面板已隐藏,可从油猴菜单重新显示'); };
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; updateStatus(`📄 自动填充标题: ${title.substring(0, 40)}...`); } }
if (!product) { updateStatus('⚠️ 请填写商品名称'); return; }
await startDownload(sku, product);
};
debugBtn.onclick = () => { updateStatus('🔍 调试信息已输出至控制台'); console.log('【调试信息】当前页面媒体列表', getFullMediaList()); console.log('【调试信息】当前页面笔记数据', getNoteData()); };
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();
GM_registerMenuCommand('显示下载面板', () => { if (panel) panel.style.display = 'block'; updateStatus('面板已显示'); });
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();
getRootDirectory().catch(e => log('目录权限预加载失败', e));
}
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);
})();
评论