您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export and clean Microsoft Teams chat transcripts and enhance the UI
// ==UserScript== // @name Teams Chat Exporter // @namespace http://tampermonkey.net/ // @version 2025-04-23 // @description Export and clean Microsoft Teams chat transcripts and enhance the UI // @author You // @match https://teams.microsoft.com/v2/* // @icon https://www.google.com/s2/favicons?sz=64&domain=microsoft.com // @grant none // @license MIT // ==/UserScript== (() => { 'use strict'; const KEY_THRESHOLD_MS = 300; const BANNER_ID = 'teams-chat-banner'; let mutationObserver = null; let collectedNodes = []; let resetTimer = null; let pressCount = 0; let lastDialogId = 0; /** Show a fixed red banner at the bottom **/ function showBanner() { if (document.getElementById(BANNER_ID)) return; const banner = document.createElement('div'); banner.id = BANNER_ID; banner.textContent = 'Observing – scroll through chat to gather messages. Press Shift Shift Shift to stop gathering'; Object.assign(banner.style, { position: 'fixed', bottom: '0', left: '0', width: '100%', background: 'red', color: 'white', textAlign: 'center', padding: '8px', fontFamily: 'Arial,sans-serif', zIndex: '9999' }); document.body.appendChild(banner); } /** Remove the observation banner **/ function removeBanner() { const banner = document.getElementById(BANNER_ID); if (banner) banner.remove(); } /** Watch for N rapid key presses **/ const watchKeyCombo = (keyName, callback) => { document.addEventListener('keydown', event => { if (!event.key.startsWith(keyName)) return; pressCount++; clearTimeout(resetTimer); resetTimer = setTimeout(() => pressCount = 0, KEY_THRESHOLD_MS); if (pressCount >= 3) { pressCount = 0; callback(); } }); }; // Bind triple-Shift and triple-Control watchKeyCombo('Shift', () => toggleGathering(true)); watchKeyCombo('Control', () => toggleGathering(false)); /** Toggle gathering mode **/ const toggleGathering = observe => { if (mutationObserver) stopGathering(); else startGathering(observe); }; /** Start collecting messages **/ function startGathering(observeChanges) { collectedNodes = []; gatherCurrentMessages(); if (!observeChanges) { stopGathering(); return; } showBanner(); const target = document.getElementById('chat-pane-list'); if (!target) { console.error('Target #chat-pane-list not found'); return; } mutationObserver = new MutationObserver(muts => { if (muts.some(m => m.type === 'childList' || m.type === 'characterData')) gatherCurrentMessages(); }); mutationObserver.observe(target, { childList: true, subtree: true, characterData: true }); console.log('Started gathering (observe)', observeChanges); } /** Stop collecting and export transcript **/ function stopGathering() { removeBanner(); if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; } console.log('Stopped gathering, total:', collectedNodes.length); try { const nodes = filterAndSort(collectedNodes); const html = buildTranscript(nodes); openTranscriptWindow(html); } catch (e) { console.error('Error exporting transcript', e); } collectedNodes = []; } /** Gather current children of chat pane **/ function gatherCurrentMessages() { const list = document.getElementById('chat-pane-list'); if (list) collectedNodes.push(...Array.from(list.children)); } /** Filter duplicates, remove GIFs, sort by ID **/ function filterAndSort(nodes) { const map = new Map(); nodes.forEach(n => { const msg = n.querySelector('[id^="message-body-"]'); if (msg && !n.querySelector('[aria-label="Animated GIF"]')) { const id = parseInt(msg.id.replace('message-body-', ''), 10); if (!map.has(id)) map.set(id, n); } }); return Array.from(map.entries()) .sort((a,b) => a[0]-b[0]) .map(entry => entry[1]); } /** Replace <img> emojis using alt/text **/ function replaceEmojiImages(node) { node.querySelectorAll('img[itemtype*="Emoji"]').forEach(img => { const span = document.createElement('span'); span.innerText = img.alt || ''; img.parentNode.replaceChild(span, img); }); } /** Build HTML transcript **/ function buildTranscript(nodes) { let lastAuthor = '', lastDate = null; return nodes.map(n => { replaceEmojiImages(n); const authorEl = n.querySelector('[data-tid="message-author-name"]'); const timeEl = n.querySelector('[id^="timestamp-"]'); const bodyEl = n.querySelector('[id^="message-body-"] [id^="content-"]'); if (!authorEl || !timeEl || !bodyEl) return ''; const author = authorEl.innerText.trim(); const ts = new Date(timeEl.getAttribute('datetime')); const tsStr = ts.toLocaleString().replace(/:([0-9]{2})(?= )/, ''); const date = tsStr.split(',')[0]; const newDay = lastDate && ts.getDate() !== lastDate.getDate(); let header = ''; if (author !== lastAuthor) header = `<hr/><b>${author}</b> [${tsStr}]:<br/>`; else if (newDay) header = `<div style="text-align:center;"><hr/>${date}<hr/></div>`; lastAuthor = author; lastDate = ts; // Select all divs with aria-label containing "Mention" in bodyEl const mentionDivs = bodyEl.querySelectorAll('div[aria-label*="Mention"]'); // Loop through the NodeList and replace each div with its corresponding span mentionDivs.forEach(div => { console.log(`Replacing div: `, div); // Debug log for divs being replaced const span = document.createElement('span'); // Copy over necessary attributes from the div to the span span.innerHTML = div.innerHTML; // Copy the inner content span.className = div.className; // Copy the class names // Insert the span into the div's parent and remove the div div.parentNode.insertBefore(span, div); div.parentNode.removeChild(div); }); // Handle quoted reply replacements const quotedReplies = bodyEl.querySelectorAll('div[data-track-module-name="messageQuotedReply"]'); quotedReplies.forEach(div => { const blockquote = document.createElement('blockquote'); blockquote.innerHTML = div.innerHTML; // Copy the inner content blockquote.className = div.className; // Copy the class names div.parentNode.insertBefore(blockquote, div); div.parentNode.removeChild(div); }); console.log(`Body HTML after replacement is: `, bodyEl.innerHTML); // Debug log for updated body HTML // Generate the updated body HTML const updatedBodyHtml = bodyEl.innerHTML; return `<div class="message">${header}<section>${updatedBodyHtml}</section></div>`; }).join(''); } /** Open transcript in new window **/ function openTranscriptWindow(content) { lastDialogId++; const dialog = document.createElement('dialog'); dialog.style.width = '80vw'; dialog.style.height = '80vh'; dialog.style.overflow = 'auto'; dialog.style.padding = '20px'; // Create content for the dialog const dialogContent = ` <style> #transcript { font-family: Arial, sans-serif; margin: 2em; } section { padding-left: 1em; border-left: 1px solid #ccc; } hr { border: none; border-top: 1px solid #ccc; margin: 1em 0; } .closeDialog {position: absolute; top:-50px; left:50%; transform:translateX(-50%);} div[aria-label*="Mention"] { display: inline; font-weight: bold; } </style> <div id='transcript'> ${content} <button class='closeDialog' id="closeDialog-${lastDialogId}">Close</button> </div> `; // Set the inner HTML of the dialog dialog.innerHTML = dialogContent; // Append dialog to the body document.body.appendChild(dialog); // Open the dialog dialog.showModal(); // Add an event listener to the close button document.getElementById(`closeDialog-${lastDialogId}`).addEventListener('click', () => { dialog.close(); }); } /** Live-page formatting: wrap galleries, emojis, code blocks **/ function formatMessages() { const sections = document.querySelectorAll('.message section'); sections.forEach(section => { // link gallery images section.querySelectorAll('img[data-gallery-src]').forEach(img => { const link = document.createElement('a'); link.href = img.getAttribute('data-gallery-src'); link.target = '_blank'; img.replaceWith(link); link.appendChild(img); }); // convert emojis section.querySelectorAll('img[alt]').forEach(img => { const alt = img.alt.trim(); if (/^(?:\p{Emoji}|[\u203C-\u3299\uD83C\uD000-\uD83F\uDC00-\uDFFF])+/u.test(alt)) { const span = document.createElement('span'); span.innerText = alt; img.replaceWith(span); } }); // structure first span const firstSpan = section.querySelector('div > div > div div:first-child span:first-child'); if (firstSpan) { const strong = document.createElement('strong'); strong.textContent = firstSpan.textContent; firstSpan.textContent = ''; firstSpan.appendChild(strong); } // add nbsp and breaks section.querySelectorAll('div > div > div div:first-child span').forEach(span => span.innerHTML += ' '); const firstDiv = section.querySelector('div > div > div div:first-child'); if (firstDiv) firstDiv.innerHTML += '<br><br>'; // replace parent div with blockquote const parentDiv = section.querySelector('div > div > div'); if (parentDiv) { const bq = document.createElement('blockquote'); while (parentDiv.firstChild) bq.appendChild(parentDiv.firstChild); parentDiv.replaceWith(bq); } }); } formatMessages(); /** Convert all page images to base64 **/ async function replaceImagesWithBase64() { const imgs = document.querySelectorAll('img'); for (const img of imgs) { try { const res = await fetch(img.src); const blob = await res.blob(); const reader = new FileReader(); reader.onloadend = () => { const newImg = document.createElement('img'); newImg.src = reader.result; newImg.alt = img.alt; img.replaceWith(newImg); }; reader.readAsDataURL(blob); } catch (e) { console.error('Image base64 error', e); } } } replaceImagesWithBase64(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址