您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动将 pony.town 的聊天记录保存到浏览器本地存储,并提供查看、复制和清除界面。
当前为
// ==UserScript== // @name PonyTown 网页聊天记录存档器 // @namespace http://tampermonkey.net/ // @version 3.3 // @description 自动将 pony.town 的聊天记录保存到浏览器本地存储,并提供查看、复制和清除界面。 // @author doucx // @match https://pony.town/* // @grant GM_addStyle // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; /* * ================================================================= * 核心功能模块 * 负责解析、处理和存储数据。 * ================================================================= */ /** * 将包含HTML的字符串转换为纯文本。 * - 忽略 style="display: none;" 的元素。 * - 将 <img> 标签转换为其 alt 属性的文本。 * @param {string} htmlString - 包含HTML的输入字符串。 * @returns {string} - 转换后的纯文本。 */ function convertChatHtmlToPlainText(htmlString) { const container = document.createElement('div'); container.innerHTML = htmlString; function extractVisibleText(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType === Node.ELEMENT_NODE) { if (node.style.display === 'none') { return ''; } if (node.tagName === 'IMG') { return node.alt || ''; } let text = ''; for (const child of node.childNodes) { text += extractVisibleText(child); } return text; } return ''; } return extractVisibleText(container); } /** * 通过分析元素背景颜色的频率,找出当前活跃的标签页。 * 这是一种不依赖特定颜色值的通用异常检测方法。 * @param {string} htmlString - 包含多个标签页链接的HTML字符串。 * @returns {string|null} - 活跃标签页的文本内容,如果未找到则返回null。 */ function findActiveTabByStyleAnomaly(htmlString) { const container = document.createElement('div'); container.innerHTML = htmlString; const tabs = container.querySelectorAll('a.chat-log-tab'); if (tabs.length < 2) { return tabs.length === 1 ? tabs[0].textContent.trim() : null; } const styleFrequencies = new Map(); for (const tab of tabs) { const bgColor = tab.style.backgroundColor; styleFrequencies.set(bgColor, (styleFrequencies.get(bgColor) || 0) + 1); } let uniqueStyle = null; for (const [style, count] of styleFrequencies.entries()) { if (count === 1) { uniqueStyle = style; break; } } if (!uniqueStyle) { return null; } for (const tab of tabs) { if (tab.style.backgroundColor === uniqueStyle) { return tab.textContent.trim(); } } return null; } /** * 在页面DOM中定位聊天相关的关键元素容器。 * @returns {{tabs: HTMLElement|null, chatLog: HTMLElement|null}} 包含已定位元素的对象。 */ function locateChatElements() { const tabsSelector = '.chat-log-tabs'; const chatLogSelector = '.chat-log-scroll-inner'; const tabsContainer = document.querySelector(tabsSelector); const chatLogContainer = document.querySelector(chatLogSelector); return { tabs: tabsContainer, chatLog: chatLogContainer }; } /** * 智能合并两个多行文本块,自动去除重叠部分,并在数据不连续时插入警告标记。 * @param {string} oldText - 已存在的旧文本。 * @param {string} newText - 需要追加的新文本。 * @returns {string} - 合并后的完整文本。 */ function mergeAndDeduplicateText(oldText, newText) { if (!oldText) return newText; if (!newText) return oldText; const oldLines = oldText.split('\n'); const newLines = newText.split('\n'); let overlapLength = 0; const maxPossibleOverlap = Math.min(oldLines.length, newLines.length); for (let i = maxPossibleOverlap; i > 0; i--) { const suffixOfOld = oldLines.slice(-i); const prefixOfNew = newLines.slice(0, i); if (suffixOfOld.toString() === prefixOfNew.toString()) { overlapLength = i; break; } } // 当旧文本和新文本都存在,但没有任何重叠时,判定为数据不连续。 const discontinuityDetected = oldLines.length > 0 && newLines.length > 0 && overlapLength === 0; const linesToAdd = newLines.slice(overlapLength); if (linesToAdd.length === 0) return oldText; let finalLines; if (discontinuityDetected) { console.warn('检测到聊天记录不连续,可能存在数据丢失。已插入警告标记。'); finalLines = oldLines.concat(['[警告 - 此处可能存在记录丢失]'], linesToAdd); } else { finalLines = oldLines.concat(linesToAdd); } return finalLines.join('\n'); } /** * 从页面中提取当前活跃的聊天标签和对应的聊天记录文本。 * @returns {{current_tab: string|null, plaintext: string}} 包含当前标签和文本的对象。 */ function extractCurrentChatState() { const elements = locateChatElements(); if (!elements.tabs || !elements.chatLog) { console.error("提取失败:未能找到关键的聊天界面元素。"); return { current_tab: null, plaintext: '' }; } const current_tab = findActiveTabByStyleAnomaly(elements.tabs.innerHTML); const lines = []; for (const element of elements.chatLog.children) { lines.push(convertChatHtmlToPlainText(element.innerHTML)); } const plaintext = lines.join('\n'); return { current_tab, plaintext }; } /** * 负责从本地存储加载、保存和更新聊天记录。 */ const STORAGE_KEY = 'chatLogArchive'; // 使用一个通用的键名 function loadMessagesFromStorage() { const storedData = localStorage.getItem(STORAGE_KEY); if (!storedData) return {}; try { return JSON.parse(storedData); } catch (error) { console.error('读取存档失败:数据可能已损坏。', error); return {}; } } function saveMessagesToStorage(messagesObject) { const jsonString = JSON.stringify(messagesObject); localStorage.setItem(STORAGE_KEY, jsonString); } function updateChannelMessage(messagesObject, channelName, newMessagesText) { if (!channelName || !newMessagesText) return; const oldText = messagesObject[channelName] || ''; messagesObject[channelName] = mergeAndDeduplicateText(oldText, newMessagesText); } /** * 主保存程序,执行一次完整的“提取-更新-保存”流程。 */ function saveCurrentChannelMessage() { console.log("正在执行存档任务..."); const chatState = extractCurrentChatState(); if (chatState.current_tab && chatState.plaintext) { let allMyMessages = loadMessagesFromStorage(); updateChannelMessage(allMyMessages, chatState.current_tab, chatState.plaintext); saveMessagesToStorage(allMyMessages); console.log(`频道 [${chatState.current_tab}] 的记录已更新。`); } else { console.log("未找到有效的聊天标签或内容,跳过本次存档。"); } } /* * ================================================================= * 用户交互界面 (UI) 模块 * 负责创建和管理界面元素及其事件。 * ================================================================= */ function createUI() { // --- 1. 添加界面样式 --- GM_addStyle(` #log-archive-ui-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 70vw; height: 80vh; background-color: rgba(30, 30, 40, 0.95); border: 2px solid #5a6673; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5); z-index: 99999; display: none; /* 默认隐藏 */ flex-direction: column; padding: 15px; font-family: monospace; color: #e0e0e0; } #log-archive-ui-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex-shrink: 0; } #log-archive-ui-header h2 { margin: 0; font-size: 1.2em; color: #8af; } #log-archive-ui-controls { display: flex; flex-wrap: wrap; /* 允许换行 */ gap: 10px; } #log-archive-ui-log-display { width: 100%; height: 100%; background-color: #111; border: 1px solid #444; color: #ddd; font-size: 0.9em; padding: 10px; white-space: pre-wrap; word-wrap: break-word; overflow-y: auto; flex-grow: 1; } .log-archive-ui-button { padding: 8px 12px; background-color: #4a545e; color: #fff; border: 1px solid #6c7886; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .log-archive-ui-button:hover { background-color: #6c7886; } #log-archive-refresh-button { background-color: #3a8c54; } #log-archive-refresh-button:hover { background-color: #4da669; } #log-archive-clear-button { background-color: #8c3a3a; } #log-archive-clear-button:hover { background-color: #a64d4d; } #log-archive-ui-toggle-button { position: fixed; bottom: 50px; right: 20px; width: 50px; height: 50px; background-color: #8af; color: #111; border-radius: 50%; border: none; font-size: 24px; line-height: 50px; text-align: center; cursor: pointer; z-index: 99998; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } `); // --- 2. 创建界面HTML结构 --- const container = document.createElement('div'); container.id = 'log-archive-ui-container'; container.innerHTML = ` <div id="log-archive-ui-header"> <h2>聊天记录存档</h2> <div id="log-archive-ui-controls"> <select id="log-archive-channel-selector" class="log-archive-ui-button"></select> <button id="log-archive-refresh-button" class="log-archive-ui-button">立刻保存并刷新</button> <button id="log-archive-copy-button" class="log-archive-ui-button">复制当前</button> <button id="log-archive-copy-all-button" class="log-archive-ui-button">复制全部 (JSON)</button> <button id="log-archive-clear-button" class="log-archive-ui-button">清除全部记录</button> <button id="log-archive-close-button" class="log-archive-ui-button">关闭</button> </div> </div> <textarea id="log-archive-ui-log-display" readonly></textarea> `; document.body.appendChild(container); const toggleButton = document.createElement('div'); toggleButton.id = 'log-archive-ui-toggle-button'; toggleButton.textContent = '📜'; document.body.appendChild(toggleButton); // --- 3. 获取界面中所有可交互元素的引用 --- const uiContainer = document.getElementById('log-archive-ui-container'); const channelSelector = document.getElementById('log-archive-channel-selector'); const logDisplay = document.getElementById('log-archive-ui-log-display'); const copyButton = document.getElementById('log-archive-copy-button'); const copyAllButton = document.getElementById('log-archive-copy-all-button'); const clearButton = document.getElementById('log-archive-clear-button'); const closeButton = document.getElementById('log-archive-close-button'); const refreshButton = document.getElementById('log-archive-refresh-button'); // --- 4. 绑定事件处理器 --- /** * 刷新整个UI,包括下拉菜单和文本显示区域。 */ function updateDisplay() { const previouslySelected = channelSelector.value; const messages = loadMessagesFromStorage(); const channels = Object.keys(messages); channelSelector.innerHTML = ''; if (channels.length === 0) { const option = document.createElement('option'); option.textContent = '无记录'; channelSelector.appendChild(option); } else { channels.forEach(channel => { const option = document.createElement('option'); option.value = channel; option.textContent = channel; channelSelector.appendChild(option); }); // 刷新后尝试恢复之前的选择 channelSelector.value = previouslySelected; } const newlySelectedChannel = channelSelector.value; logDisplay.value = messages[newlySelectedChannel] || `--- 在频道 [${newlySelectedChannel}] 中没有记录 ---`; } // 点击悬浮按钮,显示或隐藏UI toggleButton.addEventListener('click', () => { const isVisible = uiContainer.style.display === 'flex'; if (!isVisible) { // 在打开UI前,先强制执行一次保存以获取最新数据 console.log("UI打开指令触发,强制执行一次保存..."); saveCurrentChannelMessage(); // 然后刷新UI显示 updateDisplay(); } uiContainer.style.display = isVisible ? 'none' : 'flex'; }); // 关闭按钮 closeButton.addEventListener('click', () => { uiContainer.style.display = 'none'; }); // 切换下拉菜单,更新文本显示 channelSelector.addEventListener('change', updateDisplay); // “立刻保存并刷新”按钮 refreshButton.addEventListener('click', () => { console.log("“立刻保存并刷新”指令触发..."); saveCurrentChannelMessage(); updateDisplay(); console.log('记录已成功抓取并刷新!'); }); // “复制当前”按钮 copyButton.addEventListener('click', () => { logDisplay.select(); document.execCommand('copy'); console.log('当前频道的记录已复制到剪贴板。'); }); // “复制全部”按钮 copyAllButton.addEventListener('click', () => { const messages = loadMessagesFromStorage(); const jsonString = JSON.stringify(messages, null, 2); navigator.clipboard.writeText(jsonString).then(() => { console.log('所有频道的记录 (JSON格式) 已复制到剪贴板。'); }); }); // “清除全部记录”按钮 clearButton.addEventListener('click', () => { // 使用 console.warn 给出醒目提示,但不打断用户 console.warn('清除操作需要用户确认。请在弹出的对话框中选择。'); const confirmation = confirm('【警告】你确定要删除所有本地聊天记录吗?此操作不可逆!'); if (confirmation) { localStorage.removeItem(STORAGE_KEY); updateDisplay(); console.log('所有本地记录已被清除。'); } else { console.log('用户取消了清除操作。'); } }); } /* * ================================================================= * 脚本主程序和入口 * ================================================================= */ function main() { console.log("PonyTown 聊天记录存档器已启动。"); // 初始化用户界面 createUI(); // 页面加载后立即执行一次存档,以防用户快速离开 saveCurrentChannelMessage(); // 设置定时任务,每30秒自动存档一次 setInterval(saveCurrentChannelMessage, 30000); // 30000 毫秒 = 30 秒 console.log("自动存档已激活,每30秒运行一次。可点击右下角悬浮图标进行手动操作。"); } // 等待页面完全加载完毕后再执行主函数,确保所有需要监控的元素都已生成 window.addEventListener('load', main); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址