您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export ChatGPT conversations to Obsidian.
// ==UserScript== // @name Export Chat History to Obsidian // @namespace http://tampermonkey.net/ // @version 0.9.5 // @description Export ChatGPT conversations to Obsidian. // @author You // @match https://chatgpt.com/* // @match https://yuanbao.tencent.com/* // @match https://chat.deepseek.com/* // @grant GM_setClipboard // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function () { 'use strict'; var source = "others" var cssArticle = "article" var cssUser = "div.whitespace-pre-wrap" var cssCopyButton = "div.touch\\:-me-2 > button:nth-child(1)" if (window.location.hostname == 'chatgpt.com') { source = "chatgpt"; } else if (window.location.hostname == 'yuanbao.tencent.com') { source = "yuanbao"; cssArticle = "div.agent-chat__list__item" cssUser = "div.hyc-content-text" cssCopyButton = "div.agent-chat__toolbar__item.agent-chat__toolbar__copy" } else if (window.location.hostname == 'chat.deepseek.com') { source = "deepseek" } async function addExportButton() { if (document.getElementById('export-to-obsidian-btn')) return; const btn = document.createElement('button'); btn.id = 'export-to-obsidian-btn'; btn.innerText = '💾 Export'; // 默认位置右上角:top 50px, right 20px const savedTop = await GM_getValue(source + '_exportBtnTop', 50); const savedRight = await GM_getValue(source + '_exportBtnRight', 20); btn.style.position = 'fixed'; btn.style.top = savedTop + 'px'; btn.style.right = savedRight + 'px'; btn.style.zIndex = '9999'; btn.style.padding = '8px 12px'; btn.style.fontSize = '14px'; btn.style.backgroundColor = '#2670dd'; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.borderRadius = '5px'; btn.style.cursor = 'move'; btn.style.boxShadow = '0 2px 6px rgba(0,0,0,0.2)'; btn.title = "拖动以移动位置,点击导出"; makeButtonDraggable(btn); document.body.appendChild(btn); } // 拖动功能 function makeButtonDraggable(button) { let isDragging = false; let startX = 0; let startY = 0; button.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = button.getBoundingClientRect(); button._offsetX = startX - rect.right; button._offsetY = startY - rect.top; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', async (e) => { if (!isDragging) return; const newTop = e.clientY - button._offsetY; const newRight = window.innerWidth - (e.clientX - button._offsetX); button.style.top = `${newTop}px`; button.style.right = `${newRight}px`; await GM_setValue(source + '_exportBtnTop', newTop); await GM_setValue(source + '_exportBtnRight', newRight); }); document.addEventListener('mouseup', (e) => { if (!isDragging) return; isDragging = false; document.body.style.userSelect = ''; const dx = e.clientX - startX; const dy = e.clientY - startY; const distance = Math.sqrt(dx * dx + dy * dy); // 若移动很小(小于3像素),才视为点击 if (distance < 3) { exportToObsidian(); } }); } function findElementsByStyle(property, value) { const allElements = document.querySelectorAll('[style*="z-index"], div'); const result = []; allElements.forEach(element => { // 获取元素的计算样式 const computedStyle = window.getComputedStyle(element); // 获取目标属性的值(如 'zIndex') const propValue = computedStyle.getPropertyValue(property); // 检查属性值是否匹配(转换为字符串比较) if (propValue === String(value)) { result.push(element); } }); return result; } async function getContents() { const articles = document.querySelectorAll(cssArticle); const contents = []; for (var idx = 0; idx < articles.length; idx++) { const article = articles[idx] if (idx % 2 === 0) { const user = article.querySelector(cssUser); if (!user) { continue; } contents.push(user.textContent); } else { const copyButton = article.querySelector(cssCopyButton); if (!copyButton) continue; copyButton.click(); await new Promise(resolve => setTimeout(resolve, 250)); const text = await navigator.clipboard.readText(); contents.push(text); } } return contents; } async function getDeepSeekContents() { const contents = []; var copyButtons = document.querySelectorAll("div.ds-flex > div.ds-icon-button:nth-child(1)") for (const copyButton of copyButtons) { copyButton.click(); await new Promise(resolve => setTimeout(resolve, 250)); const text = await navigator.clipboard.readText(); contents.push(text); } return contents; } // 核心导出逻辑 async function exportToObsidian() { var contents; if (source == "deepseek") { contents = await getDeepSeekContents(); } else { contents = await getContents(); } if (contents.length === 0) { alert("没有找到任何对话记录!"); return; } // 构建正文内容 let body = ''; contents.forEach((text, idx) => { if (idx % 2 === 0) { body += `# ❓ User:\n> ${text.replace(/\n/g, '\n> ')}\n\n`; } else { body += `# 🤖 GPT:\n${text}\n\n`; } }); var title = document.title.replace(/[/\\?%*:|"<>]/g, '-').trim(); if (source == "yuanbao") { var titleElement = document.querySelector("span.agent-dialogue__content--common__header__name__title"); if (titleElement) { title = titleElement.textContent; } } else if (source == "deepseek") { var elements = findElementsByStyle('z-index', 12); if (elements.length) { title = elements[0].textContent; } } const timestamp = new Date().toLocaleString(); const url = document.URL const dateOnly = new Date().toLocaleDateString('sv-SE').replace(/-/g, ''); // YAML Frontmatter const yaml = `---\ntitle: "${title}"\ndate: ${timestamp}\nsource: ${source}\nURL: ${url}\n---\n\n`; const fullContent = yaml + body; // 写入剪贴板 GM_setClipboard(fullContent); const obsidianUrl = `obsidian://new?file=Chat/${source}/${dateOnly}_${encodeURIComponent(title)}&clipboard`; const link = document.createElement('a'); link.href = obsidianUrl; link.click(); // 只在用户触发的事件中使用 } // 注册菜单项 GM_registerMenuCommand("📥 导出到 Obsidian", exportToObsidian); // 初始化:添加按钮 + 观察变化 setTimeout(addExportButton, 1000); const observer = new MutationObserver(addExportButton); observer.observe(document.body, { childList: true, subtree: true }); })();