征纳互动人数和在线监控 是一个油猴用户脚本(UserScript),用于实时监控 征纳互动平台 的等待人数和在线状态。当有纳税人等待时自动语音播报提醒,支持自定义常用语快速回复,帮助坐席人员及时响应。
znhd-service/
├── public/
│ ├── commonPhrases.yaml # 常用语配置文件(YAML 格式,当前使用)
│ ├── 常用语.json # 常用语配置文件(JSON 格式,已废弃)
│ └── dida.mp3 # 操作提示音文件
├── ReadMe.md # 项目说明文档
├── relay-server/ # 手机传图配套中继服务(Node,需自行部署到公网)
│ ├── server.js # 中继服务器:手机上传页 + 长轮询取图(纯 Node 内置 http,零依赖)
│ └── package.json # 零依赖,运行:node server.js
└── znhd.user.js # 油猴脚本主文件
在浏览器中安装 Tampermonkey 或 ScriptCat(脚本猫) 扩展。
方式一:在线安装(推荐)
访问脚本主页 https://scriptcat.org/zh-CN/script-show-page/3650,点击"安装此脚本"按钮。
方式二:手动安装
znhd.user.js 文件,复制全部内容脚本在页面右下角创建一个可拖拽的浮动面板,包含:
| 元素 | 说明 |
|---|---|
| 版本号 | 显示当前脚本版本(如 v26.7.18) |
| 🔊 语音 / 🔇 静音 | 一键切换语音播报状态,按钮颜色随状态变化(绿色=开启,红色=静音) |
| 设置 | 打开设置抽屉,可配置工作时间、常用语数据源地址,查看日志和脚本链接 |
| 常用语 | 打开常用语抽屉,加载并搜索常用语 |
.count:nth-child(2) 获取当前等待人数.t-dialog__body__icon 弹窗元素SpeechSynthesis API,语言为 zh-CN,语速 1.0not-allowed 限制public/commonPhrases.yaml,可在「设置 → 常用语数据源」处修改地址)GM_setClipboard,降级到 navigator.clipboard)dida.mp3)适用场景:坐席在手机上有纳税人发来的图片(如身份证、资料截图),想快速发到电脑剪贴板,直接在征纳互动聊天框 Ctrl+V 粘贴。
工作流程:
crypto.randomUUID() 生成并持久化一个稳定设备 ID(存在 GM_setValue,刷新/重开不变);https://<中继服务器>/u/<设备ID> 及对应二维码(二维码由脚本端 qrcodejs 本地生成,无需服务器参与);image/svg+xml 类型)→ 逐张按序上传到中继服务器(服务端按设备维护 FIFO 队列,内存保护上限 100 条);GM_xmlhttpRequest 长轮询 /recv/<设备ID> 取回图片(无需点击按钮;长轮询而非 WebSocket 是为了绕过征纳互动页面的 CSP 对 connect-src 的限制);<html>,不受面板 transform 影响):单击缩略图用 Viewer.js 放大查看(缩放/旋转/多图左右切换),每张图下方「复制」按钮把图片写入系统剪贴板(此步必须由一次点击触发,满足浏览器安全策略)→ 去征纳互动 Ctrl+V 即可;「下载」按钮把原图存为文件(自动按原名/MIME 补扩展名);每张右上角 × 可单独移除,底部「清空全部」,弹窗右上角关闭(图片保留,收到新图会再次弹出)。按用户隔离:设备 ID 是每台电脑随机生成、几乎不可猜测的 UUID,因此 A 的电脑、B 的电脑各自持有不同链接与二维码,图片只进对应那台电脑,互不串。
前置条件:须自行部署配套 relay-server(见上方项目结构)。在「设置 → 中继服务器」填写该服务的公网地址(如 https://你的服务器:端口,末尾不带 /)后,抽屉内的链接与二维码才会生成。
znhd.user.js 中的 CONFIG 对象(只读的运行参数):
const CONFIG = {
CHECK_INTERVAL: 3000, // 监控检查间隔(毫秒)
MAX_LOG_ENTRIES: 20, // 面板最大日志条目数
didaUrl: '.../dida.mp3', // 提示音文件地址
SPEECH_TIMEOUT: 15000 // 单条语音播报超时保护(毫秒),防止队列卡死
};
可用户配置项(工作时间、常用语数据源、语音开关)存放在 DEFAULTS 中,运行时存于 localStorage(键 scriptCat_Allvalue),可在设置面板直接修改,无需改代码:
const DEFAULTS = {
voiceEnabled: true, // 语音播报开关
workingHours: { // 监控时间段(单位:十进制小时,13.5 = 13:30)
morningStart: 9, morningEnd: 12,
afternoonStart: 13.5, afternoonEnd: 18
},
commonPhrasesUrl: 'https://gitee.com/runos/znhd-service/raw/master/public/commonPhrases.yaml',
relayServer: '' // 手机传图中继服务器公网地址(如 https://你的服务器:端口),留空则功能不可用
};
常用语配置文件为 public/commonPhrases.yaml,采用 YAML 格式。每个键为按钮显示名称,值为点击后填入编辑器的文本内容。
配置示例:
未办理税务登记: |
【未办理税务登记】
●打开 https://etax.hunan.chinatax.gov.cn:8443/xxbg/view/ztxxbg/qssbswzxblwkyqs ,自行打印清税证明
●如果上述方法无法正常打印,这边可以给您出具一个未涉税事项证明,需要您通过法人身份登录征纳互动,并将营业执照和法人身份证发送过来
已办理税务登记: |
【已办理税务登记】
●请先和您的税管员取得联系,税管员同意之后,进入电子税务局电脑端,搜索清税申报(税务注销办理)即可,详细操作流程:https://mp.weixin.qq.com/s/JqIEoAqo-BqWqSCrQYGuMQ
| 技术 | 用途 |
|---|---|
| JavaScript (ES6+) | 脚本主语言 |
| 脚本猫UI库 | 浮动面板、抽屉、按钮等 UI 组件 |
| js-yaml | 解析 YAML 格式的常用语配置文件 |
| FingerprintJS | 浏览器指纹识别 |
| Web Speech API | 语音合成播报 |
| GM API | GM_xmlhttpRequest、GM_setClipboard、GM_notification、GM_getValue/GM_setValue 等油猴扩展 API |
| relay-server | 手机传图配套中继服务:纯 Node 内置 http(零依赖),手机上传页内联、电脑端长轮询取图;需部署到公网 |
需要安装 Tampermonkey 或 ScriptCat 扩展以提供 GM API 支持。
'speechSynthesis' in window 应返回 true)gitee.com(常用语配置文件托管在 Gitee)https://你的服务器:端口,末尾不带 /)relay-server/ 目录执行 node server.js(纯 Node 内置模块、零依赖,默认端口 3000,可用 PORT 环境变量修改)localhost 时手机无法连上relay-server/server.js,请先停掉旧进程再 node server.js(PM2/systemd/docker 对应 restart)。.jpg/.png/.svg 等)。multiple,逐张顺序发送(一张成功再发下一张,日志显示进度 n/总数,失败即停并提示已发张数)。需配合 relay v26.7.28-v5 的手机收件队列,否则连发会互相覆盖。#recvGrid 九宫格(3 列缩略图、多张累积、带序号角标),点击缩略图才放大查看(全屏遮罩 +「长按保存」);文本仍弹独立弹层。已验证:渲染页内联脚本 new Function() 语法 OK、/\.svg$/i 反斜杠未被模板字符串吞掉、recvGrid/renderRecvGrid/openRecvImage 均就位。⚠️ 需重新部署,curl /health 看到 26.7.28-v6 即生效。.svg 扩展名识别)跳过压缩原样直传,并强制修正 blob 类型为 image/svg+xml——手机预览、电脑端画廊显示、放大查看均正常(注意:SVG「复制到剪贴板」受浏览器限制可能失败,可用「下载」按钮代替)。MAX_PICK 移除,选图张数不限;服务端每设备暂存队列上限从 9 提高到 100(纯内存保护,正常收发不会触顶)。phonePending 从单槽改为 FIFO 队列(同 pending,上限 100、超出丢最旧记 [丢弃] 日志),支持电脑端多选图片连发不丢图。已端到端验证:连发 3 条按序取回、第 4 次返回 empty;SVG mime 全链路透传无损。⚠️ 需重新部署,curl /health 看到 26.7.28-v5 即生效。rgba(0,0,0,0.5),放大时会透出后面的画廊弹窗("收到的图片 · 单击放大")。现注入覆盖样式把 .viewer-backdrop / .viewer-container 改为纯黑不透明,放大查看时完全遮住背景。@require jsdelivr 的 viewer.min.js,CSS 经 @resource + GM_getResourceText 注入,失败自动回退 CDN <link>;Viewer 未加载时缩略图和复制功能不受影响)。GM_getResourceText。配合 relay v26.7.28-v4 的手机端 9 张多选上传食用最佳。<input> 加 multiple,选图后九宫格缩略图预览(可单张 × 删除、可分多次追加,合计上限 9 张);发送时逐张压缩上传并显示进度(发送中…(n/总数)),某张失败即停、剩余保留可点按钮重试。pending 为每设备单槽,连发多张且电脑端未及时取走时会互相覆盖丢图。现改为每设备队列(上限 9 条,超出丢弃最旧并记 [丢弃] 日志),/recv 长轮询每次投递队头一条,电脑端收到即自动再轮询取下一条——电脑端油猴脚本无需任何改动。empty、连发 10 条时最旧一条被正确丢弃;渲染后内联脚本 new Function() 语法校验通过。⚠️ 需重新部署中继服务,curl /health 看到 26.7.28-v4 即生效。@version 误写为 26.7.26-v25(沿用了前一天日期)。按版本号约定——跨天须将日期部分改为当天、序号重置为 v1——现更正为 26.7.27-v1。功能内容与 v26.7.26-v25 完全一致(电脑端↔手机端双向互传),无其它代码改动。⚠️ 仍须两端同步更新并重启 node server.js。server.js:新增反向通道内存表 phoneOnline/phonePending/phoneWaiting 与 PHONE_TTL=20s;新增 4 条路由——POST /phone/heartbeat/<id>(手机报活)、GET /phone/status/<id>(返回 {online},电脑端据此判断是否可发)、POST /phone/send/<id>(电脑发图/文,镜像 /u)、GET /phone/recv/<id>(手机长轮询收件,镜像 /recv)。/phone/recv,收到图片收进下方九宫格画廊(3 列缩略图,多张累积、与脚本端 Viewer.js 画廊一致),点击缩略图放大查看+「长按保存」提示、收到文本展示+「复制文本」;顶部加在线状态指示。znhd.user.js:PhoneImageDrawer 扩为「手机互传」抽屉——轮询 /phone/status 显示 🟢已连接/⚪无在线设备;文本输入框+发送按钮、图片文件选择器+发送;离线时发送按钮置灰并提示「当前无在线设备,无法发送」;发送成功/失败写面板日志。新增 sendToPhone 辅助(GM_xmlhttpRequest POST /phone/send)。@version→26.7.26-v25;两文件 node -c 通过。⚠️ 须两端同步更新并重启 node server.js;手机页由中继同源托管,重启后即含心跳+收件箱。box/textEl 只设 max-* 无下限,文本很短时弹窗会缩得很小、观感差。现给 box 加 min-width:360px;min-height:200px,给文本区 textEl 加 min-width:320px;min-height:120px;文字多时仍按 max-* 正常放大不截断。@version→26.7.26-v24;语法 node -c 通过。znhd vX.Y.Z,因版本号已在脚本面板中统一展示,属多余信息,已删除(两处弹窗的 ver 元素及其 appendChild 一并移除)。@version→26.7.26-v23;语法 node -c 通过。/u/<id> 的 POST 现同时支持 {text}(文本)与 {data,mime,name}(图片),并以 type 字段区分。pending 条目统一带 type(image/text);deliverToAll 改为回传整条(含 type),由电脑端按类型分流,广播机制对文本同样生效(多标签同时接收互不丢)。znhd.user.js:startPhoneReceive 的 /recv 轮询按 data.type 分流——image 走 onImage(弹图片预览窗),text 走新增的 onText(弹文本预览窗)。onText 在弹窗前写一条 成功 日志(含前 40 字),并调用新增 showTextPopup:网页正中弹窗展示文本 + 「复制到剪贴板」按钮(复用 safeCopyText,含日志与提示音)+ 关闭按钮,点遮罩空白也可关闭。v22,@version→26.7.26-v22;语法 node -c 两文件均通过。node server.js)才生效;手机页由中继同源托管,重启后即含文本框。onImage 回调原先只弹出预览窗、不记日志,导致"脚本确实收到了图"在设置面板日志里看不到证据。znhd.user.js:onImage 在 showImagePopup 之前新增 addLog('[手机传图] 收到图片:' + 文件名 + '(' + MIME + ')', 'success'),日志进入设置面板「日志」区,onImage 信息中明确文件名与类型。v21,@version→26.7.26-v21;语法 node -c 通过。blobToPng 对整张原图(手机常 4000px+)做全分辨率解码 + canvas + PNG 无损编码,大图点击「复制」后卡顿明显。prepareClipboardImage:写入前先降采样到最大边 1600px 再编码 PNG,像素数降至约 1/6,解码/内存/PNG 编码同步变快;已在限制内的小图不降采样。仅当「原图直写」失败(内核仅支持 image/png)时才走此预处理。sharp(libvips) 在上传时预处理,使电脑端收到即小图、无需任何客户端编码(需给 relay-server 加原生依赖,按需启用)。GM_setClipboard(blob, type) 作为图片复制首选项——而 ScriptCat 的 GM_setClipboard(data:string) 仅支持文本,传 Blob 时不报错也不真正写图,导致 resolve(true) 误报成功。unsafeWindow.navigator.clipboard.write(先拿原始 blob 直接写以保留点击手势,失败再统一转 PNG 重试);隔离世界同名 API 作兜底;彻底移除不可靠的 GM_setClipboard 图片分支。overlayComputedOpacity = 0.8(实测 runningPopupVersionMarker = "znhd v26.7.26-v14"),即弹窗遮罩自身带 opacity:0.8——黑底只盖 80%,故透出后面文字。关键 CSS 机制:opacity<1 会把元素与其全部子元素(白框/图片/按钮)整体压成同透明度一起半透明,所以一透全透。overlayRect = {0,0,1774,950} 已铺满视口、bodyTransform=none、挂到 <html> 也正常——证明「body 的 transform 改写 fixed 包含块」并非本例真因(旧推断被实测推翻)。overlay/box/ver/close/copyBtn 全部内联加 opacity:1!important(内联 !important 优先级最高,能压过任何旧副本或全局 CSS)。现场验证:在运行中的旧弹窗上注入 opacity:1!important,计算透明度即时由 0.8 → 1,证明修复有效。body transform、CAT_UI 污染、旧脚本未重载)均被实测数据逐一排除;最终靠"测量运行中弹窗的计算样式"而非"读代码猜"才定位。版本标记右下角同步改为 v15。opacity<1 会令元素与其全部子元素一起半透明,故不能用 overlay{opacity:0.55} 实现。正确做法:overlay 保持 opacity:1!important,只让背景色用半透明 rgba(0,0,0,0.55)!important——这样只有"背景那层黑"是透的,而 box(白底 #fff)/img(JPEG 不透明) 各有自己的不透明背景,互不影响,弹窗与图片完全不透明。znhd.user.js:overlay 背景由 v15 的纯黑 #000!important 改回 background:rgba(0,0,0,0.55)!important;overlay/box/ver/close/copyBtn 的 opacity:1!important 全部保留。版本标记右下角 → v16,@version→26.7.26-v16;语法 node -c 通过。/health 即时探测触发 onConnected,但用户运行的是旧版中继(无 /health 路由),该探测请求一直挂起、5 秒后超时报「连接服务器超时」;而真正的 /recv 接收始终正常,所以图片照收、markConnected 仍通过 /recv 的 onload 触发了「已自动开始接收」——两条日志同秒出现即此矛盾。checkConnectivity() 与 /health 探测(本身脆弱,依赖服务器有该端点)。/recv 长轮询用极短 maxwait=1000」快速确认已连上:服务器很快返回空响应即 markConnected → onConnected 触发「已自动开始接收」日志(约 1 秒内),后续轮询恢复 maxwait=25000 实时等待图片。/recv 首次 onerror(网络真不可达)时一次性提示,不再有矛盾的超时误报。v20,@version→26.7.26-v20;语法 node -c 通过。onImage 写法被撤销。startPhoneReceive 新增 /health 即时连通性探测(checkConnectivity(),在 poll() 前调用):GM_xhr GET /health,一旦服务器响应就 markConnected() 触发 onConnected → 日志立即显示。此方案在 v20 被推翻(见下)。onload 内的 markConnected() 保留作兜底(connected 标记去重,不会重复打日志)。useEffect 把该 addLog 重新移回 onConnected 回调(与 v17 一致),地址末尾 / 一并归一 replace(/\/+$/,'')。onChange 在校验前加 url = url.trim().replace(/\/+$/, '')——用户粘贴 http://x:5689/ 这类带 / 的地址会被即时处理为 http://x:5689 再保存/显示。startPhoneReceive 与手机传图抽屉拼链接处本就归一,显示链接始终干净。v19,@version→26.7.26-v19;语法 node -c 通过。onload)才显示",但长轮询即便未收到图片也会在 maxwait 后返回空响应,导致该日志仍会在脚本一启动轮询(约 25s 内)就打出,并非手机发送后。onImage 首次真正收到手机图片 时才 addLog,即手机端点击发送之后才显示「[手机传图] 已自动开始接收(…)」。用 firstImageLogged 标记去重,避免后续每张图都刷该日志。useEffect 删除 onConnected 用法(该回调改为不再用于此日志)。v18,@version→26.7.26-v18;语法 node -c 通过。relayServer 一填好就立刻 addLog('[手机传图] 已自动开始接收…'),此时其实还没连通服务器,属误报。startPhoneReceive 新增 onConnected 回调:长轮询首次 onload(真正收到服务器响应)时 markConnected() 触发一次 → 仅此时才打「已自动开始接收」日志(含服务器地址)。onerror)打一次性「[手机传图] 连接服务器失败,请检查中继地址/网络(…)」错误日志(loggedConnFail 去重,避免 2s 轮询刷爆),不再静默让用户误以为已连。useEffect 把该 addLog 从「启动前」移入 onConnected 回调内。版本标记右下角 → v17,@version→26.7.26-v17;语法 node -c 通过。document.documentElement(<html> 而非 <body>),以规避个别 SPA 在 body 施加 transform/filter/will-change/contain 改写 position:fixed 包含块、导致遮罩偏移的可能;并加 !important + 铺满 100vw/vh 双保险、右下角显版本标记、加自动诊断日志。bodyTransform=none 且遮罩已铺满视口,故 transform 偏移理论不成立;真因是遮罩自身 opacity:0.8(见 v15)。v14 的 documentElement 挂载与 !important 仍保留作为防御层。#000+!important+铺满 100vw/vh 且 CAT_UI 样式隔离,逻辑上不应透字,故"仍透字"只可能来自外部:①更高/同级半透明元素遮挡;②祖先 transform 把 position:fixed 限制住、遮罩未真正铺满视口。[znhd弹窗诊断] 一行,含视口/遮罩尺寸、computed 背景色、document.elementsFromPoint 中心点元素栈(顶→底),并据此 warn:遮罩非最顶层(有遮挡) / 背景非纯黑(被外部样式覆盖) / 遮罩未铺满视口(疑祖先 transform 限制)。用户重装后看一行日志即可定位。.ar.co- 前缀且面板在 Shadow DOM 内,页面级无任何无前缀全局规则(adoptedStyleSheets 0 次、document.head 仅用于加载脚本),故面板半透明不会影响挂在 document.body 的弹窗。rgba(0,0,0,0.55)(与面板半透明是两回事)。!important、遮罩铺满 100vw/vh,隔绝任何外部 CSS 覆盖;② 弹窗右下角显示版本号 znhd v26.7.26-v12,一眼确认加载版本,终结"改了却没生效"的扯皮。inset:0 简写改为显式 top/left/right/bottom:0,规避个别内核不识别 inset 导致遮罩未铺满、从而露出底层页面文字的可能;背景保持 #000 实心不透明。rgba(0,0,0,0.55)(半透明,会透出底层页面),现改为纯黑 #000 不透明,弹窗显示时不再透出后面页面内容。createImageBitmap+canvas.toBlob)发生在点击时,卡在点击与「已复制」之间。改为图片到达弹窗显示时即在后台预转换好 PNG,点击直接写入、零转换、即时响应。同时移除「先尝试原图 jpeg」的浪费分支(ScriptCat 内核仅可靠支持 image/png,该尝试必失败再转 PNG)。prepareClipboardImage 对已是 PNG 的图直接复用、不再重编码。blobToPng 改为 prepareClipboardImage,在退回 PNG 时先按最大边 1600px 降采样再编码,解码内存/Canvas 分配/PNG 编码同步变快;小图不降采样。仅在「原图直写」失败(内核仅支持 image/png)时触发。GM_setClipboard(blob, type) 放最优先,而 ScriptCat 该 API 仅支持文本,传 Blob 会静默无效且 resolve(true) 报成功、剪贴板却为空。改为唯一可靠路径 = 页面主世界 unsafeWindow.navigator.clipboard.write(先试原始 blob 直写以保留点击手势,失败再转 PNG 重试),彻底移除假成功的 GM_setClipboard 图片分支。navigator.clipboard.write 对图片只可靠支持 image/png,而中继转发的是手机原图(多为 image/jpeg)。新增 blobToPng()(用 createImageBitmap+Canvas 转 PNG,best‑effort)在写入前统一转 PNG,规避该限制。各级兜底(GM_setClipboard / unsafeWindow 页面主世界 / 隔离世界)均改为写 PNG。copyImageToClipboard 只走 navigator.clipboard.write,而 ScriptCat 隔离世界里 ClipboardItem 全局常不存在 → 直接判定失败。改为三级兜底:①GM_setClipboard(blob,type)(扩展特权)②页面主世界 unsafeWindow.navigator.clipboard.write + unsafeWindow.ClipboardItem(HTTPS 页面下必定可用,保留点击手势)③原隔离世界写法。#e4393c,hover 加深为 #c9302c)。http://45.207.199.216:5689)后脚本即自动长轮询取图,移除「开始/停止接收」按钮,无需手动点击document.body,不受 CAT_UI 面板 transform 影响):含底部「复制到剪贴板」按钮与右上角关闭按钮@version 按 YY.M.D-vN 规范递增为 26.7.26-v3@require),抽屉内用 new QRCode 渲染后读取 canvas.toDataURL() 显示,二维码本地秒出、不再依赖服务器relay-server 移除 /qr 端点与 qrcode 依赖,现为纯 Node 内置 http、零依赖,仅保留手机上传页、/recv 长轮询取图、/health@version 按 YY.M.D-vN 规范递增为 26.7.26-v2GM_setValue 持久化),拼出独立上传链接 /u/<ID> 与二维码,实现 A/B 按用户隔离relay-server/server.js(纯 Node 内置模块 + qrcode):托管手机上传页(前端 canvas 压缩)、/recv/<ID> 长轮询取图、/qr 服务端生成二维码GM_xmlhttpRequest 长轮询取图(绕过税务页面 CSP 对 connect-src 的限制),收到后「复制到剪贴板」按钮触发 navigator.clipboard.write 写图片@version 按 YY.M.D-vN 规范递增为 26.7.26-v1CONFIG.MAX_SPEECH_QUEUE=10):连续产生大量播报时,超出部分丢弃最早(最旧)的消息,避免内存堆积CONFIG.SPEECH_QUEUE_TTL=30s 视为过期,speak() 入队与 processSpeechQueue() 播放前均会剔除,避免播报过时内容clearSpeechQueue():语音开关从开启切换为关闭时立即清空队列(并 speechSynthesis.cancel() 中止当前播报),防止旧消息堆积、再次开启时集中涌出SpeechSynthesisUtterance 改为 { utterance, enqueuedAt },enqueuedAt 用于过期判断;语法校验通过@description 用途说明、@param(含名称/类型/说明,可选参数用 [name] 语法)、@returns(类型与说明)YY.M.D-vN 版本号规范dida.mp3),复制常用语时播放GM_setClipboard,降级 navigator.clipboard本项目所有代码及脚本仅供学习、研究与个人非商业使用。
使用者需自行遵守所在网络环境及相关法律法规,严禁用于非法用途、商业用途或侵犯他人权益的场景。
使用本脚本所产生的一切风险、后果及法律责任均由使用者自行承担,项目作者不承担任何直接或间接责任。
如侵犯到您的权益,请联系项目维护者进行处理。
MIT License
欢迎提交 Issue 和 Pull Request!