Export Chat Dialogues to Obsidian

Export ChatGPT conversations to Obsidian.

目前為 2025-06-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Export Chat Dialogues to Obsidian
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  Export ChatGPT conversations to Obsidian.
// @author       You
// @match        https://chatgpt.com/c/*
// @match        https://yuanbao.tencent.com/chat/*
// @match        https://chat.deepseek.com/a/chat/s/*
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @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"
    }

    // 添加导出按钮
    function addExportButton() {
        if (document.getElementById('export-to-obsidian-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'export-to-obsidian-btn';
        btn.innerText = '💾Export';
        btn.style.position = 'fixed';
        btn.style.top = '50px';
        btn.style.right = '20px';
        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 = 'pointer';
        btn.style.boxShadow = '0 2px 6px rgba(0,0,0,0.2)';
        btn.addEventListener('click', exportToObsidian);
        document.body.appendChild(btn);
    }

    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

        // 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}/${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 });
})();