您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[v2.0] 新增长按、编辑功能!感谢@船长技术支持。文字记录页面单击"复制为MD"按钮复制纯文本,长按1秒复制含说话人和时间戳的完整信息。
// ==UserScript== // @name 飞书妙记增强脚本 // @namespace https://github.com/liaozhu913/Lark-Minutes-Enhancer // @version 2.0 // @description [v2.0] 新增长按、编辑功能!感谢@船长技术支持。文字记录页面单击"复制为MD"按钮复制纯文本,长按1秒复制含说话人和时间戳的完整信息。 // @author liaozhu913, @船长 // @match https://*.feishu.cn/minutes/* // @grant GM_addStyle // @run-at document-end // @icon https://raw.githubusercontent.com/liaozhu913/Lark-Minutes-Enhancer/main/icon.png // @homepageURL https://github.com/liaozhu913/Lark-Minutes-Enhancer // @supportURL https://github.com/liaozhu913/Lark-Minutes-Enhancer/issues // ==/UserScript== (function() { 'use strict'; const SCRIPT_PREFIX = '[飞书妙记增强 v2.1]:'; // ... UI样式, expandAllChapters, summaryToMarkdown 函数与 v1.8 相同 ... GM_addStyle(` #floating-copy-button:disabled { background-color: #868e96; cursor: not-allowed; } div.ai-quota-exceed-mask, div.linear-gradient-content { display: none !important; } #floating-copy-button { position: absolute; top: 15px; right: 20px; z-index: 9999; padding: 6px 12px; font-size: 14px; font-weight: bold; color: #fff; background-color: #007AFF; border: none; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); cursor: pointer; transition: all 0.2s ease-in-out; } #floating-copy-button:hover:not(:disabled) { background-color: #0056b3; transform: scale(1.05); } #floating-copy-button.success { background-color: #28a745; } #floating-copy-button.error { background-color: #dc3545; } `); function expandAllChapters() { /* ... */ } function summaryToMarkdown(element) { /* ... */ } // --- [核心解析器 2 - 重大升级!] 文字记录滚动读取器 --- async function transcriptToText(scrollContainer, button, includeMetadata = false) { const collectedData = new Map(); // 使用Map来根据时间戳去重 const originalScrollTop = scrollContainer.scrollTop; let lastScrollTop = -1; button.textContent = '读取中...'; button.disabled = true; console.log(SCRIPT_PREFIX + '开始分段读取文字记录...'); scrollContainer.scrollTop = 0; // 从头开始 await new Promise(resolve => setTimeout(resolve, 100)); // 等待滚动生效 while (scrollContainer.scrollTop !== lastScrollTop) { lastScrollTop = scrollContainer.scrollTop; const paragraphs = scrollContainer.querySelectorAll('.paragraph-editor-wrapper'); paragraphs.forEach(p => { const timeEl = p.querySelector('.p-time'); if (timeEl) { const time = timeEl.getAttribute('time-content').trim(); if (!collectedData.has(time)) { // 如果是新的时间戳 const contentEl = p.querySelector('.mm-paragraph-content'); const content = contentEl ? contentEl.innerText.trim() : ''; if (content) { if (includeMetadata) { // 获取说话人信息 const speakerEl = p.querySelector('.p-user-name-editable'); const speaker = speakerEl ? speakerEl.getAttribute('user-name-content') || speakerEl.innerText.trim() : ''; // 格式化时间戳 const formattedTime = time; // 保存包含说话人和时间戳的完整信息,格式与网页一致 const fullContent = `[${speaker} ${formattedTime}]\n${content}`; collectedData.set(time, fullContent); } else { // 只保存纯文本内容,不包含说话人和时间戳 collectedData.set(time, content); } } } } }); // 向下滚动一屏 scrollContainer.scrollTop += scrollContainer.clientHeight; await new Promise(resolve => setTimeout(resolve, 100)); // 等待滚动和DOM更新 } button.disabled = false; scrollContainer.scrollTop = originalScrollTop; // 恢复滚动条 console.log(SCRIPT_PREFIX + `读取完毕,共收集到 ${collectedData.size} 条不重复的记录。`); // 将Map中的值拼接起来 return Array.from(collectedData.values()).join('\n\n'); } // --- [功能 - 最终版!] 上下文感知的悬浮复制按钮 --- function addFloatingCopyButton() { const rightPanel = document.querySelector('div.detail-right-content'); if (rightPanel && !document.getElementById('floating-copy-button')) { const copyButton = document.createElement('button'); copyButton.id = 'floating-copy-button'; copyButton.textContent = '复制为MD'; copyButton.addEventListener('click', async () => { let contentToCopy = ''; let contentType = '智能纪要'; const summaryTab = document.querySelector('.summary-tab.right-tab-visible'); const transcriptTab = document.querySelector('.transcript-tab.right-tab-visible'); if (summaryTab) { const contentWrapper = summaryTab.querySelector('.minutes-editable.ai-summary-content-editable'); if (contentWrapper) { contentToCopy = summaryToMarkdown(contentWrapper); } } else if (transcriptTab) { contentType = '文字记录'; const scrollContainer = transcriptTab.querySelector('.rc-virtual-list-holder'); if (scrollContainer) { contentToCopy = await transcriptToText(scrollContainer, copyButton); // 调用新的异步读取器 } } if (contentToCopy) { navigator.clipboard.writeText(contentToCopy).then(/* ... */); } }); rightPanel.style.position = 'relative'; rightPanel.appendChild(copyButton); } } // 省略重复代码的完整实现 function expandAllChapters() { const expandButton = document.querySelector('div.ai-summary-content-editable-expand-button-wrapper > button:not([data-expanded="true"])'); if (expandButton && expandButton.textContent.includes('展开')) { expandButton.click(); expandButton.setAttribute('data-expanded', 'true'); } } function summaryToMarkdown(element) { let markdownText = ''; function processNode(node, listLevel = 0) { if (node.nodeType === Node.TEXT_NODE) { markdownText += node.textContent; } else if (node.nodeType === Node.ELEMENT_NODE) { let prefix = '', suffix = ''; let children = Array.from(node.childNodes); switch (node.tagName) { case 'DIV': if (!node.classList.contains('list-div')) { suffix = '\n'; } break; case 'UL': case 'OL': children.forEach(child => processNode(child, listLevel + 1)); return; case 'LI': prefix = ' '.repeat(listLevel - 1) + '- '; suffix = '\n'; break; case 'SPAN': if (getComputedStyle(node).fontWeight === '700' || getComputedStyle(node).fontWeight === 'bold') { prefix = '**'; suffix = '**'; } if (node.hasAttribute('data-enter')) { return; } break; } markdownText += prefix; children.forEach(child => processNode(child, listLevel)); markdownText += suffix; } } processNode(element); return markdownText.replace(/\n\s*\n/g, '\n').trim(); } const fullAddFloatingCopyButton = addFloatingCopyButton; addFloatingCopyButton = function() { const rightPanel = document.querySelector('div.detail-right-content'); if (rightPanel) { // 检查是否在智能纪要页面 const summaryTab = document.querySelector('.summary-tab.right-tab-visible'); const transcriptTab = document.querySelector('.transcript-tab.right-tab-visible'); const existingEditButton = document.getElementById('floating-edit-button'); // 在智能纪要页面显示编辑按钮,在文字记录页面隐藏 if (summaryTab) { if (!existingEditButton) { const editButton = document.createElement('button'); editButton.id = 'floating-edit-button'; editButton.textContent = '编辑'; editButton.style.cssText = ` position: absolute; top: 15px; right: 115px; z-index: 9999; padding: 6px 12px; font-size: 14px; font-weight: bold; color: #007AFF; background-color: transparent; border: 1px solid #007AFF; border-radius: 6px; cursor: pointer; transition: all 0.2s ease-in-out; `; // 添加悬停效果 editButton.addEventListener('mouseenter', () => { editButton.style.backgroundColor = '#007AFF'; editButton.style.color = '#fff'; }); editButton.addEventListener('mouseleave', () => { editButton.style.backgroundColor = 'transparent'; editButton.style.color = '#007AFF'; }); // 点击事件:触发原有的编辑按钮或处理编辑状态 editButton.addEventListener('click', () => { // 检查当前是否在编辑状态 const editButtonGroup = document.querySelector('.edit-button-group'); const originalEditButton = document.querySelector('.summary-edit-btn'); if (editButtonGroup) { // 如果已在编辑状态,根据按钮文本执行相应操作 if (editButton.textContent === '取消') { const cancelButton = editButtonGroup.querySelector('.ud__button--outlined'); if (cancelButton) cancelButton.click(); } else if (editButton.textContent === '完成') { const saveButton = editButtonGroup.querySelector('.ud__button--filled'); if (saveButton) saveButton.click(); } } else if (originalEditButton) { // 如果不在编辑状态,点击编辑按钮 originalEditButton.click(); } }); // 更新编辑按钮状态的函数 function updateEditButtonState() { // 使用延迟检查,等待DOM更新 setTimeout(() => { const editButtonGroup = document.querySelector('.edit-button-group'); if (editButtonGroup) { // 编辑状态:显示取消和完成按钮 editButton.style.cssText = ` position: absolute; top: 15px; right: 190px; z-index: 9999; padding: 6px 8px; font-size: 12px; font-weight: bold; color: #666; background-color: transparent; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: all 0.2s ease-in-out; `; editButton.textContent = '取消'; // 创建完成按钮(如果不存在) let saveButton = document.getElementById('floating-save-button'); if (!saveButton) { saveButton = document.createElement('button'); saveButton.id = 'floating-save-button'; saveButton.textContent = '完成'; saveButton.style.cssText = ` position: absolute; top: 15px; right: 125px; z-index: 9999; padding: 6px 8px; font-size: 12px; font-weight: bold; color: #fff; background-color: #007AFF; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease-in-out; `; // 完成按钮点击事件 saveButton.addEventListener('click', () => { const saveBtn = document.querySelector('.edit-button-group .ud__button--filled'); if (saveBtn) saveBtn.click(); }); rightPanel.appendChild(saveButton); } saveButton.style.display = 'block'; } else { // 非编辑状态:显示编辑按钮 editButton.style.cssText = ` position: absolute; top: 15px; right: 115px; z-index: 9999; padding: 6px 12px; font-size: 14px; font-weight: bold; color: #007AFF; background-color: transparent; border: 1px solid #007AFF; border-radius: 6px; cursor: pointer; transition: all 0.2s ease-in-out; `; editButton.textContent = '编辑'; // 隐藏完成按钮 const saveButton = document.getElementById('floating-save-button'); if (saveButton) { saveButton.style.display = 'none'; } } }, 100); } // 初始状态更新 updateEditButtonState(); // 监听编辑状态变化 - 观察整个summary-tab区域 const editStateObserver = new MutationObserver(() => { updateEditButtonState(); }); // 观察更大的区域以捕获编辑状态变化 const summaryContainer = document.querySelector('.summary-tab'); if (summaryContainer) { editStateObserver.observe(summaryContainer, { childList: true, subtree: true, attributes: true }); } rightPanel.appendChild(editButton); } else { existingEditButton.style.display = 'block'; } } else if (transcriptTab && existingEditButton) { // 在文字记录页面隐藏编辑按钮 existingEditButton.style.display = 'none'; } // 添加复制按钮 if (!document.getElementById('floating-copy-button')) { const copyButton = document.createElement('button'); copyButton.id = 'floating-copy-button'; copyButton.textContent = '复制为MD'; let pressTimer = null; let isLongPress = false; // 鼠标按下事件 copyButton.addEventListener('mousedown', () => { isLongPress = false; pressTimer = setTimeout(() => { isLongPress = true; // 长按1秒后的视觉反馈 const transcriptTab = document.querySelector('.transcript-tab.right-tab-visible'); if (transcriptTab) { copyButton.style.backgroundColor = '#ff6b35'; copyButton.textContent = '含时间戳'; } }, 1000); }); // 鼠标松开事件 copyButton.addEventListener('mouseup', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } }); // 鼠标离开事件(防止拖拽时计时器继续运行) copyButton.addEventListener('mouseleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } // 恢复按钮样式 copyButton.style.backgroundColor = ''; copyButton.textContent = '复制为MD'; }); // 点击事件 copyButton.addEventListener('click', async () => { // 如果是长按,延迟一点执行以确保长按标记已设置 if (isLongPress) { await new Promise(resolve => setTimeout(resolve, 100)); } let contentToCopy = ''; let contentType = '智能纪要'; const summaryTab = document.querySelector('.summary-tab.right-tab-visible'); const transcriptTab = document.querySelector('.transcript-tab.right-tab-visible'); if (summaryTab) { const contentWrapper = summaryTab.querySelector('.minutes-editable.ai-summary-content-editable'); if (contentWrapper) { contentToCopy = summaryToMarkdown(contentWrapper); } } else if (transcriptTab) { contentType = '文字记录'; const scrollContainer = transcriptTab.querySelector('.rc-virtual-list-holder'); if (scrollContainer) { // 根据是否长按决定是否包含元数据 contentToCopy = await transcriptToText(scrollContainer, copyButton, isLongPress); } } if (contentToCopy) { const copyTypeText = isLongPress ? '(含时间戳)' : '(纯文本)'; navigator.clipboard.writeText(contentToCopy).then(() => { console.log(SCRIPT_PREFIX + `${contentType}${copyTypeText}已成功复制。`); copyButton.textContent = '复制成功!'; copyButton.className = 'success'; copyButton.style.backgroundColor = ''; setTimeout(() => { copyButton.textContent = '复制为MD'; copyButton.className = ''; }, 2000); }).catch(err => { console.error(SCRIPT_PREFIX + '复制失败:', err); copyButton.textContent = '复制失败'; copyButton.className = 'error'; copyButton.style.backgroundColor = ''; setTimeout(() => { copyButton.textContent = '复制为MD'; copyButton.className = ''; }, 2000); }); } else { console.error(SCRIPT_PREFIX + '未找到可复制的内容。'); } // 重置长按状态 isLongPress = false; }); rightPanel.style.position = 'relative'; rightPanel.appendChild(copyButton); } } }; const observer = new MutationObserver(() => { expandAllChapters(); addFloatingCopyButton(); }); observer.observe(document.body, { childList: true, subtree: true }); console.log(SCRIPT_PREFIX + '脚本已启动,终极虚拟滚动兼容模式已启用!'); })();