您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动将 pony.town 的聊天记录保存到浏览器本地存储,并提供查看、复制和清除界面。
当前为
// ==UserScript== // @name PonyTown 网页聊天记录存档器 // @namespace http://tampermonkey.net/ // @version 3.6 // @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 {Array<Object>} oldMessages - 已存在的旧消息数组。 * @param {Array<Object>} newMessages - 需要追加的新消息数组。 * @returns {Array<Object>} - 合并后的完整消息数组。 */ function mergeAndDeduplicateMessages(oldMessages, newMessages) { if (!oldMessages || oldMessages.length === 0) return newMessages; if (!newMessages || newMessages.length === 0) return oldMessages; let overlapLength = 0; const maxPossibleOverlap = Math.min(oldMessages.length, newMessages.length); for (let i = maxPossibleOverlap; i > 0; i--) { const suffixOfOld = oldMessages.slice(-i).map(msg => msg.content); const prefixOfNew = newMessages.slice(0, i).map(msg => msg.content); if (suffixOfOld.length === prefixOfNew.length && suffixOfOld.every((val, index) => val === prefixOfNew[index])) { overlapLength = i; break; } } const messagesToAdd = newMessages.slice(overlapLength); // Discontinuity check: if there's old data and new data, but no overlap, assume discontinuity const discontinuityDetected = oldMessages.length > 0 && newMessages.length > 0 && overlapLength === 0; if (messagesToAdd.length === 0) return oldMessages; let finalMessages; if (discontinuityDetected) { console.warn('检测到聊天记录不连续,可能存在数据丢失。已插入警告标记。'); finalMessages = oldMessages.concat([{ content: '[警告 - 此处可能存在记录丢失]' }], messagesToAdd); } else { finalMessages = oldMessages.concat(messagesToAdd); } return finalMessages; } /** * 从页面中提取当前活跃的聊天标签和对应的聊天记录文本。 * @returns {{current_tab: string|null, messages: Array<Object>}} 包含当前标签和消息数组的对象。 */ function extractCurrentChatState() { const elements = locateChatElements(); if (!elements.tabs || !elements.chatLog) { console.error("提取失败:未能找到关键的聊天界面元素。"); return { current_tab: null, messages: [] }; } const current_tab = findActiveTabByStyleAnomaly(elements.tabs.innerHTML); const messages = []; for (const element of elements.chatLog.children) { const plainText = convertChatHtmlToPlainText(element.innerHTML); if (plainText) { // Only add if there's actual text content messages.push({ content: plainText }); } } return { current_tab, messages }; } /** * 负责从本地存储加载、保存和更新聊天记录。 */ const STORAGE_KEY = 'chatLogArchive'; // 使用一个通用的键名 function loadMessagesFromStorage() { const storedData = localStorage.getItem(STORAGE_KEY); if (!storedData) return {}; try { // Attempt to parse existing data. If it's old format (string), convert it. const parsedData = JSON.parse(storedData); const convertedData = {}; for (const channel in parsedData) { if (typeof parsedData[channel] === 'string') { // Convert old string format to new array of objects format convertedData[channel] = parsedData[channel].split('\n').filter(line => line.trim() !== '').map(line => ({ content: line })); } else { convertedData[channel] = parsedData[channel]; } } return convertedData; } catch (error) { console.error('读取存档失败:数据可能已损坏或格式不兼容。将尝试初始化新数据结构。', error); // If parsing fails, it might be corrupted or an old, unconvertible format. Start fresh. return {}; } } function saveMessagesToStorage(messagesObject) { const jsonString = JSON.stringify(messagesObject); localStorage.setItem(STORAGE_KEY, jsonString); } function updateChannelMessage(messagesObject, channelName, newMessagesArray) { if (!channelName || !newMessagesArray || newMessagesArray.length === 0) return; const oldMessages = messagesObject[channelName] || []; messagesObject[channelName] = mergeAndDeduplicateMessages(oldMessages, newMessagesArray); } /** * 主保存程序,执行一次完整的“提取-更新-保存”流程。 */ function saveCurrentChannelMessage() { console.log("正在执行存档任务..."); const chatState = extractCurrentChatState(); if (chatState.current_tab && chatState.messages.length > 0) { let allMyMessages = loadMessagesFromStorage(); updateChannelMessage(allMyMessages, chatState.current_tab, chatState.messages); 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; resize: none; /* Prevent manual resizing */ } .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 messagesByChannel = loadMessagesFromStorage(); // Now loads objects const channels = Object.keys(messagesByChannel); channelSelector.innerHTML = ''; if (channels.length === 0) { const option = document.createElement('option'); option.textContent = '无记录'; channelSelector.appendChild(option); logDisplay.value = '--- 没有找到任何聊天记录 ---'; } else { channels.forEach(channel => { const option = document.createElement('option'); option.value = channel; option.textContent = channel; channelSelector.appendChild(option); }); // Refresh and try to restore the previous selection, or default to the first channel channelSelector.value = previouslySelected && channels.includes(previouslySelected) ? previouslySelected : channels[0]; const selectedChannelMessages = messagesByChannel[channelSelector.value] || []; logDisplay.value = selectedChannelMessages.map(msg => msg.content).join('\n') || `--- 在频道 [${channelSelector.value}] 中没有记录 ---`; } } // Click the floating button to show or hide the UI toggleButton.addEventListener('click', () => { const isVisible = uiContainer.style.display === 'flex'; if (!isVisible) { // Before opening the UI, force a save to get the latest data console.log("UI打开指令触发,强制执行一次保存..."); saveCurrentChannelMessage(); // Then refresh the UI display updateDisplay(); } uiContainer.style.display = isVisible ? 'none' : 'flex'; }); // Close button closeButton.addEventListener('click', () => { uiContainer.style.display = 'none'; }); // Change dropdown, update text display channelSelector.addEventListener('change', updateDisplay); // "Refresh Now" button refreshButton.addEventListener('click', () => { console.log("“立刻保存并刷新”指令触发..."); saveCurrentChannelMessage(); updateDisplay(); console.log('记录已成功抓取并刷新!'); }); // "Copy Current" button copyButton.addEventListener('click', () => { logDisplay.select(); document.execCommand('copy'); console.log('当前频道的记录已复制到剪贴板。'); }); // "Copy All" button copyAllButton.addEventListener('click', () => { const messages = loadMessagesFromStorage(); const jsonString = JSON.stringify(messages, null, 2); navigator.clipboard.writeText(jsonString).then(() => { console.log('所有频道的记录 (JSON格式) 已复制到剪贴板。'); }).catch(err => { console.error('复制全部记录失败:', err); alert('复制全部记录失败。请检查浏览器权限或手动复制控制台中的JSON数据。'); }); }); // "Clear All Records" button clearButton.addEventListener('click', () => { // Use console.warn to give a prominent warning, but not interrupt the user console.warn('清除操作需要用户确认。请在弹出的对话框中选择。'); const confirmation = confirm('【警告】你确定要删除所有本地聊天记录吗?此操作不可逆!'); if (confirmation) { localStorage.removeItem(STORAGE_KEY); updateDisplay(); console.log('所有本地记录已被清除。'); } else { console.log('用户取消了清除操作。'); } }); } /* * ================================================================= * 脚本主程序和入口 * ================================================================= */ function main() { console.log("PonyTown 聊天记录存档器已启动。"); // Initialize user interface createUI(); // Perform an immediate archive after page load, in case the user quickly leaves saveCurrentChannelMessage(); // Set up timed task to auto-archive every 30 seconds setInterval(saveCurrentChannelMessage, 15000); // 30000 milliseconds = 30 seconds console.log("自动存档已激活,每15秒运行一次。可点击右下角悬浮图标进行手动操作。"); } // Wait until the page is fully loaded before executing the main function, // ensuring all elements to be monitored are generated. window.addEventListener('load', main); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址