vault backup: 2026-11-09 09:42:19
This commit is contained in:
1 parent
511f907980
commit
5022dc27b2
8 files changed
+103
-9
No files matched your search
Vendored
+1
-1
@@ -199,6 +199,7 @@
|
||||
},
|
||||
"active": "b10e6fd1a0b41e79",
|
||||
"lastOpenFiles": [
|
||||
"小鱼/闲鱼消息通知/throttle-test.js",
|
||||
"小鱼/闲鱼消息通知/README.md",
|
||||
"小鱼/闲鱼消息通知/stop-watch.bat",
|
||||
"小鱼/闲鱼消息通知/start-watch-hidden.vbs",
|
||||
@@ -234,7 +235,6 @@
|
||||
"生财/04_电商AI自动化链路.md",
|
||||
"生财/06_Android程序员AI副业项目筛选.md",
|
||||
"娟/2026年医保社保缴费季度汇总.md",
|
||||
"妹妹/妹妹身高.md",
|
||||
"AndroidManifest.xml",
|
||||
"思睿/vocab_audio_output/audio/Day17.wav",
|
||||
"思睿/vocab_audio_output/audio/Day18.wav",
|
||||
|
||||
@@ -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`。
|
||||
(注意:环境变量对当前用户所有程序可见,安全性高于明文但不等于保险箱。)
|
||||
|
||||
## 通知配置
|
||||
|
||||
- 只想要本地弹窗/声音:默认即可。
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
"soundFile": "",
|
||||
"repeatSound": 2
|
||||
},
|
||||
"rateLimit": {
|
||||
"minIntervalSeconds": 60,
|
||||
"maxPerHour": 30
|
||||
},
|
||||
"webhooks": [],
|
||||
"heartbeatMinutes": 0,
|
||||
"quietHours": null,
|
||||
"notifyOnDecrease": false
|
||||
}
|
||||
|
||||
+5
-2
@@ -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,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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[2026/11/9 09:17:49] 发送测试通知…
|
||||
[2026/11/9 09:21:05] 发送测试通知…
|
||||
[2026/11/9 09:21:05] 测试通知已发出。若无弹窗,请检查 Windows 通知设置里是否允许 PowerShell/脚本宿主通知。
|
||||
Reference in new issue
Block a user