[网页插件]虎绿林消息提醒通知推送

@Ta 15小时前发布,15小时前修改 73点击

网页插件:虎绿林消息提醒通知推送

效果预览

通过浏览器系统推送,接收虎绿林的@消息和内信通知。
支持显示消息内容详情,支持点击通知跳转到消息对应页面。


使用方式

  • 导入网页插件
  • 页面顶部会请求通知权限,点击授权
    (如未显示,可在“网站设置”中手动授权)
  • 保持 hu60.cn 任意一个标签页开启,即可接收@消息和内信通知
    (只会显示在线期间收到的通知,历史未读和离线期间的消息不会显示)
  • 支持断线自动重连
  • 如遇问题,可在浏览器控制台查看推送服务日志
    (如打开多个标签页,只有一个标签页会运行推送服务并显示日志)

支持的浏览器

已测试支持的浏览器

Windows:Chrome、Edge、FireFox
Android:FireFox(受省电机制等限制,后台时可能收不到通知)

暂不支持的浏览器

Android:Chrome(似乎需要服务器支持 Service Worker)
其它浏览器暂未测试。


如何测试推送效果

  • 先关闭 hu60.cn 下的所有标签页
  • 打开 hu60.cn 首页,此时消息推送服务会运行在这个标签页中
  • 再新建一个标签页,打开 hu60.cn,然后在第二个标签页中,在任意帖子、聊天室里@自己,或给自己发内信,测试消息推送效果
    (只打开一个标签页时,无法通过@自己和给自己发内信测试推送效果,因为当你发帖和发内信时,页面会刷新,且由于没有其他 hu60.cn 下的标签页打开,因此当前页的推送消息服务会随页面刷新重启,无法收到你刚刚发给自己的推送)

导入网页插件

导入网页插件:虎绿林消息提醒通知推送(当前用户:1,总安装次数:1)
<script>
// 虎绿林消息提醒通知推送,第1版,2025年11月25日
(function(){'use strict';function a(a){if(j.has(a))return j.get(a);const b=fetch(`/q.php/user.info.${a}.json`).then(a=>a.json()).then(b=>b&&b.name?b.name:a).catch(b=>(console.error(`[UserInfo] 获取 uid=${a} 失败`,b),a));return j.set(a,b),b}function b(a){try{const b=JSON.parse(a);if(!Array.isArray(b))return"\u6536\u5230\u4E00\u6761\u65B0\u6D88\u606F";let c="";return b.forEach(a=>{"text"===a.type?c+=a.value||"":"face"===a.type?c+=`[${a.face}]`:"at"===a.type&&(c+=`@${a.tag} `)}),c.replace(/[\r\n\s]+/g," ").trim().substring(0,80)}catch(a){return"\u6536\u5230\u4E00\u6761\u65B0\u6D88\u606F"}}function c(a,b,c,d){if("Notification"in window&&"granted"===Notification.permission)try{const e=new Notification(a,{body:b,icon:"/favicon.ico",tag:c,requireInteraction:!1});e.onclick=function(){window.focus(),d&&window.open(d,"_blank"),this.close()}}catch(a){console.error(a)}}function d(){function d(){h&&(h.readyState===WebSocket.OPEN||h.readyState===WebSocket.CONNECTING)||(h=new WebSocket(f),h.onopen=()=>{console.log("[WS] \u8FDE\u63A5\u5EFA\u7ACB\uFF0C\u5F00\u59CB\u76D1\u542C\u6D88\u606F\u63A8\u9001"),h.send(JSON.stringify({action:"unsub",data:["online","offline"]})),i&&clearInterval(i),i=setInterval(()=>{h.readyState===WebSocket.OPEN&&h.send("{\"action\":\"ping\"}")},6e4)},h.onmessage=async d=>{try{const e=JSON.parse(d.data);if("msg"===e.event&&console.log("[WS] \u6536\u5230\u6D88\u606F\u5BF9\u8C61:",e),"msg"===e.event&&e.data&&(0===e.data.type||1===e.data.type)){const d=e.data,f=d.byuid,g=await a(f);let h,i,j="";const k=JSON.parse(d.content);switch(d.type){case 0:h=`${g} 给您发了一条内信`,i=b(d.content),j=`/q.php/msg.index.chat.${f}.html`;break;case 1:const a=k.find(a=>"atMsg"===a.type);h=`${g} 在${a.pos}@您`,j=a.url,i=b(JSON.stringify(a.msg));break;default:h=`${g} 给您发了一条新消息`,i=b(d.content),j=`/q.php/index.index.html`;}j=j.replace("{$BID}","html"),c(h,i,`hu60_msg_${d.id}`,j)}}catch(a){console.error("[WS] \u5904\u7406\u65B0\u6D88\u606F\u51FA\u9519",a)}},h.onclose=()=>{console.log("[WS] \u8FDE\u63A5\u65AD\u5F00\uFF0C10 \u79D2\u540E\u91CD\u8FDE"),i&&clearInterval(i),setTimeout(d,1e4)},h.onerror=()=>h.close())}console.log("[Lock] \u83B7\u5F97\u9501\uFF0C\u5F53\u524D\u6807\u7B7E\u9875\u8D1F\u8D23\u6D88\u606F\u63A8\u9001"),d()}async function e(){const a=()=>{navigator.locks?navigator.locks.request(g,{mode:"exclusive"},async()=>(d(),new Promise(()=>{}))):d()};if(!("Notification"in window))return void console.warn("[Notify] \u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u901A\u77E5 API\uFF0C\u4E0D\u542F\u52A8 WebSocket \u63A8\u9001\u670D\u52A1\u3002");if("granted"===Notification.permission)a();else if("default"===Notification.permission){console.log("[Notify] \u6743\u9650\u4E3A default\uFF0C\u663E\u793A\u624B\u52A8\u6388\u6743\u63D0\u793A");const b=document.createElement("div");b.style.cssText="position:fixed;top:0;left:0;width:100%;z-index:2147483647;background:#fff3cd;color:#856404;text-align:center;padding:8px 0;border-bottom:1px solid #ffeeba;font-size:14px;line-height:1.5;box-shadow:0 2px 5px rgba(0,0,0,0.1);",b.innerHTML="\u672A\u83B7\u5F97\u901A\u77E5\u6743\u9650\uFF0C\u63A8\u9001\u670D\u52A1\u4E0D\u4F1A\u542F\u52A8\u3002<a href=\"javascript:void(0);\" style=\"color:#533f03;text-decoration:underline;font-weight:bold;cursor:pointer;\">\u70B9\u51FB\u6388\u4E88\u901A\u77E5\u6743\u9650</a>";const c=b.querySelector("a");c.onclick=async c=>{c.preventDefault();try{const c=await Notification.requestPermission();"granted"===c?(b.remove(),a()):(console.warn("[Notify] \u7528\u6237\u62D2\u7EDD\u4E86\u6743\u9650\u8BF7\u6C42"),b.innerHTML="\u5DF2\u62D2\u7EDD\u901A\u77E5\u6743\u9650\uFF0C\u5982\u9700\u5F00\u542F\u8BF7\u5728\u6D4F\u89C8\u5668\u7F51\u7AD9\u8BBE\u7F6E\u4E2D\u6388\u4E88\u3002",setTimeout(()=>b.remove(),3e3))}catch(a){console.error("[Notify] \u8BF7\u6C42\u6743\u9650\u53D1\u751F\u9519\u8BEF",a)}},document.body.prepend(b)}else console.warn("[Notify] \u672A\u83B7\u5F97\u901A\u77E5\u6743\u9650\uFF08\u7528\u6237\u5DF2\u62D2\u7EDD\uFF09\uFF0C\u4E0D\u542F\u52A8 WebSocket \u63A8\u9001\u670D\u52A1\u3002")}const f="wss://"+location.host+"/ws/msg",g="hu60_websocket_leader_lock";let h=null,i=null;const j=new Map;window.addEventListener("load",e)})();
</script>

源码

<script>
// 虎绿林消息提醒通知推送,第1版,2025年11月25日
(function () {
    'use strict';

    const WS_URL = "wss://" + location.host + "/ws/msg";
    // 选主锁,在开启多个标签页时仅维持一个 WebSocket 连接
    const LOCK_NAME = "hu60_websocket_leader_lock";

    let socket = null;
    let keepAliveTimer = null;

    // 用户名缓存:Key=uid, Value=Promise<string>
    const userNameCache = new Map();

    // 获取用户名
    function getUserName(uid) {
        if (userNameCache.has(uid)) {
            return userNameCache.get(uid);
        }
        const fetchPromise = fetch(`/q.php/user.info.${uid}.json`)
            .then(res => res.json())
            .then(json => {
                return (json && json.name) ? json.name : uid;
            })
            .catch(err => {
                console.error(`[UserInfo] 获取 uid=${uid} 失败`, err);
                return uid;
            });
        userNameCache.set(uid, fetchPromise);
        return fetchPromise;
    }

    // 消息内容解析
    function parseMsgContent(contentStr) {
        try {
            const contentArr = JSON.parse(contentStr);
            if (!Array.isArray(contentArr)) {
                return "收到一条新消息";
            }
            let plainText = "";
            contentArr.forEach(item => {
                if (item.type === 'text') plainText += item.value || "";
                else if (item.type === 'face') plainText += `[${item.face}]`;
                else if (item.type === 'at') plainText += `@${item.tag} `;
            });
            // 多行文本转单行、压缩空格、截取前 80 字符
            return plainText.replace(/[\r\n\s]+/g, ' ').trim().substring(0, 80);
        } catch (e) { return "收到一条新消息"; }
    }

    // 消息显示
    function showNotification(title, body, tag, openUrl) {
        if (!("Notification" in window) || Notification.permission !== "granted") return;
        try {
            const notification = new Notification(title, {
                body: body,
                icon: '/favicon.ico',
                tag: tag,
                requireInteraction: false
            });
            notification.onclick = function () {
                window.focus();
                if (openUrl) {
                    window.open(openUrl, '_blank');
                }
                this.close();
            };
        } catch (e) { console.error(e); }
    }

    // WebSocket 服务
    function startWebSocketService() {
        console.log("[Lock] 获得锁,当前标签页负责消息推送");
        function connect() {
            if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
            socket = new WebSocket(WS_URL);
            socket.onopen = () => {
                console.log("[WS] 连接建立,开始监听消息推送");
                // 不订阅机器人上下线事件
                socket.send(JSON.stringify({ "action": "unsub", "data": ["online", "offline"] }));
                // 仅监听实时消息,不拉取历史未读
                if (keepAliveTimer) clearInterval(keepAliveTimer);
                keepAliveTimer = setInterval(() => {
                    if (socket.readyState === WebSocket.OPEN) socket.send('{"action":"ping"}');
                }, 60000);
            };
            socket.onmessage = async (event) => {
                try {
                    const msg = JSON.parse(event.data);
                    // DEBUG: 打印消息对象
                    if (msg.event === "msg") {
                        console.log("[WS] 收到消息对象:", msg);
                    }
                    if (msg.event === "msg" && msg.data && (msg.data.type === 0 || msg.data.type === 1)) {
                        const data = msg.data;
                        const uid = data.byuid;
                        const senderName = await getUserName(uid);
                        let title;
                        let content;
                        let targetUrl = "";
                        const contentArr = JSON.parse(data.content);
                        switch (data.type) {
                            case 0: // 内信
                                title = `${senderName} 给您发了一条内信`;
                                content = parseMsgContent(data.content);
                                targetUrl = `/q.php/msg.index.chat.${uid}.html`;
                                break;
                            case 1: // @消息
                                const atNode = contentArr.find(item => item.type === 'atMsg');
                                title = `${senderName} 在${atNode.pos}@您`;
                                targetUrl = atNode.url;
                                content = parseMsgContent(JSON.stringify(atNode.msg));
                                break;
                            default:
                                title = `${senderName} 给您发了一条新消息`;
                                content = parseMsgContent(data.content);
                                targetUrl = `/q.php/index.index.html`;
                                break;
                        }
                        targetUrl = targetUrl.replace("{$BID}", "html");
                        showNotification(title, content, `hu60_msg_${data.id}`, targetUrl);
                    }
                } catch (e) {
                    console.error("[WS] 处理新消息出错", e);
                }
            };
            socket.onclose = () => {
                console.log("[WS] 连接断开,10 秒后重连");
                if (keepAliveTimer) clearInterval(keepAliveTimer);
                setTimeout(connect, 10000);
            };
            socket.onerror = () => socket.close();
        }
        connect();
    }

    // 初始化
    async function init() {
        // 获取锁并连接 WebSocket
        const runService = () => {
            if (navigator.locks) {
                navigator.locks.request(LOCK_NAME, { mode: 'exclusive' }, async () => {
                    startWebSocketService();
                    return new Promise(() => { });
                });
            } else {
                startWebSocketService();
            }
        };
        if (!("Notification" in window)) {
            console.warn("[Notify] 浏览器不支持通知 API,不启动 WebSocket 推送服务。");
            return;
        }
        if (Notification.permission === "granted") {
            // 已获权限,直接启动
            runService();
        } else if (Notification.permission === "default") {
            // 权限未定,在页面顶部插入提示条,引导用户手动点击
            console.log("[Notify] 权限为 default,显示手动授权提示");
            const bar = document.createElement("div");
            bar.style.cssText = "position:fixed;top:0;left:0;width:100%;z-index:2147483647;background:#fff3cd;color:#856404;text-align:center;padding:8px 0;border-bottom:1px solid #ffeeba;font-size:14px;line-height:1.5;box-shadow:0 2px 5px rgba(0,0,0,0.1);";
            bar.innerHTML = '未获得通知权限,推送服务不会启动。<a href="javascript:void(0);" style="color:#533f03;text-decoration:underline;font-weight:bold;cursor:pointer;">点击授予通知权限</a>';
            // 绑定点击事件
            const link = bar.querySelector('a');
            link.onclick = async (e) => {
                e.preventDefault(); // 防止可能的默认行为
                try {
                    const permission = await Notification.requestPermission();
                    if (permission === "granted") {
                        bar.remove(); // 授权成功,移除提示条
                        runService(); // 启动服务
                    } else {
                        console.warn("[Notify] 用户拒绝了权限请求");
                        bar.innerHTML = '已拒绝通知权限,如需开启请在浏览器网站设置中授予。';
                        setTimeout(() => bar.remove(), 3000);
                    }
                } catch (err) {
                    console.error("[Notify] 请求权限发生错误", err);
                }
            };
            document.body.prepend(bar);
        } else {
            // permission === "denied"
            console.warn("[Notify] 未获得通知权限(用户已拒绝),不启动 WebSocket 推送服务。");
        }
    }
    window.addEventListener('load', init);
})();
</script>

基于虎绿林 WebSocket 消息推送服务实现。

回复列表(1|隐藏机器人聊天)
  • @Ta / 1小时前 / /
    待审核
    发言待审核,仅管理员和作者本人可见。
添加新回复
回复需要登录