// ==UserScript== // @name Perplexity to Local/Notion // @namespace http://tampermonkey.net/ // @version 1.1 // @description 兼容新版 Perplexity Library/History 列表,修复批量抓取找不到文章;保留本地保存与 Notion 写入修复 // @author sandleft // @match https://www.perplexity.ai/* // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_download // @connect api.notion.com // @downloadURL none // ==/UserScript== (function () { 'use strict'; let isProcessing = false; let autoRunTimer = null; const CONFIG = { get token() { return (GM_getValue('notion_token', '') || '').trim(); }, get dbId() { return (GM_getValue('db_id', '') || '').trim(); }, get count() { const v = parseInt(GM_getValue('save_count', '12')); return (isNaN(v) || v <= 0) ? 12 : v; }, get fileType() { const v = GM_getValue('file_type', 'md'); return (v === 'md' || v === 'txt') ? v : 'md'; }, get propTitle() { return (GM_getValue('prop_title', 'Title') || 'Title').trim(); }, get propUrl() { return (GM_getValue('prop_url', 'URL') || 'URL' ).trim(); }, get propTags() { return (GM_getValue('prop_tags', 'Tags') || 'Tags' ).trim(); }, get propTime() { return (GM_getValue('prop_time', 'Time') || 'Time' ).trim(); }, get autoRun() { return GM_getValue('auto_run', false); }, get autoRunDelay() { const v = parseInt(GM_getValue('auto_run_delay', '5')); return (isNaN(v) || v < 0) ? 5 : v; }, }; GM_addStyle(` #hiyori-panel { position: fixed; top: 5%; right: 20px; width: 290px; max-height: 88vh; overflow-y: auto; background: #fff0f5; border: 2px solid #ffb6c1; border-radius: 15px; z-index: 10000; padding: 15px; font-family: 'Microsoft YaHei', sans-serif; box-shadow: 0 4px 15px rgba(255,182,193,0.4); scrollbar-width: thin; scrollbar-color: #ffb6c1 #fff0f5; } .hiyori-title { color: #ff69b4; font-weight: bold; text-align: center; margin-bottom: 10px; font-size: 16px; } .hiyori-label { font-size: 12px; color: #ff69b4; font-weight: bold; margin-top: 5px; display: block; } .hiyori-input, .hiyori-select { width: 100%; padding: 6px; margin: 3px 0 8px 0; border: 1px solid #ffb6c1; border-radius: 5px; font-size: 12px; box-sizing: border-box; } .hiyori-btn { width: 100%; padding: 8px; margin-top: 5px; background: #ffb6c1; color: white; border: none; border-radius: 20px; cursor: pointer; font-weight: bold; transition: 0.3s; } .hiyori-btn:hover { background: #ff69b4; transform: scale(1.02); } .hiyori-float-btn { position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px; background: #ffb6c1; color: white; border-radius: 50%; border: 4px solid #fff; cursor: pointer; z-index: 9999; font-size: 30px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 10px rgba(0,0,0,0.2); } .hiyori-checkbox { margin-right: 10px; transform: scale(1.3); cursor: pointer; accent-color: #ff69b4; } .hiyori-library-item { position: relative !important; padding-left: 30px !important; } .hiyori-library-item > .hiyori-checkbox { position: absolute; left: 6px; top: 50%; transform: translateY(-50%) scale(1.15); z-index: 10001; margin: 0; } #hiyori-single-btn { position: fixed; bottom: 100px; right: 30px; width: 120px; z-index: 9998; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } #hiyori-panel details > summary { color: #ff69b4; font-size: 12px; cursor: pointer; font-weight: bold; } `); function escHtml(str) { return (str || '').replace(/&/g, '&').replace(/"/g, '"'); } function isLibraryPage() { return /^\/library(?:\/|$)/.test(window.location.pathname); } function isSearchPage() { return /^\/search(?:\/|$)/.test(window.location.pathname); } function normalizeSearchUrl(raw) { if (!raw) return ''; const value = String(raw).trim().replace(/&/g, '&'); const direct = normalizeSearchUrlCandidate(value); if (direct) return direct; const embedded = value.match( /https?:\/\/www\.perplexity\.ai\/search(?:\/[^\s"'<>]*)?(?:\?[^\s"'<>]*)?|(?:^|[\s"'=])((?:\/search(?:\/[^\s"'<>]*)?(?:\?[^\s"'<>]*)?))/ ); if (!embedded) return ''; return normalizeSearchUrlCandidate((embedded[1] || embedded[0]).trim()); } function normalizeSearchUrlCandidate(value) { if (!value) return ''; try { const url = new URL(value, window.location.origin); if (url.origin !== window.location.origin) return ''; if (!/^\/search(?:\/|$)/.test(url.pathname)) return ''; url.hash = ''; return url.href; } catch (e) { return ''; } } function getSearchUrlFromElement(el) { if (!el) return ''; const directAttrs = ['href', 'data-href', 'data-url', 'data-link', 'to']; for (const attr of directAttrs) { const url = normalizeSearchUrl(el.getAttribute && el.getAttribute(attr)); if (url) return url; } const linked = el.querySelector && el.querySelector( 'a[href*="/search/"], [href*="/search/"], [data-href*="/search/"], [data-url*="/search/"], [data-link*="/search/"]' ); if (linked) { for (const attr of directAttrs) { const url = normalizeSearchUrl(linked.getAttribute && linked.getAttribute(attr)); if (url) return url; } } if (el.outerHTML && el.outerHTML.length < 6000) { const url = normalizeSearchUrl(el.outerHTML); if (url) return url; } return ''; } function getLibraryItemElement(sourceEl) { const root = document.querySelector('main') || document.body; let el = sourceEl; while (el && el !== root && el !== document.body) { const tag = (el.tagName || '').toLowerCase(); const role = el.getAttribute && el.getAttribute('role'); const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; const text = (el.innerText || '').trim(); const looksLikeRow = tag === 'li' || tag === 'article' || role === 'listitem' || role === 'link' || role === 'button' || (rect && rect.width >= 260 && rect.height >= 24 && rect.height <= 180 && text.length > 0); if (looksLikeRow && getSearchUrlFromElement(el)) return el; el = el.parentElement; } return sourceEl.parentElement || sourceEl; } function getDirectCheckbox(item) { return Array.from(item.children || []).find(child => child.classList && child.classList.contains('hiyori-checkbox')); } function collectLibraryEntries() { const root = document.querySelector('main') || document.body; const candidates = Array.from(root.querySelectorAll( [ 'a[href]', '[role="link"]', '[role="button"]', '[data-testid*="thread"]', '[data-testid*="search"]', '[data-testid*="library"]', '[href*="/search/"]', '[data-href*="/search/"]', '[data-url*="/search/"]', '[data-link*="/search/"]' ].join(', ') )); const entries = []; const seen = new Set(); candidates.forEach(candidate => { const url = getSearchUrlFromElement(candidate); if (!url || seen.has(url)) return; const item = getLibraryItemElement(candidate); if (!item || item === document.body || item === root) return; seen.add(url); entries.push({ url, originalIndex: entries.length, item, }); }); return entries; } async function waitForLibraryEntries(timeout = 15000) { const deadline = Date.now() + timeout; let entries = collectLibraryEntries(); while (entries.length === 0 && Date.now() < deadline) { await sleep(350); entries = collectLibraryEntries(); } return entries; } function scheduleLibraryAutoRun() { if (!isLibraryPage() || !CONFIG.autoRun || GM_getValue('hiyori_running', false)) return; if (autoRunTimer) return; const delay = CONFIG.autoRunDelay; console.log(`[妃爱] 已开启自动抓取,${delay} 秒后启动...`); autoRunTimer = setTimeout(() => { autoRunTimer = null; if (!isLibraryPage() || GM_getValue('hiyori_running', false)) return; console.log('[妃爱] 自动抓取已启动!'); startBatch(false); }, delay * 1000); } function clearLibraryAutoRun() { if (!autoRunTimer) return; clearTimeout(autoRunTimer); autoRunTimer = null; } function initUI() { if (document.getElementById('hiyori-panel')) return; const panel = document.createElement('div'); panel.id = 'hiyori-panel'; panel.style.display = GM_getValue('panel_show', 'none'); panel.innerHTML = `
🌸 P2L/N 🌸
▸ Notion 字段名设置(默认通常不用改)
秒后启动

`; document.body.appendChild(panel); const floatBtn = document.createElement('div'); floatBtn.className = 'hiyori-float-btn'; floatBtn.innerHTML = '🌸'; floatBtn.onclick = () => { const show = panel.style.display === 'none' ? 'block' : 'none'; panel.style.display = show; GM_setValue('panel_show', show); }; document.body.appendChild(floatBtn); document.getElementById('h-save-config').onclick = () => { const cnt = parseInt(document.getElementById('h-count').value); const delay = parseInt(document.getElementById('h-auto-delay').value); GM_setValue('notion_token', document.getElementById('h-token').value.trim()); GM_setValue('db_id', document.getElementById('h-dbid').value.trim()); GM_setValue('prop_title', document.getElementById('h-prop-title').value.trim() || 'Title'); GM_setValue('prop_url', document.getElementById('h-prop-url').value.trim() || 'URL'); GM_setValue('prop_tags', document.getElementById('h-prop-tags').value.trim() || 'Tags'); GM_setValue('prop_time', document.getElementById('h-prop-time').value.trim() || 'Time'); GM_setValue('save_count', String(isNaN(cnt) || cnt <= 0 ? 12 : cnt)); GM_setValue('file_type', document.getElementById('h-filetype').value); GM_setValue('auto_run', document.getElementById('h-auto-run').checked); GM_setValue('auto_run_delay', String(isNaN(delay) || delay < 0 ? 5 : delay)); alert('宝宝!设置已经牢牢记住辣!'); clearLibraryAutoRun(); scheduleLibraryAutoRun(); }; document.getElementById('h-batch-auto').onclick = () => startBatch(false); document.getElementById('h-batch-selected').onclick = () => startBatch(true); document.getElementById('h-stop').onclick = () => stopBatch(true); if (GM_getValue('hiyori_running', false)) { document.getElementById('h-stop').style.display = 'block'; } injectSingleSaveBtn(); injectCheckboxes(); scheduleLibraryAutoRun(); } function injectSingleSaveBtn() { if (!isSearchPage()) return; if (document.getElementById('hiyori-single-btn')) return; const btn = document.createElement('button'); btn.id = 'hiyori-single-btn'; btn.className = 'hiyori-btn'; btn.innerHTML = '🌸 保存当前页'; btn.onclick = () => processCurrentPage(true) .catch(e => console.error('[妃爱] 单篇保存出错:', e)); document.body.appendChild(btn); } let lastHref = location.href; new MutationObserver(() => { if (location.href === lastHref) return; lastHref = location.href; const old = document.getElementById('hiyori-single-btn'); if (old) old.remove(); injectSingleSaveBtn(); if (isLibraryPage()) { injectCheckboxes(); scheduleLibraryAutoRun(); } else { clearLibraryAutoRun(); } if (isSearchPage() && GM_getValue('hiyori_running', false)) { processCurrentPage().catch(err => { console.error('[妃爱] 自动处理入口出错:', err); if (GM_getValue('hiyori_running', false)) processNextInQueue(); }); } }).observe(document.body, { childList: true, subtree: true }); function injectCheckboxes() { if (!isLibraryPage()) return; collectLibraryEntries().forEach(entry => { const item = entry.item; if (!item || getDirectCheckbox(item)) return; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'hiyori-checkbox'; cb.dataset.hiyoriUrl = entry.url; ['pointerdown', 'mousedown', 'mouseup'].forEach(type => { cb.addEventListener(type, e => e.stopPropagation()); }); cb.addEventListener('click', e => { const checked = cb.checked; e.preventDefault(); e.stopPropagation(); setTimeout(() => { cb.checked = checked; }, 0); }); item.classList.add('hiyori-library-item'); item.insertBefore(cb, item.firstChild); }); } function stopBatch(showAlert = false) { GM_setValue('hiyori_running', false); GM_setValue('hiyori_queue', []); if (showAlert) { const failures = GM_getValue('hiyori_failures', []); if (Array.isArray(failures) && failures.length > 0) { alert(`🛑 已停止!以下文章写入 Notion 失败(本地文件已保存):\n${failures.join('\n')}`); } else { alert('🛑 已经乖乖停下啦!'); } } GM_setValue('hiyori_failures', []); const btn = document.getElementById('h-stop'); if (btn) btn.style.display = 'none'; } async function startBatch(useSelected) { if (!isLibraryPage()) { alert('宝宝,请先打开 Perplexity 的 Library/历史页面再批量抓取哦!'); return; } if (GM_getValue('hiyori_running', false)) { if (!confirm('检测到已有批量任务运行中,是否强制覆盖?')) return; } const entries = await waitForLibraryEntries(); injectCheckboxes(); const items = []; const seen = new Set(); if (useSelected) { collectLibraryEntries().forEach(entry => { const cb = getDirectCheckbox(entry.item); const url = cb && cb.dataset.hiyoriUrl ? cb.dataset.hiyoriUrl : entry.url; if (!cb || !cb.checked || !url || seen.has(url)) return; seen.add(url); items.push({ url, originalIndex: entry.originalIndex }); }); } else { entries.slice(0, CONFIG.count).forEach(entry => { if (!entry.url || seen.has(entry.url)) return; seen.add(entry.url); items.push({ url: entry.url, originalIndex: entry.originalIndex }); }); } if (items.length === 0) { alert('宝宝,没有找到文章呀,请确认 Library/历史页面已经加载完毕,或者已勾选需要保存的文章哦!'); return; } items.reverse(); GM_setValue('hiyori_queue', items); GM_setValue('hiyori_running', true); GM_setValue('hiyori_failures', []); const stopBtn = document.getElementById('h-stop'); if (stopBtn) stopBtn.style.display = 'block'; processNextInQueue(); } function processNextInQueue() { if (!GM_getValue('hiyori_running', false)) return; const queue = GM_getValue('hiyori_queue', []); if (!Array.isArray(queue)) { console.error('[妃爱] 队列数据损坏,已静默重置'); stopBatch(false); alert('⚠️ 队列数据异常,已自动重置,请重新开始。'); return; } if (queue.length > 0) { const next = queue.shift(); GM_setValue('hiyori_current_tag_index', next.originalIndex); GM_setValue('hiyori_queue', queue); if (next.url === window.location.href) { window.location.reload(); } else { window.location.href = next.url; } } else { GM_setValue('hiyori_running', false); const failures = GM_getValue('hiyori_failures', []); GM_setValue('hiyori_failures', []); if (Array.isArray(failures) && failures.length > 0) { alert(`🌸 批量完成!但以下文章写入 Notion 失败(本地文件已保存):\n${failures.join('\n')}`); } else { alert('🌸 宝宝!所有的素材都已经完美归档啦!'); } window.location.href = 'https://www.perplexity.ai/library'; } } async function waitForStableContent(timeout = 15000, stableWindow = 1500) { const deadline = Date.now() + timeout; let lastLen = -1; let stableSince = 0; while (Date.now() < deadline) { const main = document.querySelector('main') || document.body; const len = (main.innerText || '').length; if (len > 200) { if (len === lastLen) { if (stableSince === 0) stableSince = Date.now(); if (Date.now() - stableSince >= stableWindow) return main; } else { stableSince = 0; lastLen = len; } } await sleep(400); } return document.querySelector('main') || document.body; } async function waitForTitle(timeout = 8000) { const generic = new Set([ '', 'Perplexity', 'Perplexity AI', 'New Thread', 'Ask anything', 'Untitled', '新建对话', '新线程', '新しいスレッド', '新しい質問', '새 스레드', ]); const deadline = Date.now() + timeout; while (Date.now() < deadline) { const t = document.title.replace(/ - Perplexity/g, '').trim(); if (t && !generic.has(t)) return t; await sleep(300); } return document.title.replace(/ - Perplexity/g, '').trim() || '未命名对话'; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function chunkText(text, maxLen) { const chars = Array.from(text); const chunks = []; for (let i = 0; i < chars.length; i += maxLen) { chunks.push(chars.slice(i, i + maxLen).join('')); } return chunks; } function safeSubstring(str, maxCodePoints) { return Array.from(str).slice(0, maxCodePoints).join(''); } // - 挂载 到 DOM 后点击(修复 Firefox 不触发未挂载元素下载问题) // - safeSubstring 截断(防服务对代理对) function downloadLocal(title, content, ext) { const safe = safeSubstring( title.replace(/[/\\:*?"<>|]/g, '_'), 40 ); const mime = ext === 'md' ? 'text/markdown' : 'text/plain'; const blobUrl = URL.createObjectURL( new Blob([content], { type: mime + ';charset=utf-8' }) ); const a = document.createElement('a'); a.href = blobUrl; a.download = `${safe}.${ext}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 10000); } function recordFailure(title) { const f = GM_getValue('hiyori_failures', []); if (!Array.isArray(f)) return; f.push(title); GM_setValue('hiyori_failures', f); } async function processCurrentPage(isSingle = false) { if (isProcessing) { console.warn('[妃爱] 已在处理中,跳过重复调用'); return; } isProcessing = true; try { const main = await waitForStableContent(); if (!isSingle && !GM_getValue('hiyori_running', false)) return; const title = await waitForTitle(); if (!isSingle && !GM_getValue('hiyori_running', false)) return; const url = window.location.href; const uniqueSources = new Set(); main.querySelectorAll('a[href^="http"]').forEach(a => { if (a.href && !a.href.includes('perplexity.ai')) { uniqueSources.add(a.href); } }); const images = Array.from(main.querySelectorAll('img')).filter(img => { const src = img.currentSrc || img.dataset.src || img.src || ''; return src.startsWith('http') && (img.naturalWidth > 50 || !!img.dataset.src); }); const bodyText = (main.innerText || '').trim(); if (!bodyText) { console.warn('[妃爱] 页面内容为空,跳过:', title); if (isSingle) alert('宝宝,页面内容还没加载出来哦,请稍后再试!'); if (!isSingle) processNextInQueue(); return; } const sourcesSection = uniqueSources.size > 0 ? '\n\n---\n### 🌸 引用来源 (Sources)\n' + Array.from(uniqueSources).map((l, i) => `[${i + 1}] ${l}`).join('\n') : ''; const imagesSection = images.length > 0 ? '\n\n---\n### 🌸 提取的附图资源\n' + images.map(img => { const src = img.currentSrc || img.dataset.src || img.src; return CONFIG.fileType === 'md' ? `![图片](<${src}>)` : `[图片链接]: ${src}`; }).join('\n\n') : ''; const fullContent = bodyText + sourcesSection + imagesSection; let tag = '综合'; if (!isSingle) { const idx = GM_getValue('hiyori_current_tag_index', 0); tag = idx < 4 ? '经济' : idx < 10 ? '政治军事' : '综合'; } downloadLocal(title, fullContent, CONFIG.fileType); if (CONFIG.token && CONFIG.dbId) { await saveToNotion(title, url, fullContent, tag, isSingle); } else { if (isSingle) { alert('宝宝,只帮您保存了本地文件哦!Notion 配置未填写。'); } else { processNextInQueue(); } } } catch (err) { console.error('[妃爱] processCurrentPage 出错:', err); if (!isSingle && GM_getValue('hiyori_running', false)) { processNextInQueue(); } } finally { isProcessing = false; } } function saveToNotion(title, url, content, tag, isSingle, retry = 0) { return new Promise(resolve => { const allChars = Array.from(content); const isTruncated = allChars.length > 99 * 1900; // block 顶层需要 type: 'paragraph' const blocks = chunkText(content, 1900).slice(0, 99).map(chunk => ({ object: 'block', type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content: chunk } }] } })); const now = new Date(); const localDate = [ now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0') ].join('-'); const props = {}; props[CONFIG.propTitle] = { title: [{ type: 'text', text: { content: safeSubstring(title, 100) } }] }; props[CONFIG.propUrl] = { url }; props[CONFIG.propTags] = { multi_select: [{ name: tag }] }; props[CONFIG.propTime] = { date: { start: localDate } }; GM_xmlhttpRequest({ method: 'POST', url: 'https://api.notion.com/v1/pages', timeout: 20000, headers: { 'Authorization': `Bearer ${CONFIG.token}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' }, data: JSON.stringify({ parent: { database_id: CONFIG.dbId }, properties: props, children: blocks }), onload(res) { if (res.status === 429 && retry < 3) { const wait = (retry + 1) * 8000; console.warn(`[妃爱] Notion 限流,${wait / 1000}s 后重试(第 ${retry + 1} 次)`); setTimeout( () => saveToNotion(title, url, content, tag, isSingle, retry + 1) .then(resolve), wait ); return; } if (res.status < 200 || res.status >= 300) { console.error('[妃爱] Notion 写入失败:', res.status, res.responseText); recordFailure(title); } else if (isTruncated) { console.warn('[妃爱] 内容超限,已截断至约 18.6 万字符:', title); } resolve(); if (!isSingle && GM_getValue('hiyori_running', false)) { processNextInQueue(); } }, onerror(err) { console.error('[妃爱] Notion 网络错误:', err); recordFailure(title); resolve(); if (!isSingle && GM_getValue('hiyori_running', false)) { processNextInQueue(); } }, ontimeout() { console.error('[妃爱] Notion 请求超时,已跳过:', title); recordFailure(title); resolve(); if (!isSingle && GM_getValue('hiyori_running', false)) { processNextInQueue(); } } }); }); } if (document.readyState === 'complete') { initUI(); } else { window.addEventListener('load', initUI); } setInterval(injectCheckboxes, 2000); if (isLibraryPage()) { scheduleLibraryAutoRun(); } if (isSearchPage() && GM_getValue('hiyori_running', false)) { processCurrentPage().catch(err => { console.error('[妃爱] 自动处理入口出错:', err); if (GM_getValue('hiyori_running', false)) processNextInQueue(); }); } })();