做交易的人大概都有过这种时刻:挂了单就离开电脑去倒水,回来一看,止损早就触发了,单子成交了半小时,而你一无所知。或者反过来——人在外面,用手机瞄一眼行情,却不知道电脑上的仓位发生了什么。
TradingView 网页端本身没有「持仓变动推送到手机」的能力(通知邮件又慢又容易淹没在收件箱里),所以我写了一个油猴(Tampermonkey)脚本:实时监听 TradingView 页面里的持仓变化和订单成交通知,通过 Bark 推送到 iPhone,从事件发生到手机响铃,通常只要一两秒。
它能推送什么
脚本在页面里做两件事:
- 持仓监听:精确匹配 Positions 表格的表头(Symbol / Side / Qty / Position ID),检测三类事件——新建持仓、加仓/减仓、全部平仓。平仓需要连续 3 次轮询确认才推送,所以你只是切到 Orders 或 Account 选项卡看一眼,它绝不会误报「已平仓」。
- 成交通知:监听页面原生的 Toast 弹窗,捕获 Order filled、止损/止盈触发、订单被拒等事件,原文推送到手机。
推送效果大概是这样:
- ⚡️ 新建持仓 (Open):
ES1! Long 2 手 @ 5234.50 - ⚡️ 持仓手数变动:
ES1!: 从 2 手变更为 3 手 (加仓 +1) - 🛑 仓位已平仓 (Flat):
ES1! Long 2 手 已全部平仓 - 🚨 订单/成交通知:
Order filled ...
准备工作
只需要两样东西:
- iPhone 上安装 Bark(App Store 免费应用)。打开 App 后复制你的专属 Key——形如
XXXXXXXXXXXXXXXX,这就是你的推送地址凭证。 - 浏览器安装 Tampermonkey 扩展(Chrome / Edge / Firefox 都有)。
安装脚本
- 复制本文末尾「完整脚本」代码块里的全部内容。
- 点击浏览器工具栏的 Tampermonkey 图标 → 「添加新脚本」(Create a new script)。
- 删掉编辑器里的示例代码,粘贴刚才复制的内容,
Ctrl/Cmd + S保存。 - 打开 tradingview.com 的任意图表页,右下角会出现一个「TV 推送监控」浮窗——脚本已经在运行了。
配置 Bark Key(不用改一行代码)
这是我最推荐的部分:你不需要打开脚本源码改配置。
- 打开 TradingView 图表页,浮窗上会显示「Bark Key 未配置,请填写」。
- 把 Bark App 里的 Key 粘贴进输入框,点击「测试发送」(或直接回车)。
- 手机收到测试推送 = 配置成功,Key 自动保存,输入框收起,显示「已配置 · XXXX…XXX」。
之后换电脑、重装脚本都不用重新配——Key 存在 Tampermonkey 的存储里,跟着浏览器账号走。
如果你更喜欢硬编码,也可以打开脚本把顶部的 const BARK_KEY = "YOUR_BARK_KEY_HERE" 改成你的 Key,效果一样(浮窗里保存的优先级更高)。日常使用
浮窗本身就是一个控制面板:
- 绿点 = 监控中,灰点 = 已暂停。点「推送监控」开关可以一键暂停/恢复全部监听与推送——比如周末复盘的时候不想被骚扰,关掉即可,状态会记住。
- 紧急消息开关:开启后推送走 critical 级别,可以突破 iPhone 的专注模式/免打扰,音量拉满。盘中挂着止损单的时候建议开着,睡觉也不会漏掉;平时可以关掉用普通通知。
- 浮窗可以拖拽(拖标题栏)、折叠(点
–),位置和折叠状态都会记住。 - 万一浮窗找不到了(窗口缩小把它挤出了屏幕之类),打开 Tampermonkey 菜单,点「📍 复位浮窗到右下角」一键找回。实际上脚本每秒都会校验位置并自动拉回可视区,图表全屏时它也依然可见,这个菜单只是最后的保险。
几个细节说明
- 脚本只监听底部面板激活状态的 Positions 选项卡,切到 Orders / Account 不会误报。
- 推送走 Bark 官方服务器
api.day.app;如果你自建了 Bark 服务端,把脚本顶部的BARK_SERVER改成自己的地址即可。 - 脚本用 Shadow DOM 隔离了样式,不会和 TradingView 的界面互相干扰;输入框做了特殊处理,不会被 TradingView 的图表快捷键拦截。
- 免费声明:依赖 TradingView 的页面 DOM 结构,如果 TradingView 大改版导致表格匹配失效,监听会安静地停止而不是误报——遇到这种情况等脚本更新就好。
完整脚本
点击代码块右上角的复制按钮,然后按上面「安装脚本」的步骤操作即可。当前版本 v3.4.2(仓库 v0.2.2)。
// ==UserScript==
// @name TradingView 交易事件实时推送 (Bark) v3.1
// @namespace https://finnapps.win/
// @version 3.4.2
// @description 精确监听持仓手数(Qty)变化与订单成交,彻底杜绝切Tab误报平仓与浮盈跳动;浮窗内可直接配置 Bark Key 并测试发送,支持紧急/普通消息切换,一键暂停全部监听与推送;浮窗永不丢失(自动拉回可视区、全屏可见、菜单可复位)
// @author Finn & AA Bot
// @match https://*.tradingview.com/chart/*
// @match https://*.tradingview.com/
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect api.day.app
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ================= 配置区域 =================
// BARK_KEY 可保持占位符不动,安装后在浮窗中填写并测试保存
const BARK_KEY = "YOUR_BARK_KEY_HERE";
const BARK_SERVER = "https://api.day.app";
const BARK_SOUND = "alarm";
const CRITICAL_ALERT = true; // 「紧急消息」开关的默认值,可在浮窗中切换
// ============================================
const BARK_KEY_STORE = "barkKey";
const CRITICAL_STORE = "criticalAlert";
// 生效的 Key:浮窗保存的优先,其次回退到脚本顶部硬编码的
function getBarkKey() {
const stored = GM_getValue(BARK_KEY_STORE, "");
if (stored) return stored;
return BARK_KEY && BARK_KEY !== "YOUR_BARK_KEY_HERE" ? BARK_KEY : "";
}
function maskKey(key) {
return key.length > 8 ? `${key.slice(0, 4)}…${key.slice(-3)}` : "已保存";
}
let criticalAlert = GM_getValue(CRITICAL_STORE, CRITICAL_ALERT);
function setCriticalAlert(on) {
criticalAlert = on;
GM_setValue(CRITICAL_STORE, on);
updatePanelUI();
console.log(`[TV-Push] 推送级别已切换为${on ? "紧急(critical)" : "普通"}`);
}
// done(ok) 可选:传入时用于测试发送的结果回调,常规推送走 console 日志
function sendBark(key, title, body, critical, done) {
let url = `${BARK_SERVER}/${key}/${encodeURIComponent(title)}/${encodeURIComponent(body)}?sound=${BARK_SOUND}&group=TradingView`;
if (critical) url += "&level=critical&volume=10";
GM_xmlhttpRequest({
method: "GET",
url: url,
timeout: 5000,
onload: (res) => {
const ok = res.status >= 200 && res.status < 300;
if (done) {
done(ok);
} else if (ok) {
console.log("[TV-Push] 推送成功:", title, body);
} else {
console.error("[TV-Push] 推送异常:", title, res.status);
}
},
onerror: (err) => {
if (done) done(false);
else console.error("[TV-Push] 推送失败:", err);
}
});
}
function pushToBark(title, body) {
const key = getBarkKey();
if (!key) return;
sendBark(key, title, body, criticalAlert, null);
}
let lastPositions = {};
let initialized = false;
let emptyConfirmCount = 0; // 平仓确认计数器
// ================= 浮窗控制面板(可拖拽 / 可折叠) =================
const PANEL_ID = "tv-bark-float-panel";
const STORAGE_KEY = "monitorEnabled";
const PANEL_POS_KEY = "floatPanelPos";
const PANEL_COLLAPSED_KEY = "floatPanelCollapsed";
let monitorEnabled = GM_getValue(STORAGE_KEY, true);
let panelHost = null;
let panelEls = null;
let panelCollapsed = GM_getValue(PANEL_COLLAPSED_KEY, false);
let keySavedAt = 0; // Key 测试成功保存的时间戳,让成功提示在浮窗多停留几秒
// 开关状态变化后重置基准:重新开启时以当时持仓重新记录,
// 避免恢复监控的瞬间把已有仓位误报成"新建持仓"
function resetMonitoringState() {
lastPositions = {};
initialized = false;
emptyConfirmCount = 0;
}
function setMonitorEnabled(enabled) {
monitorEnabled = enabled;
GM_setValue(STORAGE_KEY, enabled);
resetMonitoringState();
updatePanelUI();
console.log(`[TV-Push] 推送监控已${enabled ? "开启" : "关闭"}`);
}
// 用 Shadow DOM 隔离样式,避免与 TradingView 页面 CSS 互相干扰
function buildPanel() {
const host = document.createElement("div");
host.id = PANEL_ID;
Object.assign(host.style, { position: "fixed", zIndex: "2147483647" });
// 恢复上次拖放位置,默认停靠右下角;越界拉回由 clampPanelIntoView 在挂载后处理
const saved = GM_getValue(PANEL_POS_KEY, null);
host.style.left = `${saved && Number.isFinite(saved.left) ? saved.left : window.innerWidth - 230}px`;
host.style.top = `${saved && Number.isFinite(saved.top) ? saved.top : window.innerHeight - 190}px`;
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
.panel {
pointer-events: auto;
min-width: 190px;
background: rgba(28, 32, 36, 0.96);
border: 1px solid rgba(120, 128, 138, 0.45);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #d1d5db;
user-select: none;
overflow: hidden;
}
.header {
display: flex;
align-items: center;
gap: 7px;
padding: 7px 10px;
cursor: move;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(120, 128, 138, 0.25);
font-size: 12px;
font-weight: 600;
}
.title { flex: 1; }
.dot {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0; background: #8a8a8a;
}
.dot.on { background: #26a65b; box-shadow: 0 0 6px rgba(38, 166, 91, 0.8); }
.min-btn {
border: none; background: transparent; color: #9aa0a6;
font-size: 14px; line-height: 1; cursor: pointer;
padding: 0 5px; border-radius: 4px;
}
.min-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }
.body {
padding: 10px 12px;
display: flex; flex-direction: column; gap: 8px;
}
.row {
display: flex; align-items: center;
justify-content: space-between; gap: 12px;
}
.label { font-size: 12px; }
.status { font-size: 11px; color: #9aa0a6; }
.status.on { color: #6fbf8a; }
.switch { position: relative; width: 34px; height: 20px; flex-shrink: 0; display: inline-block; }
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.slider {
position: absolute; inset: 0;
background: #4b5259; border-radius: 20px;
cursor: pointer; transition: background 0.18s;
}
.slider::before {
content: ""; position: absolute;
width: 14px; height: 14px; left: 3px; top: 3px;
background: #fff; border-radius: 50%;
transition: transform 0.18s;
}
.switch input:checked + .slider { background: #26a65b; }
.switch input:checked + .slider::before { transform: translateX(14px); }
.key-section {
display: flex; flex-direction: column; gap: 6px;
}
.test-btn {
border: none; border-radius: 6px; cursor: pointer;
background: #26a65b; color: #fff;
font-size: 12px; font-weight: 600;
padding: 5px 0; width: 100%;
}
.test-btn:hover { background: #2f7a4f; }
.test-btn:disabled { background: #4b5259; cursor: default; }
.key-msg { font-size: 11px; color: #9aa0a6; min-height: 14px; }
.key-msg.ok { color: #6fbf8a; }
.key-msg.err { color: #e06c75; }
.key-status { font-size: 11px; color: #6fbf8a; }
</style>
<div class="panel">
<div class="header">
<span class="dot on"></span>
<span class="title">TV 推送监控</span>
<button class="min-btn" type="button" title="折叠">–</button>
</div>
<div class="body">
<div class="row">
<span class="label">推送监控</span>
<label class="switch">
<input type="checkbox" class="monitor-toggle">
<span class="slider"></span>
</label>
</div>
<div class="row">
<span class="label">紧急消息</span>
<label class="switch">
<input type="checkbox" class="critical-toggle">
<span class="slider"></span>
</label>
</div>
<div class="row key-status-row">
<span class="label">Bark Key</span>
<span class="key-status">已配置</span>
</div>
<div class="key-section">
<div class="label">Bark Key 未配置,请填写:</div>
<slot></slot>
<button type="button" class="test-btn" hidden>测试发送</button>
<div class="key-msg"></div>
</div>
<div class="status main-status">监控中</div>
</div>
</div>`;
// Bark Key 输入框必须放在 light DOM(host 的直接子节点,经上面的 <slot>
// 渲染进面板):若放进 shadow DOM,按键事件会被重定向成 host <div>,
// TradingView 的快捷键守卫(按 e.target 是否 input/textarea 判断)识别
// 不到输入框,会把按键当成图表热键 preventDefault,导致无法打字/输入法失效
const keyInput = document.createElement("input");
keyInput.type = "text";
keyInput.placeholder = "粘贴你的 Bark Key";
keyInput.autocomplete = "off";
keyInput.spellcheck = false;
Object.assign(keyInput.style, {
boxSizing: "border-box",
width: "100%",
display: "block",
background: "rgba(0, 0, 0, 0.35)",
border: "1px solid rgba(120, 128, 138, 0.45)",
borderRadius: "6px",
color: "#e5e7eb",
fontSize: "12px",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
padding: "5px 8px",
outline: "none",
userSelect: "text",
cursor: "text"
});
keyInput.addEventListener("focus", () => { keyInput.style.borderColor = "#26a65b"; });
keyInput.addEventListener("blur", () => { keyInput.style.borderColor = "rgba(120, 128, 138, 0.45)"; });
host.appendChild(keyInput);
const panel = root.querySelector(".panel");
const dot = root.querySelector(".dot");
const minBtn = root.querySelector(".min-btn");
const body = root.querySelector(".body");
const toggle = root.querySelector(".monitor-toggle");
const criticalToggle = root.querySelector(".critical-toggle");
const status = root.querySelector(".main-status");
const keyStatusRow = root.querySelector(".key-status-row");
const keyStatus = root.querySelector(".key-status");
const keySection = root.querySelector(".key-section");
const testBtn = root.querySelector(".test-btn");
const keyMsg = root.querySelector(".key-msg");
// 阻止事件冒泡到 TradingView,避免误触发图表/面板/快捷键逻辑
panel.addEventListener("mousedown", (e) => e.stopPropagation());
panel.addEventListener("click", (e) => e.stopPropagation());
panel.addEventListener("keydown", (e) => e.stopPropagation());
toggle.addEventListener("change", () => setMonitorEnabled(toggle.checked));
criticalToggle.addEventListener("change", () => setCriticalAlert(criticalToggle.checked));
// 输入 Key 后才出现「测试发送」按钮;回车等同点击
keyInput.addEventListener("input", () => {
testBtn.hidden = !keyInput.value.trim();
keyMsg.className = "key-msg";
keyMsg.textContent = "";
});
keyInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !testBtn.hidden && !testBtn.disabled) testBtn.click();
});
testBtn.addEventListener("click", () => {
const key = keyInput.value.trim();
if (!key || testBtn.disabled) return;
testBtn.disabled = true;
keyMsg.className = "key-msg";
keyMsg.textContent = "发送中…";
sendBark(key, "✅ Bark 测试消息", criticalAlert ? "紧急推送通道已打通" : "普通推送通道已打通", criticalAlert, (ok) => {
testBtn.disabled = false;
if (ok) {
GM_setValue(BARK_KEY_STORE, key);
keySavedAt = Date.now();
keyInput.value = "";
testBtn.hidden = true;
keyMsg.className = "key-msg ok";
keyMsg.textContent = "✅ 测试成功,Key 已保存";
console.log("[TV-Push] Bark Key 已通过测试并保存");
} else {
keyMsg.className = "key-msg err";
keyMsg.textContent = "❌ 发送失败,请检查 Key 与网络后重试";
}
});
});
function applyCollapsed() {
body.style.display = panelCollapsed ? "none" : "";
minBtn.textContent = panelCollapsed ? "+" : "–";
minBtn.title = panelCollapsed ? "展开" : "折叠";
}
minBtn.addEventListener("click", (e) => {
e.stopPropagation();
panelCollapsed = !panelCollapsed;
GM_setValue(PANEL_COLLAPSED_KEY, panelCollapsed);
applyCollapsed();
});
applyCollapsed();
// 拖拽:按住标题栏移动,松手后记住位置
const header = root.querySelector(".header");
header.addEventListener("mousedown", (e) => {
if (e.target === minBtn) return;
e.stopPropagation();
e.preventDefault();
const offX = e.clientX - host.getBoundingClientRect().left;
const offY = e.clientY - host.getBoundingClientRect().top;
const onMove = (ev) => {
const rect = host.getBoundingClientRect();
const x = Math.min(Math.max(0, ev.clientX - offX), Math.max(0, window.innerWidth - rect.width));
const y = Math.min(Math.max(0, ev.clientY - offY), Math.max(0, window.innerHeight - rect.height));
host.style.left = `${x}px`;
host.style.top = `${y}px`;
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
GM_setValue(PANEL_POS_KEY, {
left: parseFloat(host.style.left),
top: parseFloat(host.style.top)
});
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
panelEls = { dot, toggle, criticalToggle, status, keyStatusRow, keyStatus, keySection, keyInput, testBtn, keyMsg, applyCollapsed };
return host;
}
// 把浮窗拉回可视区内:窗口缩小/换屏幕后,保存的旧坐标可能已越界,
// 每秒轮询时校验,保证浮窗永远找得到
function clampPanelIntoView() {
if (!panelHost) return;
const rect = panelHost.getBoundingClientRect();
const maxX = Math.max(0, window.innerWidth - rect.width);
const maxY = Math.max(0, window.innerHeight - rect.height);
const x = Math.min(Math.max(0, parseFloat(panelHost.style.left) || 0), maxX);
const y = Math.min(Math.max(0, parseFloat(panelHost.style.top) || 0), maxY);
if (`${x}px` !== panelHost.style.left) panelHost.style.left = `${x}px`;
if (`${y}px` !== panelHost.style.top) panelHost.style.top = `${y}px`;
}
// TradingView 重渲染可能把浮窗从 DOM 摘掉,轮询时确保在场;
// 全屏模式下挂到全屏元素内,否则会被全屏层整个盖住导致"失踪"
function ensurePanelAttached() {
if (!panelHost) panelHost = buildPanel();
const container = document.fullscreenElement || document.body;
if (!panelHost.isConnected || panelHost.parentElement !== container) {
container.appendChild(panelHost);
updatePanelUI();
}
clampPanelIntoView();
}
// 逃生口:浮窗万一找不到,从 Tampermonkey 菜单一键复位到右下角
GM_registerMenuCommand("📍 复位浮窗到右下角", () => {
GM_setValue(PANEL_POS_KEY, null);
if (panelHost) {
panelHost.style.left = `${window.innerWidth - 230}px`;
panelHost.style.top = `${window.innerHeight - 190}px`;
clampPanelIntoView();
if (panelCollapsed) {
panelCollapsed = false;
GM_setValue(PANEL_COLLAPSED_KEY, false);
panelEls.applyCollapsed();
}
}
console.log("[TV-Push] 浮窗已复位到右下角");
});
function updatePanelUI() {
if (!panelEls) return;
panelEls.toggle.checked = monitorEnabled;
panelEls.criticalToggle.checked = criticalAlert;
panelEls.dot.className = "dot " + (monitorEnabled ? "on" : "off");
panelEls.status.className = "status main-status " + (monitorEnabled ? "on" : "off");
panelEls.status.textContent = monitorEnabled
? `监控中 · 跟踪 ${Object.keys(lastPositions).length} 个仓位`
: "已暂停 · 不监听不推送";
// Key 已配置则收起输入区(测试成功后多展示 4 秒提示),改为显示已配置状态
const key = getBarkKey();
const showKeyConfig = !key || Date.now() - keySavedAt < 4000;
panelEls.keySection.style.display = showKeyConfig ? "" : "none";
panelEls.keyStatusRow.style.display = key ? "" : "none";
if (key) panelEls.keyStatus.textContent = `已配置 · ${maskKey(key)}`;
}
// ================================================================
// --- 机制一:精确监听持仓数量(Qty)变动 ---
function inspectPositionsTable() {
// 先检查底部当前是否真正激活了 Positions 选项卡,避免切到 Orders 或 Account 误触发
const activeTab = document.querySelector('button[aria-selected="true"], [data-name="tab-item"][data-active="true"]');
const activeTabText = activeTab ? activeTab.innerText : '';
// 查找所有 table
const tables = document.querySelectorAll('table');
let currentPositions = {};
let foundPosTable = false;
tables.forEach(table => {
const thTexts = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim());
// 必须严格匹配 Positions 表格专属表头
if (thTexts.includes('Symbol') && thTexts.includes('Side') && thTexts.includes('Qty') && thTexts.includes('Position ID')) {
foundPosTable = true;
const rows = table.querySelectorAll('tbody tr, tr');
rows.forEach(tr => {
const tds = Array.from(tr.querySelectorAll('td')).map(td => td.innerText.trim());
if (tds.length >= 4) {
const symbol = tds[0];
const side = tds[1];
const qty = parseInt(tds[2], 10);
const avgPrice = tds[3];
if (symbol && !isNaN(qty) && qty > 0) {
currentPositions[symbol] = { side, qty, avgPrice };
}
}
});
}
});
// 如果连持仓表都没找到(说明切到了别的Tab或底栏被收起),直接退出,绝不误判平仓
if (!foundPosTable) {
emptyConfirmCount = 0;
return;
}
// 首次加载仅记录基准
if (!initialized) {
lastPositions = currentPositions;
initialized = true;
return;
}
// 1. 检查新增或手数变动的仓位
for (const [sym, cur] of Object.entries(currentPositions)) {
const prev = lastPositions[sym];
if (!prev) {
// 新建仓位
pushToBark("⚡️ 新建持仓 (Open)", `${sym} ${cur.side} ${cur.qty} 手 @ ${cur.avgPrice}`);
} else if (prev.qty !== cur.qty || prev.side !== cur.side) {
// 手数变化(加仓/减仓)
const diff = cur.qty - prev.qty;
const action = diff > 0 ? `加仓 +${diff}` : `减仓 ${diff}`;
pushToBark("⚡️ 持仓手数变动", `${sym}: 从 ${prev.qty} 手变更为 ${cur.qty} 手 (${action})`);
}
}
// 2. 检查平仓(需连续 3 次轮询都确认无仓位,且当前持仓表明确在场)
const hasPrev = Object.keys(lastPositions).length > 0;
const hasCur = Object.keys(currentPositions).length > 0;
if (hasPrev && !hasCur) {
emptyConfirmCount++;
if (emptyConfirmCount >= 3) { // 连续 3 秒确认真的平了
for (const [sym, prev] of Object.entries(lastPositions)) {
pushToBark("🛑 仓位已平仓 (Flat)", `${sym} ${prev.side} ${prev.qty} 手 已全部平仓`);
}
lastPositions = {};
emptyConfirmCount = 0;
}
} else {
emptyConfirmCount = 0;
lastPositions = currentPositions;
}
}
// --- 机制二:原生 Toast 浮窗监听 ---
const orderKeywords = [
"Order filled", "Order executed", "Bracket order",
"Stop loss triggered", "Take profit triggered", "Order rejected",
"订单已成交", "订单已执行", "已触及止损", "已触及止盈"
];
const toastSentHistory = new Set();
const observer = new MutationObserver((mutations) => {
// 总开关关闭时彻底不监听:持仓轮询与 Toast 成交推送全部暂停
if (!monitorEnabled) return;
inspectPositionsTable();
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const t = node.innerText || '';
if (t.length > 5 && t.length < 200) {
for (const kw of orderKeywords) {
if (t.includes(kw)) {
if (!toastSentHistory.has(t)) {
toastSentHistory.add(t);
setTimeout(() => toastSentHistory.delete(t), 5000);
pushToBark("🚨 订单/成交通知", t.trim());
}
break;
}
}
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
setInterval(() => {
ensurePanelAttached();
if (monitorEnabled) inspectPositionsTable();
updatePanelUI();
}, 1000);
console.log(`[TV-Push v3.4.2] 精确防误报监听器已启动(推送监控: ${monitorEnabled ? "开" : "关"},Bark Key: ${getBarkKey() ? "已配置" : "未配置"};浮窗丢失时可从 Tampermonkey 菜单复位)`);
})();
Discussion