From 121fa9f7bdb0e5381c0a96e4a8337674ad41d4cf Mon Sep 17 00:00:00 2001 From: ylytdeng Date: Tue, 3 Mar 2026 22:30:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8BWeC?= =?UTF-8?q?hat=E8=B7=AF=E5=BE=84=20+=20=E9=80=9A=E7=9F=A5=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.py: 自动从 %APPDATA% ini 读取数据盘符,扫描 xwechat_files 找到 db_storage 路径,多账号时交互选择,首次运行免手动配置 - monitor_web.py: 右侧设置面板支持自定义通知规则(群名/发送人模糊 匹配),命中时触发浏览器通知 + 蜂鸣声 + 金色高亮,规则存 localStorage Co-Authored-By: Claude Opus 4.6 --- monitor_web.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/monitor_web.py b/monitor_web.py index bcb9c9f..88b45ef 100644 --- a/monitor_web.py +++ b/monitor_web.py @@ -1471,6 +1471,42 @@ a.msg-link{text-decoration:none;color:inherit} .empty .icon{font-size:48px;margin-bottom:12px} ::-webkit-scrollbar{width:4px} ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.08);border-radius:2px} +/* 设置面板 */ +.settings-btn{background:none;border:1px solid rgba(255,255,255,.15);color:#888;font-size:16px;cursor:pointer;padding:4px 8px;border-radius:6px;transition:all .2s} +.settings-btn:hover{color:#ccc;border-color:rgba(255,255,255,.3)} +.settings-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:900} +.settings-overlay.show{display:block} +.settings-panel{position:fixed;top:0;right:-420px;width:400px;height:100%;background:#12121a;border-left:1px solid rgba(255,255,255,.1);z-index:901;transition:right .3s ease;display:flex;flex-direction:column;overflow:hidden} +.settings-panel.show{right:0} +.sp-header{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} +.sp-header h2{font-size:16px;color:#e0e0e0;font-weight:600} +.sp-close{background:none;border:none;color:#666;font-size:20px;cursor:pointer;padding:4px 8px} +.sp-close:hover{color:#ccc} +.sp-body{flex:1;overflow-y:auto;padding:16px 20px} +.sp-section{margin-bottom:20px} +.sp-section h3{font-size:13px;color:#888;margin-bottom:10px;text-transform:uppercase;letter-spacing:1px} +.sp-toggle{display:flex;align-items:center;justify-content:space-between;padding:8px 0} +.sp-toggle label{font-size:13px;color:#ccc} +.switch{position:relative;width:40px;height:22px;flex-shrink:0} +.switch input{display:none} +.switch .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#333;border-radius:11px;transition:.3s} +.switch input:checked+.slider{background:#4caf50} +.switch .slider:before{content:'';position:absolute;height:16px;width:16px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s} +.switch input:checked+.slider:before{transform:translateX(18px)} +.rule-card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:12px;margin-bottom:10px} +.rule-card .rule-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} +.rule-card .rule-del{background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:2px 6px} +.rule-card .rule-del:hover{color:#ef5350} +.rule-card input[type=text]{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:4px;padding:6px 8px;color:#ccc;font-size:12px;margin-bottom:6px;outline:none} +.rule-card input[type=text]:focus{border-color:rgba(79,195,247,.5)} +.rule-card input[type=text]::placeholder{color:#555} +.rule-opts{display:flex;gap:12px;margin-top:4px} +.rule-opts label{font-size:11px;color:#999;display:flex;align-items:center;gap:4px;cursor:pointer} +.rule-opts input[type=checkbox]{accent-color:#4caf50} +.add-rule-btn{width:100%;padding:8px;background:rgba(79,195,247,.1);border:1px dashed rgba(79,195,247,.3);border-radius:6px;color:#4fc3f7;font-size:12px;cursor:pointer;transition:all .2s} +.add-rule-btn:hover{background:rgba(79,195,247,.2)} +/* 通知高亮 */ +.msg.notify-hl{border-left:3px solid #ffd54f;background:rgba(255,213,79,.08);box-shadow:0 0 12px rgba(255,213,79,.1)} @@ -1478,6 +1514,23 @@ a.msg-link{text-decoration:none;color:inherit}

WeChat Monitor

SSE 实时
0 消息
+ + +
+
+

通知设置

+
+
+

全局

+
+
+
+
+

规则

+
+ +
+
@@ -1542,6 +1595,91 @@ function renderContent(m){ return linkify(wxEmoji(raw)); } +// ---- 通知过滤 ---- +const DEFAULT_NOTIFY = {enabled:false, sound_enabled:true, rules:[]}; +function loadNotifySettings(){ + try{ return JSON.parse(localStorage.getItem('wechat_notify'))||DEFAULT_NOTIFY; }catch(e){ return DEFAULT_NOTIFY; } +} +function saveNotifySettings(){ + const s = { + enabled: document.getElementById('notifyEnabled').checked, + sound_enabled: document.getElementById('soundEnabled').checked, + rules: collectRules() + }; + localStorage.setItem('wechat_notify', JSON.stringify(s)); +} +function collectRules(){ + const rules=[]; + document.querySelectorAll('.rule-card').forEach(card=>{ + const inputs=card.querySelectorAll('input[type=text]'); + const checks=card.querySelectorAll('input[type=checkbox]'); + rules.push({ + group_name: inputs[0]?.value||'', + sender_name: inputs[1]?.value||'', + notify_on_any: checks[0]?.checked||false + }); + }); + return rules; +} +function renderRules(){ + const s=loadNotifySettings(); + document.getElementById('notifyEnabled').checked=s.enabled; + document.getElementById('soundEnabled').checked=s.sound_enabled; + const c=document.getElementById('rulesContainer'); + c.innerHTML=''; + (s.rules||[]).forEach((_,i)=>addRuleCard(s.rules[i])); +} +function addRuleCard(r){ + r=r||{group_name:'',sender_name:'',notify_on_any:true}; + const c=document.getElementById('rulesContainer'); + const d=document.createElement('div'); + d.className='rule-card'; + d.innerHTML=`
规则 #${c.children.length+1}
`; + c.appendChild(d); +} +function addRule(){addRuleCard();saveNotifySettings();} +function toggleSettings(){ + const p=document.getElementById('settingsPanel'),o=document.getElementById('settingsOverlay'); + const show=!p.classList.contains('show'); + p.classList.toggle('show',show); + o.classList.toggle('show',show); + if(show) renderRules(); +} +function beep(){ + try{ + const ctx=new(window.AudioContext||window.webkitAudioContext)(); + const osc=ctx.createOscillator(); + const gain=ctx.createGain(); + osc.connect(gain);gain.connect(ctx.destination); + osc.frequency.value=880;gain.gain.value=0.3; + osc.start();osc.stop(ctx.currentTime+0.15); + }catch(e){} +} +function checkNotifyMatch(m){ + const s=loadNotifySettings(); + if(!s.enabled||!s.rules||!s.rules.length) return false; + const chat=(m.chat||'').toLowerCase(); + const sender=(m.sender||'').toLowerCase(); + for(const r of s.rules){ + if(!r.group_name) continue; + if(!chat.includes(r.group_name.toLowerCase())) continue; + if(r.sender_name && !sender.includes(r.sender_name.toLowerCase())) continue; + if(r.notify_on_any) return true; + } + return false; +} +function sendNotification(m){ + const title=m.chat+(m.sender?' - '+m.sender:''); + const body=(m.content||'').slice(0,100); + if(Notification.permission==='granted'){ + new Notification(title,{body,icon:'📡'}); + }else if(Notification.permission!=='denied'){ + Notification.requestPermission().then(p=>{if(p==='granted') new Notification(title,{body,icon:'📡'});}); + } + const s=loadNotifySettings(); + if(s.sound_enabled) beep(); +} + function addMsg(m, animate){ // 去重(包含类型,避免同时间戳的文字+图片组合被误判重复) const key = m.timestamp + '|' + (m.username||m.chat) + '|' + (m.type||''); @@ -1567,6 +1705,13 @@ function addMsg(m, animate){ const dk=m.timestamp+'|'+(m.username||m.chat); d.innerHTML=`
${m.time}${esc(m.chat)}${sn}
${m.type_icon} ${m.type}${ur}
${contentHtml}
`; + // 通知匹配检查 + if(animate && checkNotifyMatch(m)){ + d.classList.add('notify-hl'); + sendNotification(m); + setTimeout(()=>d.classList.remove('notify-hl'), 10000); + } + M.insertBefore(d, M.firstChild); if(animate){ @@ -1578,6 +1723,11 @@ function addMsg(m, animate){ while(M.children.length>200) M.removeChild(M.lastChild); } +// 页面加载时请求通知权限 +if('Notification' in window && Notification.permission==='default'){ + Notification.requestPermission(); +} + function connectSSE(){ const es=new EventSource('/stream'); es.onopen=()=>{