// ==UserScript==
// @name ChatGPT 对话记录导出/页面宽屏展示
// @namespace http://tampermonkey.net/
// @version 6.1
// @description 在任意网页检测 ChatGPT 特征,提供对话导出及宽屏显示模式。
// @license V:ChatGPT4V
// @match *://*/*
// @grant none
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
const FEATURE_SELECTORS = ['#thread', '[data-testid^="conversation-turn-"]', '[data-message-author-role]'];
const WIDESCREEN_STORAGE_KEY = 'chatgpt_widescreen_state';
function isChatGPTPage() {
return FEATURE_SELECTORS.some(selector => document.querySelector(selector));
}
function injectStyles() {
if (document.getElementById('chatgpt-helper-style')) return;
const style = document.createElement('style');
style.id = 'chatgpt-helper-style';
style.textContent = `
main.w-full.largescreen form.w-full { max-width: 90% !important; margin: auto !important; }
@media(min-width:1024px){ main.w-full.largescreen article.text-token-text-primary > div > div.w-full { max-width: 100% !important; } }
main.w-full.largescreen #thread-bottom > div > div { --thread-content-max-width: unset !important; }
.custom-ai-btn {
position: fixed; right: 20px; z-index: 9999;
background-color: #10a37f; color: #fff; border: none;
padding: 6px 10px; border-radius: 6px; font-size: 14px;
cursor: pointer; font-family: sans-serif; font-weight: 600;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: background 0.2s;
}
.custom-ai-btn:hover { background-color: #0e8f6e; }
.custom-ai-btn:disabled { background-color: #999; cursor: wait; }
.ai-image-label { font-weight: bold; margin-bottom: 5px; color: #2c3e50; border-left: 4px solid #10a37f; padding-left: 8px; font-size: 14px; }
`;
document.head.appendChild(style);
}
function toggleWidescreen(enable) {
const main = document.querySelector('main.w-full') || document.querySelector('main');
if (!main) return;
enable ? main.classList.add('largescreen') : main.classList.remove('largescreen');
localStorage.setItem(WIDESCREEN_STORAGE_KEY, enable);
const btn = document.getElementById('widescreen-toggle-btn');
if (btn) btn.innerHTML = enable ? '❌ 退出大屏' : '📺 展示大屏';
}
function createButton(id, text, bottom, onClick) {
if (document.getElementById(id)) return;
const btn = document.createElement('button');
btn.id = id;
btn.className = 'custom-ai-btn';
btn.innerHTML = text;
btn.style.bottom = bottom;
btn.onclick = onClick;
document.body.appendChild(btn);
}
function getChatTitle() {
let title = document.title || 'ChatGPT_对话记录';
return title.replace(' - ChatGPT', '').trim();
}
function escapeHtml(text) {
return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
}
async function fetchImageBlob(url) {
try {
const response = await fetch(url, { cache: 'force-cache' });
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (e) {
try {
const response = await fetch(url);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (err) {
console.warn('Image fetch failed:', url, err);
return null;
}
}
}
function generateFullHtml(bodyContent, title) {
return `
${title}
${title}
提示:点击图片可放大查看,右键图片可直接“另存为”
注意⚠:文件类为动态地址无法提取,需自行下载保存
${bodyContent}
`;
}
function isValidImage(img) {
if (img.alt === 'User' || img.alt === 'ChatGPT') return false;
if (img.src && (img.src.includes('files.oaiusercontent.com') || img.src.includes('backend-api'))) return true;
if (img.alt && (img.alt.includes('已生成') || img.alt.includes('Generated'))) return true;
const w = img.naturalWidth || img.width || img.clientWidth;
if (w > 0 && w < 50) return false;
return true;
}
function isGeneratedImage(img) {
if (!img) return false;
if (img.src && (img.src.includes('files.oaiusercontent.com') || img.src.includes('backend-api'))) return true;
if (img.alt && (img.alt.includes('已生成') || img.alt.includes('Generated'))) return true;
return false;
}
function createAiLabel() {
const label = document.createElement('p');
label.className = 'ai-image-label';
label.textContent = '图片已创建·AI出图⬇️';
return label;
}
async function startExport() {
const btn = document.getElementById('optimize-export-btn');
const originalBtnText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳ 扫描中...';
const turnList = document.querySelectorAll('[data-testid^="conversation-turn-"]');
const nodesToProcess = turnList.length ? Array.from(turnList) : [document.body];
if (!turnList.length && !document.querySelector('[data-message-author-role]')) {
alert('未检测到对话内容');
btn.disabled = false;
btn.innerHTML = originalBtnText;
return;
}
const uniqueImageUrls = new Set();
for (const turn of nodesToProcess) {
turn.querySelectorAll('img').forEach(img => {
if (isValidImage(img) && img.src) uniqueImageUrls.add(img.src);
});
}
const imageCache = new Map();
const urls = Array.from(uniqueImageUrls);
const total = urls.length;
if (total > 0) {
btn.innerHTML = `⏳ 下载图片 (0/${total})...`;
let count = 0;
// Promise.all 并发下载,cache: 'force-cache' 实现秒读
await Promise.all(urls.map(async (url) => {
const base64 = await fetchImageBlob(url);
count++;
btn.innerHTML = `⏳ 下载图片 (${count}/${total})...`;
if (base64) imageCache.set(url, base64);
}));
}
btn.innerHTML = '⏳ 生成代码...';
let chatHtmlContent = '';
for (const turn of nodesToProcess) {
const processedUrlsInTurn = new Set();
const messages = turn.querySelectorAll('[data-message-author-role]');
for (const msg of messages) {
const role = msg.getAttribute('data-message-author-role');
const textNode = msg.querySelector('.markdown') || msg.querySelector('.whitespace-pre-wrap');
const validImages = Array.from(msg.querySelectorAll('img')).filter(isValidImage);
if (!textNode && validImages.length === 0) continue;
const container = document.createElement('div');
if (textNode) container.appendChild(textNode.cloneNode(true));
if (validImages.length > 0) {
let hasAddedAiLabel = false;
validImages.forEach(img => {
if (processedUrlsInTurn.has(img.src)) return;
processedUrlsInTurn.add(img.src);
if (role !== 'user' && isGeneratedImage(img) && !hasAddedAiLabel) {
container.appendChild(createAiLabel());
hasAddedAiLabel = true;
}
const finalSrc = imageCache.get(img.src) || img.src;
const newImg = document.createElement('img');
newImg.src = finalSrc;
newImg.className = 'chat-img-thumb';
newImg.setAttribute('onclick', 'showLightbox(this.src)');
container.appendChild(newImg);
});
}
processContainerContent(container);
const innerHtml = container.innerHTML;
if (role === 'user') {
chatHtmlContent += ``;
} else {
chatHtmlContent += ``;
}
}
const allImagesInTurn = Array.from(turn.querySelectorAll('img')).filter(isValidImage);
const orphanImages = [];
allImagesInTurn.forEach(img => {
if (!processedUrlsInTurn.has(img.src)) {
orphanImages.push(img);
processedUrlsInTurn.add(img.src);
}
});
if (orphanImages.length > 0) {
const container = document.createElement('div');
if (orphanImages.some(isGeneratedImage)) {
container.appendChild(createAiLabel());
}
orphanImages.forEach(img => {
const finalSrc = imageCache.get(img.src) || img.src;
const newImg = document.createElement('img');
newImg.src = finalSrc;
newImg.className = 'chat-img-thumb';
newImg.setAttribute('onclick', 'showLightbox(this.src)');
container.appendChild(newImg);
});
const innerHtml = container.innerHTML;
chatHtmlContent += ``;
}
}
const currentTitle = getChatTitle();
const fullHtml = generateFullHtml(chatHtmlContent, currentTitle);
const blob = new Blob([fullHtml], { type: 'text/html' });
const a = document.createElement('a');
const safeTitle = currentTitle.replace(/[\\/:*?"<>|]/g, '_') || 'ChatGPT_Export';
a.href = URL.createObjectURL(blob);
a.download = `${safeTitle}.html`;
a.click();
URL.revokeObjectURL(a.href);
btn.disabled = false;
btn.innerHTML = originalBtnText;
}
function processContainerContent(container) {
container.querySelectorAll('pre').forEach(pre => {
const code = pre.querySelector('code');
if (code) {
const langClass = code.className || 'language-plaintext';
let langName = langClass.replace('language-', '');
if(!langName || langName === 'undefined') langName = 'Code';
const newBlock = document.createElement('div');
newBlock.className = 'code-wrapper';
newBlock.innerHTML = `
${escapeHtml(code.innerText)}
`;
pre.replaceWith(newBlock);
}
});
container.querySelectorAll('a').forEach(a => {
if (a.classList.contains('img-link')) return;
a.target = '_blank';
a.style.cssText = 'color:#0066cc; text-decoration:underline;';
if (a.innerText.includes('下载') || a.hasAttribute('download')) {
a.style.fontWeight = 'bold';
a.innerHTML = '📄 ' + a.innerHTML;
}
});
container.querySelectorAll('button, .icon-md, .sr-only').forEach(el => !el.classList.contains('copy-btn') && el.remove());
}
setInterval(() => {
if (!isChatGPTPage()) return;
injectStyles();
createButton('widescreen-toggle-btn', '📺 展示大屏', '20px', () => {
toggleWidescreen(localStorage.getItem(WIDESCREEN_STORAGE_KEY) !== 'true');
});
createButton('optimize-export-btn', '📥 导出对话', '60px', startExport);
const savedState = localStorage.getItem(WIDESCREEN_STORAGE_KEY) === 'true';
const main = document.querySelector('main.w-full') || document.querySelector('main');
if (main && savedState && !main.classList.contains('largescreen')) {
toggleWidescreen(true);
}
}, 1000);
})();