// ==UserScript== // @name Cursor Chat // @namespace Cursor Chat // @version 1.0.18 // @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://scriptcat.org/lib/4521/1.0.2/WebDAVClient.js?sha384-tB6ti4GhpFScW10JSgHEfmZjNRQcX6B+u5oAUnwiTi3oxmTCMCF+ffVl9hF/a4fP // @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')) return; 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', ``); 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', ``); 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 = '对话列表'; 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', `注意:该AI对话不会记忆上次的聊天内容,刷新页面聊天记录丢失!!!请及时保存内容!!!`); // ads const ftBtn = document.querySelector('.flex-shrink-0:last-child.py-1')?.firstElementChild?.firstElementChild; if(ftBtn) ftBtn.insertAdjacentHTML('afterend', `推荐免费模型:硅基 推荐国外模型:V-API 七牛大福利:如何获取上亿token? 学编程学知识:关注作者不迷路`); // 复制整个对话列表 if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', ``); 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', `

用户问题${i+1}

\n\n`)); }); chatListCopyBtn.textContent = '已复制到剪切板'; setTimeout(()=>chatListCopyBtn.textContent = '复制对话', 1500); }); // 保存整个对话列表 if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', ``); const chatListDownBtn = modelBtn.querySelector('.chat-list-down-btn'); chatListDownBtn.addEventListener('click', (e) => { e.preventDefault(); savePage(); //alert('右键另存为HTML即可'); }); // 历史对话 if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', ``); const chatListHistoryBtn = modelBtn.querySelector('.chat-list-history-btn'); chatListHistoryBtn.addEventListener('click', async (e) => { e.preventDefault(); 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 = '历史对话 (加载中...)'; // 添加搜索框 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})[_-]/); 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 = '
暂无匹配的历史对话记录
'; 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 = `
加载失败: ${error.message}
`; } }); // 帮助按钮 if(modelBtn) modelBtn.insertAdjacentHTML('beforeend', ``); 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', `
复制对话内容
`); 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 = ``; const copyOkSvg = ``; const html = ``; 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 = '\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;} textarea + .pb-2 {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', \`

用户问题\${i+1}

\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 = \`\`; const copyOkSvg = \`\`; 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 = '对话列表'; 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 = \`\`; const copyOkSvg = \`\`; 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 = '\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.substr(0, 50) + '...' : firstTitle; now = now || new Date().toLocaleString().substr(0, 16).replace(/\//g, '-').replace(/\s+/, '_').replace(/:/g, '.'); 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(''+msg+'', t, top, left); } })();