Greasy Fork

Greasy Fork is available in English.

牛牛聊天增强插件

让牛牛聊天支持发送图片,发送任何外链图片链接时,自动转换为图片,可以点击查看大图;支持粘贴图片自动上传

当前为 2025-05-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         牛牛聊天增强插件
// @namespace    https://www.milkywayidle.com/
// @version      0.1.11
// @description  让牛牛聊天支持发送图片,发送任何外链图片链接时,自动转换为图片,可以点击查看大图;支持粘贴图片自动上传
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @icon         https://www.milkywayidle.com/favicon.svg
// @license MIT
// ==/UserScript==

(function() {
	'use strict';
	GM_addStyle(`
body[data-fontsize="1"] .ChatMessage_chatMessage__2wev4{font-size:18px}
body[data-fontsize="2"] .ChatMessage_chatMessage__2wev4{font-size:20px}
body[data-fontsize="3"] .ChatMessage_chatMessage__2wev4{font-size:24px;line-height:24px}
body[data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{display:inline-flex;height:18px;grid-gap:4px;gap:4px;align-items:center;margin:1px 4px;width:-moz-fit-content;width:fit-content;border:1px solid #778be1;border-radius:4px;padding:1px 5px;white-space:nowrap;background:#4357af}
.ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:auto;}
body[data-fontsize="1"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:18px;}
body[data-fontsize="2"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:20px;}
body[data-fontsize="3"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:24px;}
body[data-ic="1"] .ChatMessage_chatMessage__2wev4 .CharacterName_gameMode__2Pvw8,.ic-icon{display:inline-block;border-radius:50%;color:#000!important;width:14px;height:14px;font-size:0;margin:3px 0 0 2px;border:1px solid #fff;background:linear-gradient(61deg,var(--color-neutral-300),var(--color-neutral-300) 3%,var(--color-neutral-100) 15%,var(--color-neutral-0) 50%,var(--color-neutral-200) 70%,var(--color-neutral-300) 95%,var(--color-neutral-300))}
.CharacterName_characterName__2FqyZ{font-size:unset}
.chat-img{display:inline-block}
.chat-img img{display:inline-flex;margin:1px 4px;max-height:60px;max-width:100px;width:fit-content;border:2px solid #778be1;border-radius:4px;padding:1px;white-space:nowrap;background:#000;cursor:pointer}
.chat-img span{padding:0 1px;border:0;margin:0;background:unset}
.chat-img-preview{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:zoom-out}
.chat-img-preview img{max-width:90%;max-height:90%;border:2px solid #fff;border-radius:4px}
.upload-status{position:fixed;bottom:20px;right:20px;padding:10px 15px;background:#4caf50;color:#fff;border-radius:4px;z-index:10000;box-shadow:0 2px 10px rgba(0,0,0,.2)}
.emoji-btn,.chat-conf{width:28px;height:28px;display:flex;justify-content:center;align-items:center;cursor:pointer;position:relative;border-radius:4px;padding:4px;background-color:var(--color-midnight-500);margin:2px}
.emoji-btn:hover,.chat-conf:hover{background-color:var(--color-midnight-300)}
.emoji-panel{display:none;position:absolute;width:450px;background:#2d2d2d;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.3);z-index:10000;border:solid 2px var(--color-midnight-100);border-radius:4px;padding:12px;background-color:var(--color-midnight-900);box-shadow:rgba(0,0,0,.3) 2px 2px 10px 6px;color:var(--color-text-dark-mode)}
.emoji-panel.show{display:block}
.emoji-header{align-items:center;font-size:18px;font-weight:600;text-align:center;padding-bottom:10px}
.emoji-tabbar{display:flex;flex-wrap:wrap}
.emoji-tab{background:0 0;border:none;padding:5px 10px;border-radius:4px;cursor:pointer}
.emoji-tab.active{background:var(--color-space-600)}
.emoji-tab:hover{background:var(--color-midnight-300)}
.emoji-content{padding-top:10px}
.emoji-close{background:0 0;border:none;position:absolute;top:6px;right:6px;height:22px;width:22px;padding:4px;cursor:pointer}
.emoji-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;padding:12px;background-color:var(--color-midnight-700);border-radius:4px;max-height:300px;overflow-y:scroll}
.emoji-item{cursor:pointer;padding:4px;border-radius:4px;transition:background .2s}
.emoji-item:hover{background:#3d3d3d}
.emoji-item img{width:100%;height:auto}
.link-tooltip{position:absolute}
.link-tooltip .GuideTooltip_paragraph__18Zcq{white-space:normal;overflow-wrap:break-word}
.chat-config{z-index:1000;position:absolute;top:0;left:0;height:100%;width:100%;color:var(--color-text-dark-mode)}
.chat-config-mask{height:100%;width:100%;background-color:var(--color-midnight-800);opacity:.8}
.chat-config-window{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;align-items:flex-end}
.chat-config-window-npc{z-index:1;margin-left:-40px;margin-right:-20px;vertical-align:bottom}
.chat-config-window-npc svg{width:130px;height:100px}
.chat-config-window-npc-name{margin:0 10px;border-radius:4px;font-size:14px;font-weight:500;background-color:var(--color-space-600);text-align:center}
.chat-config-window-content{min-width:300px;max-width:98%;min-height:100px;max-height:98%;font-weight:400;overflow:auto;display:flex;flex-direction:column;font-size:16px;line-height:18px;background:#2d2d2d;border:solid 2px var(--color-midnight-100);border-radius:4px;padding:12px;background-color:var(--color-midnight-900);box-shadow:rgba(0,0,0,.3) 2px 2px 10px 6px;color:var(--color-text-dark-mode)}
.chat-config-title{font-size:18px;font-weight:600;text-align:center}
.chat-config-close{background:0 0;border:none;position:absolute;top:6px;right:6px;height:22px;width:22px;padding:4px;cursor:pointer}
#saveConfig{border-radius:4px;width:-moz-fit-content;width:fit-content;min-width:50px;border:none;font-family:Roboto;font-weight:600;text-align:center;overflow:hidden;cursor:pointer;display:flex;align-items:center;justify-content:center;background-color:var(--color-success);color:#000;height:36px;padding:0 10px;font-size:14px;line-height:15px;align-self:flex-end}
#saveConfig:hover{background-color:var(--color-success-hover)}
.chat-config-main{display:flex;gap:8px;padding:12px;background-color:var(--color-midnight-700);border-radius:4px;margin:13px 0;flex-direction:column}
.chat-config-item:not(:last-child){border-bottom:1px dashed var(--color-midnight-100);padding-bottom:10px}
.chat-config-item-title{font-size:18px;padding-bottom:6px}
.chat-config-preview{background:#191b24;margin-top:10px;padding:10px;font-size:14px;line-height:20px;text-align:left;border-radius:4px}
.chat-config-preview .ChatMessage_chatMessage__2wev4{white-space:unset;padding:10px 0}
.chat-config-tip{color:var(--color-success);font-size:14px;padding-top:6px}
.ic-icon-default{color:var(--color-neutral-300)}
    `);
	const chatHistorySelector = '.ChatHistory_chatHistory__1EiG3';
	const chatMessageSelector = '.ChatMessage_chatMessage__2wev4';
	const chatInputSelector = '.Chat_chatInput__16dhX';

	const historyObservers = new WeakMap();
	let inputObserver = null;
	let globalObserver;
	const handledInputs = new WeakSet();
	let isProcessing = false;
	let emojiBtnObserver;
	let emojiPanel;
	let tooltip;
	const COMPRESSED_EMOJI_DATA = [
		["Adela", "2025/05/13", ["6822787e1f9a3", "682278801af4c", "68227876259d7","68227880e8b5b", "682278829c625", "682278b51e4ce"]],
		["Adriana", "2025/05/13", ["682278dc64bc8", "682278df2aec2", "682278df9f902","682278dec78c1", "682278e19aeef", "682278e57cc98","682278e80ef54", "68227924b3820", "682279269379c","6822792ad0801", "68227927a2726", "6822792e99f9d","6822792495782"]],
		["Aiden", "2025/05/13", ["682279ee7d34a", "682279e92545b", "682279ef77d86","682279fbc0d07", "682279e9083d6", "682279f2098d4","682279fbbcad6"]],
		["Alex", "2025/05/13", ["68227a8437342", "68227a836aedc", "68227a7e3b1e4","68227a85b5b3b", "68227a89a93e4", "68227a7e4090e","68227ac730057", "68227aca492d5", "68227ac87c6a9","68227acc230e7", "68227ad188f42", "68227ac961e61"]],
		["Angelika", "2025/05/13", ["68227b12b2d6b", "68227b08f0c8f", "68227b09592c0","68227b1146ce5", "68227b101397b", "68227b0a74f09"]],
		["Arda", "2025/05/13", ["68227b5a04178", "68227b42d179b", "68227b42dc9ee","68227b43504c5", "68227b43aa87c", "68227b4d2723d"]],
		["Aya", "2025/05/13", ["68227bae53837", "68227bb199b76", "68227baf9e9f6","68227bbe4ca1e", "68227bc80c410", "68227bae947b1","68227baf17e05", "68227bed10bad", "68227bef7fbe2","68227bf144282", "68227befad25f", "68227bf5c9372","68227bedf39e0"]],
		["Azuko", "2025/05/13", ["68227c26ec0fe", "68227c26e940c", "68227c28409ae","68227c2b464a5", "68227c309dca2", "68227c26ed151"]],
		["Barbara", "2025/05/13", ["68227c61bf4c6", "68227c6237b22", "68227c57ebdfd","68227c5d4423b", "68227c591a910", "68227c59b5200","68227c5c26676"]],
		["Bernice", "2025/05/13", ["68227ca1a7788", "68227c981d78b", "68227c9b7ac7c","68227c9886e21", "68227c9c8be28", "68227c992289b"]],
		["Bianca", "2025/05/13", ["6822858c83f7e", "6822856d2124a", "68228560c0411","6822856bed04b", "6822857080c78", "6822856132aa0"]],
		["Camilo", "2025/05/13", ["682285dda8f27", "682285ee5344b", "682285f009ce2","682285f1550dc", "682285c91b753", "682285c5e5d85","68228617e9e7c", "6822861e34e07", "6822861964d77","6822861aa750e", "6822861f903f5", "6822862391bd6"]],
		["Cathy", "2025/05/13", ["6822acb40087a", "6822acba3cd86", "6822acbceb492","6822acc0d932b", "6822acb69050b", "6822acb64842b"]],
		["Celine", "2025/05/13", ["6822acfcb9a01", "6822ad00edbae", "6822ad0279376","6822ad044678c", "6822acfcd1fdf", "6822acfd162d9"]],
		["Chiara", "2025/05/13", ["6822ad395e9e6", "6822aea264109", "6822aea5c9b49","6822ae8c74312", "6822aea71a27a", "6822aea2dc7b7","6822ae8deec53"]],
		["Chloe", "2025/05/13", ["6822aee5d8781", "6822aee543990", "6822aee6163ad","6822aef034223", "6822aee60e506", "6822aee7ee8fb"]],
		["Dailin", "2025/05/13", ["6822af2e22c67", "6822af243c5dc", "6822af2475945","6822af2701928", "6822af297f693", "6822af25ed9c9"]],
		["Daniel", "2025/05/13", ["6822af6cca77a","6822af6e3408b","6822af6964153","6822af72aaa16","6822af69c393e","6822af6cca77a"]],
		["Echion", "2025/05/13", ["6822afcab90f4","6822afcb91e5b","6822afce7977b","6822afbc30f0b","6822afcbe1d9e","6822afcab90f4"]],
		["Elena", "2025/05/13", ["6822b010afdcd","6822b009b3312","6822b00f64656","6822b010d46df","6822b009ed6fe","6822b00ba6d2a"]],
		["Eleven", "2025/05/13", ["6822b04e22136","6822b05403d3a","6822b0596558f","6822b0520afb4","6822b0520470c","6822b04e85508"]],
		["Emma", "2025/05/13", ["6822cb1434582","6822cb1577f35","6822cb0b55713","6822cb1641e76","6822cb0ca4e96","6822cb0c9bb38","6822cb18ecf07","6822cb0d58e5f","6822cb0e73a13","6822cb11e7275","6822cb13c0bcb","6822cb1ad69a6"]],
		["Ersha", "2025/05/13", ["6822cb873ce8f","6822cb86a51a4","6822cb8a2a04e","6822cb8366e28","6822cb8747d70","6822cb8466238"]],
	];
	function decompressEmojiData() {
		const baseUrl = "https://tupian.li/images/";
		return COMPRESSED_EMOJI_DATA.map(([name, date, files]) => ({
			name,
			list: files.map(file => `${baseUrl}${date}/${file}.png`)
		}));
	}
	const emojiData = decompressEmojiData();
	function isImageUrl(url) {// 检查链接是否是图片
		return url && /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
	}

	function createPreviewOverlay(imgSrc) {//创建预览
		const overlay = document.createElement('div');
		overlay.className = 'chat-img-preview';
		const previewImg = document.createElement('img');
		previewImg.src = imgSrc;
		overlay.appendChild(previewImg);
		document.body.appendChild(overlay);

		overlay.addEventListener('click', (e) => {// 点击后关闭图片预览
			if (e.target === overlay || e.target === previewImg) {
				document.body.removeChild(overlay);
			}
		});
		document.addEventListener('keydown', function handleEsc(e) {// ESC关闭图片预览
			if (e.key === 'Escape') {
				document.body.removeChild(overlay);
				document.removeEventListener('keydown', handleEsc);
			}
		});
	}
	function createPreviewableLink(url, altText,emoji) {//创建可预览的链接
		emoji = emoji || null;
		const link = document.createElement('a');
		link.href = url;
		link.target = '_blank';
		link.rel = 'noreferrer noopener nofollow';
		link.className = 'chat-img';
		if(emoji || GM_getValue('option_img',0) == 1){
			var img = document.createElement('img');
			img.src = url;
			img.alt = altText;
		}else{
			var img = document.createElement('span');
			img.innerHTML = GM_getValue('img_title','[图片]');
		}
		link.appendChild(img);
		link.addEventListener('click', function(e) {
			if (e.ctrlKey || e.metaKey) return; // 允许Ctrl+点击在新标签打开
			e.preventDefault();
			e.stopImmediatePropagation();
			createPreviewOverlay(url);
		});
		return link;
	}

	function replaceLinkContentWithImage(link) {//修改A标签内的图片
		const href = link.getAttribute('href');
		if (!isImageUrl(href)){//普通链接
			if (link.querySelector('.chat-link')) return;
			link.className = 'chat-link';
			link.innerHTML = '[网页链接]';
			
			if(!tooltip) {
				tooltip = document.createElement('div');
				tooltip.className = 'link-tooltip MuiPopper-root MuiTooltip-popper css-112l0a2';
				tooltip.innerHTML = `
					<div class="MuiTooltip-tooltip MuiTooltip-tooltipPlacementBottom css-1spb1s5">
						<div class="GuideTooltip_guideTooltipText__PhA_Q">
							<div class="GuideTooltip_title__1QDN9">网页链接</div>
							<div class="GuideTooltip_content__1_yqJ">
								<div class="GuideTooltip_paragraph__18Zcq">${href}</div>
							</div>
						</div>
					</div>
				`;
				document.body.appendChild(tooltip);
			}
			link.addEventListener('mouseover', (e) => {
				const contentEl = tooltip.querySelector('.GuideTooltip_paragraph__18Zcq');
				contentEl.textContent = e.target.href;
				positionTooltip(e.target);
			});
			link.addEventListener('mouseout', () => {
				tooltip.style.display = 'none';
			});	
			return
		}
		if (link.querySelector('.chat-img') || link.querySelector('img')) return;

		const newLink = createPreviewableLink(href, '图片预览');
		link.parentNode.replaceChild(newLink, link);
	}
	function positionTooltip(link) {
		tooltip.style.display = 'block';
		tooltip.style.left = '0';
		tooltip.style.top = '0';
		const linkRect = link.getBoundingClientRect();
		const tooltipRect = tooltip.getBoundingClientRect();
		const windowWidth = window.innerWidth;
		let left = linkRect.left + (linkRect.width - tooltipRect.width) / 2;
		let top = linkRect.top - tooltipRect.height - 5;
		if(left + tooltipRect.width > windowWidth) left = windowWidth - tooltipRect.width - 5;
		if(left < 5) left = 5;
		if(top < window.scrollY) top = linkRect.bottom + window.scrollY + 5;
		tooltip.style.left = `${left}px`;
		tooltip.style.top = `${top}px`;
	}
	function convertEmojiCodes(container) {
		const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT,{
			acceptNode: (node) => {
				if (node.parentNode.classList?.contains('processed-emoji')) {
					return NodeFilter.FILTER_REJECT;
				}
				return /{::\d+_\d+}/.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
			}
		},false);
	
		let node;
		while ((node = walker.nextNode())) {
			const fragment = document.createDocumentFragment();
			const parts = node.nodeValue.split(/({::\d+_\d+})/);
	
			parts.forEach(part => {
				if (!part) return;
	
				const emojiMatch = part.match(/{::(\d+)_(\d+)}/);
				if (emojiMatch) {
					const groupIndex = parseInt(emojiMatch[1]) - 1;
					const emojiIndex = parseInt(emojiMatch[2]) - 1;
					if (emojiData[groupIndex]?.list[emojiIndex]) {
						const url = emojiData[groupIndex].list[emojiIndex];
						const link = createPreviewableLink(url, `emoji:${groupIndex+1}_${emojiIndex+1}`,1);
						fragment.appendChild(link);
						return;
					}
				}
				fragment.appendChild(document.createTextNode(part));
			});
	
			if (node.parentNode) {
				const wrapper = document.createElement('span');
				wrapper.className = 'processed-emoji';
				wrapper.appendChild(fragment);
				node.parentNode.replaceChild(wrapper, node);
			}
		}
	}
	function processExistingMessages(container) {//聊天页面消息处理
		const messages = container.querySelectorAll(chatMessageSelector);
		messages.forEach(msg => {
			const links = msg.querySelectorAll('a');
			links.forEach(replaceLinkContentWithImage);
			convertEmojiCodes(msg);
		});
	}
	function observeChatHistory(chatHistory) {//监听聊天页面变化
		processExistingMessages(chatHistory);
		const observer = new MutationObserver(mutations => {
			mutations.forEach(mutation => {
				mutation.addedNodes.forEach(node => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const messages = node.matches(chatMessageSelector) ? [node] : node
							.querySelectorAll(chatMessageSelector);
						messages.forEach(msg => {
							const links = msg.querySelectorAll('a');
							links.forEach(replaceLinkContentWithImage);
							convertEmojiCodes(msg);
						});
					}
				});
			});
		});
		observer.observe(chatHistory, {
			childList: true,
			subtree: true
		});
	}
	function initClipboardUpload() {
		if (inputObserver && typeof inputObserver.disconnect === 'function') {
			inputObserver.disconnect();
		}
		const chatInput = document.querySelector(chatInputSelector);
		if (chatInput && !handledInputs.has(chatInput)) {
			setupPasteHandler(chatInput);
			return;
		}
		inputObserver = new MutationObserver((mutations) => {
			initEmojiPanel();
			mutations.forEach((mutation) => {
				mutation.addedNodes.forEach((node) => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const input = node.matches(chatInputSelector) ? node : node.querySelector(chatInputSelector);
						if (input && !handledInputs.has(input)) {
							setupPasteHandler(input);
						}
					}
				});
			});
		});
		inputObserver.observe(document.body, {
			childList: true,
			subtree: true
		});
	}
	function setupPasteHandler(inputElement) {
		handledInputs.add(inputElement);
		inputElement.removeEventListener('paste', handlePaste);
		inputElement.addEventListener('paste', handlePaste);
		let isProcessing = false;
		async function handlePaste(e) {
			if (isProcessing) {
				e.preventDefault();
				return;
			}
			isProcessing = true;
			try {
				const items = e.clipboardData.items;
				for (let i = 0; i < items.length; i++) {
					if (items[i].type.indexOf('image') !== -1) {
						e.preventDefault();
						const blob = items[i].getAsFile();
						if (blob) await uploadAndInsertImage(blob, inputElement);
						break;
					}
				}
			} finally {
				isProcessing = false;
			}
		}
	}
	function uploadAndInsertImage(blob, inputElement) {//上传图片
		const statusDiv = document.createElement('div');
		statusDiv.className = 'upload-status';
		statusDiv.textContent = '正在上传图片...';
		document.body.appendChild(statusDiv);

		const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
		const formParts = [];

		function appendFile(name, file) {
			formParts.push(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"; filename="${file.name}"\r\nContent-Type: ${file.type}\r\n\r\n`);
			formParts.push(file);
			formParts.push('\r\n');
		}
		appendFile('file', blob);
		formParts.push(`--${boundary}--\r\n`);
		const bodyBlob = new Blob(formParts);

		GM_xmlhttpRequest({
			method: 'POST',
			url: 'https://tupian.li/api/v1/upload',
			data: bodyBlob,
			headers: {
				'Content-Type': `multipart/form-data; boundary=${boundary}`,
				'Accept': 'application/json'
			},
			binary: true,
			onload: function(response) {
				statusDiv.remove();
				if (response.status === 200) {
					try {
						const result = JSON.parse(response.responseText);
						if (result.status) {
							const url = result.data.links.url;

							const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
								window.HTMLInputElement.prototype, 'value'
							).set;

							const currentValue = inputElement.value;
							const newValue = currentValue ? `${currentValue} ${url}` : url;

							nativeInputValueSetter.call(inputElement, newValue);
							inputElement.dispatchEvent(new Event('input', { bubbles: true }));
							inputElement.focus();

							const successDiv = document.createElement('div');
							successDiv.className = 'upload-status';
							successDiv.textContent = '上传成功!';
							document.body.appendChild(successDiv);
							setTimeout(() => successDiv.remove(), 2000);
						} else {
							throw new Error(result.message || '上传失败');
						}
					} catch (e) {
						showError('解析失败: ' + e.message);
					}
				} else {
					showError('服务器错误: ' + response.status);
				}
			},
			onerror: function(error) {
				statusDiv.remove();
				showError('上传失败: ' + error.statusText);
			}
		});

		function showError(message) {
			const errorDiv = document.createElement('div');
			errorDiv.className = 'upload-status error';
			errorDiv.textContent = message;
			document.body.appendChild(errorDiv);
			setTimeout(() => errorDiv.remove(), 3000);
			console.error(message);
		}
	}
	function insertAtCursor(inputElement, text) {//插入文本,兼容SB VUE
		const start = inputElement.selectionStart;
		const end = inputElement.selectionEnd;
		const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
			window.HTMLInputElement.prototype,
			"value"
		).set;
		nativeInputValueSetter.call(inputElement,inputElement.value.substring(0, start) + text + inputElement.value.substring(end)
		);
		const event = new Event('input', {
			bubbles: true,
			cancelable: true
		});
		inputElement.dispatchEvent(event);
		inputElement.selectionStart = inputElement.selectionEnd = start + text.length;
		inputElement.focus();
	}
	function showStatus(message, duration = 3000, isError = false) {//上传状态
		const existingStatus = document.querySelector('.upload-status');
		if (existingStatus) existingStatus.remove();

		const statusDiv = document.createElement('div');
		statusDiv.className = 'upload-status';
		statusDiv.textContent = message;
		statusDiv.style.background = isError ? '#F44336' : '#4CAF50';
		document.body.appendChild(statusDiv);

		setTimeout(() => {
			statusDiv.remove();
		}, duration);
	}
	function setupHistoryObserver(historyElement) {//设置聊天记录监听
		if (historyObservers.has(historyElement)) {
			historyObservers.get(historyElement).disconnect();
		}
		const observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				mutation.addedNodes.forEach((node) => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const messages = node.matches(chatMessageSelector) ? [node] : node.querySelectorAll(chatMessageSelector);
						messages.forEach((msg) => {
							const links = msg.querySelectorAll('a');
							links.forEach(replaceLinkContentWithImage);
							convertEmojiCodes(msg);
						});
					}
				});
			});
		});
		observer.observe(historyElement, {
			childList: true,
			subtree: true
		});
		historyObservers.set(historyElement, observer);
		const messages = historyElement.querySelectorAll(chatMessageSelector);
		messages.forEach((msg) => {
			const links = msg.querySelectorAll('a');
			links.forEach(replaceLinkContentWithImage);
			convertEmojiCodes(msg);
		});
	}
	function setupGlobalObserver() {//全局监听
		globalObserver = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				mutation.addedNodes.forEach((node) => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const newHistories = node.matches(chatHistorySelector) ? [node] : node.querySelectorAll(chatHistorySelector);
						newHistories.forEach(setupHistoryObserver);
					}
				});
			});
		});
		globalObserver.observe(document.body, {
			childList: true,
			subtree: true
		});
	}
	function initEmojiPanel() {
		const chatInput = document.querySelector(chatInputSelector);
		if (!chatInput || chatInput.previousElementSibling?.classList.contains('emoji-btn')) {
			return;
		}

		const emojiBtn = document.createElement('div');
		emojiBtn.className = 'emoji-btn';
		emojiBtn.innerHTML = '<svg role="img" aria-label="action icon" width="100%" height="100%"><use href="/static/media/actions_sprite.e6388cbc.svg#cow"></use></svg>';
		chatInput.parentNode.insertBefore(emojiBtn, chatInput);

		const panelContainer = document.createElement('div');
		panelContainer.innerHTML = `
		<div class="emoji-panel">
			<div class="emoji-header">
				<span>选择表情</span>
				<button class="emoji-close"><svg role="img" aria-label="Close" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#close_menu"></use></svg></button>
			</div>
			${createEmojiPanelHTML()}
		</div>`;
		document.body.appendChild(panelContainer);

		emojiPanel = document.querySelector('.emoji-panel');
		if (!emojiPanel) return;

		emojiBtn.addEventListener('click', (e) => {//打开表情按钮
			e.stopPropagation();
			emojiPanel.classList.toggle('show');
			const btnRect = emojiBtn.getBoundingClientRect();
			emojiPanel.style.bottom = `${window.innerHeight - btnRect.top + 3}px`;
			let left = btnRect.left;
			let width = document.querySelector('.Chat_chatInputContainer__2euR8').getBoundingClientRect().width - 4;
			if(width < 600 && window.innerWidth >= 600) width = 600;
			if(window.innerWidth < left + width) left = window.innerWidth - width;
			emojiPanel.style.left = `${left}px`;
			emojiPanel.style.width = `${width}px`
		});

		const closeBtn = emojiPanel.querySelector('.emoji-close');//关闭表情面板
		if (closeBtn) {
			closeBtn.addEventListener('click', (e) => {
				e.stopPropagation();
				emojiPanel.classList.remove('show');
			});
		}
		emojiPanel.querySelectorAll('.emoji-tab').forEach(tab => {
			tab.addEventListener('click', () => {
				const groupIndex = parseInt(tab.dataset.group);
				if (isNaN(groupIndex)) return;
				emojiPanel.querySelector('.emoji-content').innerHTML = createEmojiGroupHTML(groupIndex);
				emojiPanel.querySelectorAll('.emoji-tab').forEach(t => 
					t.classList.remove('active'));
				tab.classList.add('active');
			});
		});
		if(!emojiPanel._hasEmojiListener) {//表情按钮 修复一个重复执行的BUG
			emojiPanel.addEventListener('click', (e) => {
				const emojiItem = e.target.closest('.emoji-item');
				if (!emojiItem) return;
				e.stopPropagation();
				e.stopImmediatePropagation();
				const chatInput = document.querySelector(chatInputSelector);
				if (chatInput) {
					const groupId = emojiItem.dataset.group;
					const emojiId = emojiItem.dataset.emoji;
					insertAtCursor(chatInput, `{::${groupId}_${emojiId}}`);
				}
				emojiPanel.classList.remove('show');
			});
			emojiPanel._hasEmojiListener = true;
		}
		document.addEventListener('click', (e) => {//关闭面板
			if (!emojiPanel.contains(e.target) && e.target !== emojiBtn) {
				emojiPanel.classList.remove('show');
			}
		});
		return panelContainer;
	}
	function createEmojiPanelHTML() {
		return `
		<div class="emoji-tabbar">
			${emojiData.map((group, index) => `
				<button class="emoji-tab ${index === 0 ? 'active' : ''}" 
						data-group="${index}">
					${group.name}
				</button>
			`).join('')}
		</div>
		<div class="emoji-content">
			${createEmojiGroupHTML(0)}
		</div>
		`;
	}
	function createEmojiGroupHTML(groupIndex) {
		const group = emojiData[groupIndex];
		if (!group) return '';
		
		return `
		<div class="emoji-grid" data-group="${groupIndex}">
			${group.list.map((url, emojiIndex) => `
				<div class="emoji-item" 
					 data-group="${groupIndex + 1}" 
					 data-emoji="${emojiIndex + 1}">
					<img src="${url}" alt="{::${groupIndex + 1}_${emojiIndex + 1}}">
				</div>
			`).join('')}
		</div>
		`;
	}
	function setupEmojiPanel() {
		document.querySelectorAll('.emoji-item').forEach(item => {
			item.addEventListener('click', () => {
				const key = item.dataset.key;
				const input = document.querySelector(chatInputSelector);
				if (input) {
					insertAtCursor(input, `{::${key}}`);
					document.querySelector('.emoji-panel')?.classList.remove('show');
				}
			});
		});
	}
	function initEmojiButtonSystem() {
		emojiBtnObserver?.disconnect();
		tryAddEmojiButton();
		emojiBtnObserver = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				if (mutation.addedNodes.length) {
					tryAddEmojiButton();
				}
			});
		});
		emojiBtnObserver.observe(document.body, {
			childList: true,
			subtree: true
		});
	}

	function tryAddEmojiButton() {
		const chatInput = document.querySelector(chatInputSelector);
		if (!chatInput || chatInput.previousElementSibling?.classList.contains('emoji-btn')) {
			return;
		}
		if (!document.querySelector('.emoji-panel')) {
			initEmojiPanel();
		}
	}
	// ---------- 配置功能 ----------
	function showConfigDialog() {
		if (document.getElementById('configDialog')) {
			document.getElementById('configDialog').style.display = 'block';
			return;
		}
		let html = `
			<div id="configDialog" class="chat-config">
				<div class="chat-config-mask"></div>
				<div class="chat-config-window">
					<div class="chat-config-window-npc">
						<svg role="img" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#purple_cow_hello"></use></svg>
						<div class="chat-config-window-npc-name">小牛紫</div>
					</div>
					<div class="chat-config-window-content">
						<span class="chat-config-title">插件配置</span>
						<button id="closeConfig" class="chat-config-close"><svg role="img" aria-label="Close" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#close_menu"></use></svg></button>
						<div class="chat-config-main">
							<div class="chat-config-item">
								<div class="chat-config-item-title">图片预览</div>
								<div class="chat-config-item-set">
									<input id="chatconf_1" checked="checked" value="1" name="chatconf_img" type="radio">
									<label for="chatconf_1">直接显示图片</label>
									<input id="chatconf_2" value="0" name="chatconf_img" type="radio">
									<label for="chatconf_2">显示为文本:<input type="text" id="img_title"></label>
								</div>
								<div class="chat-config-tip">图片预览相关设置需要收起并重新打开聊天栏才会生效</div>
							</div>
							<div class="chat-config-item">
								<div class="chat-config-item-title">文本设置</div>
								<div class="chat-config-item-set">
									<label><input type="checkbox" id="option_username">突出用户名</label>
									<div class="chat-config-item-title">文本大小</div>
									<input id="chatconf_3" checked="checked" value="0" name="chatconf_fontsize" type="radio">
									<label for="chatconf_3" style="font-size:14px">小</label>
									<input id="chatconf_4" value="1" name="chatconf_fontsize" type="radio">
									<label for="chatconf_4" style="font-size:18px">中</label>
									<input id="chatconf_5" value="2" name="chatconf_fontsize" type="radio">
									<label for="chatconf_5" style="font-size:20px">大</label>
									<input id="chatconf_6" value="3" name="chatconf_fontsize" type="radio">
									<label for="chatconf_6" style="font-size:24px">超大</label>
									<div class="chat-config-item-title">铁牛图标</div>
									<input id="chatconf_7" checked="checked" value="0" name="chatconf_ic" type="radio">
									<label for="chatconf_7">默认 <span class="ic-icon-default">[IC]</span></label>
									<input id="chatconf_8" value="1" name="chatconf_ic" type="radio">
									<label for="chatconf_8" style="display:inline-flex">图标 <span class="ic-icon">[IC]</span></label>
									<div class="chat-config-preview">
										<div class="ChatMessage_chatMessage__2wev4">
											<span class="ChatMessage_timestamp__1iRZO">[11:45:14] </span>
											<span class="ChatMessage_name__1W9tB ChatMessage_clickable__58ej2">
												<div class="CharacterName_characterName__2FqyZ" translate="no">
													<div class="CharacterName_chatIcon__22lxV">
														<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#moderator"></use></svg>
													</div>
													<div class="CharacterName_chatIcon__22lxV">
														<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#anniversary_purple"></use></svg>
													</div>
													<div class="CharacterName_name__1amXp CharacterName_rainbow__1GTos" data-name="Stella">
														<span>Stella</span>
													</div>
													<div class="CharacterName_gameMode__2Pvw8">[IC]</div>
												</div>
											</span>
											<span>: </span>
											<span>杀!</span>
										</div>
									</div>
								</div>
							</div>
						</div>
						<button id="saveConfig">保存</button>
					</div>
				</div>
			</div>
		`;
		document.body.insertAdjacentHTML('beforeend', html);
		document.getElementById('option_username').checked = GM_getValue('option_username',0);
		document.getElementById('img_title').value = GM_getValue('img_title','[图片]');
		document.querySelector(`input[name="chatconf_img"][value="${GM_getValue('option_img', '0')}"]`).checked = true;
		document.querySelector(`input[name="chatconf_fontsize"][value="${GM_getValue('option_fontsize', '0')}"]`).checked = true;
		document.querySelector(`input[name="chatconf_ic"][value="${GM_getValue('option_ic', '0')}"]`).checked = true;
		document.getElementById('saveConfig').addEventListener('click', function() {
			GM_setValue('img_title', document.getElementById('img_title').value);
			document.getElementById('configDialog').style.display = 'none';
		});
		document.querySelectorAll('input[name="chatconf_img"]').forEach(radio => {
			radio.addEventListener('click', function() {
				GM_setValue('option_img', this.value);
			});
		});
		document.querySelectorAll('input[name="chatconf_fontsize"]').forEach(radio => {
			radio.addEventListener('click', function() {
				const font_val = this.value;
				document.body.setAttribute('data-fontsize', font_val);
				GM_setValue('option_fontsize', font_val);
			});
		});
		document.querySelectorAll('input[name="chatconf_ic"]').forEach(radio => {
			radio.addEventListener('click', function() {
				const ic_val = this.value;
				document.body.setAttribute('data-ic', ic_val);
				GM_setValue('option_ic', ic_val);
			});
		});
		document.getElementById('option_username').addEventListener('click', function() {
			var username = document.getElementById('option_username').checked ? 1 : 0;
			GM_setValue('option_username', username);
			document.body.setAttribute('data-username', username);
		});
		document.getElementById('closeConfig').addEventListener('click', function() {
			document.getElementById('configDialog').style.display = 'none';
		});
		document.querySelector('.chat-config-mask').addEventListener('click', function() {
			document.getElementById('configDialog').style.display = 'none';
		});
	}
	GM_registerMenuCommand("配置", showConfigDialog);
	function chatConfigInit() {
		document.body.setAttribute('data-fontsize', GM_getValue('option_fontsize', '0'));
		document.body.setAttribute('data-username', GM_getValue('option_username', '0'));
		document.body.setAttribute('data-ic', GM_getValue('option_ic', '0'));
		let btn = document.querySelector('.chat-conf');
		if (!btn) {
			btn = document.createElement('div');
			btn.innerHTML = '<svg width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#settings"></use></svg>';
			btn.className = 'chat-conf';
			btn.addEventListener('click', showConfigDialog);
		}
		const insertButton = () => {
			const targetElement = document.querySelector('.TabsComponent_expandCollapseButton__6nOWk');
			
			if (targetElement && targetElement.parentNode) {
				if (btn.parentNode && btn.parentNode !== targetElement.parentNode) {
					btn.remove();
				}
				targetElement.parentNode.insertBefore(btn, targetElement);
				return true;
			}
			return false;
		};
		if (!insertButton()) {
			const observer = new MutationObserver((mutations) => {
				const targetElement = document.querySelector('.TabsComponent_expandCollapseButton__6nOWk');
				if (targetElement && !btn.isConnected) {
					insertButton();
				}
			});
			observer.observe(document.body, {
				childList: true,
				subtree: true,
			});
			setTimeout(() => observer.disconnect(), 1000);
		}
	}
	// ---------- 插件,启动! ----------
	function init() {
		document.querySelectorAll(chatHistorySelector).forEach(setupHistoryObserver);
		const chatHistories = document.querySelectorAll(chatHistorySelector);
		if (chatHistories.length === 0) {
			setTimeout(init, 1000);
			return;
		}
		chatHistories.forEach(observeChatHistory);
		const globalObserver = new MutationObserver(mutations => {
			initEmojiButtonSystem()
			mutations.forEach(mutation => {
				mutation.addedNodes.forEach(node => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const newHistories = node.querySelectorAll?.(chatHistorySelector) || [];
						newHistories.forEach(observeChatHistory);
					}
				});
			});
		});
		if(document.readyState === 'complete') {
			chatConfigInit();
		}else{
			window.addEventListener('load', chatConfigInit);
		}
		globalObserver.observe(document.body, {
			childList: true,
			subtree: true
		});
	}
	window.addEventListener('unload', () => {
		globalObserver?.disconnect();
		historyObservers.forEach(obs => obs.disconnect());
	});
	if (window.ReactRouter) {
		window.ReactRouter.useEffect(() => {
			chatConfigInit();
		}, [window.location.pathname]);
	}
	const spaContentObserver = new MutationObserver(() => {
		if (!document.querySelector('.chat-conf')) {
			chatConfigInit();
		}
	});
	spaContentObserver.observe(document.getElementById('root'), { childList: true, subtree: true });
	init();
	initClipboardUpload();
	setupGlobalObserver();
	initEmojiButtonSystem();
	if (!emojiPanel) {
		emojiPanel = initEmojiPanel();
	}
})();