您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在腾讯元宝对话中添加一键复制Markdown按钮(含思考过程),可通过油猴菜单配置导出选项。Refactored for modularity.
// ==UserScript== // @name Yuanbao Markdown Copy // @namespace http://tampermonkey.net/ // @version 0.9 // @description 在腾讯元宝对话中添加一键复制Markdown按钮(含思考过程),可通过油猴菜单配置导出选项。Refactored for modularity. // @author LouisLUO // @match https://yuanbao.tencent.com/* // @icon https://cdn-bot.hunyuan.tencent.com/logo-v2.png // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== // 鸣谢:本脚本部分思路和实现参考了 [腾讯元宝对话导出器 | Tencent Yuanbao Exporter](https://gf.qytechs.cn/zh-CN/scripts/532431-%E8%85%BE%E8%AE%AF%E5%85%83%E5%AE%9D%E5%AF%B9%E8%AF%9D%E5%AF%BC%E5%87%BA%E5%99%A8-tencent-yuanbao-exporter)(by Gao + Gemini 2.5 Pro),并受益于 GitHub Copilot 及 GPT-4.1 的辅助。 // Thanks: Some logic and implementation are inspired by [腾讯元宝对话导出器 | Tencent Yuanbao Exporter](https://gf.qytechs.cn/zh-CN/scripts/532431-%E8%85%BE%E8%AE%AF%E5%85%83%E5%AE%9D%E5%AF%B9%E8%AF%9D%E5%AF%BC%E5%87%BA%E5%99%A8-tencent-yuanbao-exporter) (by Gao + Gemini 2.5 Pro), with help from GitHub Copilot and GPT-4.1. (function () { 'use strict'; // --- Configuration & Constants --- const SCRIPT_NAME = 'Yuanbao Markdown Copy'; const BTN_STYLE = { background: '#13172c', // 修正: 使用 background 而不是 bg backgroundHover: '#24293c', // 新增: hover 时的背景色 color: '#efefef', marginLeft: '8px', padding: '2px 8px', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '14px', transition: 'background 0.2s', }; const EXPORT_BTN_STYLE = { display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '1 1 0', padding: '4px 0', margin: '4px', gap: '4px', }; // SVG Icons const ICON_EXPORT_ALL = ` <svg fill="${BTN_STYLE.color}" width="1em" height="1em" viewBox="0 0 20 20" style="margin-right:4px;vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><path d="M15 15H2V6h2.595s.689-.896 2.17-2H1a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h15a1 1 0 0 0 1-1v-3.746l-2 1.645V15zm-1.639-6.95v3.551L20 6.4l-6.639-4.999v3.131C5.3 4.532 5.3 12.5 5.3 12.5c2.282-3.748 3.686-4.45 8.061-4.45z"/></svg> `; const ICON_DIALOGUE = ` <svg fill="${BTN_STYLE.color}" width="1em" height="1em" viewBox="0 0 24 24" style="margin-right:4px;vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><path d="M21 2H3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h5v3.382a1 1 0 0 0 1.447.894L15.764 18H21a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1Zm-1 14h-5.382a1 1 0 0 0-.447.105L10 17.618V16a1 1 0 0 0-1-1H4V4h16ZM7 7h10v2H7Zm0 4h7v2H7Z"/></svg> `; // --- State Management --- let state = { latestDetailResponse: null, latestResponseSize: 0, latestResponseUrl: null, lastUpdateTime: null }; // --- Settings Management --- const DEFAULT_SETTINGS = { autoInjectCopyBtn: true, exportFormat: 'markdown', // Reserved for future extensions replaceFormulas: true, exportThinkProcess: true, thinkProcessFormat: 'tag', // 'tag' or 'markdown' keepSearchResults: true, headerDowngrade: false }; function getSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('yuanbao_md_settings') || '{}') }; } catch { return { ...DEFAULT_SETTINGS }; } } function saveSettings(settings) { localStorage.setItem('yuanbao_md_settings', JSON.stringify(settings)); } function showSettingsDialog() { const settings = getSettings(); const html = ` <div style="font-size:14px; line-height: 1.8;"> <label><input type="checkbox" id="autoInjectCopyBtn" ${settings.autoInjectCopyBtn ? 'checked' : ''}> 自动注入“复制MD”按钮 (刷新生效)</label><br> <label><input type="checkbox" id="replaceFormulas" ${settings.replaceFormulas ? 'checked' : ''}> 替换行内/块公式语法 (<code>\\(..\\)</code> -> <code>$...$</code>, <code>\\[..\\]</code> -> <code>$$...$$</code>)</label><br> <label><input type="checkbox" id="exportThinkProcess" ${settings.exportThinkProcess ? 'checked' : ''}> 导出思考过程</label><br> <label style="padding-left: 20px;"> 思考过程格式: <select id="thinkProcessFormat" ${!settings.exportThinkProcess ? 'disabled' : ''}> <option value="tag" ${settings.thinkProcessFormat === 'tag' ? 'selected' : ''}><think>标签</option> <option value="markdown" ${settings.thinkProcessFormat === 'markdown' ? 'selected' : ''}>Markdown引用</option> </select> </label><br> <label><input type="checkbox" id="keepSearchResults" ${settings.keepSearchResults ? 'checked' : ''}> 保留网页搜索内容和脚标</label><br> <label><input type="checkbox" id="headerDowngrade" ${settings.headerDowngrade ? 'checked' : ''}> 标题降级 (例: # -> ##)</label><br> <label style="display:none;">导出格式:<select id="exportFormat"><option value="markdown" ${settings.exportFormat === 'markdown' ? 'selected' : ''}>Markdown</option></select></label> </div> `; const wrapper = document.createElement('div'); wrapper.innerHTML = html; const modal = document.createElement('div'); Object.assign(modal.style, { position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%,-50%)', background: '#222', color: '#fff', padding: '24px', borderRadius: '12px', zIndex: 99999, boxShadow: '0 2px 16px #0008' }); modal.appendChild(wrapper); const btnSave = document.createElement('button'); btnSave.textContent = '保存'; btnSave.style.margin = '16px 8px 0 0'; btnSave.onclick = () => { const newSettings = { autoInjectCopyBtn: wrapper.querySelector('#autoInjectCopyBtn').checked, exportFormat: wrapper.querySelector('#exportFormat').value, replaceFormulas: wrapper.querySelector('#replaceFormulas').checked, exportThinkProcess: wrapper.querySelector('#exportThinkProcess').checked, thinkProcessFormat: wrapper.querySelector('#thinkProcessFormat').value, keepSearchResults: wrapper.querySelector('#keepSearchResults').checked, headerDowngrade: wrapper.querySelector('#headerDowngrade').checked }; saveSettings(newSettings); document.body.removeChild(modal); alert('设置已保存,部分设置需刷新页面生效'); }; const btnCancel = document.createElement('button'); btnCancel.textContent = '取消'; btnCancel.onclick = () => document.body.removeChild(modal); modal.appendChild(btnSave); modal.appendChild(btnCancel); document.body.appendChild(modal); const exportThinkProcessCheckbox = wrapper.querySelector('#exportThinkProcess'); const thinkProcessFormatSelect = wrapper.querySelector('#thinkProcessFormat'); exportThinkProcessCheckbox.addEventListener('change', function () { thinkProcessFormatSelect.disabled = !this.checked; }); } // --- Network Interception --- function processYuanbaoResponse(text, url) { if (!url || !url.includes('/api/user/agent/conversation/v1/detail')) return; try { if (text && text.includes('"convs":') && text.includes('"createTime":')) { state.latestDetailResponse = text; state.latestResponseSize = text.length; state.latestResponseUrl = url; state.lastUpdateTime = new Date().toLocaleTimeString(); } } catch (e) { console.error(`${SCRIPT_NAME}: Error processing response`, e); } } function setupNetworkInterceptors() { const originalFetch = window.fetch; window.fetch = async function (...args) { const url = args[0] instanceof Request ? args[0].url : args[0]; const response = await originalFetch.apply(this, args); if (typeof url === 'string' && url.includes('/api/user/agent/conversation/v1/detail')) { response.clone().text().then(text => processYuanbaoResponse(text, url)); } return response; }; const originalXhrOpen = XMLHttpRequest.prototype.open; const originalXhrSend = XMLHttpRequest.prototype.send; const xhrUrlMap = new WeakMap(); XMLHttpRequest.prototype.open = function (method, url) { xhrUrlMap.set(this, url); if (typeof url === 'string' && url.includes('/api/user/agent/conversation/v1/detail')) { this.addEventListener('load', function () { if (this.readyState === 4 && this.status === 200) { processYuanbaoResponse(this.responseText, xhrUrlMap.get(this)); } }); } return originalXhrOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { return originalXhrSend.apply(this, arguments); }; } // --- Markdown Conversion Utilities --- function adjustHeaderLevels(text, increaseBy = 1) { if (!text) return ''; let adjustedText = text.replace(/^(#+)(\s*)(.*?)\s*$/gm, (match, hashes, space, content) => '#'.repeat(hashes.length + increaseBy) + ' ' + content.trim() ); adjustedText = adjustedText.replace(/^>\s*(#+)(\s*)(.*?)\s*$/gm, (match, blockquotePrefix, hashes, space, content) => blockquotePrefix + '#'.repeat(hashes.length + increaseBy) + ' ' + content.trim() ); return adjustedText; } function applyFormulaReplacements(text, shouldReplace) { if (!shouldReplace || !text) return text || ''; return text .replace(/\\\((.+?)\\\)/g, (m, p1) => `$${p1}$`) .replace(/\\\[(.+?)\\\]/gs, (m, p1) => `$$${p1}$$`); } function formatThinkContent(thinkContent, settings) { let content = applyFormulaReplacements(thinkContent, settings.replaceFormulas); if (settings.thinkProcessFormat === 'markdown') { return `> ${content.replace(/\n/g, '\n> ')}\n\n`; } return `<think>\n${content}\n</think>\n\n`; } function processContentBlock(block, settings, refsContext) { let markdown = ''; switch (block.type) { case 'text': { let msg = applyFormulaReplacements(block.msg || '', settings.replaceFormulas); if (settings.keepSearchResults && msg.includes('(@ref)')) { msg = msg.replace(/\[(\d+)\]\(@ref\)/g, (_, n) => `[^${n}]`); // Placeholder, will be adjusted by searchGuid } else if (!settings.keepSearchResults && msg.includes('(@ref)')) { msg = msg.replace(/\[\d+\]\(@ref\)/g, '').trim(); } markdown += `${msg}\n\n`; break; } case 'think': if (settings.exportThinkProcess && block.content) { markdown += formatThinkContent(block.content, settings); } break; case 'searchGuid': { let text = applyFormulaReplacements(block.msg || block.content || '', settings.replaceFormulas); if (settings.keepSearchResults) { let blockScopedRefStartIndex = refsContext.nextRefIdx; if (block.docs && block.docs.length > 0) { block.docs.forEach(doc => { refsContext.refsArray.push({ idx: refsContext.nextRefIdx++, title: doc.title || '无标题', url: doc.url || '#' }); }); } let currentRefPlaceholderIdx = blockScopedRefStartIndex; text = text.replace(/\[(\d+)\]\(@ref\)/g, () => `[^${currentRefPlaceholderIdx++}]`); markdown += text + '\n\n'; } else if (text) { text = text.replace(/\[\d+\]\(@ref\)/g, '').trim(); markdown += text + '\n\n'; } break; } case 'image': case 'code': case 'pdf': markdown += `[${block.fileName || '未知文件'}](${block.url || '#'})\n\n`; break; } return markdown; } function processSpeech(speech, settings, refsContext) { let markdown = ''; if (speech.content && speech.content.length > 0) { speech.content.forEach(block => { markdown += processContentBlock(block, settings, refsContext); }); } return markdown; } function extractUserMessageAndMedia(turn, settings) { let userTextMsg = ''; let mediaMarkdown = ''; if (turn.speechesV2 && turn.speechesV2.length > 0 && turn.speechesV2[0].content) { const textBlock = turn.speechesV2[0].content.find(block => block.type === 'text'); if (textBlock && typeof textBlock.msg === 'string') { userTextMsg = textBlock.msg; } let uploadedMedia = []; turn.speechesV2[0].content.forEach(block => { if (block.type !== 'text' && block.fileName && block.url) { uploadedMedia.push(`[${block.fileName || '未知文件'}](${block.url || '#'})`); } }); if (uploadedMedia.length > 0) { mediaMarkdown = `\n${uploadedMedia.join('\n')}\n`; } } // Fallback to displayPrompt if text message is still empty if (!userTextMsg && typeof turn.displayPrompt === 'string') { userTextMsg = turn.displayPrompt; } userTextMsg = applyFormulaReplacements(userTextMsg, settings.replaceFormulas); return (userTextMsg + '\n' + mediaMarkdown).trim() + '\n'; } function processTurnToMarkdown(turn, settings, refsContext, isFullExportContext = false) { let markdown = ''; if (turn.speaker === 'human') { if (isFullExportContext) { markdown += (settings.headerDowngrade ? '> ## user\n' : '> # user\n'); } markdown += extractUserMessageAndMedia(turn, settings); } else if (turn.speaker === 'ai') { if (isFullExportContext) { markdown += (settings.headerDowngrade ? '> ## agent\n' : '> # agent\n'); } if (turn.speechesV2 && turn.speechesV2.length > 0) { turn.speechesV2.forEach(speech => { markdown += processSpeech(speech, settings, refsContext); }); } } return markdown; } function convertSingleTurnJsonToMarkdown(jsonData, targetTurnIndex, settings) { if (!jsonData || !jsonData.convs || !Array.isArray(jsonData.convs)) { return '# 错误:无效的JSON数据\n\n无法解析对话内容。'; } const turn = jsonData.convs.find(t => t.index === targetTurnIndex); if (!turn) return ''; let refsContext = { refsArray: [], nextRefIdx: 1 }; let markdownContent = processTurnToMarkdown(turn, settings, refsContext, false); if (settings.keepSearchResults && refsContext.refsArray.length > 0) { markdownContent += '\n'; refsContext.refsArray.forEach(ref => { markdownContent += `[^${ref.idx}]: [${ref.title}](${ref.url})\n`; }); } if (settings.headerDowngrade) { markdownContent = adjustHeaderLevels(markdownContent); } return markdownContent.trim(); } function convertAllTurnsJsonToMarkdown(jsonData, settings) { if (!jsonData || !Array.isArray(jsonData.convs)) { return '# 错误:无效的JSON数据\n\n无法解析对话内容。'; } let markdownContent = ''; let refsContext = { refsArray: [], nextRefIdx: 1 }; // jsonData.convs is newest first. Reverse to process oldest first for chronological output. jsonData.convs.slice().reverse().forEach(turn => { markdownContent += processTurnToMarkdown(turn, settings, refsContext, true); }); if (settings.keepSearchResults && refsContext.refsArray.length > 0) { markdownContent += '\n'; refsContext.refsArray.forEach(ref => { markdownContent += `[^${ref.idx}]: [${ref.title}](${ref.url})\n`; }); } if (settings.headerDowngrade) { // Note: Speaker headers are already downgraded by processTurnToMarkdown if isFullExportContext. // This call will downgrade any other headers within the content. markdownContent = adjustHeaderLevels(markdownContent); } return markdownContent.trim(); } // --- UI Injection & Event Handlers --- function createStyledButton(text, onclick, iconSvg = '', customStyles = {}) { const btn = document.createElement('button'); btn.type = 'button'; btn.innerHTML = iconSvg + text; Object.assign(btn.style, BTN_STYLE, customStyles); // Base style, then specific overrides btn.onmouseover = () => { btn.style.background = BTN_STYLE.backgroundHover; }; btn.onmouseout = () => { btn.style.background = BTN_STYLE.background; }; btn.onclick = onclick; // 确保初始状态下背景色正确 btn.style.background = BTN_STYLE.background; return btn; } function injectCopyButtonToBubble(copyBtnElement, allBubbles, jsonData) { if (copyBtnElement.parentElement.querySelector('.agent-chat__toolbar__copy-md')) return; const mdBtn = createStyledButton('复制MD', null, '', { fontSize: '14px' }); // Use shared styling mdBtn.title = '复制Markdown(接口数据)'; mdBtn.className = 'agent-chat__toolbar__copy-md'; mdBtn.onclick = function (e) { e.stopPropagation(); const bubble = copyBtnElement.closest('.agent-chat__bubble'); if (!bubble) { alert('未找到对话泡'); return; } const domIdx = allBubbles.indexOf(bubble); const jsonConvs = jsonData && Array.isArray(jsonData.convs) ? jsonData.convs : []; // Assuming jsonData.convs is newest first, and allBubbles is oldest first. const jsonTargetIdx = jsonConvs.length - 1 - domIdx; let targetTurnUniqueIndex = null; if (jsonConvs[jsonTargetIdx]) { targetTurnUniqueIndex = jsonConvs[jsonTargetIdx].index; } if (targetTurnUniqueIndex === null || targetTurnUniqueIndex === undefined) { alert('无法匹配到正确的对话轮次'); return; } const settings = getSettings(); const md = convertSingleTurnJsonToMarkdown(jsonData, targetTurnUniqueIndex, settings); if (!md) { alert('未提取到Markdown内容'); return; } navigator.clipboard.writeText(md).then(() => { mdBtn.title = '已复制!'; mdBtn.style.opacity = 0.5; setTimeout(() => { mdBtn.title = '复制Markdown(接口数据)'; mdBtn.style.opacity = 1; }, 1000); }).catch(err => { console.error(`${SCRIPT_NAME}: Failed to copy: `, err); alert('复制失败,详情请查看控制台。'); }); }; copyBtnElement.parentElement.insertBefore(mdBtn, copyBtnElement.nextSibling); } function injectCopyButtonsToAllBubbles() { const settings = getSettings(); if (!settings.autoInjectCopyBtn || !state.latestDetailResponse) return; let jsonData; try { jsonData = JSON.parse(state.latestDetailResponse); } catch (e) { console.error(`${SCRIPT_NAME}: JSON parsing failed for injecting copy buttons.`, e); // Do not alert here as this runs frequently. return; } if (!jsonData || !jsonData.convs) return; const allBubbles = Array.from(document.querySelectorAll('.agent-chat__bubble')); document.querySelectorAll('.agent-chat__toolbar__copy').forEach(copyBtn => { injectCopyButtonToBubble(copyBtn, allBubbles, jsonData); }); } function injectExportButtonsToToolbar(toolbarElement) { if (!toolbarElement || toolbarElement.dataset.mdInjected) return; toolbarElement.innerHTML = ''; // Clear existing content Object.assign(toolbarElement.style, { display: 'flex', gap: '4px', width: '150px', alignItems: 'center', height: '34px' }); const settings = getSettings(); const btnAllOnClick = () => { if (!state.latestDetailResponse) { alert('未捕获到对话数据,请刷新页面或重新进入对话。'); return; } let jsonData; try { jsonData = JSON.parse(state.latestDetailResponse); } catch (e) { alert('JSON 解析失败'); return; } const md = convertAllTurnsJsonToMarkdown(jsonData, settings); if (!md) { alert('未提取到Markdown内容'); return; } navigator.clipboard.writeText(md).then(() => { alert('全部对话已复制到剪贴板!'); }).catch(err => { console.error(`${SCRIPT_NAME}: Failed to copy all: `, err); alert('复制失败,详情请查看控制台。'); }); }; const btnAll = createStyledButton('全部', btnAllOnClick, ICON_EXPORT_ALL, EXPORT_BTN_STYLE); const btnDialogue = createStyledButton('对话', injectCopyButtonsToAllBubbles, ICON_DIALOGUE, EXPORT_BTN_STYLE); toolbarElement.appendChild(btnAll); toolbarElement.appendChild(btnDialogue); toolbarElement.dataset.mdInjected = '1'; } // --- DOM Observation --- function observeDOMChanges() { const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.classList && node.classList.contains('agent-dialogue__tool')) { injectExportButtonsToToolbar(node); } else if (node.querySelectorAll) { node.querySelectorAll('.agent-dialogue__tool').forEach(el => injectExportButtonsToToolbar(el)); } } }); } } // Always try to inject copy buttons if new nodes are added and auto-inject is on if (getSettings().autoInjectCopyBtn) { injectCopyButtonsToAllBubbles(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // --- Main Initialization --- function init() { setupNetworkInterceptors(); observeDOMChanges(); // Initial call to inject buttons if content is already present if (getSettings().autoInjectCopyBtn) { injectCopyButtonsToAllBubbles(); } // Attempt to inject toolbar buttons if already present const existingToolbar = document.querySelector('.agent-dialogue__tool'); if (existingToolbar) { injectExportButtonsToToolbar(existingToolbar); } } // --- Script Execution --- if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('脚本设置', showSettingsDialog); } if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址