Size
11.9 KB
Version
1.0.2
Created
Dec 19, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name ChatGPT 聊天内容提取器
3// @description 一键提取 ChatGPT 对话内容,支持复制和导出
4// @version 1.0.2
5// @match https://*.chatgpt.com/*
6// @icon https://chatgpt.com/cdn/assets/favicon-l4nq08hd.svg
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('ChatGPT 聊天内容提取器已加载');
12
13 // 提取聊天内容的核心函数
14 function extractChatContent() {
15 console.log('开始提取聊天内容...');
16
17 const messages = [];
18
19 // 查找所有对话轮次
20 const conversationTurns = document.querySelectorAll('article[data-testid^="conversation-turn-"]');
21 console.log(`找到 ${conversationTurns.length} 条消息`);
22
23 conversationTurns.forEach((turn, index) => {
24 try {
25 // 判断是用户消息还是助手消息
26 const isUser = turn.getAttribute('data-turn') === 'user';
27 const isAssistant = turn.getAttribute('data-turn') === 'assistant';
28
29 let role = 'unknown';
30 let content = '';
31
32 if (isUser) {
33 role = '用户';
34 // 提取用户消息
35 const userMessage = turn.querySelector('[data-message-author-role="user"] .whitespace-pre-wrap');
36 if (userMessage) {
37 content = userMessage.textContent.trim();
38 }
39 } else if (isAssistant) {
40 role = 'ChatGPT';
41 // 提取助手消息
42 const assistantMessage = turn.querySelector('[data-message-author-role="assistant"] .markdown');
43 if (assistantMessage) {
44 content = assistantMessage.textContent.trim();
45 }
46 }
47
48 if (content) {
49 messages.push({
50 index: index + 1,
51 role: role,
52 content: content
53 });
54 console.log(`提取消息 ${index + 1}: ${role}`);
55 }
56 } catch (error) {
57 console.error(`提取消息 ${index + 1} 时出错:`, error);
58 }
59 });
60
61 return messages;
62 }
63
64 // 格式化聊天内容为文本
65 function formatChatAsText(messages) {
66 let text = '=== ChatGPT 对话记录 ===\n\n';
67
68 messages.forEach(msg => {
69 text += `【${msg.role}】\n${msg.content}\n\n`;
70 text += '---\n\n';
71 });
72
73 text += `总计: ${messages.length} 条消息\n`;
74 text += `提取时间: ${new Date().toLocaleString('zh-CN')}\n`;
75
76 return text;
77 }
78
79 // 复制到剪贴板
80 async function copyToClipboard(text) {
81 try {
82 await GM.setClipboard(text);
83 console.log('内容已复制到剪贴板');
84 return true;
85 } catch (error) {
86 console.error('复制失败:', error);
87 return false;
88 }
89 }
90
91 // 下载为文本文件
92 function downloadAsFile(text, filename) {
93 try {
94 const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
95 const url = URL.createObjectURL(blob);
96 const a = document.createElement('a');
97 a.href = url;
98 a.download = filename;
99 document.body.appendChild(a);
100 a.click();
101 document.body.removeChild(a);
102 URL.revokeObjectURL(url);
103 console.log('文件下载成功');
104 return true;
105 } catch (error) {
106 console.error('下载失败:', error);
107 return false;
108 }
109 }
110
111 // 显示提示消息
112 function showNotification(message, type = 'success') {
113 const notification = document.createElement('div');
114 notification.textContent = message;
115 notification.style.cssText = `
116 position: fixed;
117 top: 20px;
118 right: 20px;
119 padding: 15px 20px;
120 background: ${type === 'success' ? '#10a37f' : '#ef4444'};
121 color: white;
122 border-radius: 8px;
123 font-size: 14px;
124 font-weight: 500;
125 z-index: 10000;
126 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
127 animation: slideIn 0.3s ease-out;
128 `;
129
130 document.body.appendChild(notification);
131
132 setTimeout(() => {
133 notification.style.animation = 'slideOut 0.3s ease-out';
134 setTimeout(() => {
135 document.body.removeChild(notification);
136 }, 300);
137 }, 3000);
138 }
139
140 // 添加动画样式
141 function addStyles() {
142 const style = document.createElement('style');
143 style.textContent = `
144 @keyframes slideIn {
145 from {
146 transform: translateX(400px);
147 opacity: 0;
148 }
149 to {
150 transform: translateX(0);
151 opacity: 1;
152 }
153 }
154
155 @keyframes slideOut {
156 from {
157 transform: translateX(0);
158 opacity: 1;
159 }
160 to {
161 transform: translateX(400px);
162 opacity: 0;
163 }
164 }
165
166 .extract-chat-btn {
167 transition: all 0.2s ease;
168 }
169
170 .extract-chat-btn:hover {
171 transform: scale(1.05);
172 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
173 }
174
175 .extract-chat-btn:active {
176 transform: scale(0.98);
177 }
178 `;
179 document.head.appendChild(style);
180 }
181
182 // 创建提取按钮
183 function createExtractButton() {
184 console.log('创建提取按钮...');
185
186 // 检查按钮是否已存在
187 if (document.getElementById('chatgpt-extract-btn')) {
188 console.log('按钮已存在');
189 return;
190 }
191
192 const button = document.createElement('button');
193 button.id = 'chatgpt-extract-btn';
194 button.innerHTML = `
195 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
196 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
197 <polyline points="7 10 12 15 17 10"></polyline>
198 <line x1="12" y1="15" x2="12" y2="3"></line>
199 </svg>
200 <span style="margin-left: 8px;">提取对话</span>
201 `;
202
203 button.style.cssText = `
204 position: fixed;
205 bottom: 30px;
206 right: 30px;
207 padding: 12px 20px;
208 background: #10a37f;
209 color: white;
210 border: none;
211 border-radius: 8px;
212 font-size: 14px;
213 font-weight: 600;
214 cursor: pointer;
215 z-index: 9999;
216 display: flex;
217 align-items: center;
218 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
219 `;
220
221 button.className = 'extract-chat-btn';
222
223 // 点击事件
224 button.addEventListener('click', async () => {
225 console.log('按钮被点击');
226
227 // 提取内容
228 const messages = extractChatContent();
229
230 if (messages.length === 0) {
231 showNotification('未找到聊天内容', 'error');
232 return;
233 }
234
235 const formattedText = formatChatAsText(messages);
236
237 // 创建操作菜单
238 showActionMenu(formattedText, messages.length);
239 });
240
241 document.body.appendChild(button);
242 console.log('按钮已添加到页面');
243 }
244
245 // 显示操作菜单
246 function showActionMenu(text, messageCount) {
247 // 移除已存在的菜单
248 const existingMenu = document.getElementById('chatgpt-action-menu');
249 if (existingMenu) {
250 existingMenu.remove();
251 }
252
253 const menu = document.createElement('div');
254 menu.id = 'chatgpt-action-menu';
255 menu.innerHTML = `
256 <div style="margin-bottom: 15px; font-size: 16px; font-weight: 600; color: #1f2937;">
257 已提取 ${messageCount} 条消息
258 </div>
259 <div style="display: flex; gap: 10px;">
260 <button id="copy-btn" style="
261 flex: 1;
262 padding: 10px;
263 background: #10a37f;
264 color: white;
265 border: none;
266 border-radius: 6px;
267 font-size: 14px;
268 font-weight: 500;
269 cursor: pointer;
270 ">📋 复制</button>
271 <button id="download-btn" style="
272 flex: 1;
273 padding: 10px;
274 background: #2563eb;
275 color: white;
276 border: none;
277 border-radius: 6px;
278 font-size: 14px;
279 font-weight: 500;
280 cursor: pointer;
281 ">💾 下载</button>
282 <button id="close-menu-btn" style="
283 padding: 10px 15px;
284 background: #6b7280;
285 color: white;
286 border: none;
287 border-radius: 6px;
288 font-size: 14px;
289 font-weight: 500;
290 cursor: pointer;
291 ">✕</button>
292 </div>
293 `;
294
295 menu.style.cssText = `
296 position: fixed;
297 bottom: 90px;
298 right: 30px;
299 padding: 20px;
300 background: white;
301 border-radius: 12px;
302 box-shadow: 0 8px 24px rgba(0,0,0,0.2);
303 z-index: 10000;
304 min-width: 300px;
305 `;
306
307 document.body.appendChild(menu);
308
309 // 复制按钮
310 document.getElementById('copy-btn').addEventListener('click', async () => {
311 const success = await copyToClipboard(text);
312 if (success) {
313 showNotification('✓ 已复制到剪贴板');
314 } else {
315 showNotification('复制失败', 'error');
316 }
317 menu.remove();
318 });
319
320 // 下载按钮
321 document.getElementById('download-btn').addEventListener('click', () => {
322 const filename = `ChatGPT对话_${new Date().toISOString().slice(0,10)}.txt`;
323 const success = downloadAsFile(text, filename);
324 if (success) {
325 showNotification('✓ 文件已下载');
326 } else {
327 showNotification('下载失败', 'error');
328 }
329 menu.remove();
330 });
331
332 // 关闭按钮
333 document.getElementById('close-menu-btn').addEventListener('click', () => {
334 menu.remove();
335 });
336 }
337
338 // 初始化函数
339 function init() {
340 console.log('初始化 ChatGPT 聊天内容提取器...');
341
342 // 添加样式
343 addStyles();
344
345 // 等待页面加载完成
346 if (document.readyState === 'loading') {
347 document.addEventListener('DOMContentLoaded', createExtractButton);
348 } else {
349 createExtractButton();
350 }
351
352 // 监听页面变化,确保按钮始终存在
353 const observer = new MutationObserver(() => {
354 if (!document.getElementById('chatgpt-extract-btn')) {
355 createExtractButton();
356 }
357 });
358
359 observer.observe(document.body, {
360 childList: true,
361 subtree: true
362 });
363 }
364
365 // 启动扩展
366 init();
367
368})();