飞书妙记增强脚本

[v2.0] 新增长按、编辑功能!感谢@船长技术支持。文字记录页面单击"复制为MD"按钮复制纯文本,长按1秒复制含说话人和时间戳的完整信息。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         飞书妙记增强脚本
// @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 + '脚本已启动,终极虚拟滚动兼容模式已启用!');
})();