你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
(我已經安裝了使用者樣式管理器,讓我安裝!)
// ==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 + '脚本已启动,终极虚拟滚动兼容模式已启用!');
})();