// ==UserScript== // @name 征纳互动人数和在线监控v2 // @namespace https://scriptcat.org/ // @description 实施监控征纳互动等待人数和在线状态,支持语音播报、自定义常用语 // @version 26.2.24 // @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 // @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://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.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://gitee.com/runos/znhd-service/raw/master/public/dida.mp3', commonPhrasesUrl: 'https://gitee.com/runos/znhd-service/raw/master/public/commonPhrases.yaml' }; // ==========日志管理========== // 全局日志状态管理 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, }; // 从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); // 设置日志回调函数 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 = jsyaml.load(response.responseText); setPhrasesData(data); CAT_UI.Message.success('常用语加载成功'); } catch (error) { addLog('YAML 解析失败: ' + error.message, 'error', true); CAT_UI.Message.error('YAML 解析失败: ' + 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(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" } }), ], { 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.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()) return; try { // 清理缓存中已失效的元素 if (!isElementInDocument(domCache.ocurrentElement)) domCache.ocurrentElement = null; if (!isElementInDocument(domCache.offlineElement)) domCache.offlineElement = null; // 重新查找元素 if (!domCache.ocurrentElement) { domCache.ocurrentElement = document.querySelector('.count:nth-child(2)'); } if (!domCache.offlineElement) { // 关键修复:使用 nth-of-type(2) 选择第2个 icon 元素 domCache.offlineElement = document.querySelector('.t-dialog__body__icon:nth-of-type(2)'); } const ocurrentElement = domCache.ocurrentElement; if (!ocurrentElement) { addLog('找不到人数元素', 'warning'); return; } const currentCount = parseInt(ocurrentElement.textContent.trim()); 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 && offlineEl.textContent.includes('掉线')) { addLog('征纳互动已掉线', 'error'); speak("征纳互动已掉线"); // 可选:清除缓存,下次重新查找 domCache.offlineElement = null; } } 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; } }); // 播放提示音函数 function playDidaSound() { if (!CONFIG.didaUrl) return; try { const player = new Audio(); player.src = CONFIG.didaUrl; player.volume = 0.5; player.play().catch(() => { }); } catch (e) { } } // 安全复制工具:仅在页面聚焦且支持 clipboard 时尝试复制 function safeCopyText(text) { if (!text) return; // 1) 优先使用 GM_setClipboard(无需焦点) if (typeof GM_setClipboard === 'function') { try { GM_setClipboard(text); addLog('[复制] 已复制到剪贴板 (GM_setClipboard)', 'success', true); playDidaSound(); 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); playDidaSound(); }).catch(err => { addLog('[复制] 复制到剪贴板失败: ' + err.message, 'error', true); }); return; } } // ========== 页面启动 ========== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startMonitoring); } else { startMonitoring(); }