// ==UserScript== // @name ChatGPT 图片生成优化提示词提取器 // @namespace https://github.com/kadevin/chatgpt-revised-prompt // @version 5.2.1 // @description 手动提取 ChatGPT 图片生成优化提示词,支持缩略图预览、多选批量下载 // @author iLab // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @run-at document-idle // @grant none // @require https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js // @license MIT // ==/UserScript== (function () { 'use strict'; const DEBUG = true; function log(...a) { if (DEBUG) console.log('%c[RP]', 'color:#10a37f;font-weight:bold', ...a); } function getConversationId() { const m = location.pathname.match(/\/c\/([a-f0-9-]+)/); return m ? m[1] : null; } let _cachedToken = null; let _tokenExpiry = 0; function getAccessToken() { // 先从 DOM 获取 try { const el = document.getElementById('client-bootstrap'); if (el) { const d = JSON.parse(el.textContent); const t = d?.accessToken || d?.session?.accessToken || null; if (t) { _cachedToken = t; _tokenExpiry = Date.now() + 8 * 60 * 1000; return t; } } } catch(e) {} // 返回缓存的 token(如果未过期) if (_cachedToken && Date.now() < _tokenExpiry) return _cachedToken; return null; } async function refreshAccessToken() { try { log('🔑 刷新 access token...'); const resp = await fetch('https://chatgpt.com/api/auth/session', { credentials: 'include' }); if (resp.ok) { const data = await resp.json(); const t = data?.accessToken; if (t) { _cachedToken = t; _tokenExpiry = Date.now() + 8 * 60 * 1000; // 缓存 8 分钟 log('🔑 Token 已刷新'); return t; } } } catch(e) { log('⚠️ 刷新 token 失败:', e.message); } return null; } // 全局状态:按轮次分组 let allRounds = []; // [{roundIndex, userText, prompts:[{prompt,source,imageUrls,selected,id}]}] const seenPrompts = new Set(); let lastFetchTime = 0; // 请求节流 let lastFetchConvId = ''; // 避免重复请求同一对话 const FETCH_COOLDOWN = 5000; // 最小请求间隔 5 秒 let isFetchingPrompts = false; let _userUploadedFileIds = new Set(); // 用户上传图片的 file ID 排除集 function injectStyles() { if (document.getElementById('rp-styles')) return; const s = document.createElement('style'); s.id = 'rp-styles'; s.textContent = ` #rp-fab{position:fixed;bottom:80px;right:20px;z-index:99998;width:44px;height:44px;border-radius:50%; border:none;cursor:pointer;display:flex;align-items:center;justify-content:center; background:linear-gradient(135deg,#10a37f,#1a7f64);color:#fff; box-shadow:0 4px 16px rgba(16,163,127,.35);transition:all .25s;user-select:none} #rp-fab:hover{transform:scale(1.08)} #rp-fab .rp-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px; border-radius:9px;background:#ef4444;color:#fff;font-size:10px;font-weight:700; display:flex;align-items:center;justify-content:center;padding:0 4px} #rp-panel{position:fixed;bottom:136px;right:20px;z-index:99997;width:400px;max-height:68vh; background:#fff;border-radius:14px;overflow:hidden;display:none;flex-direction:column; box-shadow:0 8px 40px rgba(0,0,0,.15),0 0 0 1px rgba(0,0,0,.05); animation:rp-up .25s ease;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif} #rp-panel.open{display:flex} html.dark #rp-panel{background:#1e1e2e;box-shadow:0 8px 40px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.06)} @keyframes rp-up{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} .rp-hdr{display:flex;align-items:center;gap:8px;padding:12px 14px;flex-shrink:0; border-bottom:1px solid rgba(0,0,0,.06);font-weight:600;font-size:13px;color:#202123} html.dark .rp-hdr{border-bottom-color:rgba(255,255,255,.06);color:#e5e5e5} .rp-hdr-right{margin-left:auto;display:flex;align-items:center;gap:6px} .rp-sel-all{font-size:11px;padding:3px 8px;border:1px solid rgba(16,163,127,.3);border-radius:5px; background:none;color:#10a37f;cursor:pointer;font-family:inherit} .rp-sel-all:hover{background:rgba(16,163,127,.07)} .rp-refresh{font-size:11px;padding:3px 8px;border:1px solid rgba(16,163,127,.3);border-radius:5px; background:#10a37f;color:#fff;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:4px} .rp-refresh:hover{background:#0d8a6b} .rp-refresh:disabled{background:#9ca3af;border-color:#9ca3af;cursor:not-allowed} .rp-count-badge{font-size:11px;font-weight:500;color:#6e6e80;background:rgba(0,0,0,.05); padding:2px 8px;border-radius:10px} html.dark .rp-count-badge{background:rgba(255,255,255,.08);color:#9ca3af} .rp-body{overflow-y:auto;flex:1;padding:6px} .rp-round-divider{display:flex;align-items:center;gap:8px;margin:10px 4px 6px;font-size:11px; font-weight:600;color:#9ca3af} .rp-round-divider::before{content:'';flex:1;height:1px;background:rgba(0,0,0,.08)} .rp-round-divider::after{content:'';flex:1;height:1px;background:rgba(0,0,0,.08)} html.dark .rp-round-divider::before,html.dark .rp-round-divider::after{background:rgba(255,255,255,.08)} .rp-round-dl{font-size:10px;padding:2px 7px;border:1px solid rgba(16,163,127,.3);border-radius:4px; background:none;color:#10a37f;cursor:pointer;font-family:inherit;white-space:nowrap; flex-shrink:0;display:inline-flex;align-items:center;gap:3px} .rp-round-dl:hover{background:rgba(16,163,127,.08)} html.dark .rp-round-dl{border-color:rgba(16,163,127,.4);color:#10a37f} .rp-card{border-radius:10px;margin-bottom:5px;overflow:hidden; border:1px solid rgba(0,0,0,.07);transition:border-color .18s,background .18s} .rp-card:hover{border-color:rgba(16,163,127,.25)} html.dark .rp-card{border-color:rgba(255,255,255,.07)} html.dark .rp-card:hover{border-color:rgba(16,163,127,.3)} .rp-card.selected{border-color:#10a37f;background:rgba(16,163,127,.04)} html.dark .rp-card.selected{background:rgba(16,163,127,.08)} .rp-card-hdr{display:flex;align-items:center;gap:7px;padding:8px 10px;cursor:pointer; user-select:none;font-size:11px;font-weight:500;color:#6e6e80;transition:background .13s} .rp-card-hdr:hover{background:rgba(0,0,0,.02)} html.dark .rp-card-hdr{color:#9ca3af} html.dark .rp-card-hdr:hover{background:rgba(255,255,255,.03)} .rp-cb{appearance:none;-webkit-appearance:none;width:16px;height:16px; border:1px solid rgba(0,0,0,.2);border-radius:4px;background:#fff; cursor:pointer;flex-shrink:0;margin:0;position:relative;transition:all .15s} html.dark .rp-cb{background:transparent;border-color:rgba(255,255,255,.3)} .rp-cb:checked{background:#10a37f;border-color:#10a37f} html.dark .rp-cb:checked{background:#10a37f;border-color:#10a37f} .rp-cb:checked::after{content:'';position:absolute;left:4.5px;top:1.5px;width:4px;height:8px; border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)} .rp-thumb-strip{display:flex;gap:3px;flex-shrink:0;position:relative} .rp-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px; border:1px solid rgba(0,0,0,.08);background:#f3f4f6;cursor:pointer} html.dark .rp-thumb{border-color:rgba(255,255,255,.1);background:#374151} .rp-thumb-ph{width:48px;height:48px;border-radius:6px;border:1px dashed rgba(0,0,0,.12); background:rgba(0,0,0,.03);display:flex;align-items:center;justify-content:center;flex-shrink:0} html.dark .rp-thumb-ph{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.03)} .rp-hover-preview{position:fixed;z-index:100000;pointer-events:none; max-width:320px;max-height:420px;width:auto;height:auto; border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.35); opacity:0;transition:opacity .18s} .rp-hover-preview.show{opacity:1} .rp-card-meta{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0} .rp-tag{padding:1px 5px;border-radius:4px;font-size:10px;font-weight:600;align-self:flex-start; background:rgba(0,0,0,.06);color:#9ca3af;line-height:1.4;font-family:'SF Mono',Menlo,monospace} html.dark .rp-tag{background:rgba(255,255,255,.08);color:#6b7280} .rp-preview{font-size:11px;color:#aaa;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} html.dark .rp-preview{color:#555} .rp-arrow{flex-shrink:0;transition:transform .18s;color:#10a37f;opacity:.7} .rp-card.open .rp-arrow{transform:rotate(90deg)} .rp-card-body{display:none;padding:0 10px 10px} .rp-card.open .rp-card-body{display:block} .rp-txt{background:rgba(0,0,0,.03);border-radius:7px;padding:9px 11px; font-family:'SF Mono',Menlo,Consolas,monospace;font-size:11px;line-height:1.6; white-space:pre-wrap;word-break:break-word;max-height:160px;overflow-y:auto;color:#374151} html.dark .rp-txt{background:rgba(255,255,255,.04);color:#d1d5db} .rp-card-acts{display:flex;gap:6px;margin-top:8px} .rp-copy-btn,.rp-dl-btn{display:inline-flex;align-items:center;justify-content:center;gap:5px; padding:7px 12px;border:none;border-radius:7px;font-size:12px;font-weight:500; cursor:pointer;font-family:inherit;transition:all .15s;flex:1} .rp-copy-btn{background:rgba(16,163,127,.1);color:#10a37f} .rp-copy-btn:hover{background:rgba(16,163,127,.2)} .rp-copy-btn.ok{background:#10a37f;color:#fff} .rp-dl-btn{background:rgba(59,130,246,.08);color:#3b82f6} .rp-dl-btn:hover{background:rgba(59,130,246,.15)} .rp-footer{padding:8px 10px;border-top:1px solid rgba(0,0,0,.06);display:flex;gap:6px;flex-shrink:0} html.dark .rp-footer{border-top-color:rgba(255,255,255,.06)} .rp-dl-sel-btn,.rp-dl-all-btn{display:inline-flex;align-items:center;justify-content:center; gap:5px;padding:7px 10px;border:none;border-radius:8px;font-size:12px;font-weight:500; cursor:pointer;font-family:inherit;transition:all .15s} .rp-dl-sel-btn{background:#10a37f;color:#fff;flex:1} .rp-dl-sel-btn:hover{background:#0d8a6b} .rp-dl-sel-btn:disabled{background:#9ca3af;cursor:not-allowed} html.dark .rp-dl-sel-btn:disabled{background:rgba(255,255,255,.15);color:rgba(255,255,255,.3)} .rp-dl-all-btn{background:rgba(16,163,127,.1);color:#10a37f;flex:1} .rp-dl-all-btn:hover{background:rgba(16,163,127,.2)} html.dark .rp-dl-all-btn{background:rgba(16,163,127,.15);color:#10a37f} html.dark .rp-dl-all-btn:hover{background:rgba(16,163,127,.25)} .rp-toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(16px); background:#10a37f;color:#fff;padding:7px 18px;border-radius:8px;font-size:13px; z-index:99999;opacity:0;transition:all .28s;pointer-events:none} .rp-toast.show{opacity:1;transform:translateX(-50%) translateY(0)} .rp-status{position:fixed;bottom:10px;right:10px;z-index:99999;background:rgba(0,0,0,.8); color:#0f0;font-size:11px;padding:3px 9px;border-radius:5px;font-family:monospace} `; document.head.appendChild(s); } const SVG = { brush: ``, arrow: ``, copy: ``, check: ``, refresh: ``, download: ``, img: ``, }; function toast(m) { let t = document.querySelector('.rp-toast'); if (!t) { t = document.createElement('div'); t.className = 'rp-toast'; document.body.appendChild(t); } t.textContent = m; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 1800); } function showStatus(msg) { let el = document.querySelector('.rp-status'); if (!el) { el = document.createElement('div'); el.className = 'rp-status'; document.body.appendChild(el); } el.textContent = '[RP] ' + msg; clearTimeout(el._t); el._t = setTimeout(() => el.remove(), 8000); } function escHtml(s) { return s.replace(/&/g,'&').replace(//g,'>'); } function createFab() { if (document.getElementById('rp-fab')) return; const fab = document.createElement('button'); fab.id = 'rp-fab'; fab.title = '手动提取图片优化提示词'; fab.innerHTML = SVG.brush; fab.style.display = getConversationId() ? 'flex' : 'none'; fab.onclick = async () => { const p = document.getElementById('rp-panel'); const wasOpen = p?.classList.contains('open'); if (p) p.classList.toggle('open'); if (wasOpen) return; await manualFetchPrompts(); }; document.body.appendChild(fab); } function createPanel() { if (document.getElementById('rp-panel')) return; const panel = document.createElement('div'); panel.id = 'rp-panel'; panel.innerHTML = `
${SVG.brush.replace('width="20" height="20"','width="16" height="16"')} 优化提示词
0
`; document.body.appendChild(panel); document.getElementById('rp-refresh').onclick = manualFetchPrompts; document.getElementById('rp-sel-all').onclick = toggleSelectAll; document.getElementById('rp-dl-sel').onclick = downloadSelected; document.getElementById('rp-dl-all').onclick = downloadAll; } function updateFab() { const fab = document.getElementById('rp-fab'); if (!fab) return; const total = allRounds.reduce((s,r) => s + r.prompts.length, 0); fab.style.display = getConversationId() ? 'flex' : 'none'; const old = fab.querySelector('.rp-badge'); if (old) old.remove(); if (total > 0) { const b = document.createElement('span'); b.className = 'rp-badge'; b.textContent = total; fab.appendChild(b); } const ce = document.getElementById('rp-count'); if (ce) ce.textContent = total + ' 条'; } function updateFooter() { const allP = allRounds.flatMap(r => r.prompts); const selCount = allP.filter(p => p.selected).length; const btn = document.getElementById('rp-dl-sel'); if (btn) { btn.disabled = selCount === 0; btn.innerHTML = `${SVG.download} 下载选中 (${selCount})`; } const allBtn = document.getElementById('rp-dl-all'); if (allBtn) allBtn.innerHTML = `${SVG.download} 全部下载 (${allP.length})`; } function toggleSelectAll() { const allP = allRounds.flatMap(r => r.prompts); const allSelected = allP.every(p => p.selected); allP.forEach(p => p.selected = !allSelected); document.querySelectorAll('.rp-cb').forEach(cb => cb.checked = !allSelected); document.querySelectorAll('.rp-card').forEach(card => card.classList.toggle('selected', !allSelected)); document.getElementById('rp-sel-all').textContent = allSelected ? '全选' : '取消全选'; updateFooter(); } async function downloadImage(url, filename) { try { const r = await fetch(url, { credentials: 'include' }); const blob = await r.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); } catch(e) { window.open(url, '_blank'); } } // ===== ZIP 打包下载 ===== function getJSZip() { // 通过 @require 加载,直接使用全局 JSZip if (typeof JSZip !== 'undefined') return JSZip; if (window.JSZip) return window.JSZip; return null; } async function downloadAsZip(urls, zipName) { if (!urls.length) { toast('没有可下载的图片'); return; } try { toast(`正在打包 ${urls.length} 张图片...`); const ZipClass = getJSZip(); if (!ZipClass) throw new Error('JSZip 未加载'); const zip = new ZipClass(); let done = 0; for (let i = 0; i < urls.length; i++) { try { const r = await fetch(urls[i], { credentials: 'include' }); const blob = await r.blob(); const ext = blob.type?.includes('png') ? 'png' : blob.type?.includes('webp') ? 'webp' : 'jpg'; zip.file(`image-${i+1}.${ext}`, blob); done++; } catch(e) { log('⚠️ 图片下载失败:', urls[i]); } } if (done === 0) { toast('所有图片下载失败'); return; } const content = await zip.generateAsync({ type: 'blob' }); const a = document.createElement('a'); a.href = URL.createObjectURL(content); a.download = zipName; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); toast(`已打包 ${done} 张图片`); } catch(e) { log('❌ ZIP 打包失败:', e.message); toast('打包失败,改为逐张下载...'); for (let i = 0; i < urls.length; i++) { await downloadImage(urls[i], `chatgpt-img-${i+1}.png`); await new Promise(r => setTimeout(r, 300)); } } } async function downloadSelected() { const selP = allRounds.flatMap(r => r.prompts).filter(p => p.selected); const urls = selP.flatMap(p => p.imageUrls); if (!urls.length) return; await downloadAsZip(urls, `chatgpt-selected-${urls.length}imgs.zip`); } async function downloadAll() { const urls = allRounds.flatMap(r => r.prompts).flatMap(p => p.imageUrls); if (!urls.length) { toast('没有可下载的图片'); return; } await downloadAsZip(urls, `chatgpt-all-${urls.length}imgs.zip`); } // 懒解析:在渲染前尝试从 DOM 匹配 fileIds 到图片 URL function resolveFileIds(excludeFileIds = _userUploadedFileIds) { const allP = allRounds.flatMap(r => r.prompts); for (const item of allP) { if (item.imageUrls.length > 0 || !item.fileIds || item.fileIds.length === 0) continue; for (const fid of item.fileIds) { // 跳过用户上传的图片 if (excludeFileIds.has(fid)) continue; const img = document.querySelector(`img[src*="${fid}"]`); if (img && img.src && !item.imageUrls.includes(img.src)) { item.imageUrls.push(img.src); } } } // 兜底:收集所有 DOM 图片按顺序分配 if (allP.some(p => p.imageUrls.length === 0 && p.fileIds?.length > 0)) { const domImgs = getAllDomImages(excludeFileIds); const usedUrls = new Set(allP.flatMap(p => p.imageUrls)); const unused = domImgs.filter(u => !usedUrls.has(u)); let idx = 0; for (const item of allP) { if (item.imageUrls.length === 0 && idx < unused.length) { item.imageUrls = [unused[idx++]]; } } } } function renderPanel() { resolveFileIds(); // 每次渲染前尝试解析图片 const body = document.getElementById('rp-body'); if (!body) return; body.innerHTML = ''; let globalIdx = 0; for (const round of allRounds) { if (!round.prompts.length) continue; const div = document.createElement('div'); div.className = 'rp-round-divider'; div.innerHTML = `第 ${round.roundIndex} 轮`; // 本轮下载按钮(紧跟标题) const roundImgs = round.prompts.flatMap(p => p.imageUrls); if (roundImgs.length > 0) { const btn = document.createElement('button'); btn.className = 'rp-round-dl'; btn.innerHTML = `${SVG.download} 下载 (${roundImgs.length})`; btn.onclick = async (e) => { e.stopPropagation(); await downloadAsZip(roundImgs, `chatgpt-round${round.roundIndex}-${roundImgs.length}imgs.zip`); }; div.appendChild(btn); } body.appendChild(div); for (const item of round.prompts) { globalIdx++; body.appendChild(buildCard(item, globalIdx)); } } updateFab(); updateFooter(); } function buildCard(item, index) { const card = document.createElement('div'); card.className = 'rp-card'; card.dataset.id = item.id; const preview = item.prompt.substring(0,55).replace(/\n/g,' ') + (item.prompt.length>55?'...':''); // Build thumbnail strip (show up to 2 thumbs in header) let thumbsHtml = ''; if (item.imageUrls.length > 0) { thumbsHtml = '
'; item.imageUrls.slice(0, 2).forEach(url => { thumbsHtml += ``; }); thumbsHtml += '
'; } else { thumbsHtml = `
${SVG.img}
`; } card.innerHTML = `
${thumbsHtml}
#${index || '?'} ${escHtml(preview)}
${SVG.arrow}
${escHtml(item.prompt)}
${item.imageUrls.length > 0 ? `` : ''}
`; const hdr = card.querySelector('.rp-card-hdr'); const cb = card.querySelector('.rp-cb'); cb.onclick = e => { e.stopPropagation(); item.selected = cb.checked; card.classList.toggle('selected', cb.checked); updateFooter(); }; // 缩略图悬浮预览 + 点击新标签打开 card.querySelectorAll('.rp-thumb').forEach(thumb => { const previewUrl = thumb.dataset.previewUrl; let previewEl = null; thumb.addEventListener('mouseenter', e => { e.stopPropagation(); if (!previewUrl) return; if (!previewEl) { previewEl = document.createElement('img'); previewEl.className = 'rp-hover-preview'; previewEl.src = previewUrl; document.body.appendChild(previewEl); } const rect = thumb.getBoundingClientRect(); previewEl.style.top = Math.max(8, rect.top - 96) + 'px'; previewEl.style.left = Math.max(8, rect.left - 252) + 'px'; requestAnimationFrame(() => previewEl.classList.add('show')); }); thumb.addEventListener('mouseleave', () => { if (previewEl) previewEl.classList.remove('show'); }); thumb.addEventListener('click', e => { e.stopPropagation(); if (previewUrl) window.open(previewUrl, '_blank'); }); }); hdr.onclick = e => { if (e.target === cb || e.target.classList?.contains('rp-thumb')) return; card.classList.toggle('open'); }; const copyBtn = card.querySelector('.rp-copy-btn'); copyBtn.onclick = e => { e.stopPropagation(); navigator.clipboard.writeText(item.prompt).catch(()=>{}); copyBtn.classList.add('ok'); copyBtn.innerHTML = SVG.check + ' 已复制'; toast('已复制到剪贴板'); setTimeout(() => { copyBtn.classList.remove('ok'); copyBtn.innerHTML = SVG.copy + ' 复制提示词'; }, 1500); }; const dlBtn = card.querySelector('.rp-dl-btn'); if (dlBtn) dlBtn.onclick = async e => { e.stopPropagation(); toast(`下载 ${item.imageUrls.length} 张图片...`); for (let i = 0; i < item.imageUrls.length; i++) { await downloadImage(item.imageUrls[i], `chatgpt-img-${i+1}.png`); await new Promise(r => setTimeout(r, 300)); } }; return card; } // ===== 对话路径排序 ===== // 深度优先遍历整棵对话树,按 create_time 排序,确保覆盖所有分支节点 function getOrderedPath(mapping) { const childrenOf = {}; for (const [id, node] of Object.entries(mapping)) { const pid = node.parent; if (pid) { if (!childrenOf[pid]) childrenOf[pid] = []; childrenOf[pid].push(id); } } // 对每个节点的 children 按 create_time 排序(旧 → 新) for (const pid of Object.keys(childrenOf)) { childrenOf[pid].sort((a, b) => { const ta = mapping[a]?.message?.create_time || 0; const tb = mapping[b]?.message?.create_time || 0; return ta - tb; }); } const root = Object.keys(mapping).find(id => !mapping[id].parent); if (!root) return Object.keys(mapping); // 深度优先遍历,收集所有节点 const path = []; const visited = new Set(); const stack = [root]; while (stack.length > 0) { const cur = stack.pop(); if (visited.has(cur)) continue; visited.add(cur); path.push(cur); // 逆序入栈,保证先遍历排序靠前(时间较早)的子节点 const ch = childrenOf[cur] || []; for (let i = ch.length - 1; i >= 0; i--) { stack.push(ch[i]); } } // 最终按 create_time 排序保证时间顺序 path.sort((a, b) => { const ta = mapping[a]?.message?.create_time || 0; const tb = mapping[b]?.message?.create_time || 0; return ta - tb; }); return path; } // ===== 提取图片 URL ===== // ChatGPT 图片在 API 中使用 asset_pointer 格式: "file-service://file-xxxx" // 需要从 DOM 中匹配对应的 元素获取真实 URL function extractImageUrlsFromParts(parts, excludeFileIds = _userUploadedFileIds) { const urls = []; const fileIds = []; for (const part of parts) { if (!part || typeof part !== 'object') continue; // asset_pointer: "file-service://file-xxxx" (最常见的格式) if (part.asset_pointer && typeof part.asset_pointer === 'string') { const fid = part.asset_pointer.replace('file-service://', ''); if (fid) fileIds.push(fid); } // Direct URL fallbacks if (typeof part.url === 'string' && part.url.startsWith('http')) urls.push(part.url); if (part.image_url?.url) urls.push(part.image_url.url); // metadata sources const gen = part.metadata?.generation; if (gen?.image_url) urls.push(gen.image_url); if (gen?.url) urls.push(gen.url); const dalle = part.metadata?.dalle; if (dalle?.image_url) urls.push(dalle.image_url); if (dalle?.url) urls.push(dalle.url); } // 从 DOM 中通过 file ID 查找实际图片 URL(排除用户上传的) for (const fid of fileIds) { if (excludeFileIds.has(fid)) { log('🚫 extractImageUrls: 跳过用户上传 file ID:', fid); continue; } const img = document.querySelector(`img[src*="${fid}"]`); if (img && img.src) { urls.push(img.src); log('🖼️ 通过 asset_pointer 匹配到图片:', fid); } else { log('⚠️ DOM 中未找到 asset_pointer 图片:', fid); } } return [...new Set(urls)]; } // 收集 parts 中所有 file ID (用于延迟匹配) function extractFileIdsFromParts(parts) { const ids = []; for (const part of parts) { if (!part || typeof part !== 'object') continue; if (part.asset_pointer && typeof part.asset_pointer === 'string') { ids.push(part.asset_pointer.replace('file-service://', '')); } } return ids; } // 判断一个 img 元素是否位于用户消息区域内 function isImageInUserMessage(img) { // ChatGPT DOM 中用户消息的容器带有 data-message-author-role="user" const msgEl = img.closest('[data-message-author-role]'); if (msgEl && msgEl.getAttribute('data-message-author-role') === 'user') return true; // 备用检测:向上查找带 data-message-id 的元素,检查其内部是否标记为 user const msgContainer = img.closest('[data-message-id]'); if (msgContainer) { const roleEl = msgContainer.querySelector('[data-message-author-role="user"]'); if (roleEl) return true; } return false; } function getAllDomImages(excludeFileIds = _userUploadedFileIds) { // 在主聊天区内查找所有可能的生成图片 const mainArea = document.querySelector('#thread') || document.querySelector('main') || document.body; const allImgs = [...mainArea.querySelectorAll('img[src^="https"]')]; const results = allImgs .filter(img => { const s = img.src; if (s.includes('cdn.openai.com') || s.includes('favicon') || s.includes('sprites') || s.includes('avatar') || s.includes('og.png')) return false; // 排除用户消息中的图片(通过 DOM 位置判断) if (isImageInUserMessage(img)) { log('🚫 排除用户消息中的图片 (DOM位置)'); return false; } // 排除用户上传的图片(通过 file ID 匹配) for (const fid of excludeFileIds) { if (s.includes(fid)) { log('🚫 排除用户上传图片 (fileID):', fid); return false; } } if (s.includes('oaiusercontent') || s.includes('openai.com/file') || s.includes('dalleprodsec')) return true; if (img.naturalWidth >= 100 || img.width >= 100) return true; if (img.alt && img.alt.length > 5) return true; return false; }) .map(img => img.src); log('🖼️ getAllDomImages:', results.length, '张 (排除', excludeFileIds.size, '个用户上传)'); return [...new Set(results)]; } function getImagesFromDomByMsgId(msgId, excludeFileIds = _userUploadedFileIds) { if (!msgId) return []; let el = document.querySelector(`[data-message-id="${msgId}"]`); if (el) { const imgs = [...el.querySelectorAll('img[src^="https"]')] .filter(img => { const s = img.src; if (s.includes('cdn.openai.com') || s.includes('favicon') || s.includes('sprites')) return false; // 排除用户消息中的图片 if (isImageInUserMessage(img)) return false; // 排除用户上传的文件 for (const fid of excludeFileIds) { if (s.includes(fid)) return false; } return true; }) .map(img => img.src); if (imgs.length) return imgs; } return []; } // ===== 核心提取 ===== function buildRounds(conversationData) { const mapping = conversationData?.mapping; if (!mapping) return { rounds: [], userUploadedFileIds: new Set() }; const path = getOrderedPath(mapping); const rounds = []; let currentRound = null; let lastAssistantMsgId = null; const userUploadedFileIds = new Set(); // 收集用户上传图片的 file ID // Helper: add prompt to current round function addPrompt(prompt, source, imageUrls, toolMsgId, fileIds) { if (!currentRound) { currentRound = { roundIndex: rounds.length + 1, userText: '...', prompts: [] }; rounds.push(currentRound); } const cleaned = prompt.replace(/<\|[a-z_]+\|>/gi, '').replace(/\s+$/, ''); if (cleaned.length < 10 || seenPrompts.has(cleaned)) return; seenPrompts.add(cleaned); currentRound.prompts.push({ id: String(Math.random()).slice(2), prompt: cleaned, source, imageUrls, fileIds: fileIds || [], selected: false, toolMsgId, lastAssistantMsgId, }); } for (const nodeId of path) { const node = mapping[nodeId]; const msg = node?.message; if (!msg) continue; const role = msg.author?.role; const ct = msg.content?.content_type; const parts = msg.content?.parts; const msgId = msg.id; if (role === 'user') { // 跳过 system context 等非真实用户消息 const firstPart = parts?.[0]; if (ct === 'user_editable_context') continue; currentRound = { roundIndex: rounds.length + 1, userText: '', prompts: [] }; const up = typeof firstPart === 'string' ? firstPart.substring(0,40) : ''; currentRound.userText = up; rounds.push(currentRound); lastAssistantMsgId = null; // 收集用户上传图片的 file ID(用于后续排除) if (Array.isArray(parts)) { for (const part of parts) { if (part && typeof part === 'object' && part.asset_pointer && typeof part.asset_pointer === 'string') { const fid = part.asset_pointer.replace('file-service://', ''); if (fid) { userUploadedFileIds.add(fid); log('📎 记录用户上传图片:', fid); } } } } } // 跳过 system 角色 if (role === 'system') continue; if (role === 'assistant') lastAssistantMsgId = msgId; if (role === 'assistant' && ct === 'code' && Array.isArray(parts)) { for (const part of parts) { if (typeof part !== 'string') continue; for (const cp of extractPromptsFromCode(part)) addPrompt(cp, 'code', [], msgId); } } if (role === 'tool' && ct === 'multimodal_text' && Array.isArray(parts)) { const imgUrls = extractImageUrlsFromParts(parts); const fids = extractFileIdsFromParts(parts); for (const part of parts) { if (typeof part === 'string' && part.startsWith('Model caption:')) { const cap = part.substring('Model caption:'.length).trim(); if (cap.length > 20) addPrompt(cap, 'caption', imgUrls, msgId, fids); } if (part && typeof part === 'object' && part.metadata) { const gen = part.metadata.generation; if (typeof gen === 'string' && gen.length > 20) addPrompt(gen, 'generation', imgUrls, msgId, fids); else if (gen?.prompt?.length > 20) addPrompt(gen.prompt, 'gen.prompt', imgUrls, msgId, fids); const pd = part.metadata.dalle; if (pd) { const rp = pd.revised_prompt || (pd.prompt?.length > 10 ? pd.prompt : null); if (rp) addPrompt(rp, 'dalle', imgUrls, msgId, fids); } } } } const dalle = msg.metadata?.dalle; if (dalle) { const rp = dalle.revised_prompt || (dalle.prompt?.length > 10 ? dalle.prompt : null); if (rp) addPrompt(rp, 'meta.dalle', [], msgId); } const igMeta = msg.metadata?.image_generation || msg.metadata?.image_gen_metadata; if (igMeta?.revised_prompt) addPrompt(igMeta.revised_prompt, 'ig_meta', [], msgId); const aggP = msg.metadata?.aggregate_result?.dalle?.prompts; if (Array.isArray(aggP)) for (const dp of aggP) { if (dp?.revised_prompt) addPrompt(dp.revised_prompt, 'agg', [], msgId); } } // 重新编号轮次(过滤空轮后) const filtered = rounds.filter(r => r.prompts.length > 0); filtered.forEach((r, i) => r.roundIndex = i + 1); log('📎 用户上传图片 file ID 共', userUploadedFileIds.size, '个:', [...userUploadedFileIds]); return { rounds: filtered, userUploadedFileIds }; } function extractPromptsFromCode(codeText) { const prompts = []; const r1 = /prompt\s*=\s*(?:"""([\s\S]*?)"""|'''([\s\S]*?)''')/g; let m; while ((m = r1.exec(codeText)) !== null) { const p=(m[1]||m[2]||'').trim(); if(p.length>20) prompts.push(p); } if (!prompts.length) { const r2 = /prompt\s*=\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g; while ((m = r2.exec(codeText)) !== null) { const p=(m[1]||m[2]||'').trim(); if(p.length>20) prompts.push(p); } } if (!prompts.length) { const r3 = /text2im\s*\(\s*(?:"""([\s\S]*?)"""|'''([\s\S]*?)'''|"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g; while ((m = r3.exec(codeText)) !== null) { const p=(m[1]||m[2]||m[3]||m[4]||'').trim(); if(p.length>20) prompts.push(p); } } return prompts; } // ===== 用 DOM 补充图片 URL ===== function enrichWithDomImages(rounds, conversationData, excludeFileIds = _userUploadedFileIds) { const domImgs = getAllDomImages(excludeFileIds); log('🖼️ DOM 中找到图片数:', domImgs.length, domImgs.slice(0, 3)); const allP = rounds.flatMap(r => r.prompts); const noImg = allP.filter(p => p.imageUrls.length === 0); if (!noImg.length && domImgs.length === 0) return; // 策略1: 用 file ID 从 API 数据重新匹配 DOM if (conversationData?.mapping) { for (const item of noImg) { if (!item.toolMsgId) continue; // 找到对应的 tool message for (const [, node] of Object.entries(conversationData.mapping)) { if (node?.message?.id === item.toolMsgId) { const parts = node.message.content?.parts; if (!Array.isArray(parts)) break; const fileIds = extractFileIdsFromParts(parts); for (const fid of fileIds) { // 跳过用户上传的图片 if (excludeFileIds.has(fid)) continue; const domMatch = domImgs.find(url => url.includes(fid)); if (domMatch && !item.imageUrls.includes(domMatch)) { item.imageUrls.push(domMatch); log('🖼️ 延迟匹配 file ID:', fid); } } break; } } } } // 策略2: 通过 data-message-id 在 DOM 中查找 for (const item of allP.filter(p => p.imageUrls.length === 0)) { const tryIds = [item.toolMsgId, item.lastAssistantMsgId].filter(Boolean); for (const mid of tryIds) { const urls = getImagesFromDomByMsgId(mid); if (urls.length) { item.imageUrls = urls; break; } } } // 策略3: 按顺序分配剩余 DOM 图片 if (domImgs.length > 0) { const usedUrls = new Set(allP.flatMap(p => p.imageUrls)); const unusedDomImgs = domImgs.filter(u => !usedUrls.has(u)); let idx = 0; for (const item of allP.filter(p => p.imageUrls.length === 0)) { if (idx < unusedDomImgs.length) { item.imageUrls = [unusedDomImgs[idx++]]; } } } } // ===== API 主流程 ===== async function fetchAndExtractPrompts(forceRefresh) { const convId = getConversationId(); if (!convId) return; // 节流:避免频繁请求 const now = Date.now(); if (!forceRefresh && now - lastFetchTime < FETCH_COOLDOWN) { log('⏸️ 请求节流,跳过'); return; } // 避免重复请求同一对话(已有数据时) if (!forceRefresh && convId === lastFetchConvId && allRounds.length > 0) { log('⏸️ 对话已加载,跳过重复请求'); return; } let token = getAccessToken(); if (!token) { // 尝试刷新 token token = await refreshAccessToken(); if (!token) { showStatus('无法获取 token,请刷新页面'); return; } } lastFetchTime = now; log('📡 请求对话数据:', convId); showStatus('正在获取对话数据...'); try { let resp = await fetch(`https://chatgpt.com/backend-api/conversation/${convId}`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', }); // Token 过期:自动刷新并重试一次 if (resp.status === 401 || resp.status === 403) { log('🔑 Token 过期,尝试刷新...'); token = await refreshAccessToken(); if (token) { resp = await fetch(`https://chatgpt.com/backend-api/conversation/${convId}`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', }); } } // 限流处理 if (resp.status === 429) { log('⚠️ 请求被限流'); showStatus('请求太频繁,请稍后手动重试'); return; } if (!resp.ok) { log('❌ API 返回:', resp.status); showStatus(`API错误: ${resp.status}`); return; } const data = await resp.json(); lastFetchConvId = convId; seenPrompts.clear(); const result = buildRounds(data); allRounds = result.rounds; _userUploadedFileIds = result.userUploadedFileIds; // 第一次尝试匹配图片 enrichWithDomImages(allRounds, data, _userUploadedFileIds); const total = allRounds.reduce((s,r) => s + r.prompts.length, 0); log(`✅ 提取 ${total} 个提示词,${allRounds.length} 轮对话`); showStatus(`找到 ${total} 个提示词 / ${allRounds.length} 轮`); renderPanel(); // 延迟 3 秒再试一次 (等待 DOM 图片加载) const noImgCount = allRounds.flatMap(r => r.prompts).filter(p => p.imageUrls.length === 0).length; if (noImgCount > 0) { log(`⏳ ${noImgCount} 个提示词无图片,3秒后重试DOM匹配...`); setTimeout(() => { enrichWithDomImages(allRounds, data, _userUploadedFileIds); renderPanel(); log('🔄 延迟图片匹配完成'); }, 3000); } } catch(e) { log('❌', e.message); showStatus('请求失败: ' + e.message); } } async function manualFetchPrompts() { if (!getConversationId()) { toast('请先打开一个对话'); updateFab(); return; } if (isFetchingPrompts) return; const refreshBtn = document.getElementById('rp-refresh'); const fab = document.getElementById('rp-fab'); isFetchingPrompts = true; if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.innerHTML = `${SVG.refresh} 提取中`; } if (fab) fab.disabled = true; try { await fetchAndExtractPrompts(true); } finally { isFetchingPrompts = false; if (refreshBtn) { refreshBtn.disabled = false; refreshBtn.innerHTML = `${SVG.refresh} 提取`; } if (fab) fab.disabled = false; } } // ===== 主循环 ===== let lastUrl = ''; function startMonitoring() { const checkUrl = () => { if (location.href !== lastUrl) { lastUrl = location.href; allRounds = []; seenPrompts.clear(); _userUploadedFileIds = new Set(); // 重置用户上传图片排除集 lastFetchConvId = ''; // 重置,允许新对话请求 const body = document.getElementById('rp-body'); if (body) body.innerHTML = ''; const panel = document.getElementById('rp-panel'); if (panel) panel.classList.remove('open'); updateFab(); updateFooter(); } }; setInterval(checkUrl, 1000); let debounce = null; const obs = new MutationObserver(() => { if (debounce) clearTimeout(debounce); debounce = setTimeout(() => { const total = allRounds.reduce((s,r) => s + r.prompts.length, 0); if (total === 0) return; // 已手动提取到提示词后,仅补充图片匹配,不再重复 API 请求 const noImgCount = allRounds.flatMap(r => r.prompts).filter(p => p.imageUrls.length === 0).length; if (noImgCount === 0) return; const imgs = getAllDomImages(_userUploadedFileIds); if (imgs.length > 0) { enrichWithDomImages(allRounds, null, _userUploadedFileIds); renderPanel(); } }, 5000); // 从 3 秒改为 5 秒,减少触发频率 }); obs.observe(document.body, { childList: true, subtree: true }); if (getConversationId()) { log('当前对话:', getConversationId()); updateFab(); } } function boot() { log('🎨 v5.2 启动 (修复多轮提取+排除用户上传图)'); injectStyles(); createFab(); createPanel(); startMonitoring(); } if (document.readyState === 'complete') boot(); else window.addEventListener('load', boot); })();