// ==UserScript== // @name PicWish 提示词提取器 // @namespace local.picwish.prompt.extractor // @version 0.3.0 // @description 从 PicWish listing image generator 页面提取可见提示词,并在新标签页中整理、复制和导出。 // @match https://picwish.cn/listing-image-generator* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; const BUTTON_ID = 'picwish-prompt-extractor-button'; function isVisible(element) { // ScriptCat may expose page DOM nodes from a different JS realm, so // `instanceof Element` is not reliable in every injection mode. if (!element || element.nodeType !== 1) return false; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } if (element.closest('[hidden], [aria-hidden="true"]')) return false; // Do not require a non-zero rectangle: prompts can be inside a scroll // container or a virtualized list while still being present in the DOM. return true; } function documentRoots() { const roots = []; const visited = new Set(); const visit = (root) => { if (!root || visited.has(root)) return; visited.add(root); roots.push(root); // Include open Shadow DOM trees when the site uses web components. if (root.querySelectorAll) { root.querySelectorAll('*').forEach((element) => { if (element.shadowRoot) visit(element.shadowRoot); }); // A same-origin iframe can contain the detail panel. Cross-origin // frames are ignored safely because their DOM is inaccessible. root.querySelectorAll('iframe, frame').forEach((frame) => { try { visit(frame.contentDocument); } catch (_) { /* ignore */ } }); } }; visit(document); return roots; } function cleanPrompt(value) { return String(value || '') .replace(/\r\n?/g, '\n') .split('\n') // Keep intentional indentation in nested bullet points; only remove // trailing whitespace introduced by the rendered DOM. .map((line) => line.replace(/[ \t]+$/g, '')) .join('\n') .replace(/\n{3,}/g, '\n\n') .replace(/^\n+|\n+$/g, ''); } function promptTextFrom(element) { if (!element) return ''; // A prompt card may contain controls or other nested markup. Prefer the // dedicated text node and keep the card itself as the de-duplication unit. const textNode = element.matches('.whitespace-pre-wrap') ? element : element.querySelector('.whitespace-pre-wrap'); if (!textNode) return ''; return cleanPrompt(textNode.innerText || textNode.textContent); } function extractPrompts() { const prompts = []; const seen = new Set(); const addPrompt = (prompt) => { if (!prompt || seen.has(prompt)) return; seen.add(prompt); prompts.push(prompt); }; // Current PicWish structure: one complete prompt per data-scene-prompt card. // Search all reachable document roots so iframe/Shadow DOM placement does // not make an otherwise valid prompt invisible to the script. documentRoots().forEach((root) => { root.querySelectorAll('[data-scene-prompt]').forEach((card) => { addPrompt(promptTextFrom(card)); }); }); // Keep compatibility with alternate/older wrappers if the current structure // is absent. Each candidate is reduced to its dedicated text node first. if (prompts.length === 0) { documentRoots().forEach((root) => { root.querySelectorAll('.scene-prompt, [data-testid*="prompt" i], [class*="prompt" i], .whitespace-pre-wrap') .forEach((element) => addPrompt(promptTextFrom(element))); }); } return prompts; } function scanDiagnostics() { let sceneCards = 0; let textNodes = 0; documentRoots().forEach((root) => { sceneCards += root.querySelectorAll('[data-scene-prompt]').length; textNodes += root.querySelectorAll('.whitespace-pre-wrap').length; }); return { sceneCards, textNodes }; } function safeJson(value) { return JSON.stringify(value) .replace(//g, '\\u003e') .replace(/&/g, '\\u0026') .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); } function openResults(prompts, resultWindow) { if (!resultWindow) { resultWindow = window.open('about:blank', '_blank'); if (!resultWindow) { window.alert('浏览器阻止了新标签页,请允许此网站打开弹出窗口后重试。'); return; } } const promptData = safeJson(prompts); resultWindow.document.open(); resultWindow.document.write(` PicWish 提示词

PicWish 提示词

`); resultWindow.document.close(); } function triggerDetailPanel() { const candidates = []; const selector = 'button, a, [role="button"], [tabindex], div, span'; document.querySelectorAll(selector).forEach((element) => { if (!isVisible(element) || element.id === BUTTON_ID) return; const text = cleanPrompt(element.innerText || element.textContent).replace(/\s+/g, ' '); if (!text || text.length > 32) return; let score = 0; if (/^详情页\/A\+$/.test(text)) score = 100; else if (/^(查看详情|查看提示词)$/.test(text)) score = 90; else if (/^详情页$/.test(text)) score = 80; else if (/^查看$/.test(text)) score = 60; if (!score) return; const clickable = element.closest('button, a, [role="button"]') || element; if (clickable === document.body || clickable === document.documentElement) return; candidates.push({ clickable, score }); }); candidates.sort((a, b) => b.score - a.score); const target = candidates[0]?.clickable; if (!target) return false; target.click(); return true; } async function collectPromptsAfterOpening() { let prompts = extractPrompts(); if (prompts.length) return prompts; // The panel is not mounted until the user opens the generated detail view. // Try the visible detail trigger once, then wait for Vue to render it. triggerDetailPanel(); const deadline = Date.now() + 2500; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); prompts = extractPrompts(); if (prompts.length) return prompts; } return prompts; } function addFloatingButton() { // If an older copy of this userscript is still enabled, replace its // button so its old click handler cannot keep reporting stale failures. document.getElementById(BUTTON_ID)?.remove(); const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = '提取提示词'; Object.assign(button.style, { position: 'fixed', right: '20px', bottom: '20px', zIndex: '2147483647', border: '1px solid #2876c7', borderRadius: '999px', padding: '11px 16px', background: '#2876c7', color: '#fff', boxShadow: '0 5px 18px rgba(20, 69, 120, .28)', cursor: 'pointer', font: '600 14px "Segoe UI", "Microsoft YaHei", sans-serif' }); // PicWish closes and unmounts its detail panel from a document-level click // handler. Capture a synchronous snapshot before that handler can run. // The snapshot is also useful if Vue replaces the panel immediately after // the button is pressed. let pressSnapshot = []; let pressSnapshotAt = 0; const captureBeforePicWishClick = (event) => { if (event.button !== undefined && event.button !== 0) return; pressSnapshot = extractPrompts(); pressSnapshotAt = Date.now(); event.stopPropagation(); }; button.addEventListener('pointerdown', captureBeforePicWishClick, true); button.addEventListener('mousedown', captureBeforePicWishClick, true); button.addEventListener('touchstart', captureBeforePicWishClick, { capture: true, passive: true }); button.addEventListener('mouseenter', () => { button.style.background = '#1f64ac'; }); button.addEventListener('mouseleave', () => { button.style.background = '#2876c7'; }); button.addEventListener('click', async (event) => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); const resultWindow = window.open('about:blank', '_blank'); if (!resultWindow) { window.alert('浏览器阻止了新标签页,请允许此网站打开弹出窗口后重试。'); return; } resultWindow.document.write('正在读取 PicWish 提示词…

正在打开提示词详情,请稍候…

'); resultWindow.document.close(); button.disabled = true; const oldText = button.textContent; button.textContent = '正在读取…'; const cachedPrompts = pressSnapshotAt && Date.now() - pressSnapshotAt < 3000 ? pressSnapshot : []; const prompts = cachedPrompts.length ? cachedPrompts : await collectPromptsAfterOpening(); button.disabled = false; button.textContent = oldText; if (!prompts.length) { resultWindow.close(); const { sceneCards, textNodes } = scanDiagnostics(); console.warn('[PicWish 提示词提取器] 未提取到提示词', { sceneCards, textNodes, url: location.href }); window.alert(`未找到提示词。当前页面检测到 ${sceneCards} 个提示词容器、${textNodes} 个文本节点。请先在页面中打开“详情页/A+”或“查看详情”,再重试。`); return; } openResults(prompts, resultWindow); }); document.body.appendChild(button); } if (document.body) addFloatingButton(); else window.addEventListener('DOMContentLoaded', addFloatingButton, { once: true }); })();