Cursor Chat

Cursor Chat(基于 Cursor 文档的聊天助手,改进了使用体验和增加易用性)支持以下模型:claude-sonnet-4.5、gpt-5-nano 和 gemini-2.5-flash。⚠️ 重要提示:不会记住上次的聊天历史!刷新页面将导致聊天记录丢失!请及时保存您的内容!

Tính đến 01-11-2025. Xem phiên bản mới nhất.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Cursor Chat
// @namespace    Cursor Chat
// @version      1.0.22
// @description  Cursor Chat(基于 Cursor 文档的聊天助手,改进了使用体验和增加易用性)支持以下模型:claude-sonnet-4.5、gpt-5-nano 和 gemini-2.5-flash。⚠️ 重要提示:不会记住上次的聊天历史!刷新页面将导致聊天记录丢失!请及时保存您的内容!
// @author       Wilson
// @match        *://*/*
// @icon         https://cursor.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      jike.teracloud.jp
// @connect      *
// @grant        GM_xmlhttpRequest
// @require      https://fastly.jsdelivr.net/gh/wish5115/my-softs@f1f427637e20ac61b10ffef8d25b63e4d0e29711/libs/WebDAVClient.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    //////////////// 用户配置区 //////////////////////

    // 这里输入自定义提示词(在每个对话的前面都会添加)
    const customPrompt = ``;

    // 保存对话历史配置
    const historyConfig = {
        enable: true, // 是否开启
        showDays: 30, // 显示多少天的记录
        webdav: {
            // 如果你也使用 InfiniCLOUD webdav可以输入我的推荐码:QEU7Z 你可以额外增加5G永久空间(方法:点击顶部导航进入 My Page页面,找到找到 Enter Friends Referral Code输入即可)
            url: 'https://jike.teracloud.jp/dav/', // 如果port不是默认的可加到url里
            username: '',
            password: '',
            savePath: '/cursor-chat-history', // 目前仅支持根目录下的一级目录,请勿放于二级目录下或让AI帮修改支持
        }
    };

    // 新建聊天时,是否下载当前聊天页面 true 下载 false 不下载 默认true
    const newChatDownPage = true;

    //////////////// 代码逻辑区,非必要勿动 //////////////////////

    if (window.top !== window.self) return;
    GM_registerMenuCommand(
        "打开 Cursor Chat",
        function () {
            window.open('https://cursor.com/cn/docs?chat');
        },
        "o"
    );
    const urlParams = new URLSearchParams(window.location.search);
    if(!location.href.includes('cursor.com/cn/docs') || (!urlParams.has('chat') && !urlParams.has('ai'))) return;
    historyConfig.webdav.url = historyConfig.webdav.url || localStorage.getItem('_url') || '';
    historyConfig.webdav.username = historyConfig.webdav.username || localStorage.getItem('_username') || '';
    historyConfig.webdav.password = historyConfig.webdav.password || localStorage.getItem('_password') || '';
    GM_addStyle(`
        div[class~="md:block"]{
          width: 100%!important;
        }
        div[class~="md:block"] > div {
          width: auto!important;
        }
        div[class~="md:block"] > div > div:first-child,
        main,
        div[class~="lg:block"],
        div[data-silk]:has(header) {
            display: none!important;
        }
        div[class~="md:block"]::before {
            content: "AI Loading...";
            padding-left: calc(50% - 44.24px);
            font-size: 24px;
        }
        div[class~="md:block"].loaded::before {
          content: none;
        }
        div:has(> button[data-slot="popover-trigger"][aria-haspopup="dialog"][aria-controls="radix-_r_1i_"]){
          display: none;
        }
        form textarea[placeholder] {
            overflow: auto;
        }
        form textarea[placeholder],
        form textarea[placeholder].auto-h {
            height: 40px!important;
        }
        form textarea[placeholder].auto-h.focus {
            height: 150px!important;
            background-color: white;
        }
        div[data-sender="user"] > div {
            background-color: #e2e7ee;
            color: #000;
            border-left: 3px solid blue;
            white-space: pre-wrap;
            font-family: monospace;
            word-break: break-word;
            max-height: 200px;
            overflow: auto;
        }
        div[data-sender="assistant"] > div {
            background-color: white;
        }
        .ai-tips {
            color: coral;
            font-weight:bold;
            font-size: 12px;
        }
        .ai-ads {
            color: #333;
        }
        .ai-ads a {
            color: blue;
        }
        .ai-ads a:hover {
            text-decoration: underline;
        }
        .chat-help-btn,
        .new-chat-btn,
        .chat-list-btn,
        .chat-list-copy-btn,
        .chat-list-down-btn,
        .chat-list-history-btn{
            font-size: 12px;
            color: #666;
            margin-right: 5px;
        }
        div[data-sender="assistant"] .chat-copy-btn{
            width: fit-content;
            padding: 2px 10px;
            border-radius: 14px;
            font-size: 13.5px;
            cursor: pointer;
            margin-top: 4px;
        }
        .chat-copy-btn:hover{
            color: forestgreen;
        }
        /* 窄屏卡片元素 */
        .LongSheet-scrollContent .bg-card.h-\\[90dvh\\] {
            height: 97vh;
            padding-top: 0;
        }
        /* 窄屏触发卡片按钮 */
        div[data-silk] [data-silk][aria-controls].inline-flex:nth-child(1) {
            margin-bottom: 118px;
        }
        /* 窄屏关闭按钮和@文档隐藏 */
        .LongSheet-innerContent [data-silk][aria-controls] {
            display: none;
        }
        /*form textarea::placeholder {
          color: #0b4c92;
        }*/
        .flex-shrink-0 > .absolute:has(.shimmer) {width: 125px;margin-left: calc(50% - 62.5px);}
        .text-card-foreground:has(>div>pre) {
            max-height: 500px;
            overflow: auto;
        }
        .thinking div[data-sender="assistant"]:last-of-type .text-card-foreground:has(>div>pre) {
            max-height: none;  /* 或者直接删除这个属性让它继承 */
        }

        /* 适配黑色主题 */
        @media (prefers-color-scheme: dark) {
          div[data-sender="user"] > div {
            background-color: #343b48;
            color: #fff;
          }
          div[data-sender="assistant"] > div {
              background-color: #000000;
          }
          form textarea[placeholder].auto-h.focus {
              background-color: #010101;
          }
          .chat-copy-btn:hover {
              color: #2cc9b6;
          }
          .chat-help-btn, .new-chat-btn, .chat-list-btn, .chat-list-copy-btn, .chat-list-down-btn, .chat-list-history-btn{
              color: #999;
          }
          .bg-card:has(textarea){
              border-color:#3e3e3e;
          }
          .ai-ads {
              color: #bcbcbc;
          }
          .ai-ads a {
              color: #00BCD4;
          }
        }
    `);
    let now = '';
    let loaded = false;
    const showAI = () => {
        document.title = 'Cursor Chat';
        document.querySelector('div[class~="md:block"]')?.classList.add('loaded');
        document.querySelector('div[class~="md:block"] > div > div:first-child > button')?.click();
        //document.querySelector('[data-slot="popover-trigger"][aria-haspopup="dialog"][aria-controls="radix-_r_1i_"]')?.nextElementSibling?.click();
        // 窄屏自动打开卡片
        const msgBtn = document.querySelector('div[data-silk] [data-silk][aria-controls].inline-flex:nth-child(1)');
        if(msgBtn?.getBoundingClientRect()?.width) {
            msgBtn.click();
            setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题', 1000);
            setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题', 2000);
            setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题', 3000);
        }
        // 窗口拖动自适应
        let clicking = false;
        window.addEventListener('resize', ()=>{
            if(clicking) return;
            const mBtn = document.querySelector('div[data-silk] [data-silk][aria-controls].inline-flex:nth-child(1)');
            const mCard = document.querySelector('.LongSheet-scrollContent .bg-card.h-\\[90dvh\\]');
            // 当从大屏到窄屏时触发
            if(mBtn?.getBoundingClientRect()?.width && !mCard?.getBoundingClientRect()?.width) {
                mBtn.click();
                clicking = true;
                setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题', 100);
                setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题', 300);
                setTimeout(() => document.querySelector('form textarea[placeholder]:not(.auto-h)').placeholder  = '请输入您的问题',1000);
                setTimeout(()=>clicking = false, 5000);
            }
            // 当从窄屏到大屏时触发
            if(!mBtn?.getBoundingClientRect()?.width && mCard?.getBoundingClientRect()?.width) {
                mBtn.click();
                clicking = true;
                setTimeout(()=>clicking = false, 5000);
            }
        });
        setTimeout(()=>{
            const textarea = document.querySelector('form textarea[placeholder]');
            if(!textarea) return;
            loaded = true;
            textarea.placeholder = '请输入您的问题';
            setTimeout(()=>{
                textarea.classList.add('auto-h');
                // 文档被点击
                document.addEventListener('click', (e) => {
                    if(e.target.closest('form textarea[placeholder]')) {
                        textarea.classList.add('focus');
                    } else {
                        textarea.classList.remove('focus');
                    }
                });
                // 按下回车
                textarea.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
                        textarea.classList.remove('focus');
                        listenFinishedChat();
                        localStorage.setItem('_textarea_cache', '');
                        document.body.classList.add('thinking');
                    }
                    //setTimeout(()=>textarea.classList.remove('focus'), 100);
                });
                // 提交按钮被点击
                document.querySelector('button[type="submit"]').addEventListener('click', (e) => {
                    listenFinishedChat();
                    localStorage.setItem('_textarea_cache', '');
                    document.body.classList.add('thinking');
                });

                const modelBtn = textarea.parentElement?.nextElementSibling?.firstElementChild?.firstElementChild;

                // 插入新建对话按钮
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="new-chat-btn">新建对话</button>`);
                const newChatBtn = modelBtn.querySelector('.new-chat-btn');
                newChatBtn.addEventListener('click', async (e) => {
                    e.preventDefault();
                    // shift+单击强制新建
                    if(e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
                        location.reload();
                        return;
                    }
                    if(newChatDownPage) {
                        // 当有真实对话时才先保存网页
                        if(document.querySelectorAll(' div[data-sender="user"]')?.length) {
                            const result = await savePage();
                            // 默认下载失败不新建
                            if (!result.success) {
                                alert('由于保存聊天失败,如果强制新建请使用shift+单击操作!');
                                return;
                            }
                        }
                        location.reload();
                    } else {
                        // 未开启 newChatDownPage 直接新建
                        location.reload();
                    }
                });

                // 插入对话记录按钮
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="chat-list-btn">对话列表</button>`);
                const chatListBtn = modelBtn.querySelector('.chat-list-btn');
                chatListBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    // 创建弹出层,居中,带关闭按钮,超出可滚动显示
                    const modal = document.createElement('div');
                    modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:500px;max-height:80vh;background:white;border:1px solid #ccc;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:10000;display:flex;flex-direction:column;';
                    const header = document.createElement('div');
                    header.style.cssText = 'padding:10px;display:flex;justify-content:space-between;align-items:center;';
                    header.innerHTML = '<span style="font-weight:bold;" class="chat-list-modal-title">对话列表</span><button style="border:none;background:none;font-size:20px;cursor:pointer;color:#666;">×</button>';
                    const list = document.createElement('div');
                    list.style.cssText = 'overflow-y:auto;padding:10px;padding-top:0;flex:1;';
                    modal.appendChild(header);
                    modal.appendChild(list);
                    const overlay = document.createElement('div');
                    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;';
                    document.body.appendChild(overlay);
                    document.body.appendChild(modal);
                    header.querySelector('button').onclick = () => {overlay.remove();modal.remove();};
                    overlay.onclick = () => {overlay.remove();modal.remove();};
                    // 深色主题适配
                    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                        modal.style.background = '#1e1e1e';
                        modal.style.borderColor = '#444';
                        modal.style.color = '#e0e0e0';
                        header.querySelector('button').style.color = '#aaa';
                    }
                    // 获取对话列表
                    const allAsks  = document.querySelectorAll('div[data-sender="user"] > div');
                    document.querySelector('.chat-list-modal-title').textContent = `对话列表 (${allAsks.length})`;
                    allAsks.forEach((item, index) => {
                        // 截取item.textContent前200字符(两行大概需要更多字符)
                        const title = item.textContent.trim();
                        // 添加到弹出层列表
                        const listItem = document.createElement('div');
                        listItem.style.cssText = 'padding:10px;margin:5px 0;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:14px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-all;';
                        listItem.textContent = `${index + 1}. ${title.substring(0, 200)}`;
                        listItem.title = title;
                        listItem.onmouseover = () => listItem.style.background = '#f0f0f0';
                        listItem.onmouseout = () => listItem.style.background = 'white';
                        listItem.onclick = () => {item.scrollIntoView({behavior:'smooth',block:'start'});overlay.remove();modal.remove();};

                        // 深色主题适配列表项
                        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                            listItem.style.background = '#2a2a2a';
                            listItem.style.borderColor = '#444';
                            listItem.style.color = '#e0e0e0';
                            listItem.onmouseover = () => listItem.style.background = '#3a3a3a';
                            listItem.onmouseout = () => listItem.style.background = '#2a2a2a';
                        }

                        list.appendChild(listItem);
                    });
                });

                // 插入注意事项
                if(modelBtn) modelBtn.insertAdjacentHTML('afterend', `<span class="ai-tips">注意:该AI对话不会记忆上次的聊天内容,刷新页面聊天记录丢失!!!请及时保存内容!!!</span>`);

                // ads
                const ftBtn = document.querySelector('.flex-shrink-0:last-child.py-1')?.firstElementChild?.firstElementChild;
                if(ftBtn) ftBtn.insertAdjacentHTML('afterend', `<span class="ai-ads">推荐免费模型:<a href="https://cloud.siliconflow.cn/i/8kP68u0B" target="_blank">硅基</a> 推荐国外模型:<a href="https://api.gpt.ge/register?aff=GlNE" target="_blank">V-API</a> 七牛大福利:<a href="https://zhuanlan.zhihu.com/p/1962631242630534169" target="_blank">如何获取上亿token?</a>  学编程学知识:<a href="https://www.zhihu.com/people/wilsonses" target="_blank">关注作者不迷路</a></span>`);

                // 复制整个对话列表
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="chat-list-copy-btn">复制对话</button>`);
                const chatListCopyBtn = modelBtn.querySelector('.chat-list-copy-btn');
                chatListCopyBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    copyRichText(document.querySelector('[data-radix-scroll-area-viewport] > div'), [], (el) => {
                        // 每个问题前添加h1
                        const userMsgs = el.querySelectorAll('.message-container[data-sender="user"]');
                        userMsgs.forEach((msg, i) => msg.insertAdjacentHTML('beforebegin', `<h1>用户问题${i+1}</h1>\n\n`));
                    });
                    chatListCopyBtn.textContent = '已复制到剪切板';
                    setTimeout(()=>chatListCopyBtn.textContent = '复制对话', 1500);
                });
                // 保存整个对话列表
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="chat-list-down-btn">下载对话</button>`);
                const chatListDownBtn = modelBtn.querySelector('.chat-list-down-btn');
                chatListDownBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    savePage();
                    //alert('右键另存为HTML即可');
                });

                // 历史对话
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="chat-list-history-btn">历史对话</button>`);
                const chatListHistoryBtn = modelBtn.querySelector('.chat-list-history-btn');
                chatListHistoryBtn.addEventListener('click', async (e) => {
                    e.preventDefault();
                    if(!window.webdavClient) {
                        let url, username, password;
                        {
                            let title = '请输入webdav的URL';
                            while(true) {
                                url = prompt(title, historyConfig.webdav.url || '');
                                if(url === null) return;
                                if(url?.trim() === '') title = '用户名不能为空,请重新输入';
                                if(url?.trim()) {
                                    historyConfig.webdav.url = url.trim();
                                    localStorage.setItem('_url', url.trim());
                                    break;
                                }
                            }
                        }
                        {
                            let title = '请输入webdav的用户名';
                            while(true) {
                                username = prompt(title, historyConfig.webdav.username || '');
                                if(username === null) return;
                                if(username?.trim() === '') title = '用户名不能为空,请重新输入';
                                if(username?.trim()) {
                                    historyConfig.webdav.username = username.trim();
                                    localStorage.setItem('_username', username.trim());
                                    break;
                                }
                            }
                        }
                        {
                            let title = '请输入webdav的密码';
                            while(true) {
                                password = prompt(title, historyConfig.webdav.password || '');
                                if(password === null) return;
                                if(password?.trim() === '') title = '密码不能为空,请重新输入';
                                if(password?.trim()) {
                                    historyConfig.webdav.password = password.trim();
                                    localStorage.setItem('_password', password.trim());
                                    break;
                                }
                            }
                        }
                        createWebdavClient();
                        if(!window.webdavClient) {
                            alert('Webdav信息配置有误');
                            return;
                        }
                    }
                    const client = window.webdavClient;
                    // 创建弹出层(修改:固定高度防止跳动)
                    const modal = document.createElement('div');
                    modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:500px;height:80vh;max-height:600px;background:white;border:1px solid #ccc;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:10000;display:flex;flex-direction:column;';
                    const header = document.createElement('div');
                    header.style.cssText = 'padding:10px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #eee;';
                    header.innerHTML = '<span style="font-weight:bold;" class="chat-list-modal-title">历史对话 (加载中...)</span><button style="border:none;background:none;font-size:20px;cursor:pointer;color:#666;">×</button>';
                    // 添加搜索框
                    const searchContainer = document.createElement('div');
                    searchContainer.style.cssText = 'padding:10px;border-bottom:1px solid #eee;';
                    const searchInput = document.createElement('input');
                    searchInput.type = 'text';
                    searchInput.placeholder = '搜索历史对话...';
                    searchInput.style.cssText = 'width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;font-size:14px;';
                    searchContainer.appendChild(searchInput);
                    const list = document.createElement('div');
                    list.style.cssText = 'overflow-y:auto;padding:10px;padding-top:0;flex:1;';
                    modal.appendChild(header);
                    modal.appendChild(searchContainer);
                    modal.appendChild(list);
                    const overlay = document.createElement('div');
                    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;';
                    document.body.appendChild(overlay);
                    document.body.appendChild(modal);
                    header.querySelector('button').onclick = () => {overlay.remove();modal.remove();};
                    overlay.onclick = () => {overlay.remove();modal.remove();};
                    // 深色主题适配
                    const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
                    if (isDarkMode) {
                        modal.style.background = '#1e1e1e';
                        modal.style.borderColor = '#444';
                        modal.style.color = '#e0e0e0';
                        header.style.borderBottomColor = '#444';
                        searchContainer.style.borderBottomColor = '#444';
                        searchInput.style.background = '#2a2a2a';
                        searchInput.style.borderColor = '#444';
                        searchInput.style.color = '#e0e0e0';
                        header.querySelector('button').style.color = '#aaa';
                    }
                    try {
                        // 获取文件列表
                        let fileList = await client.getDirectoryContents(historyConfig.webdav.savePath);
                        fileList = fileList.filter(f => f.type === 'file' && f.filename.endsWith('.html'));
                        // 根据 historyConfig.showDays 配置过滤最近n天的文件
                        const showDays = historyConfig.showDays || 30;
                        const cutoffDate = new Date();
                        cutoffDate.setDate(cutoffDate.getDate() - showDays);
                        // 过滤并排序文件(按日期降序)
                        fileList = fileList.filter(f => {
                            const match = f.filename.match(/^\[?(\d{4}-\d{1,2}-\d{1,2})[_-\s]/);
                            if (match) {
                                const fileDate = new Date(match[1]);
                                return fileDate >= cutoffDate;
                            }
                            return true;
                        }).sort((a, b) => {
                            return b.filename.localeCompare(a.filename);
                        });
                        // 渲染文件列表的函数
                        const renderList = (filteredFiles) => {
                            list.innerHTML = '';
                            // 更新标题显示文件数量
                            document.querySelector('.chat-list-modal-title').textContent = `历史对话 (${filteredFiles.length})`;
                            if (filteredFiles.length === 0) {
                                list.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">暂无匹配的历史对话记录</div>';
                                return;
                            }
                            filteredFiles.forEach((file, index) => {
                                // 从文件名中提取标题
                                let displayTitle = file.filename.replace(/\.html$/, '');
                                displayTitle = decodeURIComponent(displayTitle);
                                // 修改:使用容器包装列表项和删除按钮
                                const listItemContainer = document.createElement('div');
                                listItemContainer.style.cssText = 'position:relative;margin:5px 0;';
                                const listItem = document.createElement('div');
                                listItem.style.cssText = 'padding:10px;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:14px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-all;transition:background 0.2s;';
                                listItem.textContent = `${index + 1}. ${displayTitle}`;
                                listItem.title = `${file.filename}\n点击在新标签页打开`;
                                // 创建删除按钮
                                const deleteBtn = document.createElement('button');
                                deleteBtn.innerHTML = '🗑';
                                deleteBtn.style.cssText = 'position:absolute;right:10px;top:50%;transform:translateY(-50%);background:rgba(128,128,128,0.2);color:inherit;border:none;border-radius:4px;padding:5px 10px;cursor:pointer;font-size:16px;opacity:0;transition:opacity 0.2s,background 0.2s;z-index:1;';
                                deleteBtn.title = '删除此历史记录';
                                // 鼠标悬停显示/隐藏删除按钮
                                listItemContainer.onmouseover = () => {
                                    listItem.style.background = isDarkMode ? '#3a3a3a' : '#f0f0f0';
                                    deleteBtn.style.opacity = '1';
                                };
                                listItemContainer.onmouseout = () => {
                                    listItem.style.background = isDarkMode ? '#2a2a2a' : 'white';
                                    deleteBtn.style.opacity = '0';
                                };
                                // 删除按钮悬停效果
                                deleteBtn.onmouseover = () => {
                                    deleteBtn.style.background = isDarkMode ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.1)';
                                };
                                deleteBtn.onmouseout = () => {
                                    deleteBtn.style.background = 'rgba(128,128,128,0.2)';
                                };
                                // 删除按钮点击事件
                                deleteBtn.onclick = async (e) => {
                                    e.stopPropagation(); // 阻止触发列表项的点击事件

                                    if (!confirm(`确定要删除这条历史记录吗?\n\n${displayTitle}`)) {
                                        return;
                                    }
                                    try {
                                        // 调用 WebDAV 删除接口
                                        await client.deleteFile(file.path.replace('/dav', ''));

                                        // 从 fileList 中移除该项
                                        const fileIndex = filteredFiles.indexOf(file);
                                        if (fileIndex > -1) {
                                            filteredFiles.splice(fileIndex, 1);
                                        }

                                        // 同时从原始 fileList 中移除
                                        const originalIndex = fileList.indexOf(file);
                                        if (originalIndex > -1) {
                                            fileList.splice(originalIndex, 1);
                                        }
                                        // 重新渲染列表(会自动更新序号)
                                        renderList(filteredFiles);

                                    } catch (err) {
                                        console.error('删除文件失败:', err);
                                        alert('删除失败: ' + err.message);
                                    }
                                };
                                // 列表项点击事件
                                listItem.onclick = async () => {
                                    try {
                                        // 获取文件内容
                                        const fileContent = await client.getFileContents(file.path.replace('/dav', ''), { format: 'text' });
                                        // 使用 Blob URL 方式打开HTML内容
                                        const blob = new Blob([fileContent], { type: 'text/html' });
                                        const blobUrl = URL.createObjectURL(blob);
                                        const newWindow = window.open(blobUrl, '_blank');
                                        if (newWindow) {
                                            // 在新窗口加载完成后释放 blob URL
                                            newWindow.addEventListener('load', () => {
                                                setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
                                            });
                                        } else {
                                            // 如果窗口打开失败,立即释放 blob URL
                                            URL.revokeObjectURL(blobUrl);
                                            alert('请允许弹出窗口以查看历史对话');
                                        }
                                    } catch (err) {
                                        console.error('读取文件失败:', err);
                                        alert('读取文件失败: ' + err.message);
                                    }
                                };
                                // 深色主题适配列表项
                                if (isDarkMode) {
                                    listItem.style.background = '#2a2a2a';
                                    listItem.style.borderColor = '#444';
                                    listItem.style.color = '#e0e0e0';
                                }
                                listItemContainer.appendChild(listItem);
                                listItemContainer.appendChild(deleteBtn);
                                list.appendChild(listItemContainer);
                            });
                        };
                        // 初始渲染完整列表
                        renderList(fileList);
                        // 搜索功能(修改:支持中文输入法)
                        let isComposing = false;
                        searchInput.addEventListener('compositionstart', () => {
                            isComposing = true;
                        });
                        searchInput.addEventListener('compositionend', (e) => {
                            isComposing = false;
                            const searchTerm = e.target.value.toLowerCase().trim();
                            if (!searchTerm) {
                                renderList(fileList);
                                return;
                            }
                            const filteredFiles = fileList.filter(file => {
                                const displayTitle = decodeURIComponent(file.filename.replace(/\.html$/, '')).toLowerCase();
                                return displayTitle.includes(searchTerm);
                            });
                            renderList(filteredFiles);
                        });
                        searchInput.addEventListener('input', (e) => {
                            if (isComposing) return;
                            const searchTerm = e.target.value.toLowerCase().trim();
                            if (!searchTerm) {
                                renderList(fileList);
                                return;
                            }
                            const filteredFiles = fileList.filter(file => {
                                const displayTitle = decodeURIComponent(file.filename.replace(/\.html$/, '')).toLowerCase();
                                return displayTitle.includes(searchTerm);
                            });
                            renderList(filteredFiles);
                        });
                    } catch (error) {
                        console.error('获取历史对话列表失败:', error);
                        document.querySelector('.chat-list-modal-title').textContent = '历史对话 (加载失败)';
                        list.innerHTML = `<div style="text-align:center;padding:20px;color:#f44;">加载失败: ${error.message}</div>`;
                    }
                });

                // 帮助按钮
                if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', `<button class="chat-help-btn">帮助</button>`);
                const helpBtn = modelBtn.querySelector('.chat-help-btn');
                helpBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    window.open('https://zhuanlan.zhihu.com/p/1966090276255793472');
                });

                // 输入框实时保存输入
                let inputTimeId;
                textarea.addEventListener('input', () => {
                    if(inputTimeId) clearTimeout(inputTimeId);
                    inputTimeId = setTimeout(() => {
                        if(textarea.value.trim()!=='') {
                           localStorage.setItem('_textarea_cache', textarea.value);
                        }
                    }, 500);
                });
                if(localStorage.getItem('_textarea_cache') && textarea.value.trim()==='') {
                    textarea.value = localStorage.getItem('_textarea_cache') || '';
                }

                //通过参数自动查询(该站无法触发输入事件)
//                 if(urlParams.has('q') && urlParams.get('q')) {
//                     const q = urlParams.get('q');
//                     textarea.value = q;
//                     const event = new Event('input', { bubbles: true });
//                     textarea.dispatchEvent(event);
//                     textarea.nextElementSibling?.firstElementChild?.lastElementChild?.firstElementChild?.click();
//                 }

            }, 1500);
        }, 100);
    };
    setTimeout(()=>{
        document.title = 'Cursor Chat';
    }, 800);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 3000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 5000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 8000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 10000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 15000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 20000);
    setTimeout(()=>{
        if(!loaded) showAI();
    }, 30000);
    window.addEventListener('beforeunload', function (event) {
        if(document.querySelectorAll(' div[data-sender="user"]')?.length) {
            // 设置 returnValue 为非空字符串,会触发浏览器的确认弹窗
            event.returnValue = '你确定要离开此页面吗?未保存的聊天内容可能会丢失!!';
            // 注意:现代浏览器通常忽略自定义消息,只显示默认提示
            return event.returnValue;
        }
    });
    // 防止标签页被丢弃
    setInterval(() => {
        // 轻量级操作,告诉浏览器"我还有用"
        performance.mark('keep-alive');
    }, 30000); // 每30秒
    document.addEventListener('mouseover', (e) => {
        // ai消息复制
        const assistantMsgEl = e.target.closest('.message-container[data-sender="assistant"]');
        if(assistantMsgEl) {
            if(assistantMsgEl?.querySelector('.chat-copy-btn')) return;
            assistantMsgEl.insertAdjacentHTML('beforeend', `<div class="chat-copy-btn">复制对话内容</div>`);
            const chatCopyBtn = assistantMsgEl.querySelector('.chat-copy-btn');
            chatCopyBtn.addEventListener('click', (e) => {
                copyRichText(assistantMsgEl.firstElementChild);
                chatCopyBtn.textContent = '已复制到剪切板';
                setTimeout(()=>chatCopyBtn.textContent = '复制对话内容', 1500);
            });
        } else {
            // 用户消息复制
            const userMsgEl = e.target.closest('.message-container[data-sender="user"]');
            if(userMsgEl) {
                const msg1 = userMsgEl.firstElementChild;
                if(!msg1 ||  msg1.querySelector('.user-msg-copy-btn')) return;
                const copySvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>`;
                const copyOkSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>`;
                const html = `<button class="user-msg-copy-btn inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg:not([class*='size-'])]:size-4 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-7 shrink-0 absolute right-2 z-10 opacity-0 transition-opacity duration-200 group-hover:opacity-100 top-2" data-slot="button" type="button"><div class="relative size-3.5">${copySvg}</div></button>`;
                msg1.insertAdjacentHTML('beforeend', html);
                const copyBtn = msg1.querySelector('.user-msg-copy-btn');
                const copyBtnDiv = copyBtn.firstElementChild;
                copyBtn.addEventListener('click', (e) => {
                    copyRichText(msg1.firstElementChild);
                    copyBtnDiv.innerHTML = copyOkSvg;
                    setTimeout(()=>{
                        copyBtnDiv.innerHTML = copySvg;
                    }, 1500);
                });
            }
        }
    });
    async function copyRichText(element, excludes = [], beforeCallback) {
        try {
            const clonedElement = element.cloneNode(true);

            // 支持传入自定义排除类
            const defaultExcludes = ['.chat-copy-btn', '.user-msg-copy-btn'];
            const allExcludes = [...defaultExcludes, ...excludes];

            // 组合选择器一次性查询
            const combinedSelector = allExcludes.join(', ');
            clonedElement.querySelectorAll(combinedSelector).forEach(el => el.remove());

            if(typeof beforeCallback === 'function') beforeCallback(clonedElement, excludes);

            const html = clonedElement.innerHTML;
            const text = clonedElement.innerText;
            const blob = new Blob([html], { type: 'text/html' });
            const textBlob = new Blob([text], { type: 'text/plain' });
            const clipboardItem = new ClipboardItem({
                'text/html': blob,
                'text/plain': textBlob
            });
            await navigator.clipboard.write([clipboardItem]);
            //console.log('✅ 富文本已复制');
        } catch (err) {
            console.error('❌ 复制失败:', err);
        }
    }
    async function savePage(realdown = true) {
        try {
            // 1. 克隆整个文档
            const clonedDoc = document.cloneNode(true);

            // 2. 内联所有外部 CSS
            const styleSheets = Array.from(document.styleSheets);
            let inlineStyles = '<style>\n';

            for (const sheet of styleSheets) {
                if(sheet.href && sheet.href.startsWith('chrome-extension://')) continue;
                try {
                    const rules = Array.from(sheet.cssRules || sheet.rules);
                    rules.forEach(rule => {
                        inlineStyles += rule.cssText + '\n';
                    });
                } catch (e) {
                    // 跨域 CSS 无法访问,尝试重新获取
                    if (sheet.href) {
                        try {
                            const response = await fetch(sheet.href);
                            const css = await response.text();
                            inlineStyles += css + '\n';
                        } catch (err) {
                            console.warn('无法加载样式表:', sheet.href);
                        }
                    }
                }
            }
            inlineStyles += '</style>\n';

            // 3. 将内联样式插入到 head
            const head = clonedDoc.querySelector('head');
            const styleElement = clonedDoc.createElement('div');
            styleElement.innerHTML = inlineStyles;
            head.appendChild(styleElement.firstChild);

            // 4. 移除原有的外部样式表链接
            clonedDoc.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
                link.remove();
            });

            const replaceLink = (clonedDoc, selector, attr, act) => {
                clonedDoc.querySelectorAll(selector).forEach(el => {
                    if(act === 'remove') {el.remove();return;}
                    const attrVal = el.getAttribute(attr);
                    if(attrVal.startsWith('http')) ; // pass
                    else if(attrVal.startsWith('/')) el[attr] = location.origin + attrVal;
                    else if(attrVal.startsWith('./')) el[attr] = location.origin + location.pathname + attrVal.replace(/^\./, '');
                    else if(!(attrVal.startsWith('data:')||attrVal.startsWith('blob:'))) el[attr] = location.origin + location.pathname + '/' + attrVal;
                });
            }
            replaceLink(clonedDoc, 'script[src]', 'src', 'remove');
            replaceLink(clonedDoc, 'link[href]', 'href');
            replaceLink(clonedDoc, 'a[href]', 'href');
            replaceLink(clonedDoc, 'img[src]', 'src');

            // 5. 转换所有图片为 Base64(可选,会增加文件大小)
            const images = clonedDoc.querySelectorAll('img');
            const imagePromises = Array.from(images).map(async (img, index) => {
                const originalImg = document.querySelectorAll('img')[index];
                try {
                    const actualWidth = originalImg.naturalWidth || originalImg.width;

                    // 只对宽度128以下的图片进行编码
                    if (actualWidth <= 128) {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        canvas.width = actualWidth;
                        canvas.height = originalImg.naturalHeight || originalImg.height;
                        ctx.drawImage(originalImg, 0, 0);
                        const dataURL = canvas.toDataURL('image/png');
                        img.src = dataURL;
                    }
                } catch (e) {
                    console.warn('无法转换图片:', img.src, e);
                }
            });

            await Promise.all(imagePromises);

            const cssContent = `
                .ries-translation-extension-container, textarea, .new-chat-btn,
                div[data-silk] [data-silk][aria-controls].inline-flex:nth-child(1),
                .ai-tips, textarea + div > :first-child > :last-child,.chat-list-down-btn,.chat-list-history-btn {display:none}
                .bg-card.min-h-\\[80px\\]{min-height: auto;}
                div:has(>textarea) + .pb-2, div.flex-shrink-0:has(textarea) {padding-bottom: 0;}
                .hidden {display: block;}
            `;
            const style = clonedDoc.createElement('style');
            style.type = 'text/css';
            style.innerHTML = cssContent;
            clonedDoc.head.appendChild(style);

            const jsContent = `
                ${copyRichText.toString()}
                document.addEventListener('click', (e) => {
                // 全部复制
                if(e.target.closest('.chat-list-copy-btn')) {
                    e.preventDefault();
                    const chatListCopyBtn = e.target.closest('.chat-list-copy-btn');
                    copyRichText(document.querySelector('[data-radix-scroll-area-viewport] > div'), [], (el) => {
                        // 每个问题前添加h1
                        const userMsgs = el.querySelectorAll('.message-container[data-sender="user"]');
                        userMsgs.forEach((msg, i) => msg.insertAdjacentHTML('beforebegin', \`<h1>用户问题\${i+1}</h1>\n\n\`));
                    });
                    chatListCopyBtn.textContent = '已复制到剪切板';
                    setTimeout(()=>chatListCopyBtn.textContent = '复制对话', 1500);
                    return;
                }
                // 单个复制
                if(e.target.closest('.chat-copy-btn')) {
                    e.preventDefault();
                    const copyBtn = e.target.closest('.chat-copy-btn');
                    copyRichText(copyBtn.previousElementSibling);
                    copyBtn.textContent = '已复制到剪切板';
                    setTimeout(()=>copyBtn.textContent = '复制对话内容', 1500);
                    return;
                }
                // 用户消息复制
                if(e.target.closest('.user-msg-copy-btn')) {
                    e.preventDefault();
                    const userMsgEl = e.target.closest('.message-container[data-sender="user"]');
                    if(userMsgEl) {
                        const msg1 = userMsgEl.firstElementChild;
                        copyRichText(msg1.firstElementChild);
                        const copySvg = \`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>\`;
                        const copyOkSvg = \`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>\`;
                        const copyBtn = e.target.closest('.user-msg-copy-btn');
                        const copyBtnDiv = copyBtn.firstElementChild;
                        copyBtnDiv.innerHTML = copyOkSvg;
                        setTimeout(()=>{
                            copyBtnDiv.innerHTML = copySvg;
                        }, 1500);
                    }
                    return;
                }
                // 对话列表
                if(e.target.closest('.chat-list-btn')) {
                    e.preventDefault();
                    // 创建弹出层,居中,带关闭按钮,超出可滚动显示
                    const modal = document.createElement('div');
                    modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:500px;max-height:80vh;background:white;border:1px solid #ccc;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:10000;display:flex;flex-direction:column;';
                    const header = document.createElement('div');
                    header.style.cssText = 'padding:10px;display:flex;justify-content:space-between;align-items:center;';
                    header.innerHTML = '<span style="font-weight:bold;" class="chat-list-modal-title">对话列表</span><button style="border:none;background:none;font-size:20px;cursor:pointer;color:#666;">×</button>';
                    const list = document.createElement('div');
                    list.style.cssText = 'overflow-y:auto;padding:10px;padding-top:0;flex:1;';
                    modal.appendChild(header);
                    modal.appendChild(list);
                    const overlay = document.createElement('div');
                    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;';
                    document.body.appendChild(overlay);
                    document.body.appendChild(modal);
                    header.querySelector('button').onclick = () => {overlay.remove();modal.remove();};
                    overlay.onclick = () => {overlay.remove();modal.remove();};
                    // 深色主题适配
                    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                        modal.style.background = '#1e1e1e';
                        modal.style.borderColor = '#444';
                        modal.style.color = '#e0e0e0';
                        header.querySelector('button').style.color = '#aaa';
                    }
                    // 获取对话列表
                    const allAsks  = document.querySelectorAll('div[data-sender="user"] > div');
                    document.querySelector('.chat-list-modal-title').textContent = \`对话列表 (\${allAsks.length})\`;
                    allAsks.forEach((item, index) => {
                        // 截取item.textContent前200字符(两行大概需要更多字符)
                        const title = item.textContent.trim();
                        // 添加到弹出层列表
                        const listItem = document.createElement('div');
                        listItem.style.cssText = 'padding:10px;margin:5px 0;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:14px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-all;';
                        listItem.textContent = \`\${index + 1}. \${title.substring(0, 200)}\`;
                        listItem.title = title;
                        listItem.onmouseover = () => listItem.style.background = '#f0f0f0';
                        listItem.onmouseout = () => listItem.style.background = 'white';
                        listItem.onclick = () => {item.scrollIntoView({behavior:'smooth',block:'start'});overlay.remove();modal.remove();};

                        // 深色主题适配列表项
                        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                            listItem.style.background = '#2a2a2a';
                            listItem.style.borderColor = '#444';
                            listItem.style.color = '#e0e0e0';
                            listItem.onmouseover = () => listItem.style.background = '#3a3a3a';
                            listItem.onmouseout = () => listItem.style.background = '#2a2a2a';
                        }

                        list.appendChild(listItem);
                    });
                    return;
                }
                // 复制code
                if(e.target.closest('button:has(svg.lucide-copy):not(.user-msg-copy-btn)')) {
                    e.preventDefault();
                    const copyBtn = e.target.closest('button:has(svg.lucide-copy):not(.user-msg-copy-btn)');
                    copyRichText(copyBtn.nextElementSibling.querySelector('code'));
                    const copySvg = \`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>\`;
                    const copyOkSvg = \`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-0 scale-50" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check absolute inset-0 size-3.5 transition-all duration-200 ease-in-out text-muted-foreground opacity-100 scale-100" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>\`;
                    const copyBtnDiv = copyBtn.firstElementChild;
                    copyBtnDiv.innerHTML = copyOkSvg;
                    setTimeout(()=>{
                        copyBtnDiv.innerHTML = copySvg;
                    }, 1500);
                    return;
                }
                // 帮助按钮
                if(e.target.closest('.chat-help-btn')){
                    e.preventDefault();
                    window.open('https://zhuanlan.zhihu.com/p/1966090276255793472');
                }
            });
            `;
            const script = clonedDoc.createElement('script');
            script.type = 'text/javascript';
            script.innerHTML = jsContent;
            clonedDoc.body.appendChild(script);

            // 6. 添加 meta 标签确保编码正确
            if (!clonedDoc.querySelector('meta[charset]')) {
                const meta = clonedDoc.createElement('meta');
                meta.setAttribute('charset', 'UTF-8');
                head.insertBefore(meta, head.firstChild);
            }

            // 7. 获取完整 HTML
            const doctype = '<!DOCTYPE html>\n';
            const html = doctype + clonedDoc.documentElement.outerHTML;

            // 使用页面标题作为文件名
            const userMsgEl = document.querySelector('.message-container[data-sender="user"]');
            let firstTitle = userMsgEl?.firstElementChild?.firstElementChild?.textContent.trim();
            firstTitle = firstTitle?.length > 50 ? firstTitle.substring(0, 50) + '...' : firstTitle;
            now = now || '['+new Date().toLocaleString().substring(0, 15).replace(/\//g, '-').replace(/\s+/, ' ').replace(/:/g, '.').replace(/-(\d)([-_\s])/, '-0$1$2')+']';
            const title = now + ' ' + (firstTitle || document.title || 'cursor-chat');
            const filename = title + '.html';

            if(!realdown) return { success: true, filename, html };

            // 8. 创建 Blob 并下载
            const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const link = document.createElement('a');
            link.href = url;
            link.download = filename;

            // 触发下载
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            // 清理
            setTimeout(() => URL.revokeObjectURL(url), 100);

            //console.log('页面已保存为:', filename);
            return { success: true, filename };

        } catch (error) {
            console.error('保存失败:', error);
            alert('保存失败,请使用浏览器的 网页另存为 功能');
            return { success: false, error };
        }
    }
    // 拦截api
    function interceptFetch() {
        let originalFetch = window.fetch;
        window.fetch = async function(url, init={}) {
            // 过滤跟踪信息保护用户隐私
            if (url.toString().endsWith('_vercel/insights/event')) {
                // 直接构造一个成功的 Response,无需原始 response
                return new Response('OK', {
                    status: 200,
                    statusText: 'OK',
                    headers: {
                        'Content-Type': 'text/plain; charset=utf-8'
                    }
                });
            }
            // 增加自定义提示词
           else if(url.toString().endsWith('/api/chat')) {
               // 克隆 init 避免修改原始对象(尤其 headers/body 是只读或已使用过)
               const modifiedInit = { ...init };

               // 读取并解析请求体
               let bodyText = typeof init.body === 'string' ? init.body : null;
               if (!bodyText && init.body instanceof ReadableStream) {
                   // 如果 body 是流,需要先读取(但通常在浏览器中 fetch 拦截时 body 是字符串)
                   // 为简化,假设 body 是字符串(如你提供的 curl 中是 --data-raw 字符串)
                   // 若实际使用中 body 是流,需用更复杂的代理方式(不推荐在浏览器中修改流)
                   console.warn('Body is a stream; cannot modify. Skipping prompt injection.');
               }
               if (bodyText) {
                   try {
                       const payload = JSON.parse(bodyText);

                       // 去除默认文档
                       if(payload.context?.[0]?.filePath) payload.context[0].filePath = '';

                       // 在每条用户消息的 text 前添加自定义提示词
                       const customPrompt = '{{customPrompt}}' ? "{{customPrompt}} \n\n" : '';
                       if (Array.isArray(payload.messages) && payload.messages.length > 0) {
                            const lastMessage = payload.messages[payload.messages.length - 1];
                            // 仅当最后一条是用户消息时才注入提示词
                            if (lastMessage.role === 'user' && Array.isArray(lastMessage.parts)) {
                                lastMessage.parts.forEach(part => {
                                    if (part.type === 'text' && typeof part.text === 'string') {
                                        // 避免重复添加
                                        if (!part.text.trim().startsWith(customPrompt.trim())) {
                                            part.text = customPrompt + part.text;
                                        }
                                    }
                                });
                            }
                        }

                       // 更新请求体
                       modifiedInit.body = JSON.stringify(payload);
                       // 确保 Content-Type 正确(虽然通常已有)
                       modifiedInit.headers = new Headers(init.headers || {});
                       modifiedInit.headers.set('content-type', 'application/json');
                   } catch (e) {
                       console.error('Failed to parse or modify /api/chat request body:', e);
                   }

                   // 使用修改后的 init 发送请求
                   return originalFetch(url, modifiedInit);
              }
           }
           return originalFetch(url, init);
        };
    }
    // 过滤跟踪信息保护用户隐私(网速过快时不生效)
    function interceptCJS() {
        const obs = new MutationObserver(muts => {
            for (const mut of muts) {
                for (const node of mut.addedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (node.tagName === 'SCRIPT') {
                        const src = node.src || '';
                        const txt = node.textContent || '';
                        if (
                            src.includes('/c.js') ||
                            txt.includes('V_C = window.V_C') ||
                            txt.includes('_vercel/insights')
                        ) {
                            //console.log('🚫 阻止脚本执行:', src || 'inline');
                            node.remove();
                        }
                    }
                }
            }
        });
        obs.observe(document, { childList: true, subtree: true });
    }
    function injectContentJs() {
        const script = document.createElement('script');
        script.textContent = `
            (${interceptFetch.toString()?.replace(/\{\{customPrompt\}\}/g, customPrompt)})();
            (${interceptCJS.toString()})();
        `;
        document.body.appendChild(script);
    }
    injectContentJs();

    function createWebdavClient() {
        if (historyConfig && historyConfig.enable && historyConfig.webdav) {
            console.log(window.fetch);
            const webdav = historyConfig.webdav;
            if(!webdav.url || !webdav.username || !webdav.password) return;
            window.webdavClient = new WebDAVClient({
                url: webdav.url,
                username: webdav.username,
                password: webdav.password
            });
        }
    }
    createWebdavClient();


    function listenFinishedChat() {
        // 完成对话时保存到webdav
        onFinishedChat(() => {
            document.body.classList.remove('thinking');
            if(!window.webdavClient) return;
            setTimeout(async () => {
                const result = await savePage(false);
                if (!result.success) {
                    console.log('获取聊天信息失败');
                    toastError('同步失败:获取聊天信息失败');
                    return;
                }
                const client = window.webdavClient;
                const remoteDir = '/' + historyConfig.webdav.savePath.replace(/^\/|\/$/g, '');
                const remotePath = remoteDir + '/' + result.filename;
                const localContent = result.html;
                try {
                    // 检查并创建目录(如果不存在)
                    console.log('syncing');
                    const dirExists = await client.exists(remoteDir);
                    if (!dirExists) {
                        console.log('目录不存在,正在创建:', remoteDir);
                        await client.createDirectory(remoteDir);
                        console.log('目录创建成功');
                    }
                    const exists = await client.exists(remotePath);
                    if (exists) {
                        const remoteContent = await client.getFileContents(remotePath, { format: "text" });
                        if (remoteContent !== localContent) {
                            console.log('文件已变更,正在同步...');
                            await client.putFileContents(remotePath, localContent, { overwrite: true });
                            console.log('同步完成');
                        } else {
                            console.log('文件无变化,无需同步');
                        }
                    } else {
                        console.log('文件不存在,正在创建...');
                        await client.putFileContents(remotePath, localContent);
                        console.log('文件创建成功');
                    }
                } catch (error) {
                    toastError('同步失败:' + error.message);
                    console.error('同步失败:', error);
                }
            }, 500);
        });
    }
    function onFinishedChat(callback) {
        // 元素子节点被删除
        const onSubsRemove = (callback) => {
            const targetParent = document.querySelector('div.flex-shrink-0 > div.absolute'); // thinking按钮
            if (targetParent && !targetParent.handOnSubsRemove) {
              targetParent.handOnSubsRemove = true;
              // 创建一个 MutationObserver 实例
              const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                  if (mutation.type === 'childList') {
                    // mutation.removedNodes 包含被删除的节点
                    if (mutation.removedNodes.length > 0) {
                      // 如果你只想知道“有直接子元素被删除”,可以在这里执行操作
                      //console.log('有直接子元素被删除了:', mutation.removedNodes);
                      callback();
                    }
                  }
                }
              });

              // 开始观察 targetParent 的子节点变化
              observer.observe(targetParent, {
                childList: true // 只监听直接子节点的增删
              });
            }
        };
        setTimeout(()=>onSubsRemove(callback), 500);
        // 元素被添加
//         const observer = new MutationObserver((mutations) => {
//             mutations.forEach((mutation) => {
//                 mutation.addedNodes.forEach((node) => {
//                     if (node.nodeType === 1) {
//                         // 检查新增节点是否匹配目标选择器
//                         if (node.matches('div.flex-shrink-0 > div.absolute')) {
//                             //console.log('检测到目标元素被添加1:', node);
//                             if(!node.handOnSubsRemove) {onSubsRemove(callback); node.handOnSubsRemove = true;}
//                             //observer.disconnect();
//                         }
//                         // 也检查新增节点的子元素
//                         const targets = node.querySelectorAll('div.flex-shrink-0 > div.absolute');
//                         if(targets.length > 0) {
//                             targets.forEach(target => {
//                                 //console.log('检测到目标元素被添加2:', target);
//                                 if(!target.handOnSubsRemove) {onSubsRemove(callback); target.handOnSubsRemove = true;}
//                             });
//                             //observer.disconnect();
//                         }
//                     }
//                 });
//             });
//         });
//         observer.observe(document.body, {
//             childList: true,
//             subtree: true  // 观察所有后代节点
//         });
    }
    // 吐司提示窗
    function toast(msg, t = 7000, top, left) {
        const el = Object.assign(document.createElement('div'), {innerHTML: msg, style: `position:fixed;top:${top||20}px;left:${(left?left+'px':'')||'50%'};${left?'':'transform:translateX(-50%);'}background:#333;color:#fff;padding:8px 16px;border-radius:4px;font-size:14px;z-index:9999;opacity:0;transition:opacity .3s;`});
        document.body.appendChild(el);void el.offsetHeight;el.style.opacity = 1;
        setTimeout(() => { el.style.opacity = 0; setTimeout(() => el.remove(), 300);}, t);
    }
    function toastError(msg, t = 2000, top, left) {
        toast('<span style="color:#FF6F61">'+msg+'</span>', t, top, left);
    }
})();