vault backup: 2026-11-09 09:42:19

This commit is contained in:
admin committed 2026-11-09 09:42:19 +08:00
1 parent 511f907980
commit 5022dc27b2
8 files changed
+103 -9

No files matched your search

+28
View File
@@ -15,6 +15,34 @@
2. **双击 `probe.bat`** → 生成 `probe-result.json`,把结果发给助手校准未读元素的选择器。
3. **双击 `start-watch.bat`** 测试;确认能弹窗后,改用 `start-watch-hidden.vbs` 后台常驻。
## 防刷屏 / 防乱发
`config.json` 里的 `rateLimit` 是 webhook 的闸门:
```json
"rateLimit": { "minIntervalSeconds": 60, "maxPerHour": 30 }
```
- **合并而非丢弃**:60 秒内的多次新消息不会发多条,而是攒起来,到点合成一条"合并 N 条提醒"发出去。信息不丢,群里不刷屏。
- **小时上限**:企业微信群机器人官方限制 20 条/分钟(超了返回 `45009`),这里默认 30 条/小时,远低于阈值,不会触发封禁。
- **本地弹窗不走节流**,因为那是给你自己看的;只有转发到群的受限制。
- **只在未读数变大时触发**,已读、刷新页面、重复渲染都不会误报。
## 密钥安全(重要)
webhook 的 URL 本身就是密码:拿到 URL 的任何人都能往你群里发任意消息。
这个文件夹在 **Obsidian 库里**,如果你的库有同步/进 Git,密钥会跟着上云。所以配置文件支持不写明文:
```json
"webhooks": [
{ "type": "wecom", "url": "env:WECOM_HOOK" }
]
```
然后在系统环境变量里设 `WECOM_HOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`。
(注意:环境变量对当前用户所有程序可见,安全性高于明文但不等于保险箱。)
## 通知配置
- 只想要本地弹窗/声音:默认即可。
+4 -1
View File
@@ -14,9 +14,12 @@
"soundFile": "",
"repeatSound": 2
},
"rateLimit": {
"minIntervalSeconds": 60,
"maxPerHour": 30
},
"webhooks": [],
"heartbeatMinutes": 0,
"quietHours": null,
"notifyOnDecrease": false
}
+5 -2
View File
@@ -26,8 +26,11 @@ async function postJson(url, payload, headers = {}) {
async function sendWebhook(hook, title, text, extra) {
const type = (hook.type || 'custom').toLowerCase();
const url = hook.url;
if (!url) throw new Error('webhook.url 为空');
// url 支持 "env:变量名" —— 密钥不落在配置文件里(Obsidian 库会同步/进 Git)
const url = typeof hook.url === 'string' && hook.url.startsWith('env:')
? process.env[hook.url.slice(4)]
: hook.url;
if (!url) throw new Error('webhook.url 为空(或 env: 指定的环境变量未设置)');
switch (type) {
case 'feishu': // 飞书自定义机器人
+1
View File
@@ -1,4 +1,5 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
echo === Goofish probe ===
node probe.js
@@ -1,4 +1,5 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
title Goofish Watcher
echo === Goofish watcher (visible mode, Ctrl+C to stop) ===
+63 -2
View File
@@ -6,7 +6,7 @@
const fs = require('fs');
const path = require('path');
const { CDP, listPages, EXPR } = require('./cdp');
const { notify } = require('./notify');
const { notify, sendWebhook } = require('./notify');
const DIR = __dirname;
const CONFIG_PATH = path.join(DIR, 'config.json');
@@ -25,6 +25,7 @@ function log(...parts) {
if (fs.existsSync(LOG_PATH) && fs.statSync(LOG_PATH).size > 2 * 1024 * 1024) {
fs.renameSync(LOG_PATH, LOG_PATH + '.old');
}
if (!fs.existsSync(LOG_PATH)) fs.writeFileSync(LOG_PATH, '\uFEFF', 'utf8'); // BOM: 记事本/编辑器不乱码
fs.appendFileSync(LOG_PATH, line + '\r\n', 'utf8');
} catch { }
}
@@ -117,6 +118,58 @@ function savePrev(map) {
} catch { }
}
// ---------- webhook 节流 + 合并(防刷屏、防企微 20条/分钟限流) ----------
const pushLog = []; // 已推送时间戳
let pendingPush = []; // 被节流时暂存的提醒,攒批后一次发出
function rateState() {
const rl = cfg.rateLimit || {};
const minI = Math.max(0, Number(rl.minIntervalSeconds ?? 60)) * 1000;
const maxH = Number(rl.maxPerHour ?? 30);
const now = Date.now();
while (pushLog.length && now - pushLog[0] > 3600000) pushLog.shift();
if (maxH > 0 && pushLog.length >= maxH) {
return { ok: false, reason: `已达每小时上限 ${maxH} 条` };
}
const last = pushLog[pushLog.length - 1];
if (last && now - last < minI) {
return { ok: false, reason: `距上条推送 ${Math.round((now - last) / 1000)}s(下限 ${minI / 1000}s)` };
}
return { ok: true };
}
async function queuePush(lines, pageUrl) {
const hooks = cfg.webhooks || [];
if (!hooks.length) return;
pendingPush.push({ lines, pageUrl, at: new Date().toLocaleTimeString('zh-CN', { hour12: false }) });
const st = rateState();
if (!st.ok) {
log(`webhook 节流:${st.reason},已并入待发队列(${pendingPush.length} 条)`);
return;
}
await flushPush();
}
async function flushPush() {
if (!pendingPush.length) return;
const hooks = cfg.webhooks || [];
if (!hooks.length) { pendingPush = []; return; }
const batch = pendingPush.slice();
pendingPush = [];
const lastUrl = batch[batch.length - 1].pageUrl;
const body = batch.map(b => `[${b.at}] ${b.lines.join(' / ')}`).join('\n') + `\n${lastUrl}`;
const title = batch.length > 1 ? `闲鱼新消息(合并 ${batch.length} 条提醒)` : '闲鱼有新消息';
pushLog.push(Date.now());
await Promise.all(hooks.map(async (h) => {
try {
const r = await sendWebhook(h, title, body, { url: lastUrl });
log(`[webhook] ${h.type} -> ${r.status}`);
} catch (e) {
log(`[webhook] ${h.type} 失败: ${e.message}`);
}
}));
}
function inQuietHours() {
const q = cfg.quietHours;
if (!q || !q.from || !q.to) return false;
@@ -185,10 +238,18 @@ async function tick(prev) {
log('静默时段,跳过通知:', JSON.stringify(events));
} else {
log('检测到新消息:', JSON.stringify(events));
await notify(cfg, title, text, { url: head.page }, log);
// 本地弹窗不节流(这才是工具的意义);webhook 走节流 + 合并
await notify({ ...cfg, webhooks: [] }, title, text, { url: head.page }, log);
await queuePush(lines, head.page);
}
}
// 有积压且节流窗口已过,补发
if (pendingPush.length) {
const st = rateState();
if (st.ok) await flushPush();
}
savePrev(map);
return map;
}
-3
View File
@@ -1,3 +0,0 @@
[2026/11/9 09:17:49] 发送测试通知…
[2026/11/9 09:21:05] 发送测试通知…
[2026/11/9 09:21:05] 测试通知已发出。若无弹窗,请检查 Windows 通知设置里是否允许 PowerShell/脚本宿主通知。