// ==UserScript== // @name 征纳互动人数和在线监控v2 // @namespace https://scriptcat.org/ // @description 实施监控征纳互动等待人数和在线状态,支持语音播报、自定义常用语 // @version 26.2.23 // @author runos // @match https://znhd.hunan.chinatax.gov.cn:8443/* // @match https://example.com/* // @icon https://znhd.hunan.chinatax.gov.cn:8443/favicon.ico // @grant GM_addStyle // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_notification // @grant GM_getValue // @grant GM_setValue // @connect * // @connect znhd-service.zeabur.app // @connect 122050.xyz // @connect drop.122050.xyz // @homepageURL https://scriptcat.org/zh-CN/script-show-page/3650 // @homepageURL https://scriptcat.org/zh-CN/script-show-page/3650 // @require https://scriptcat.org/lib/1167/1.0.0/%E8%84%9A%E6%9C%AC%E7%8C%ABUI%E5%BA%93.js?sha384-jXdR3hCwnDJf53Ue6XHAi6tApeudgS/wXnMYBD/ZJcgge8Xnzu/s7bkEf2tPi2KS // @require https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@5/dist/fp.min.js // @require https://fastly.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js // ==/UserScript== // ==========配置========== // 配置对象,集中管理可配置项 const CONFIG = { // 检查间隔(毫秒) CHECK_INTERVAL: 3000, // 最大日志条目数 MAX_LOG_ENTRIES: 20, WORKING_HOURS: { MORNING: { START: 9, END: 12 }, AFTERNOON: { START: 13.5, END: 18 } }, didaUrl: 'https://cdn.jsdelivr.net/gh/Run-os/UserScript/%E5%B7%A5%E4%BD%9C%E7%9B%B8%E5%85%B3/znhd/dida.mp3', commonPhrasesUrl: 'https://gitee.com/runos/other/raw/master/znhd/%E5%B8%B8%E7%94%A8%E8%AF%AD.json', // 麒麟传送相关配置(局域网P2P服务) qilinConfig: { host: 'drop.122050.xyz', //可选:qilindrop.cn || node-snapdrop.onrender.com || drop.122050.xyz } }; // ==========麒麟传送二维码显示函数========== function showQilinQRCode() { // 生成二维码URL(使用qilinConfig.host) const host = CONFIG.qilinConfig.host; const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:'; const qrUrl = `${protocol}//${host}`; // 创建遮罩层 const overlay = document.createElement('div'); overlay.id = 'temp-qr-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center;'; // 创建内容容器 const content = document.createElement('div'); content.id = 'temp-qr-content'; content.style.cssText = 'background:white;padding:20px;border-radius:8px;text-align:center;max-width:320px;position:relative;cursor:default;'; // 添加关闭按钮 const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = 'position:absolute;top:8px;right:12px;font-size:24px;cursor:pointer;color:#999;line-height:1;'; closeBtn.title = '点击关闭'; closeBtn.addEventListener('click', function (e) { e.stopPropagation(); document.body.removeChild(overlay); }); content.appendChild(closeBtn); // 添加标题 const title = document.createElement('h3'); title.textContent = '麒麟传送连接二维码'; title.style.cssText = 'margin:0 0 15px;font-size:16px;color:#333;'; content.appendChild(title); // 二维码和URL const qrDiv = document.createElement('div'); qrDiv.id = 'temp-qrcode'; content.appendChild(qrDiv); const urlText = document.createElement('p'); urlText.style.cssText = 'margin:15px 0 0;font-size:13px;color:#666;word-break:break-all;'; urlText.textContent = qrUrl; content.appendChild(urlText); // 5秒后关闭提示 const tip = document.createElement('p'); tip.style.cssText = 'margin:15px 0 0;font-size:12px;color:#999;'; tip.textContent = '5秒后自动关闭'; content.appendChild(tip); // 点击遮罩层关闭 overlay.addEventListener('click', function (e) { if (e.target === overlay) { document.body.removeChild(overlay); } }); overlay.appendChild(content); document.body.appendChild(overlay); // 生成二维码 if (typeof QRCode !== 'undefined') { new QRCode(qrDiv, { text: qrUrl, width: 200, height: 200, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); addLog(`[麒麟传送] 显示连接二维码: ${qrUrl}`, 'info', true); } else { content.innerHTML += '

二维码库未加载

'; } // 5秒后自动关闭 setTimeout(() => { if (document.body.contains(overlay)) { document.body.removeChild(overlay); } }, 5000); } // ==========日志管理========== // 全局日志状态管理 let setLogEntriesCallback = null; // 存储上一次的日志文本(用于重复内容检测) let lastLogMessage = null; // 添加日志条目函数 function addLog(message, type = 'info', logenabled = false) { const timestamp = new Date().toTimeString().slice(0, 8); // 检查是否为重复内容(忽略事件等动态信息) const pureMessage = message; if (lastLogMessage && pureMessage === lastLogMessage) { // 如果内容相同(忽略事件),不输出本次内容 console.log('[监控] 重复日志,已忽略:', message); return; } // 更新上一次的日志文本 lastLogMessage = pureMessage; const logItem = { timestamp, message, type }; // 更新React状态 if (setLogEntriesCallback) { setLogEntriesCallback(prevEntries => { const newEntries = [logItem, ...prevEntries]; if (newEntries.length > CONFIG.MAX_LOG_ENTRIES) { newEntries.pop(); } return newEntries; }); } if (logenabled) { console.log(`[监控] ${timestamp} ${message}`); } } // ==========存储管理========== // 存储键名 const STORAGE_KEY = 'scriptCat_Allvalue'; const DEFAULTS = { voiceEnabled: true, isChecked: false, // qilindrop相关默认值 qilindropEnabled: true, // 启用qilindrop局域网传输 autoCopyEnabled: true, // 自动复制图片/文本到剪贴板 autoDownloadEnabled: true, // 自动下载其他文件 iframeEnabled: true, // 启用iframe嵌入 }; // 从localStorage加载Allvalue数据 function loadAllvalue() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); return { ...DEFAULTS, ...parsed }; } } catch (error) { addLog('加载存储数据失败: ' + error.message, 'error', true); } // 返回默认值 return { ...DEFAULTS }; } // 保存Allvalue数据到localStorage function saveAllvalue(data) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); addLog('数据已保存到localStorage', 'success', true); } catch (error) { addLog('保存数据失败: ' + error.message, 'error', true); CAT_UI.Message.error('保存设置失败: ' + error.message); } } // ==========UI部分========== // 抽屉/模态框组件示例 function DM() { // 使用加载的数据初始化Allvalue const [Allvalue, setAllvalue] = CAT_UI.useState(loadAllvalue()); // 包装setAllvalue函数,实现自动保存 const updateAllvalue = (newValue) => { setAllvalue(newValue); // 自动保存到localStorage saveAllvalue(newValue); }; const patchAllvalue = (kv) => updateAllvalue({ ...Allvalue, ...kv }); // 解构状态变量,方便后续使用 const { voiceEnabled, isChecked } = Allvalue; const voiceEnabledText = voiceEnabled ? "🔊 语音" : "🔇 静音"; // 设置抽屉显示状态管理 const [visible, setVisible] = CAT_UI.useState(false); // 常用语抽屉显示状态管理 const [commonPhrasesVisible, setCommonPhrasesVisible] = CAT_UI.useState(false); // 日志条目状态管理 const [logEntries, setLogEntries] = CAT_UI.useState([]); // 常用语数据状态管理 const [phrasesData, setPhrasesData] = CAT_UI.useState({}); // 常用语加载状态 const [phrasesLoading, setPhrasesLoading] = CAT_UI.useState(false); // 麒麟传送连接状态 const [qilinStatus, setQilinStatus] = CAT_UI.useState({ connected: false, peerCount: 0 }); // 监听麒麟传送连接状态变化 CAT_UI.useEffect(() => { const handleStatusChange = (event) => { // 获取最新状态 const status = window.qilinClient ? window.qilinClient.getStatus() : null; setQilinStatus({ connected: event.detail.connected, peerCount: event.detail.peerCount || 0, displayName: status?.displayName || '' }); }; window.addEventListener('qilinConnectionStatusChange', handleStatusChange); // 初始状态 if (window.qilinClient) { const status = window.qilinClient.getStatus(); setQilinStatus({ connected: status.connected, peerCount: status.peers.length, displayName: status.displayName || '' }); } return () => { window.removeEventListener('qilinConnectionStatusChange', handleStatusChange); }; }, []); // 设置日志回调函数 CAT_UI.useEffect(() => { setLogEntriesCallback = setLogEntries; return () => { setLogEntriesCallback = null; }; }, []); // 加载常用语数据的函数 const loadPhrasesData = () => { setPhrasesLoading(true); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.commonPhrasesUrl, onload: function (response) { try { const data = JSON.parse(response.responseText); setPhrasesData(data); CAT_UI.Message.success('常用语加载成功'); } catch (error) { addLog('JSON 解析失败: ' + error.message, 'error', true); CAT_UI.Message.error('JSON 解析失败: ' + error.message); setPhrasesData({}); } finally { setPhrasesLoading(false); } }, onerror: function (error) { addLog('加载常用语失败: ' + (error.message || error), 'error', true); CAT_UI.Message.error('加载常用语失败'); setPhrasesLoading(false); setPhrasesData({}); } }); }; // 常用语抽屉打开时自动加载数据 CAT_UI.useEffect(() => { if (commonPhrasesVisible) { loadPhrasesData(); } }, [commonPhrasesVisible]); // =========主UI布局========== return CAT_UI.Space( [ CAT_UI.Space( [ CAT_UI.Text("麒麟传送: "), CAT_UI.Button( qilinStatus.connected ? `已连接(${qilinStatus.displayName || '未知'})` : '未连接', { type: "primary", onClick: () => showQilinQRCode(), style: { fontWeight: "bold", backgroundColor: qilinStatus.connected ? "#52c41a" : "#ff4d4f", borderColor: qilinStatus.connected ? "#52c41a" : "#ff4d4f" } } ), ], { direction: "horizontal", // 横向排列(默认值,可省略) size: "middle", // 元素间间距(可选:small/middle/large,默认middle) style: { marginBottom: "8px" } // 可选:给这一行加底部间距,避免与下方元素拥挤 } ), CAT_UI.Space( [ CAT_UI.Text("语音播报状态: "), CAT_UI.Button(voiceEnabledText, { type: "primary", onClick: () => { const newVoiceEnabled = !voiceEnabled; patchAllvalue({ voiceEnabled: newVoiceEnabled }); // 更新状态,触发重新渲染 // 启用语音时,初始化语音合成(解决浏览器not-allowed限制) if (newVoiceEnabled && 'speechSynthesis' in window) { // 播放一个静默语音来激活语音功能 const testUtterance = new SpeechSynthesisUtterance(''); window.speechSynthesis.speak(testUtterance); CAT_UI.Message.success('语音功能已启用'); } }, // 动态样式:根据静音状态切换颜色 style: { //字体加粗 fontWeight: "bold", backgroundColor: !voiceEnabled ? "#990018" : "#007e44", borderColor: !voiceEnabled ? "#990018" : "#007e44", } }), ], { direction: "horizontal", // 横向排列(默认值,可省略) size: "middle", // 元素间间距(可选:small/middle/large,默认middle) style: { marginBottom: "8px" } // 可选:给这一行加底部间距,避免与下方元素拥挤 } ), CAT_UI.Space( [ CAT_UI.Button("设置", { type: "primary", onClick: () => setVisible(true), // 显示抽屉 }), CAT_UI.Button("常用语", { type: "primary", onClick() { setCommonPhrasesVisible(true); }, }), ], { direction: "horizontal", // 横向排列(默认值,可省略) size: "middle", // 元素间间距(可选:small/middle/large,默认middle) } ), //抽屉 CAT_UI.Space( [ // 设置抽屉组件 CAT_UI.Drawer( // 抽屉内容 CAT_UI.createElement("div", { style: { textAlign: "left" } }, [ CAT_UI.Space( [ CAT_UI.Button("[脚本主页]", { type: "link", onClick: () => { window.open('https://scriptcat.org/zh-CN/script-show-page/3650', '_blank'); }, style: { padding: "0 8px" //蓝色字体 , color: "#1890ff", fontWeight: "bold" } }), CAT_UI.Button("[使用教程]", { type: "link", onClick: () => { window.open('https://flowus.cn/runos/share/e48623a2-f273-4327-8597-639e08902be8?code=1YD5Z5', '_blank'); }, style: { padding: "0 8px" //蓝色字体 , color: "#1890ff", fontWeight: "bold" } }), ], { direction: "horizontal", size: "small" } ), CAT_UI.Divider("注意事项"), CAT_UI.createElement( "p", { style: { marginBottom: "16px", color: "#666", lineHeight: "1.6", textAlign: "left", whiteSpace: "pre-line" } }, "1. 🔘[使用教程]里面可查看脚本详细介绍\n", ), CAT_UI.Divider("其他设置"), // 带文本的分隔线 CAT_UI.Checkbox("自动复制图片/文本到剪贴板", { checked: Allvalue.autoCopyEnabled !== false, onChange(checked) { patchAllvalue({ autoCopyEnabled: checked }); addLog(`自动复制: ${checked}`, 'info'); } }), CAT_UI.Checkbox("自动下载其他文件", { checked: Allvalue.autoDownloadEnabled !== false, onChange(checked) { patchAllvalue({ autoDownloadEnabled: checked }); addLog(`自动下载: ${checked}`, 'info'); } }), // 日志显示区域 CAT_UI.Divider("日志内容"), // 日志标题分隔线 CAT_UI.createElement( "div", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "500px", overflowY: "auto", backgroundColor: "#f5f5f5", padding: "10px", borderRadius: "4px", fontFamily: "monospace", fontSize: "12px" } }, logEntries.map((entry, index) => { // 根据日志类型定义颜色 const colorMap = { info: "#1890ff", // 蓝色 warning: "#faad14", // 橙黄色 success: "#52c41a", // 绿色 error: "#ff4d4f" // 红色 }; const color = colorMap[entry.type] || "#333333"; return CAT_UI.createElement( "div", { key: index, style: { color: color, marginBottom: "4px", borderLeft: `3px solid ${color}`, paddingLeft: "8px", fontWeight: "bold" // 加粗 } }, `${entry.timestamp} - ${entry.message}` ); }) ), ]), // 抽屉属性 { title: "设置菜单", // 抽屉标题 visible, // 控制显示/隐藏 width: 400, // 抽屉宽度(像素) focusLock: true, // 聚焦锁定 autoFocus: false, // 禁用自动聚焦 zIndex: 10000, // 层级 onOk: () => { setVisible(false); }, // 确定按钮回调 onCancel: () => { setVisible(false); }, // 取消按钮回调 } ), // 常用语抽屉组件 CAT_UI.Drawer( // 抽屉内容 CAT_UI.createElement("div", { style: { textAlign: "left" } }, [ // 显示当前JsonUrl CAT_UI.createElement( "div", { style: { marginBottom: "16px", color: "#666", fontSize: "12px", wordBreak: "break-all" } }, `数据源: ${decodeURIComponent(CONFIG.commonPhrasesUrl)}` ), // 重新加载按钮 CAT_UI.Button("重新加载常用语", { type: "primary", loading: phrasesLoading, onClick: loadPhrasesData, style: { marginBottom: "16px", width: "100%" } }), CAT_UI.Divider("常用语列表"), // 动态生成常用语按钮 phrasesLoading ? CAT_UI.createElement("div", { style: { textAlign: "center", padding: "20px" } }, "加载中...") : (Object.keys(phrasesData).length === 0 ? CAT_UI.createElement("div", { style: { textAlign: "center", padding: "20px", color: "#999" } }, "暂无常用语数据,请点击上方按钮加载") : CAT_UI.Space( Object.entries(phrasesData).map(([key, value]) => CAT_UI.Button(key, { type: "default", onClick() { safeCopyText(value); //CAT_UI.Message.success("已复制: " + key); setCommonPhrasesVisible(false); // 2. 把 value 追加到 TinyMCE 已有内容后面 appendToTinyMCE(value); addLog(`添加文本: ${value}`, 'success'); CAT_UI.Message.success("添加文本: " + value); }, style: { marginBottom: "8px", width: "100%" } }) ), { direction: "vertical", style: { width: "100%" } } ) ), CAT_UI.Divider(""), ]), // 抽屉属性 { title: "常用语", visible: commonPhrasesVisible, width: 400, focusLock: true, autoFocus: false, zIndex: 10001, // 比设置抽屉层级高一点 onOk: () => { setCommonPhrasesVisible(false); }, onCancel: () => { setCommonPhrasesVisible(false); }, } ), ], { direction: "horizontal", // 横向排列(默认值,可省略) size: "middle", // 元素间间距(可选:small/middle/large,默认middle) } ), ], { direction: "vertical" } // 垂直排列 ); } CAT_UI.createPanel({ // 强制固定Drawer和Panel位置 header: { title: CAT_UI.Space( [ CAT_UI.Icon.ScriptCat({ style: { width: "24px", verticalAlign: "middle" }, draggable: "false", }), CAT_UI.Text("征纳互动监控", { style: { fontSize: "16px" }, }), // 获取并显示版本号 CAT_UI.Text(`v${GM_info.script.version}`, { style: { fontSize: "12px", color: "#999", marginLeft: "8px" }, }), ], { style: { marginLeft: "5px" } } ), style: { borderBottom: "1px solid var(--color-neutral-3)" }, }, render: DM, // 面板初始位置 point: { x: window.screen.width - 500, // 距离右侧400px y: 20 // 距离顶部20px }, }); // ==========监控部分========== // 工具函数:获取当前小时(支持小数) function getCurrentHour() { const now = new Date(); return now.getHours() + now.getMinutes() / 60; } // 检查是否在工作时间内 function isWorkingHours() { const currentHour = getCurrentHour(); return (currentHour >= CONFIG.WORKING_HOURS.MORNING.START && currentHour <= CONFIG.WORKING_HOURS.MORNING.END) || (currentHour >= CONFIG.WORKING_HOURS.AFTERNOON.START && currentHour <= CONFIG.WORKING_HOURS.AFTERNOON.END); } // 缓存DOM元素引用 const domCache = { ocurrentElement: null, offlineElement: null }; // 检测DOM元素是否仍然存在于文档中 function isElementInDocument(element) { return element && element.isConnected; } // 修改主要检测函数 function checkCount() { if (!isWorkingHours()) { addLog('当前不在工作时间,已停止脚本', 'warning'); return; } try { // 获取等待人数 - 使用更灵活的选择器 // 检查缓存的元素是否还存在,不存在则重新查找 if (!isElementInDocument(domCache.ocurrentElement)) { domCache.ocurrentElement = null; } if (!domCache.ocurrentElement) { domCache.ocurrentElement = document.querySelector('.count:nth-child(2)'); } if (!isElementInDocument(domCache.offlineElement)) { domCache.offlineElement = null; } if (!domCache.offlineElement) { domCache.offlineElement = document.querySelector('.t-dialog__body__icon'); } const ocurrentElement = domCache.ocurrentElement; if (!ocurrentElement) { addLog('找不到人数元素', 'warning'); speak("找不到人数元素"); return; } const currentCount = parseInt(ocurrentElement.textContent.trim()); // 检查currentCount是否为有效数字 if (isNaN(currentCount)) { addLog(`无法解析等待人数,元素内容: "${ocurrentElement.textContent.trim()}"`, 'warning'); return; } // 更新人数状态日志和语音提示 if (currentCount === 0) { addLog('当前等待人数为0', 'success'); } else if (currentCount < 10) { addLog(`当前等待人数: ${currentCount}`, 'info'); speak("征纳互动有人来了"); } else { // 添加更多人数的情况处理 addLog(`当前等待人数: ${currentCount}`, 'info'); } const offlineEl = domCache.offlineElement; if (offlineEl?.textContent.includes('掉线')) { addLog('征纳互动已掉线', 'error'); speak("征纳互动已掉线"); return; // 移除返回值 } } catch (error) { addLog(`检测错误: ${error.message}`, 'error', true); } } /** * 向【页面里第一个 TinyMCE】追加文本并立即生效 * @param {string} text2append 要追加的文本 * @returns {string} 追加后的完整纯文本 */ function appendToTinyMCE(text2append = 'xxxxx') { /* 1. 拿到编辑器实例(动态匹配,不依赖 id) */ const editors = window.tinymce?.editors ?? []; // 所有 TinyMCE 实例 const ed = editors.find(e => e.inline === false); // 先拿第一个非 inline 的 // 如果上面没拿到,再随便拿一个 const editor = ed || editors[0]; // 检查输入框是否为空 let isInputEmpty = true; if (editor) { const body = editor.getBody(); isInputEmpty = !body.textContent.trim(); } else { const iframe = document.querySelector('.input-box iframe.tox-edit-area__iframe') || document.querySelector('iframe.tox-edit-area__iframe') || document.querySelector('iframe[class*="tox"]'); if (iframe) { try { const body = iframe.contentDocument.querySelector('body#tinymce') || iframe.contentDocument.body; isInputEmpty = !body.textContent.trim(); } catch (e) { addLog('无法访问iframe内容: ' + e.message, 'warning', true); } } } /* 2. 使用
换行处理 */ // HTML转义函数 const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; // 转义文本并将换行符替换为
const escapedText = escapeHtml(text2append); let processedContent = escapedText.replace(/\n/g, '
'); // 如果输入框不为空,在内容前添加
实现换行 if (!isInputEmpty) { processedContent = '
' + processedContent; } /* 3. 真正干活 */ if (editor) { const body = editor.getBody(); // 等同于 iframe.body if (isInputEmpty) { // 输入框为空时直接设置内容(不加额外换行) editor.setContent(processedContent); } else { // 输入框不为空时使用处理后的内容 editor.execCommand('mceInsertContent', false, processedContent); } editor.save(); // 同步回 textarea editor.setDirty(true); // 标记脏 editor.selection.select(body, true); // 把光标放末尾 editor.selection.collapse(false); } else { /* 4. 兜底:直接改 DOM + 触发事件 */ const iframe = document.querySelector('iframe.tox-edit-area__iframe') || document.querySelector('iframe[class*="tox"]'); if (!iframe) { addLog('找不到 TinyMCE iframe', 'error', true); return ''; } try { const body = iframe.contentDocument.body; if (!body) { addLog('找不到 body', 'error', true); return ''; } if (isInputEmpty) { body.innerHTML = processedContent; } else { body.insertAdjacentHTML('beforeend', processedContent); } // 触发单个 input 事件即可 body.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) { addLog('无法访问 iframe 内容: ' + e.message, 'error', true); return ''; } } const finalText = editor ? editor.getContent({ format: 'text' }) : document.querySelector('body#tinymce')?.textContent ?? ''; addLog('已使用
换行追加并同步: ' + finalText, 'success', true); return finalText; } // 语音播报函数 const speechQueue = []; let isSpeaking = false; function speak(text) { // 从localStorage获取语音状态 const savedData = loadAllvalue(); const voiceEnabled = savedData.voiceEnabled; if (!voiceEnabled || !('speechSynthesis' in window)) { return; } const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'zh-CN'; utterance.rate = 1.0; // 添加到队列 speechQueue.push(utterance); processSpeechQueue(); } // 处理语音队列 function processSpeechQueue() { if (isSpeaking || speechQueue.length === 0) { return; } isSpeaking = true; const utterance = speechQueue.shift(); utterance.onend = () => { isSpeaking = false; processSpeechQueue(); }; utterance.onerror = (event) => { isSpeaking = false; // 如果是not-allowed错误,清空队列避免堆积 if (event.error === 'not-allowed') { speechQueue.length = 0; } else { processSpeechQueue(); } }; // 在播放前确保语音合成已恢复(某些浏览器会暂停) if (window.speechSynthesis.paused) { window.speechSynthesis.resume(); } window.speechSynthesis.speak(utterance); } // 全局定时器引用,用于清理 let monitoringInterval = null; // 页面加载完成后启动监控 function startMonitoring() { // 立即执行一次检查 checkCount(); // 启动定时检查 monitoringInterval = setInterval(checkCount, CONFIG.CHECK_INTERVAL); } // 页面关闭时清理定时器 window.addEventListener('beforeunload', () => { if (monitoringInterval) { clearInterval(monitoringInterval); monitoringInterval = null; } }); // 安全复制工具:仅在页面聚焦且支持 clipboard 时尝试复制 function safeCopyText(text) { if (!text) return; // 1) 优先使用 GM_setClipboard(无需焦点) if (typeof GM_setClipboard === 'function') { try { GM_setClipboard(text); addLog('[复制] 已复制到剪贴板 (GM_setClipboard)', 'success', true); const player = new Audio(); player.src = CONFIG.didaUrl; player.play(); return; } catch (e) { addLog('[复制] GM_setClipboard 失败: ' + e.message, 'error', true); } } // 2) 浏览器异步 clipboard API if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text).then(() => { addLog('[复制] 已复制到剪贴板 (navigator.clipboard)', 'success', true); const player = new Audio(); player.src = CONFIG.didaUrl; player.play(); }).catch(err => { addLog('[复制] 复制到剪贴板失败: ' + err.message, 'error', true); }); return; } } // 格式化字节大小为易读单位(通用函数) function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; } function isBase64ImageString(text) { if (typeof text !== 'string') { return false; } const trimmed = text.trim(); if (trimmed.startsWith('data:image/') && trimmed.includes(';base64,')) { return true; } if (trimmed.length < 100) { return false; } const cleaned = trimmed.replace(/\s+/g, ''); return /^[A-Za-z0-9+/]+={0,2}$/.test(cleaned); } function buildDataUrlFromBase64(text) { if (text.startsWith('data:image/')) { return text; } return `data:image/png;base64,${text}`; } function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function convertImageBlobToPng(blob) { try { const bitmap = await createImageBitmap(blob); const canvas = document.createElement('canvas'); canvas.width = bitmap.width; canvas.height = bitmap.height; const ctx = canvas.getContext('2d'); ctx.drawImage(bitmap, 0, 0); return await new Promise((resolve) => canvas.toBlob(resolve, 'image/png')); } catch (err) { addLog('[复制] 转换图片为 PNG 失败: ' + err.message, 'error', true); return blob; // 退化:返回原始 blob 继续尝试 } } // 图片复制函数(通用版本,用于监控部分) async function copyBase64ImageToClipboard(text) { try { const dataUrl = buildDataUrlFromBase64(text.trim()); const res = await fetch(dataUrl); const blob = await res.blob(); const pngBlob = await convertImageBlobToPng(blob); const mime = 'image/png'; // 首选 Clipboard API(强制使用 PNG 以兼容多数实现) if (navigator.clipboard && typeof navigator.clipboard.write === 'function' && typeof window.ClipboardItem === 'function') { try { await navigator.clipboard.write([new ClipboardItem({ [mime]: pngBlob })]); const player = new Audio(); player.src = CONFIG.didaUrl; player.play(); addLog('图片已复制到剪贴板', 'success'); return true; } catch (clipErr) { addLog('[复制] Clipboard API 图片写入失败: ' + clipErr.message, 'error', true); } } // 退化方案:尝试 GM_setClipboard 写入 dataURL if (typeof GM_setClipboard === 'function') { try { const b64DataUrl = await blobToBase64(pngBlob); GM_setClipboard(b64DataUrl, { type: 'image', mimetype: mime }); const player = new Audio(); player.src = CONFIG.didaUrl; player.play(); addLog('图片已复制到剪贴板 (GM_setClipboard)', 'success'); return true; } catch (gmErr) { addLog('[复制] GM_setClipboard 图片写入失败: ' + gmErr.message, 'error', true); } } addLog('当前环境不支持图片剪贴板写入', 'warning'); return false; } catch (err) { addLog('[复制] 复制图片到剪贴板失败: ' + err.message, 'error', true); addLog(`复制图片到剪贴板失败: ${err && err.message ? err.message : '未知错误'}`, 'error'); return false; } } // ========== qilindrop 局域网传输功能 ========== // 存储键名 const DEVICE_NAME_KEY = 'qilindrop_deviceName'; // 获取设备名(优先使用自定义,否则生成默认) function getDeviceName() { const saved = localStorage.getItem(DEVICE_NAME_KEY); if (saved && saved.trim()) { return saved.trim(); } // 生成随机设备名 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = 'ZNHD-'; for (let i = 0; i < 6; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } localStorage.setItem(DEVICE_NAME_KEY, result); return result; } // ========== 麒麟传送(P2P局域网传输) ========== let qilinClient = null; // 麒麟传送工具函数 function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } // 麒麟传送音效函数 function qilinPlaySound() { if (!CONFIG.didaUrl) return; try { const audio = new Audio(CONFIG.didaUrl); audio.volume = 0.5; audio.play().catch(err => { // 静默处理自动播放限制等常见错误 }); } catch (err) { // 静默处理 } } // 麒麟传送安全复制文本函数 async function qilinSafeCopyText(text) { if (!text) return false; if (typeof GM_setClipboard === 'function') { try { GM_setClipboard(text); addLog('[麒麟传送] 文本已复制 (GM_setClipboard)', 'success', true); qilinPlaySound(); return true; } catch (e) { addLog('[麒麟传送] GM_setClipboard 失败: ' + e.message, 'error', true); } } if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { try { await navigator.clipboard.writeText(text); addLog('[麒麟传送] 文本已复制 (navigator.clipboard)', 'success', true); qilinPlaySound(); return true; } catch (err) { addLog('[麒麟传送] navigator.clipboard 失败: ' + err.message, 'error', true); } } try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); const success = document.execCommand('copy'); document.body.removeChild(textarea); if (success) { addLog('[麒麟传送] 文本已复制 (execCommand)', 'success', true); qilinPlaySound(); return true; } } catch (err) { addLog('[麒麟传送] execCommand 失败: ' + err.message, 'error', true); } return false; } // 麒麟传送复制图片到剪贴板函数 async function qilinCopyImageToClipboard(blob, originalFilename = '') { try { const pngBlob = await convertImageBlobToPng(blob); const mime = 'image/png'; if (navigator.clipboard && typeof navigator.clipboard.write === 'function' && typeof window.ClipboardItem === 'function') { try { await navigator.clipboard.write([new ClipboardItem({ [mime]: pngBlob })]); addLog('[麒麟传送] 图片已复制 (Clipboard API)', 'success', true); qilinPlaySound(); return true; } catch (clipErr) { addLog('[麒麟传送] Clipboard API 失败: ' + clipErr.message, 'error', true); } } if (typeof GM_setClipboard === 'function') { try { const dataUrl = await blobToBase64(pngBlob); GM_setClipboard(dataUrl, { type: 'image', mimetype: mime }); addLog('[麒麟传送] 图片已复制 (GM_setClipboard)', 'success', true); qilinPlaySound(); return true; } catch (gmErr) { addLog('[麒麟传送] GM_setClipboard 图片失败: ' + gmErr.message, 'error', true); } } addLog('[麒麟传送] 当前环境不支持图片剪贴板,降级为下载', 'warning', true); return false; } catch (err) { addLog('[麒麟传送] 复制图片失败: ' + err.message, 'error', true); return false; } } // 麒麟传送复制Base64图片到剪贴板函数 async function qilinCopyBase64ImageToClipboard(text) { try { const dataUrl = buildDataUrlFromBase64(text.trim()); const res = await fetch(dataUrl); const blob = await res.blob(); return await qilinCopyImageToClipboard(blob); } catch (err) { addLog('[麒麟传送] 复制 Base64 图片失败: ' + err.message, 'error', true); return false; } } // 麒麟传送客户端类 class QilinDropClient { constructor() { this.socket = null; this.peerId = null; this.connected = false; this.peers = new Map(); this.rtcPeers = new Map(); this.currentFile = null; this.displayName = null; // 保存显示名称 this.init(); } async init() { this.peerId = await this.getPeerId(); addLog('[麒麟传送] 设备ID: ' + this.peerId, 'info', true); this.connect(); } generateUUID() { let uuid = ''; for (let ii = 0; ii < 32; ii++) { switch (ii) { case 8: case 20: uuid += '-'; uuid += (Math.random() * 16 | 0).toString(16); break; case 12: uuid += '-'; uuid += '4'; break; case 16: uuid += '-'; uuid += (Math.random() * 4 | 8).toString(16); break; default: uuid += (Math.random() * 16 | 0).toString(16); } } return uuid; } async getPeerId() { let peerId = GM_getValue('qilin_peer_id'); if (!peerId) { peerId = this.generateUUID(); GM_setValue('qilin_peer_id', peerId); } return peerId; } getWebSocketURL() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const port = CONFIG.qilinConfig.port ? `:${CONFIG.qilinConfig.port}` : ''; const isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); const nortc = isRtcSupported ? '' : '/nortc'; return `${protocol}//${CONFIG.qilinConfig.host}${port}${nortc}`; } connect() { addLog('[麒麟传送] 正在连接服务器...', 'info', true); try { this.socket = new WebSocket(this.getWebSocketURL()); this.socket.binaryType = 'arraybuffer'; this.socket.onopen = () => { addLog('[麒麟传送] WebSocket 已连接', 'success', true); this.connected = true; this.updateConnectionStatus(); this.showNotification('已连接到麒麟传送', 'success'); }; this.socket.onmessage = (e) => this.handleMessage(e.data); this.socket.onclose = () => { addLog('[麒麟传送] 连接已断开', 'warning', true); this.connected = false; this.updateConnectionStatus(); this.peers.clear(); this.rtcPeers.forEach(peer => { if (peer.conn) peer.conn.close(); }); this.rtcPeers.clear(); this.showNotification('连接已断开,5秒后重连...', 'warning'); setTimeout(() => this.connect(), 5000); }; this.socket.onerror = (error) => { addLog('[麒麟传送] 连接错误: ' + error.message, 'error', true); }; } catch (error) { addLog('[麒麟传送] 无法连接: ' + error.message, 'error', true); this.showNotification(`连接失败: ${error.message}`, 'error'); } } updateConnectionStatus() { // 触发UI更新 window.dispatchEvent(new CustomEvent('qilinConnectionStatusChange', { detail: { connected: this.connected, peerCount: this.peers.size } })); } handleMessage(data) { if (data instanceof ArrayBuffer) { this.handleFileChunk(data); return; } try { const message = JSON.parse(data); switch (message.type) { case 'peers': this.handlePeers(message.peers); break; case 'peer-joined': this.handlePeerJoined(message.peer); break; case 'peer-left': this.handlePeerLeft(message.peerId); break; case 'signal': this.handleSignal(message); break; case 'ping': this.send({ type: 'pong' }); break; case 'display-name': this.handleDisplayName(message); break; } } catch (error) { addLog('[麒麟传送] 消息解析错误: ' + error.message, 'error', true); } } handlePeers(peers) { addLog(`[麒麟传送] 发现 ${peers.length} 个在线设备`, 'info', true); this.peers.clear(); peers.forEach(peer => { this.peers.set(peer.id, peer); //addLog(` - ${peer.name.displayName} (${peer.name.deviceName})`, 'info', true); if (peer.rtcSupported && window.RTCPeerConnection) { this.createRTCConnection(peer.id, peer, true); } }); this.updateConnectionStatus(); if (peers.length > 0) { this.showNotification(`发现 ${peers.length} 个设备`, 'success'); } } handlePeerJoined(peer) { addLog(`[麒麟传送] 设备加入: ${peer.name.displayName}`, 'info', true); this.peers.set(peer.id, peer); this.updateConnectionStatus(); this.showNotification(`${peer.name.displayName} 加入`, 'info'); if (peer.rtcSupported && window.RTCPeerConnection) { this.createRTCConnection(peer.id, peer, true); } } handlePeerLeft(peerId) { const peer = this.peers.get(peerId); if (peer) { addLog(`[麒麟传送] 设备离开: ${peer.name.displayName}`, 'warning', true); this.showNotification(`${peer.name.displayName} 离开`, 'warning'); } this.peers.delete(peerId); this.updateConnectionStatus(); const rtcPeer = this.rtcPeers.get(peerId); if (rtcPeer) { if (rtcPeer.conn) rtcPeer.conn.close(); this.rtcPeers.delete(peerId); } } createRTCConnection(peerId, peer, isCaller = false) { // 避免快速重复创建连接(用于重建时的防抖) const recentRecreate = this._recentRecreateMap?.get(peerId); if (recentRecreate && Date.now() - recentRecreate < 3000) { addLog(`[麒麟传送] 3秒内跳过重建连接: ${peer.name.displayName}`, 'warning', true); return null; } // 记录重建时间 if (!this._recentRecreateMap) this._recentRecreateMap = new Map(); this._recentRecreateMap.set(peerId, Date.now()); // 检查是否存在旧连接,如果存在则先关闭 const existingPeer = this.rtcPeers.get(peerId); if (existingPeer) { addLog(`[麒麟传送] 关闭旧 RTC 连接: ${peer.name.displayName}`, 'warning', true); if (existingPeer.conn) { existingPeer.conn.close(); } if (existingPeer.channel) { existingPeer.channel.close(); } this.rtcPeers.delete(peerId); } const config = { sdpSemantics: 'unified-plan', iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; const conn = new RTCPeerConnection(config); let channel = null; conn.onconnectionstatechange = () => { switch (conn.connectionState) { case 'connected': this.showNotification(`已连接: ${peer.name.displayName}`, 'success'); break; case 'disconnected': case 'failed': setTimeout(() => { if (this.peers.has(peerId)) { this.rtcPeers.delete(peerId); this.createRTCConnection(peerId, peer, isCaller); } }, 3000); break; case 'closed': addLog(`[麒麟传送] 连接关闭,3秒后尝试重连: ${peer.name.displayName}`, 'warning', true); setTimeout(() => { if (this.peers.has(peerId)) { this.rtcPeers.delete(peerId); this.createRTCConnection(peerId, peer, isCaller); } }, 3000); break; } }; conn.onicecandidate = (event) => { if (event.candidate) { this.sendSignal(peerId, { ice: event.candidate }); } }; const setupChannel = (ch) => { channel = ch; channel.binaryType = 'arraybuffer'; channel.onopen = () => { addLog(`[麒麟传送] 数据通道已打开: ${peer.name.displayName}`, 'success', true); }; channel.onmessage = (e) => { // 只在开始和结束时输出简洁日志 if (e.data instanceof ArrayBuffer) { // 文件块 - 只在接收完成后输出 this.handlePeerMessage(peerId, e.data); } else { // JSON 消息 try { const msg = JSON.parse(e.data); if (msg.type === 'header' || msg.type === 'transfer-complete') { addLog(`[麒麟传送] ${msg.type}: ${msg.name || ''}`, 'info', true); } } catch (err) { } this.handlePeerMessage(peerId, e.data); } }; channel.onclose = () => { addLog(`[麒麟传送] 数据通道已关闭: ${peer.name.displayName}`, 'warning', true); }; }; if (isCaller) { channel = conn.createDataChannel('data-channel', { ordered: true, reliable: true }); setupChannel(channel); conn.createOffer().then(offer => conn.setLocalDescription(offer)) .then(() => { this.sendSignal(peerId, { sdp: conn.localDescription }); }) .catch(error => { addLog('[麒麟传送] 创建 offer 失败: ' + error.message, 'error', true); }); } else { conn.ondatachannel = (event) => { addLog(`[麒麟传送] 收到数据通道: ${peer.name.displayName}`, 'info', true); setupChannel(event.channel); }; } const rtcPeer = { conn, channel, peer, isCaller }; this.rtcPeers.set(peerId, rtcPeer); return rtcPeer; } handleSignal(signal) { const senderId = signal.sender; let rtcPeer = this.rtcPeers.get(senderId); if (!rtcPeer) { const peer = this.peers.get(senderId); if (!peer) { addLog(`[麒麟传送] 收到信令但未找到对应设备: ${senderId}`, 'warning', true); return; } rtcPeer = this.createRTCConnection(senderId, peer, false); } const conn = rtcPeer.conn; if (signal.sdp) { const currentState = conn.signalingState; // 检查 SDP 顺序是否匹配 if (signal.sdp.type === 'answer' && currentState !== 'have-local-offer') { addLog(`[麒麟传送] SDP 状态不匹配,忽略 answer(当前: ${currentState})`, 'warning', true); // 检查是否在3秒重建期内,是则跳过 const recentRecreate = this._recentRecreateMap?.get(senderId); if (recentRecreate && Date.now() - recentRecreate < 3000) { addLog('[麒麟传送] 3秒内跳过重建', 'warning', true); return; } // 尝试重建连接 this.rtcPeers.delete(senderId); const peer = this.peers.get(senderId); if (peer) { this.createRTCConnection(senderId, peer, false); } return; } if (signal.sdp.type === 'offer' && currentState !== 'stable' && currentState !== 'have-remote-answer') { addLog(`[麒麟传送] SDP 状态不匹配,忽略 offer(当前: ${currentState})`, 'warning', true); // 检查是否在3秒重建期内,是则跳过 const recentRecreate = this._recentRecreateMap?.get(senderId); if (recentRecreate && Date.now() - recentRecreate < 3000) { addLog('[麒麟传送] 3秒内跳过重建', 'warning', true); return; } // 尝试重建连接 this.rtcPeers.delete(senderId); const peer = this.peers.get(senderId); if (peer) { this.createRTCConnection(senderId, peer, false); } return; } conn.setRemoteDescription(new RTCSessionDescription(signal.sdp)) .then(() => { if (signal.sdp.type === 'offer') { return conn.createAnswer(); } }) .then(answer => { if (answer) { return conn.setLocalDescription(answer); } }) .then(() => { if (signal.sdp.type === 'offer') { this.sendSignal(senderId, { sdp: conn.localDescription }); } }) .catch(error => { addLog('[麒麟传送] SDP 处理失败: ' + error.message, 'error', true); }); } else if (signal.ice) { // 检查连接状态是否准备好接收 ICE candidate if (conn.remoteDescription && conn.iceConnectionState !== 'closed') { conn.addIceCandidate(new RTCIceCandidate(signal.ice)) .catch(error => { addLog('[麒麟传送] ICE 添加失败(可能状态不一致): ' + error.message, 'warning', true); }); } } } sendSignal(peerId, signal) { signal.type = 'signal'; signal.to = peerId; this.send(signal); } handlePeerMessage(peerId, data) { if (data instanceof ArrayBuffer) { this.handleFileChunk(data, peerId); return; } try { const message = JSON.parse(data); switch (message.type) { case 'text': this.handleTextReceived(message); break; case 'header': this.handleFileHeader(message, peerId); break; case 'partition': this.sendToPeer(peerId, { type: 'partition-received', offset: message.offset }); break; case 'progress': addLog(`[麒麟传送] 对方接收进度: ${(message.progress * 100).toFixed(1)}%`, 'info', true); break; case 'transfer-complete': addLog('[麒麟传送] 对方接收完成', 'success', true); break; } } catch (error) { addLog('[麒麟传送] 对等消息解析错误: ' + error.message, 'error', true); } } sendToPeer(peerId, message) { const rtcPeer = this.rtcPeers.get(peerId); if (rtcPeer && rtcPeer.channel && rtcPeer.channel.readyState === 'open') { rtcPeer.channel.send(JSON.stringify(message)); return true; } return false; } handleDisplayName(message) { const displayName = message.message?.displayName || message.displayName; const deviceName = message.message?.deviceName || message.deviceName; // 保存显示名称 this.displayName = displayName; // 保存设备名称到配置 CONFIG.qilinConfig.deviceName = deviceName; addLog(`[麒麟传送] 本机设备名: ${displayName} (${deviceName})`, 'success', true); this.showNotification(`已连接: ${displayName}`, 'success'); // 触发状态更新事件,让UI刷新 this.updateConnectionStatus(); } async handleTextReceived(message) { try { const text = decodeURIComponent(atob(message.text)); addLog('[麒麟传送] 收到文本: ' + text, 'info', true); if (isBase64ImageString(text)) { const success = await qilinCopyBase64ImageToClipboard(text); if (success) { this.showNotification('🖼️ 图片已复制到剪贴板', 'success'); } else { this.showNotification('⚠️ 图片复制失败,已作为文本处理', 'warning'); await this.copyText(text); } } else { await this.copyText(text); } } catch (error) { addLog('[麒麟传送] 文本处理错误: ' + error.message, 'error', true); this.showNotification('文本处理失败', 'error'); } } async copyText(text) { const success = await qilinSafeCopyText(text); if (success) { this.showNotification('📋 文本已复制到剪贴板', 'success'); } else { this.showNotification('⚠️ 复制失败,请手动复制', 'warning'); prompt('[麒麟传送] 请手动复制:', text); } } handleFileHeader(message, peerId) { addLog('[麒麟传送] 收到文件头: ' + message.name + ' 来自: ' + peerId, 'info', true); this.currentFile = { name: message.name, mime: message.mime, size: message.size, chunks: [], receivedBytes: 0, senderId: peerId }; const type = message.mime.startsWith('image/') ? '图片' : '文件'; const size = formatSize(message.size); this.showNotification(`正在接收${type}: ${message.name} (${size})`, 'info'); } handleFileChunk(chunk, peerId) { if (!this.currentFile) { addLog('[麒麟传送] 收到文件块但没有文件头(来自: ' + peerId + ')', 'warning', true); return; } this.currentFile.chunks.push(chunk); this.currentFile.receivedBytes += chunk.byteLength; const progress = (this.currentFile.receivedBytes / this.currentFile.size * 100).toFixed(1); if (Math.floor(progress) % 20 === 0) { addLog(`[麒麟传送] 接收进度: ${progress}%`, 'info', true); } if (this.currentFile.receivedBytes >= this.currentFile.size) { this.completeFileDownload(); } } async completeFileDownload() { try { const fileInfo = { name: this.currentFile.name, mime: this.currentFile.mime, chunks: this.currentFile.chunks, senderId: this.currentFile.senderId }; this.currentFile = null; const blob = new Blob(fileInfo.chunks, { type: fileInfo.mime }); const url = URL.createObjectURL(blob); if (fileInfo.mime.startsWith('image/')) { await this.handleImageFile(blob, url, fileInfo.name); } else { this.downloadFile(url, fileInfo.name); } this.showNotification(`✅ 接收完成: ${fileInfo.name}`, 'success'); // ✅ 向发送端发送传输完成确认(解决第二次无法传输问题) if (fileInfo.senderId) { this.sendToPeer(fileInfo.senderId, { type: 'transfer-complete' }); addLog('[麒麟传送] 已发送传输完成确认给发送端', 'success', true); } } catch (error) { addLog('[麒麟传送] 文件处理错误: ' + error.message, 'error', true); this.showNotification('文件处理失败', 'error'); this.currentFile = null; } } async handleImageFile(blob, url, filename) { addLog('[麒麟传送] 处理图片文件...', 'info', true); const success = await qilinCopyImageToClipboard(blob, filename?.name || filename); if (success) { this.showNotification('🖼️ 图片已复制到剪贴板', 'success'); setTimeout(() => URL.revokeObjectURL(url), 100); } else { addLog('[麒麟传送] 无法复制图片,改为下载', 'warning', true); this.downloadFile(url, filename?.name || filename); } } downloadFile(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); qilinPlaySound(); this.showNotification(`📥 文件已下载: ${filename}`, 'success'); } send(message) { if (!this.connected || !this.socket) { addLog('[麒麟传送] 未连接,无法发送', 'warning', true); return; } this.socket.send(JSON.stringify(message)); } showNotification(message, type = 'info') { const colors = { success: '#4CAF50', error: '#f44336', warning: '#ff9800', info: '#2196F3' }; const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${colors[type]}; color: white; padding: 15px 20px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); z-index: 999999; font-size: 14px; max-width: 300px; animation: slideIn 0.3s ease-out; font-family: Arial, sans-serif; `; notification.textContent = message; const show = () => { document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => notification.remove(), 300); }, 3000); }; if (document.body) { show(); } else { document.addEventListener('DOMContentLoaded', show); } } getStatus() { const rtcStatus = []; this.rtcPeers.forEach((peer, id) => { rtcStatus.push({ name: peer.peer.name.displayName, connectionState: peer.conn.connectionState, signalingState: peer.conn.signalingState, iceState: peer.conn.iceConnectionState, channelState: peer.channel?.readyState || 'none' }); }); return { connected: this.connected, peerId: this.peerId, displayName: this.displayName, deviceName: CONFIG.qilinConfig.deviceName, peers: Array.from(this.peers.values()).map(p => p.name.displayName), rtcConnections: this.rtcPeers.size, rtcStatus, currentFile: this.currentFile ? { name: this.currentFile.name, size: formatSize(this.currentFile.size), progress: `${(this.currentFile.receivedBytes / this.currentFile.size * 100).toFixed(1)}%` } : null }; } } // 初始化麒麟传送 function initQilinDrop() { // 添加CSS动画样式 const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; if (document.head) { document.head.appendChild(style); } qilinClient = new QilinDropClient(); unsafeWindow.qilinClient = qilinClient; window.qilinClient = qilinClient; // 同时设置到window,供CAT_UI面板访问 addLog('[麒麟传送] 🦄 麒麟传送助手已启动', 'success', true); } // 获取麒麟传送状态 function getQilinStatus() { if (qilinClient) { return qilinClient.getStatus(); } return { connected: false, peerId: null, peers: [], rtcConnections: 0 }; } // ========== 页面启动 ========== // 麒麟传送初始化(立即启动,无需等待DOM) initQilinDrop(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startMonitoring); } else { startMonitoring(); }