// ==UserScript==
// @name DeepSeek Chat to word | image
// @name:zh-CN DeepSeek对话导出增强
// @namespace http://tampermonkey.net/
// @version 1.6
// @description Enhances DeepSeek Chat to export conversations as Word documents and generate beautiful knowledge cards. Adds buttons for single messages and global controls to process multiple selected messages.
// @description:zh-CN 增强 DeepSeek Chat,轻松将对话导出为 Word 文档或生成精美的知识卡片。为每条消息添加独立操作按钮,并通过侧边栏全局控件,批量处理勾选的多条消息。
// @author licc168
// @license MIT
// @match *://chat.deepseek.com/*
// @grant GM_addStyle
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_log
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const dislikeIconSvgPathStart = "M18.304";
const generateButtonText = '生成卡片';
const generateButtonTitle = '生成图文卡片 (API)';
const API_ENDPOINT = 'https://api.any2card.com/api/generate-image';
const API_ENDPOINT_WORD = 'https://api.any2card.com/api/md-to-word';
const API_ENDPOINT_PDF = 'https://api.any2card.com/api/md-to-pdf';
const API_ENDPOINT_MINDMAP = 'https://api.any2card.com/api/md-to-mindmap';
const API_KEY_STORAGE = 'deepseek_generate_card_api_key';
let modalOverlay = null;
let apiKeyInput, markdownTextarea, templateSelect, splitModeSelect, widthInput, heightInput, aspectRatioSelect, fontSelect, watermarkToggle, watermarkTextInput, modalGenerateCardButton, modalCancelButton, cardResultDiv, heightOverflowHiddenToggle, heightOverflowHiddenRow;
let floatingActionPanel = null;
const floatingButtonStyles = {
padding: '8px 12px',
backgroundColor: 'rgb(0, 123, 255)',
color: 'rgb(255, 255, 255)',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
transition: 'all 0.3s ease 0s',
fontFamily: 'Arial, sans-serif',
boxShadow: 'rgba(0, 0, 0, 0.2) 0px 2px 5px',
whiteSpace: 'nowrap',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px'
};
// --- Data from constants.ts ---
const imageTextTemplates = [
{ id: "memo", name: "备忘录" },
{ id: "popart", name: "波普艺术" },
{ id: "traditionalchinese", name: "中国传统" },
{ id: "coilnotebook", name: "线圈笔记本" },
{ id: "purpleticket", name: "紫色小红书" },
{ id: "bytedance", name: "字节范" },
{ id: "warm", name: "温暖柔和" },
{ id: 'alibaba', name: '阿里橙' },
{ id: "notebook", name: "笔记本" },
{ id: "darktech", name: "黑色科技" },
{ id: "fairytale", name: "儿童童话" },
{ id: "boardgamestyle", name: "桌游风格" },
{ id: "cyberpunk", name: "赛博朋克" },
{ id: "glassmorphism", name: "玻璃拟态" },
{ id: "neonglow", name: "霓虹发光" },
{ id: "vintagenewspaper", name: "复古报纸" },
{ id: "handwrittennote", name: "手写笔记" },
{ id: "vintagemap", name: "古旧地图" },
];
const splitModes = [
{ id: 'long', name: '长图文 (不分割)' },
{ id: 'auto', name: '自动切割 (智能)' },
{ id: 'line', name: '横线拆分 (---)' },
];
const aspectRatios = [
{ id: 'custom', name: '自定义尺寸', width: 0, height: 0 },
{ id: '3:4', name: '3:4 竖屏', width: 3, height: 4 },
{ id: '1:1', name: '1:1 方形', width: 1, height: 1 },
{ id: '4:3', name: '4:3 横屏', width: 4, height: 3 },
{ id: '16:9', name: '16:9 横屏宽', width: 16, height: 9 },
{ id: '9:16', name: '9:16 竖屏长', width: 9, height: 16 },
];
const fontOptionsFromConstants = [
{ label: "思源黑体", value: "var(--font-noto-sans-sc)" },
{ label: "思源宋体", value: "var(--font-noto-serif-sc)" },
{ label: "苹方中黑", value: "var(--font-pingfang-medium)" },
{ label: "方正公文黑", value: "var(--font-fangzheng-gongwenhei)" },
{ label: "汇文明朝", value: "var(--font-huiwen-mincho)" },
{ label: "霞鹜文楷", value: "var(--font-lxgw-wenkai-lite)" },
{ label: "马善政手写体", value: "var(--font-ma-shan-zheng)" },
{ label: "字酷快乐体", value: "var(--font-zcool-kuaile)" },
{ label: "字酷倾颜黄油体", value: "var(--font-zcool-qingke-huangyou)" },
{ label: "龙藏体", value: "var(--font-long-cang)" },
{ label: "智芒星体", value: "var(--font-zhi-mang-xing)" },
{ label: "柳简毛草体", value: "var(--font-liu-jian-mao-cao)" },
{ label: "字酷小薇体", value: "var(--font-zcool-xiaowei)" },
{ label: "禅丸哥特体", value: "var(--font-zen-maru-gothic)" },
{ label: "东叔原宋", value: "var(--font-dong-shu-yuan-song)" },
];
const fontOptions = fontOptionsFromConstants.map(font => {
let shortValue = font.value;
if (font.value.startsWith("var(--font-") && font.value.endsWith(")")) {
shortValue = font.value.substring(11, font.value.length - 1);
}
return { label: font.label, value: shortValue };
});
// --- Add Styles ---
GM_addStyle(`
.gm-generate-card-button {
margin-left: 8px;
padding: 5px 10px;
font-size: 12px;
font-weight: bold;
line-height: 1.4;
cursor: pointer;
border: 1px solid #ffb6c1;
background: linear-gradient(145deg, #ffd1dc, #ffb6c1);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
border-radius: 16px;
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(255, 105, 180, 0.3);
transition: all 0.2s ease-in-out;
}
.gm-generate-card-button:hover {
background: linear-gradient(145deg, #ffb6c1, #ffd1dc);
border-color: #ff99aa;
box-shadow: 0 3px 6px rgba(255, 105, 180, 0.5);
transform: translateY(-1px);
}
.gm-generate-card-button:active {
transform: translateY(0px);
box-shadow: 0 1px 2px rgba(255, 105, 180, 0.4);
}
.gm-card-modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center;
z-index: 10000; opacity: 0; visibility: hidden;
transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}
.gm-card-modal-overlay.gm-modal-visible { opacity: 1; visibility: visible; transition: opacity 0.3s ease; }
.gm-card-modal-content {
background-color: #fff; color: #333; padding: 20px; border-radius: 8px;
box-shadow: 0 5px 20px rgba(0,0,0,0.3); width: 90%; max-width: 1000px;
display: flex; flex-direction: column; gap: 15px;
max-height: 90vh;
}
.gm-card-modal-main-layout {
display: flex; gap: 15px; flex-grow: 1; overflow: hidden; min-height: 400px;
}
.gm-card-modal-column {
display: flex; flex-direction: column; gap: 10px; padding: 5px;
}
.gm-card-modal-column.gm-left-column { flex: 1.2; }
.gm-card-modal-column.gm-middle-column {
flex: 1; overflow-y: auto; max-height: calc(90vh - 100px); padding-right: 10px;
}
.gm-card-modal-column.gm-right-column { flex: 1.2; }
.gm-card-modal-content label { display: block; margin-bottom: 3px; font-weight: bold; font-size: 13px; }
.gm-card-modal-content textarea,
.gm-card-modal-content input[type="number"],
.gm-card-modal-content input[type="text"],
.gm-card-modal-content select {
width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;
background-color: #fff; color: #333; box-sizing: border-box;
}
.gm-card-modal-content textarea#gmCardMarkdown {
flex-grow: 1; min-height: 200px; resize: vertical; background-color: #f9f9f9;
}
.gm-card-modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.gm-card-modal-watermark-group { display: flex; align-items: center; gap: 10px; }
.gm-card-modal-watermark-group input[type="checkbox"] { width: auto; flex-shrink: 0; }
.gm-card-modal-watermark-group label { font-weight: normal; margin-bottom: 0; }
.gm-card-modal-buttons {
display: flex; justify-content: flex-end; gap: 10px; margin-top: auto;
padding-top: 10px; border-top: 1px solid #eee;
}
.gm-card-modal-buttons button {
padding: 8px 15px; border-radius: 4px; border: none; cursor: pointer;
font-size: 14px; transition: background-color 0.2s ease;
}
button#gmCardGenerateBtn { background-color: #4CAF50; color: white; }
button#gmCardGenerateBtn:hover { background-color: #45a049; }
button#gmCardGenerateBtn:disabled { background-color: #aaa; cursor: not-allowed; }
button#gmCardCancelBtn { background-color: #f0f0f0; color: #333; border: 1px solid #ccc; }
button#gmCardCancelBtn:hover { background-color: #e0e0e0; }
#gmCardResultDiv {
flex-grow: 1; border: 1px dashed #ccc; min-height: 150px; text-align: left;
background-color: #f9f9f9; padding: 10px; overflow-y: auto;
position: relative;
}
#gmCardResultDiv img {
max-width: 100%; max-height: 400px; border: 1px solid #eee;
border-radius: 4px; background-color: white; display: block;
}
.gm-preview-image-button {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 5px 10px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
z-index: 10;
font-size: 12px;
}
.gm-preview-image-button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
#gmCardResultDiv a {
display: inline-block; padding: 10px 15px; background-color: #007bff; color: white;
text-decoration: none; border-radius: 4px; margin-top: 10px;
}
#gmCardResultDiv a:hover { background-color: #0056b3; }
.gm-spinner {
border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff;
width: 14px; height: 14px; animation: spin 1s linear infinite;
display: inline-block; margin-right: 8px; vertical-align: middle;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.gm-api-key-group {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.gm-api-key-group input[type="text"] {
flex-grow: 1;
min-width: 0;
}
.gm-api-key-link {
font-size: 12px;
color: #007bff;
text-decoration: none;
white-space: nowrap;
}
.gm-api-key-link:hover {
text-decoration: underline;
color: #0056b3;
}
/* Styles for new features */
/* The main button is now cloned, so it doesn't need extensive styling here */
#gmMainGenerateCardBtn {
cursor: pointer;
}
/* Using a selector that is hopefully stable enough for message items */
.gm-message-item-for-checkbox {
position: relative !important;
padding-left: 40px !important; /* Increased padding to make space for shifted checkbox */
}
.gm-message-checkbox-container {
position: absolute;
top: 10px;
left: -25px; /* Moved further to the left */
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.gm-message-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: #4CAF50; /* Style the checkbox color */
}
`);
function createSelect(options, id, defaultSelectedValue) {
const select = document.createElement('select');
select.id = id;
options.forEach(opt => {
const optionEl = document.createElement('option');
optionEl.value = opt.id || opt.value;
optionEl.textContent = opt.name || opt.label;
if ((opt.id || opt.value) === defaultSelectedValue) {
optionEl.selected = true;
}
select.appendChild(optionEl);
});
return select;
}
function htmlToMarkdown(element) {
if (!element) return "";
// Create a temporary clone to work on to avoid modifying the live DOM
const clone = element.cloneNode(true);
// --- 1. Pre-process and replace special elements ---
// Handle KaTeX math formulas by extracting LaTeX from annotations
clone.querySelectorAll('.katex, .katex-display').forEach(katexEl => {
const annotation = katexEl.querySelector('annotation[encoding="application/x-tex"]');
if (annotation && annotation.textContent) {
const tex = annotation.textContent.trim();
const isBlock = katexEl.classList.contains('katex-display');
katexEl.replaceWith(document.createTextNode(isBlock ? `\n\n$$${tex}$$` + `\n\n` : `\\(${tex}\\)`));
} else {
// Fallback if no annotation is found, just use text content to avoid showing raw HTML
katexEl.replaceWith(document.createTextNode(katexEl.textContent || ''));
}
});
// Handle code blocks
clone.querySelectorAll('pre code').forEach(codeBlock => {
const parentPre = codeBlock.parentElement;
const lang = [...codeBlock.classList].find(cls => cls.startsWith('language-'))?.replace('language-', '') || '';
// Replace the <pre> element with a markdown code block as a single text node
parentPre.replaceWith(document.createTextNode(`\n\n\`\`\`${lang}\n${codeBlock.textContent.trim()}\n\`\`\`\n\n`));
});
// --- 2. Convert remaining HTML to a text-based representation with Markdown-like newlines ---
// This is a simplified process. We'll use innerHTML and a series of regex replacements.
// It's not as robust as a full DOM traversal, but it's much simpler and handles the main cases.
let markdown = clone.innerHTML;
// Add newlines for block elements
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
markdown = markdown.replace(/<\/h[1-6]>/gi, '\n\n');
markdown = markdown.replace(/<\/p>/gi, '\n\n');
markdown = markdown.replace(/<\/li>/gi, '\n');
markdown = markdown.replace(/<hr[^>]*>/gi, '\n\n---\n\n');
// Add markdown prefixes for lists and headings
markdown = markdown.replace(/<h1[^>]*>/gi, '# ');
markdown = markdown.replace(/<h2[^>]*>/gi, '## ');
markdown = markdown.replace(/<h3[^>]*>/gi, '### ');
markdown = markdown.replace(/<h4[^>]*>/gi, '#### ');
markdown = markdown.replace(/<li[^>]*>/gi, (match) => {
// A rough way to detect list level by indentation in the source HTML
const indentation = match.search(/\S|$/);
const level = Math.floor(indentation / 2); // Assuming 2 spaces indentation
return ' '.repeat(level) + '* ';
});
// Handle inline elements
markdown = markdown.replace(/<strong>(.*?)<\/strong>/gis, '**$1**');
markdown = markdown.replace(/<b>(.*?)<\/b>/gis, '**$1**');
markdown = markdown.replace(/<em>(.*?)<\/em>/gis, '*$1*');
markdown = markdown.replace(/<i>(.*?)<\/i>/gis, '*$1*');
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gis, '`$1`');
markdown = markdown.replace(/<a href="(.*?)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
// --- 3. Strip all remaining tags and clean up whitespace ---
const tempDiv = document.createElement('div');
tempDiv.innerHTML = markdown;
markdown = tempDiv.textContent || tempDiv.innerText;
// Normalize newlines, removing excessive blank lines
return markdown.replace(/(\n\s*){3,}/g, '\n\n').trim();
}
function createCardModal() {
if (document.getElementById('gmCardModalOverlay')) return;
modalOverlay = document.createElement('div');
modalOverlay.id = 'gmCardModalOverlay';
modalOverlay.className = 'gm-card-modal-overlay';
const modalContent = document.createElement('div');
modalContent.className = 'gm-card-modal-content';
modalContent.addEventListener('click', e => e.stopPropagation());
const mainLayout = document.createElement('div');
mainLayout.className = 'gm-card-modal-main-layout';
const leftColumn = document.createElement('div');
leftColumn.className = 'gm-card-modal-column gm-left-column';
let label = document.createElement('label');
label.htmlFor = 'gmCardMarkdown';
label.textContent = '卡片内容 (Markdown):';
leftColumn.appendChild(label);
markdownTextarea = document.createElement('textarea');
markdownTextarea.id = 'gmCardMarkdown';
markdownTextarea.readOnly = true;
leftColumn.appendChild(markdownTextarea);
mainLayout.appendChild(leftColumn);
const middleColumn = document.createElement('div');
middleColumn.className = 'gm-card-modal-column gm-middle-column';
label = document.createElement('label');
label.htmlFor = 'gmCardApiKey';
label.textContent = 'API Key:';
middleColumn.appendChild(label);
const apiKeyGroup = document.createElement('div');
apiKeyGroup.className = 'gm-api-key-group';
apiKeyInput = document.createElement('input');
apiKeyInput.type = 'text';
apiKeyInput.id = 'gmCardApiKey';
apiKeyInput.placeholder = '请输入您的 API Key';
const apiKeyLink = document.createElement('a');
apiKeyLink.href = 'https://any2card.com/zh/blog/api-key';
apiKeyLink.target = '_blank';
apiKeyLink.rel = 'noopener noreferrer';
apiKeyLink.textContent = '获取 API Key';
apiKeyLink.className = 'gm-api-key-link';
apiKeyGroup.appendChild(apiKeyInput);
apiKeyGroup.appendChild(apiKeyLink);
middleColumn.appendChild(apiKeyGroup);
label = document.createElement('label');
label.htmlFor = 'gmCardTemplate';
label.textContent = '图文模板:';
middleColumn.appendChild(label);
templateSelect = createSelect(imageTextTemplates, 'gmCardTemplate', imageTextTemplates[0]?.id);
middleColumn.appendChild(templateSelect);
label = document.createElement('label');
label.htmlFor = 'gmCardSplitMode';
label.textContent = '分割模式:';
middleColumn.appendChild(label);
splitModeSelect = createSelect(splitModes, 'gmCardSplitMode', splitModes[0]?.id);
middleColumn.appendChild(splitModeSelect);
// 新增:超出高度隐藏开关
heightOverflowHiddenRow = document.createElement('div');
heightOverflowHiddenRow.style.display = 'flex';
heightOverflowHiddenRow.style.alignItems = 'center';
heightOverflowHiddenRow.style.margin = '6px 0 0 0';
let heightOverflowHiddenLabel = document.createElement('label');
heightOverflowHiddenLabel.htmlFor = 'gmCardHeightOverflowHidden';
heightOverflowHiddenLabel.textContent = '超出高度隐藏';
heightOverflowHiddenLabel.style.marginRight = '8px';
heightOverflowHiddenToggle = document.createElement('input');
heightOverflowHiddenToggle.type = 'checkbox';
heightOverflowHiddenToggle.id = 'gmCardHeightOverflowHidden';
// 顺序调整:label在前,checkbox在后
heightOverflowHiddenRow.appendChild(heightOverflowHiddenLabel);
heightOverflowHiddenRow.appendChild(heightOverflowHiddenToggle);
middleColumn.appendChild(heightOverflowHiddenRow);
// 监听分割模式变化,只有long时显示
splitModeSelect.addEventListener('change', function() {
if (splitModeSelect.value === 'long') {
heightOverflowHiddenRow.style.display = '';
} else {
heightOverflowHiddenRow.style.display = 'none';
}
});
// 初始化时根据默认分割模式显示/隐藏
if (splitModeSelect.value === 'long') {
heightOverflowHiddenRow.style.display = '';
}
const dimensionGrid = document.createElement('div');
dimensionGrid.className = 'gm-card-modal-grid';
let div = document.createElement('div');
label = document.createElement('label');
label.htmlFor = 'gmCardWidth';
label.textContent = '宽度 (px):';
div.appendChild(label);
widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.id = 'gmCardWidth';
div.appendChild(widthInput);
dimensionGrid.appendChild(div);
div = document.createElement('div');
label = document.createElement('label');
label.htmlFor = 'gmCardHeight';
label.textContent = '高度 (px):';
div.appendChild(label);
heightInput = document.createElement('input');
heightInput.type = 'number';
heightInput.id = 'gmCardHeight';
div.appendChild(heightInput);
dimensionGrid.appendChild(div);
middleColumn.appendChild(dimensionGrid);
label = document.createElement('label');
label.htmlFor = 'gmCardAspectRatio';
label.textContent = '宽高比:';
middleColumn.appendChild(label);
aspectRatioSelect = createSelect(aspectRatios, 'gmCardAspectRatio', aspectRatios.find(r => r.id === '3:4')?.id);
aspectRatioSelect.addEventListener('change', function() {
const selectedRatioInfo = aspectRatios.find(r => r.id === this.value);
if (selectedRatioInfo && selectedRatioInfo.id !== 'custom' && selectedRatioInfo.width > 0 && selectedRatioInfo.height > 0) {
const currentWidth = parseInt(widthInput.value, 10) || 440;
widthInput.value = currentWidth;
heightInput.value = Math.round((currentWidth * selectedRatioInfo.height) / selectedRatioInfo.width);
heightInput.readOnly = true;
} else {
heightInput.readOnly = false;
}
});
middleColumn.appendChild(aspectRatioSelect);
label = document.createElement('label');
label.htmlFor = 'gmCardFont';
label.textContent = '字体选择:';
middleColumn.appendChild(label);
fontSelect = createSelect(fontOptions, 'gmCardFont', fontOptions.find(f => f.value === 'lxgw-wenkai-lite')?.value || fontOptions[0]?.value);
middleColumn.appendChild(fontSelect);
const watermarkGroup = document.createElement('div');
watermarkGroup.className = 'gm-card-modal-watermark-group';
watermarkToggle = document.createElement('input');
watermarkToggle.type = 'checkbox';
watermarkToggle.id = 'gmCardWatermarkToggle';
watermarkGroup.appendChild(watermarkToggle);
let watermarkLabelElement = document.createElement('label');
watermarkLabelElement.htmlFor = 'gmCardWatermarkToggle';
watermarkLabelElement.textContent = '启用水印';
watermarkGroup.appendChild(watermarkLabelElement);
watermarkTextInput = document.createElement('input');
watermarkTextInput.type = 'text';
watermarkTextInput.id = 'gmCardWatermarkText';
watermarkTextInput.placeholder = '水印文字';
watermarkTextInput.style.flexGrow = '1';
watermarkTextInput.disabled = true;
watermarkToggle.addEventListener('change', () => watermarkTextInput.disabled = !watermarkToggle.checked);
watermarkGroup.appendChild(watermarkTextInput);
middleColumn.appendChild(watermarkGroup);
mainLayout.appendChild(middleColumn);
const rightColumn = document.createElement('div');
rightColumn.className = 'gm-card-modal-column gm-right-column';
label = document.createElement('label');
label.htmlFor = 'gmCardResultDiv';
label.textContent = '生成结果:';
rightColumn.appendChild(label);
cardResultDiv = document.createElement('div');
cardResultDiv.id = 'gmCardResultDiv';
rightColumn.appendChild(cardResultDiv);
mainLayout.appendChild(rightColumn);
modalContent.appendChild(mainLayout);
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'gm-card-modal-buttons';
modalCancelButton = document.createElement('button');
modalCancelButton.textContent = '取消';
modalCancelButton.id = 'gmCardCancelBtn';
modalCancelButton.addEventListener('click', () => modalOverlay.classList.remove('gm-modal-visible'));
modalGenerateCardButton = document.createElement('button');
modalGenerateCardButton.textContent = '生成卡片';
modalGenerateCardButton.id = 'gmCardGenerateBtn';
buttonsDiv.appendChild(modalCancelButton);
buttonsDiv.appendChild(modalGenerateCardButton);
modalContent.appendChild(buttonsDiv);
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
modalOverlay.addEventListener('click', () => modalOverlay.classList.remove('gm-modal-visible'));
}
function handleGenerateCard() {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
alert('请输入 API Key。');
apiKeyInput.focus();
return;
}
GM_setValue(API_KEY_STORAGE, apiKey);
const settings = {
templateType: "imageText",
selectedImageTextTemplate: templateSelect.value,
splitMode: splitModeSelect.value,
cardWidth: parseInt(widthInput.value, 10) || 440,
cardHeight: parseInt(heightInput.value, 10) || 587,
fontFamily: fontSelect.value,
watermarkEnabled: watermarkToggle.checked,
watermarkText: watermarkToggle.checked ? watermarkTextInput.value.trim() : "",
deviceScaleFactor: 2,
heightOverflowHidden: splitModeSelect.value === 'long' ? heightOverflowHiddenToggle.checked : false,
};
const markdownContent = markdownTextarea.value;
if (!markdownContent) { alert('无法获取卡片内容。'); return; }
if (settings.cardWidth <= 0 || settings.cardHeight <= 0) { alert('宽度和高度必须是正数。'); return; }
console.log("API Request Parameters:");
console.log("Settings:", JSON.parse(JSON.stringify(settings)));
console.log("Markdown Content:", markdownContent);
modalGenerateCardButton.disabled = true;
modalGenerateCardButton.innerHTML = '<span class="gm-spinner"></span>生成中...';
cardResultDiv.innerHTML = '正在请求 API...';
GM_xmlhttpRequest({
method: "POST",
url: API_ENDPOINT,
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey
},
data: JSON.stringify({ markdownContent, settings }),
onload: function(response) {
try {
const result = JSON.parse(response.responseText);
if (response.status >= 200 && response.status < 300 && result.code === 0) {
cardResultDiv.innerHTML = '';
if (result.data && result.data.url) {
if (result.data.type === 'png') {
const img = document.createElement('img');
img.src = result.data.url;
img.alt = '生成的卡片';
cardResultDiv.appendChild(img);
const previewButton = document.createElement('button');
previewButton.textContent = '预览原图';
previewButton.className = 'gm-preview-image-button';
previewButton.onclick = function() {
GM_openInTab(result.data.url, { active: true, insert: true });
};
cardResultDiv.insertBefore(previewButton, img);
} else if (result.data.type === 'zip') {
const link = document.createElement('a');
link.href = result.data.url;
link.textContent = `下载卡片 (ZIP${result.data.pages ? ' - ' + result.data.pages + '页' : ''})`;
link.target = '_blank';
cardResultDiv.appendChild(link);
if(confirm("ZIP文件已生成,是否立即在新标签页打开下载链接?")) {
GM_openInTab(result.data.url, { active: true, insert: true });
}
} else {
cardResultDiv.textContent = `收到未知类型的响应: ${result.data.type}`;
}
} else {
cardResultDiv.textContent = 'API 响应中未找到有效的图片或文件 URL。';
}
} else {
throw new Error(result.message || `API 请求失败 (状态 ${response.status})`);
}
} catch (error) {
console.error('解析响应或处理数据错误:', error);
cardResultDiv.textContent = `错误: ${error.message}`;
} finally {
modalGenerateCardButton.disabled = false;
modalGenerateCardButton.textContent = '生成卡片';
}
},
onerror: function(response) {
console.error('GM_xmlhttpRequest 错误:', response);
cardResultDiv.textContent = `请求错误: ${response.statusText || '无法连接到服务器'}`;
modalGenerateCardButton.disabled = false;
modalGenerateCardButton.textContent = '生成卡片';
}
});
}
function showCardModal(defaultMarkdown) {
if (!modalOverlay) createCardModal();
markdownTextarea.value = defaultMarkdown;
apiKeyInput.value = GM_getValue(API_KEY_STORAGE, "");
widthInput.value = '440';
heightInput.value = '587';
aspectRatioSelect.value = aspectRatios.find(r => r.id === '3:4')?.id || aspectRatios[0].id;
aspectRatioSelect.dispatchEvent(new Event('change'));
// 新增:初始化超出高度隐藏开关
heightOverflowHiddenToggle.checked = false;
if (splitModeSelect.value === 'long') {
heightOverflowHiddenRow.style.display = '';
} else {
heightOverflowHiddenRow.style.display = 'none';
}
watermarkToggle.checked = false;
watermarkTextInput.value = '';
watermarkTextInput.disabled = true;
cardResultDiv.innerHTML = '';
const oldBtn = modalGenerateCardButton;
if (oldBtn && oldBtn.parentNode) {
modalGenerateCardButton = oldBtn.cloneNode(true);
oldBtn.parentNode.replaceChild(modalGenerateCardButton, oldBtn);
}
modalGenerateCardButton.addEventListener('click', handleGenerateCard);
modalOverlay.classList.add('gm-modal-visible');
if (!apiKeyInput.value) apiKeyInput.focus();
}
function addGenerateCardButton(targetButtonElement) {
if (targetButtonElement.dataset.hasGenerateCardButton) return;
const genButton = document.createElement('button');
genButton.textContent = generateButtonText;
genButton.title = generateButtonTitle;
genButton.className = 'gm-generate-card-button';
genButton.addEventListener('click', async function(event) {
event.stopPropagation();
event.preventDefault();
let promptText = '';
// --- Start: New clipboard logic ---
const actionsContainer = targetButtonElement.parentElement;
let copyButton = null;
if (actionsContainer) {
const buttonsInContainer = actionsContainer.querySelectorAll('div.ds-icon-button');
for (let i = 0; i < buttonsInContainer.length; i++) {
// Check for the specific SVG path of the copy button
// The first path element's 'd' attribute starts with "M3.65169" for the copy button
const svgPath = buttonsInContainer[i].querySelector('svg path[d^="M3.65169"]');
if (svgPath) {
copyButton = buttonsInContainer[i];
break;
}
}
}
if (copyButton) {
try {
copyButton.click(); // Simulate click on the copy button
// Wait a short moment for the clipboard to be populated
await new Promise(resolve => setTimeout(resolve, 150)); // 150ms delay
promptText = await navigator.clipboard.readText();
if (promptText && promptText.trim() !== '') {
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
showCardModal(promptText);
return; // Successfully got content from clipboard
} else {
GM_log('DeepSeek Script: Clipboard was empty after copy or contained only whitespace. Falling back to text extraction.');
console.warn('DeepSeek Script: Clipboard was empty after copy or contained only whitespace. Falling back to text extraction.');
promptText = ''; // Ensure promptText is reset if clipboard was empty
}
} catch (err) {
GM_log('DeepSeek Script: Failed to read from clipboard. Error: ' + err.message + '. Falling back to text extraction.');
console.warn('DeepSeek Script: Failed to read from clipboard. Error:', err, '. Falling back to text extraction.');
promptText = ''; // Ensure promptText is reset on error
}
} else {
GM_log('DeepSeek Script: Could not find the copy button. Falling back to text extraction.');
console.warn('DeepSeek Script: Could not find the copy button. Falling back to text extraction.');
}
// --- End: New clipboard logic ---
// --- Fallback: Existing text extraction logic (if clipboard failed or copy button not found) ---
// This logic uses targetButtonElement (the dislike button) as its reference.
// Only proceed if promptText is still empty (meaning clipboard method failed or was skipped)
if (!promptText) {
let messageWrapper = null;
let currentElement = targetButtonElement;
const messageWrapperSelectors = [
'.group', '.chat-message-item', '.message-container',
'div[class*="message-bubble"]', 'div[class*="content-container"]',
];
for (let i = 0; i < 7 && currentElement && currentElement.parentElement; i++) {
currentElement = currentElement.parentElement;
for (const selector of messageWrapperSelectors) {
if (currentElement.matches(selector)) {
messageWrapper = currentElement;
break;
}
}
if (!messageWrapper &&
currentElement.querySelector('div.ds-markdown.ds-markdown--block:not(:empty)') &&
currentElement.contains(targetButtonElement)) {
messageWrapper = currentElement;
}
if (messageWrapper) break;
}
if (messageWrapper) {
const markdownElements = Array.from(messageWrapper.querySelectorAll('div.ds-markdown.ds-markdown--block:not(:empty)'));
let foundMd = null;
for (let i = markdownElements.length - 1; i >= 0; i--) {
if (!markdownElements[i].contains(targetButtonElement)) {
foundMd = markdownElements[i];
break;
}
}
if (foundMd) {
promptText = (foundMd.innerText || foundMd.textContent || "").trim();
}
}
if (!promptText) {
let searchStartNode = targetButtonElement.parentElement;
for (let i = 0; i < 3 && searchStartNode; i++) {
let sibling = searchStartNode.previousElementSibling;
while (sibling) {
if (sibling.matches('div.ds-markdown.ds-markdown--block:not(:empty)')) {
promptText = (sibling.innerText || sibling.textContent || "").trim();
break;
}
const mdBlock = sibling.querySelector('div.ds-markdown.ds-markdown--block:not(:empty)');
if (mdBlock) {
promptText = (mdBlock.innerText || mdBlock.textContent || "").trim();
break;
}
sibling = sibling.previousElementSibling;
}
if (promptText) break;
searchStartNode = searchStartNode.parentElement;
}
}
GM_log('DeepSeek Script: Using fallback text extraction. Found: ' + (promptText ? 'content' : 'no content'));
}
// --- End of Fallback logic ---
if (!promptText) {
GM_log('DeepSeek Script: Could not reliably find message content using any method. Using a default prompt.');
console.warn('DeepSeek Script: Could not reliably find message content using any method. Using a default prompt.');
promptText = "# 内容提取失败\\n\\n请手动复制内容或检查脚本的DOM选择器。";
}
showCardModal(promptText);
});
if (targetButtonElement.nextSibling) {
targetButtonElement.parentNode.insertBefore(genButton, targetButtonElement.nextSibling);
} else {
targetButtonElement.parentNode.appendChild(genButton);
}
targetButtonElement.dataset.hasGenerateCardButton = 'true';
}
function findAndProcessTargetButtons() {
const specificDislikeButtonSelector = `div.ds-icon-button svg path[d^="${dislikeIconSvgPathStart}"]`;
const iconPaths = document.querySelectorAll(specificDislikeButtonSelector);
iconPaths.forEach(svgPath => {
const buttonElement = svgPath.closest('div.ds-icon-button');
if (buttonElement) {
const messageRoleCheck = buttonElement.closest('[class*="agent"], [class*="assistant"], [class*="response"]');
const userRoleCheck = buttonElement.closest('[class*="user"], [class*="prompt"]');
if (userRoleCheck && messageRoleCheck && userRoleCheck.contains(messageRoleCheck)) { /* Skip nested */ }
else if (userRoleCheck) { return; }
addGenerateCardButton(buttonElement);
}
});
}
// --- New Feature Functions ---
function createFloatingActionPanel() {
if (document.getElementById('gm-floating-action-panel')) return;
floatingActionPanel = document.createElement('div');
floatingActionPanel.id = 'gm-floating-action-panel';
Object.assign(floatingActionPanel.style, {
position: 'fixed',
top: '45%',
right: '10px',
zIndex: '9999',
display: 'flex',
flexDirection: 'column',
gap: '10px',
opacity: '0.7',
transition: 'opacity 0.3s ease',
cursor: 'move'
});
floatingActionPanel.onmouseenter = () => { floatingActionPanel.style.opacity = '1'; };
floatingActionPanel.onmouseleave = () => { floatingActionPanel.style.opacity = '0.7'; };
let isDragging = false;
let initialX, initialY;
let xOffset = 0, yOffset = 0;
function dragStart(e) {
if (e.target === floatingActionPanel) {
isDragging = true;
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
const currentX = e.clientX - initialX;
const currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
floatingActionPanel.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
}
}
function dragEnd() {
isDragging = false;
}
floatingActionPanel.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.body.appendChild(floatingActionPanel);
}
function addExportWordButton() {
if (document.getElementById('gmMainExportWordBtn') || !floatingActionPanel) return;
const exportWordButton = document.createElement('button');
exportWordButton.id = 'gmMainExportWordBtn';
Object.assign(exportWordButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 11l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 4v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
const buttonText = '导出 Word';
exportWordButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
const setLoading = (isLoading) => {
if (isLoading) {
exportWordButton.disabled = true;
exportWordButton.innerHTML = '<span class="gm-spinner"></span><span>导出中...</span>';
} else {
exportWordButton.disabled = false;
exportWordButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
}
};
exportWordButton.addEventListener('click', async () => {
const checkedBoxes = document.querySelectorAll('.gm-message-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('请先勾选需要导出的对话。');
return;
}
setLoading(true);
let combinedMarkdown = [];
let firstTitle = 'DeepSeek';
let foundFirstTitle = false;
for (const checkbox of checkedBoxes) {
const messageWrapper = checkbox.closest('.gm-message-item-for-checkbox');
if (!messageWrapper) continue;
let promptText = '';
const copyButton = messageWrapper.querySelector('div.ds-icon-button svg path[d^="M3.65169"]')?.closest('div.ds-icon-button');
if (copyButton) {
try {
copyButton.click();
await new Promise(resolve => setTimeout(resolve, 150));
promptText = await navigator.clipboard.readText();
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
} catch (err) {
GM_log(`DeepSeek Script: Clipboard copy failed: ${err.message}. Falling back to htmlToMarkdown.`);
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
}
}
} else {
GM_log('DeepSeek Script: Copy button not found, falling back to direct text extraction.');
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
} else {
const children = Array.from(messageWrapper.children || []);
for (const child of children) {
if (!child.querySelector('.ds-flex') &&
!child.classList.contains('gm-message-checkbox-container')) {
promptText = child.innerText || child.textContent || "";
if (promptText.trim()) break;
}
}
}
}
if (promptText) {
if (!foundFirstTitle) {
const titleMatch = promptText.match(/^(?:#\s+)(.+)/);
if (titleMatch && titleMatch[1]) {
firstTitle = titleMatch[1].trim();
foundFirstTitle = true;
}
}
combinedMarkdown.push(promptText);
}
}
if (combinedMarkdown.length === 0) {
alert('未能提取已勾选对话的内容。');
setLoading(false);
return;
}
const fullMarkdown = combinedMarkdown.join('\n\n---\n\n');
GM_xmlhttpRequest({
method: "POST",
url: API_ENDPOINT_WORD,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ markdown: fullMarkdown, title: firstTitle }),
responseType: 'blob',
onload: function(response) {
setLoading(false);
if (response.status >= 200 && response.status < 300) {
try {
const dispositionHeader = response.responseHeaders.match(/content-disposition:.*/i) ? response.responseHeaders.match(/content-disposition:.*/i)[0] : '';
const filenameMatch = dispositionHeader.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
const filename = filenameMatch && filenameMatch[1] ? decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')) : 'document.docx';
const blob = response.response;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
alert('下载文件时出错: ' + e.message);
console.error("处理下载时出错:", e);
}
} else {
const reader = new FileReader();
reader.onload = function() {
try {
const errorResult = JSON.parse(this.result);
alert(`导出失败: ${errorResult.error || '未知错误'}`);
} catch (e) {
alert(`导出失败,状态码: ${response.status}。无法解析错误信息。`);
}
};
reader.readAsText(response.response);
}
},
onerror: function(response) {
setLoading(false);
alert(`请求错误: ${response.statusText || '无法连接到服务器'}`);
}
});
});
floatingActionPanel.appendChild(exportWordButton);
}
function addExportPdfButton() {
if (document.getElementById('gmMainExportPdfBtn') || !floatingActionPanel) return;
const exportPdfButton = document.createElement('button');
exportPdfButton.id = 'gmMainExportPdfBtn';
Object.assign(exportPdfButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 2v7h7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const buttonText = '导出 PDF';
exportPdfButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
const setLoading = (isLoading) => {
if (isLoading) {
exportPdfButton.disabled = true;
exportPdfButton.innerHTML = '<span class="gm-spinner"></span><span>导出中...</span>';
} else {
exportPdfButton.disabled = false;
exportPdfButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
}
};
exportPdfButton.addEventListener('click', async () => {
const checkedBoxes = document.querySelectorAll('.gm-message-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('请先勾选需要导出的对话。');
return;
}
setLoading(true);
let combinedMarkdown = [];
let firstTitle = 'DeepSeek';
let foundFirstTitle = false;
for (const checkbox of checkedBoxes) {
const messageWrapper = checkbox.closest('.gm-message-item-for-checkbox');
if (!messageWrapper) continue;
let promptText = '';
const copyButton = messageWrapper.querySelector('div.ds-icon-button svg path[d^="M3.65169"]')?.closest('div.ds-icon-button');
if (copyButton) {
try {
copyButton.click();
await new Promise(resolve => setTimeout(resolve, 150));
promptText = await navigator.clipboard.readText();
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
} catch (err) {
GM_log(`DeepSeek Script: Clipboard copy failed: ${err.message}. Falling back to htmlToMarkdown.`);
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
}
}
} else {
GM_log('DeepSeek Script: Copy button not found, falling back to direct text extraction.');
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
} else {
const children = Array.from(messageWrapper.children || []);
for (const child of children) {
if (!child.querySelector('.ds-flex') &&
!child.classList.contains('gm-message-checkbox-container')) {
promptText = child.innerText || child.textContent || "";
if (promptText.trim()) break;
}
}
}
}
if (promptText) {
if (!foundFirstTitle) {
const titleMatch = promptText.match(/^(?:#\s+)(.+)/);
if (titleMatch && titleMatch[1]) {
firstTitle = titleMatch[1].trim();
foundFirstTitle = true;
}
}
combinedMarkdown.push(promptText);
}
}
if (combinedMarkdown.length === 0) {
alert('未能提取已勾选对话的内容。');
setLoading(false);
return;
}
const fullMarkdown = combinedMarkdown.join('\n\n---\n\n');
GM_xmlhttpRequest({
method: "POST",
url: API_ENDPOINT_PDF,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ markdown: fullMarkdown, title: firstTitle }),
responseType: 'blob',
onload: function(response) {
setLoading(false);
if (response.status >= 200 && response.status < 300) {
try {
const dispositionHeader = response.responseHeaders.match(/content-disposition:.*/i) ? response.responseHeaders.match(/content-disposition:.*/i)[0] : '';
const filenameMatch = dispositionHeader.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
const filename = filenameMatch && filenameMatch[1] ? decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')) : 'document.pdf';
const blob = response.response;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
alert('下载文件时出错: ' + e.message);
console.error("处理下载时出错:", e);
}
} else {
const reader = new FileReader();
reader.onload = function() {
try {
const errorResult = JSON.parse(this.result);
alert(`导出失败: ${errorResult.error || '未知错误'}`);
} catch (e) {
alert(`导出失败,状态码: ${response.status}。无法解析错误信息。`);
}
};
reader.readAsText(response.response);
}
},
onerror: function(response) {
setLoading(false);
alert(`请求错误: ${response.statusText || '无法连接到服务器'}`);
}
});
});
floatingActionPanel.appendChild(exportPdfButton);
}
function addExportMindMapButton() {
if (document.getElementById('gmMainExportMindMapBtn') || !floatingActionPanel) return;
const exportMindMapButton = document.createElement('button');
exportMindMapButton.id = 'gmMainExportMindMapBtn';
Object.assign(exportMindMapButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 12H3M7 12C7 8.68629 9.68629 6 13 6V6C16.3137 6 19 8.68629 19 12V12C19 15.3137 16.3137 18 13 18V18C9.68629 18 7 15.3137 7 12V12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 6V3M13 18V21M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const buttonText = '导出思维导图';
exportMindMapButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
const setLoading = (isLoading) => {
if (isLoading) {
exportMindMapButton.disabled = true;
exportMindMapButton.innerHTML = '<span class="gm-spinner"></span><span>导出中...</span>';
} else {
exportMindMapButton.disabled = false;
exportMindMapButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
}
};
exportMindMapButton.addEventListener('click', async () => {
const checkedBoxes = document.querySelectorAll('.gm-message-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('请先勾选需要导出的对话。');
return;
}
setLoading(true);
let combinedMarkdown = [];
let firstTitle = 'DeepSeek';
let foundFirstTitle = false;
for (const checkbox of checkedBoxes) {
const messageWrapper = checkbox.closest('.gm-message-item-for-checkbox');
if (!messageWrapper) continue;
let promptText = '';
const copyButton = messageWrapper.querySelector('div.ds-icon-button svg path[d^="M3.65169"]')?.closest('div.ds-icon-button');
if (copyButton) {
try {
copyButton.click();
await new Promise(resolve => setTimeout(resolve, 150));
promptText = await navigator.clipboard.readText();
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
} catch (err) {
GM_log(`DeepSeek Script: Clipboard copy failed: ${err.message}. Falling back to htmlToMarkdown.`);
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
}
}
} else {
GM_log('DeepSeek Script: Copy button not found, falling back to direct text extraction.');
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
} else {
const children = Array.from(messageWrapper.children || []);
for (const child of children) {
if (!child.querySelector('.ds-flex') &&
!child.classList.contains('gm-message-checkbox-container')) {
promptText = child.innerText || child.textContent || "";
if (promptText.trim()) break;
}
}
}
}
if (promptText) {
if (!foundFirstTitle) {
const titleMatch = promptText.match(/^(?:#\s+)(.+)/);
if (titleMatch && titleMatch[1]) {
firstTitle = titleMatch[1].trim();
foundFirstTitle = true;
}
}
combinedMarkdown.push(promptText);
}
}
if (combinedMarkdown.length === 0) {
alert('未能提取已勾选对话的内容。');
setLoading(false);
return;
}
const fullMarkdown = combinedMarkdown.join('\n\n---\n\n');
GM_xmlhttpRequest({
method: "POST",
url: API_ENDPOINT_MINDMAP,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ markdown: fullMarkdown, title: firstTitle }),
responseType: 'blob',
onload: function(response) {
setLoading(false);
if (response.status >= 200 && response.status < 300) {
try {
const dispositionHeader = response.responseHeaders.match(/content-disposition:.*/i) ? response.responseHeaders.match(/content-disposition:.*/i)[0] : '';
const filenameMatch = dispositionHeader.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
const filename = filenameMatch && filenameMatch[1] ? decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')) : 'mindmap.mm';
const blob = response.response;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
alert('下载文件时出错: ' + e.message);
console.error("处理下载时出错:", e);
}
} else {
const reader = new FileReader();
reader.onload = function() {
try {
const errorResult = JSON.parse(this.result);
alert(`导出失败: ${errorResult.error || '未知错误'}`);
} catch (e) {
alert(`导出失败,状态码: ${response.status}。无法解析错误信息。`);
}
};
reader.readAsText(response.response);
}
},
onerror: function(response) {
setLoading(false);
alert(`请求错误: ${response.statusText || '无法连接到服务器'}`);
}
});
});
floatingActionPanel.appendChild(exportMindMapButton);
}
function addExportMarkdownButton() {
if (document.getElementById('gmMainExportMarkdownBtn') || !floatingActionPanel) return;
const exportMarkdownButton = document.createElement('button');
exportMarkdownButton.id = 'gmMainExportMarkdownBtn';
Object.assign(exportMarkdownButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 7V17M15 7V17M9 12H15M3 10V4C3 3.44772 3.44772 3 4 3H16L21 8V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const buttonText = '导出 MD';
exportMarkdownButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
const originalInnerHTML = exportMarkdownButton.innerHTML;
const setLoading = (isLoading) => {
if (isLoading) {
exportMarkdownButton.disabled = true;
exportMarkdownButton.innerHTML = '<span class="gm-spinner"></span><span>导出中...</span>';
} else {
exportMarkdownButton.disabled = false;
exportMarkdownButton.innerHTML = originalInnerHTML;
}
};
exportMarkdownButton.addEventListener('click', async () => {
const checkedBoxes = document.querySelectorAll('.gm-message-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('请先勾选需要导出的对话。');
return;
}
setLoading(true);
let combinedMarkdown = [];
let firstTitle = 'DeepSeek';
let foundFirstTitle = false;
for (const checkbox of checkedBoxes) {
const messageWrapper = checkbox.closest('.gm-message-item-for-checkbox');
if (!messageWrapper) continue;
let promptText = '';
const copyButton = messageWrapper.querySelector('div.ds-icon-button svg path[d^="M3.65169"]')?.closest('div.ds-icon-button');
if (copyButton) {
try {
copyButton.click();
await new Promise(resolve => setTimeout(resolve, 150));
promptText = await navigator.clipboard.readText();
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
} catch (err) {
GM_log(`DeepSeek Script: Clipboard copy failed: ${err.message}. Falling back to htmlToMarkdown.`);
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
}
}
} else {
GM_log('DeepSeek Script: Copy button not found, falling back to direct text extraction.');
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
} else {
const children = Array.from(messageWrapper.children || []);
for (const child of children) {
if (!child.querySelector('.ds-flex') &&
!child.classList.contains('gm-message-checkbox-container')) {
promptText = child.innerText || child.textContent || "";
if (promptText.trim()) break;
}
}
}
}
if (promptText) {
if (!foundFirstTitle) {
const titleMatch = promptText.match(/^(?:#\s+)(.+)/);
if (titleMatch && titleMatch[1]) {
firstTitle = titleMatch[1].trim();
foundFirstTitle = true;
}
}
combinedMarkdown.push(promptText);
}
}
if (combinedMarkdown.length === 0) {
alert('未能提取已勾选对话的内容。');
setLoading(false);
return;
}
const fullMarkdown = combinedMarkdown.join('\n\n---\n\n');
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
let chatName = document.title.split(' - ')[0] || firstTitle;
chatName = `DeepSeek - ${chatName}`.replace(/[\/\\?%*:|"<>]/g, '-');
const fileName = `${chatName}_${timestamp}.md`;
const blob = new Blob([fullMarkdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
alert('下载文件时出错: ' + e.message);
console.error("处理下载时出错:", e);
} finally {
setLoading(false);
}
});
floatingActionPanel.appendChild(exportMarkdownButton);
}
function addMainGenerateCardButton() {
if (document.getElementById('gmMainGenerateCardBtn') || !floatingActionPanel) return;
const mainGenButton = document.createElement('button');
mainGenButton.id = 'gmMainGenerateCardBtn';
Object.assign(mainGenButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 15.6538V18C21 19.1046 20.1046 20 19 20H5C3.89543 20 3 19.1046 3 18V6C3 4.89543 3.89543 4 5 4H14.3462" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 14L10.0718 10.9282C10.4522 10.5478 11.0854 10.5478 11.4658 10.9282L14 13.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M12.5 15L14.0718 13.4282C14.4522 13.0478 15.0854 13.0478 15.4658 13.4282L18 16" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M18 4L22 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17 9L21 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
const buttonText = '生成卡片';
mainGenButton.innerHTML = iconHTML + `<span>${buttonText}</span>`;
mainGenButton.addEventListener('click', async () => {
const checkedBoxes = document.querySelectorAll('.gm-message-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('请先勾选需要生成卡片内容的对话。');
return;
}
let combinedMarkdown = [];
for (const checkbox of checkedBoxes) {
const messageWrapper = checkbox.closest('.gm-message-item-for-checkbox');
if (!messageWrapper) continue;
let promptText = '';
const copyButton = messageWrapper.querySelector('div.ds-icon-button svg path[d^="M3.65169"]')?.closest('div.ds-icon-button');
if (copyButton) {
try {
copyButton.click();
await new Promise(resolve => setTimeout(resolve, 150));
promptText = await navigator.clipboard.readText();
GM_log('DeepSeek Script: Successfully retrieved content from clipboard.');
} catch (err) {
GM_log(`DeepSeek Script: Clipboard copy failed: ${err.message}. Falling back to htmlToMarkdown.`);
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
}
}
} else {
GM_log('DeepSeek Script: Copy button not found, falling back to direct text extraction.');
const assistantContent = messageWrapper.querySelector('.ds-markdown.ds-markdown--block:not(:empty)');
if (assistantContent) {
promptText = htmlToMarkdown(assistantContent);
} else {
const children = Array.from(messageWrapper.children || []);
for (const child of children) {
if (!child.querySelector('.ds-flex') &&
!child.classList.contains('gm-message-checkbox-container')) {
promptText = child.innerText || child.textContent || "";
if (promptText.trim()) break;
}
}
}
}
if (promptText) {
combinedMarkdown.push(promptText);
}
}
if (combinedMarkdown.length > 0) {
showCardModal(combinedMarkdown.join('\n\n---\n\n'));
} else {
alert('未能提取已勾选对话的内容。');
}
});
floatingActionPanel.appendChild(mainGenButton);
}
function addCheckboxesToMessages() {
// 查找所有可能的对话内容区域
// 提问部分:在ds-flex节点的上级的上级的前面兄弟节点中查找内容
// 回答部分:在ds-flex节点的上级的前面兄弟节点中查找内容
const flexElements = document.querySelectorAll('.ds-flex');
flexElements.forEach(flexEl => {
// 跳过已经处理过的对话区域
const processedParent = flexEl.closest('.gm-message-item-for-checkbox');
if (processedParent) {
return;
}
// 检查是否是有效的对话操作区域
const actionButtons = flexEl.querySelectorAll('.ds-icon-button');
if (actionButtons.length < 2) {
return; // 不是我们要找的操作区域
}
let messageWrapper = null;
let contentElement = null;
// 检查是否是回答部分 - 回答部分的flex直接父级是对话容器
const parentElement = flexEl.parentElement;
if (parentElement) {
const markdownElement = parentElement.querySelector('.ds-markdown.ds-markdown--block');
if (markdownElement && !markdownElement.contains(flexEl)) {
messageWrapper = parentElement;
contentElement = markdownElement;
}
}
// 检查是否是提问部分 - 提问部分需要往上查找两级,然后查找前面的兄弟节点
if (!messageWrapper) {
const grandParent = flexEl.parentElement?.parentElement;
if (grandParent) {
// 在提问部分,内容通常在祖父节点的前一个兄弟节点中
const prevSibling = grandParent.previousElementSibling;
if (prevSibling) {
messageWrapper = prevSibling.parentElement;
contentElement = prevSibling;
}
}
}
// 如果找到了对话容器和内容元素
if (messageWrapper && contentElement) {
// 确保没有重复添加
if (messageWrapper.querySelector('.gm-message-checkbox-container')) {
return;
}
// 添加标记类
messageWrapper.classList.add('gm-message-item-for-checkbox');
const container = document.createElement('div');
container.className = 'gm-message-checkbox-container';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'gm-message-checkbox';
container.appendChild(checkbox);
messageWrapper.prepend(container); // 添加到对话容器的开始位置
}
});
}
function addSelectAllButton() {
if (document.getElementById('gmMainSelectAllBtn') || !floatingActionPanel) return;
const selectAllButton = document.createElement('button');
selectAllButton.id = 'gmMainSelectAllBtn';
Object.assign(selectAllButton.style, floatingButtonStyles);
const iconHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 5.5L4.5 7L7.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 11.5L4.5 13L7.5 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 17.5L4.5 19L7.5 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11 6H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11 18H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
const selectAllText = '全选';
const deselectAllText = '取消全选';
const textSpan = document.createElement('span');
textSpan.textContent = selectAllText;
selectAllButton.innerHTML = iconHTML;
selectAllButton.appendChild(textSpan);
selectAllButton.addEventListener('click', () => {
const allCheckboxes = document.querySelectorAll('.gm-message-checkbox');
if (allCheckboxes.length === 0) {
return;
}
const shouldSelectAll = Array.from(allCheckboxes).some(cb => !cb.checked);
allCheckboxes.forEach(checkbox => {
checkbox.checked = shouldSelectAll;
});
textSpan.textContent = shouldSelectAll ? deselectAllText : '全选';
});
floatingActionPanel.appendChild(selectAllButton);
}
function runFeatureInjections() {
findAndProcessTargetButtons(); // Existing feature
// These buttons will be added to the floating panel
addSelectAllButton();
addExportWordButton();
addExportPdfButton();
addExportMindMapButton();
addExportMarkdownButton();
addMainGenerateCardButton();
addCheckboxesToMessages(); // New feature
}
// --- Main Execution ---
createCardModal();
createFloatingActionPanel();
setTimeout(runFeatureInjections, 3000);
const observer = new MutationObserver((mutationsList) => {
let needsUpdate = false;
let needsCheckboxUpdate = false;
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查新增节点是否包含内容元素或操作按钮
if (node.querySelector && (
node.querySelector('.ds-flex') ||
node.querySelector('.ds-markdown.ds-markdown--block') ||
node.querySelector('.ds-icon-button')
)) {
needsCheckboxUpdate = true;
}
// 整体更新标志
needsUpdate = true;
}
});
}
}
// 如果检测到新的内容元素,立即添加复选框
if (needsCheckboxUpdate) {
addCheckboxesToMessages();
}
// 整体功能更新(可能包括其他功能)
if (needsUpdate) {
// Using a timeout to let the DOM settle and avoid over-firing
setTimeout(runFeatureInjections, 500);
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
})();