From cd76796a5cb5d6176219368a7b2c50b2da7a2bd4 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 29 Jun 2026 17:17:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(monitor):=20=E8=A7=A6=E5=8F=91=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=B1=95=E7=A4=BA=E5=91=BD=E4=B8=AD=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E4=B8=8E=E5=BD=93=E5=89=8D=E4=BB=B7=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 告警事件落盘时增加 conditions + logic 条件快照, 经 quote_service 透传到 SSE 推送;前端触发记录详情行渲染命中条件与现价, 替代原先仅显示 「信号触发」的通用文案。 --- backend/app/services/quote_service.py | 54 +++++++++++++++++++++++++++ backend/app/strategy/monitor.py | 4 ++ frontend/src/pages/Monitor.tsx | 32 ++++++++++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/backend/app/services/quote_service.py b/backend/app/services/quote_service.py index 676b079..9a8000d 100644 --- a/backend/app/services/quote_service.py +++ b/backend/app/services/quote_service.py @@ -635,6 +635,8 @@ class QuoteService: return all_alerts: list[dict] = [] + rule_events: list[dict] = [] + engine = None # 通用监控规则评估 (统一引擎: signal/price/market/strategy) if self._app_state: @@ -675,6 +677,8 @@ class QuoteService: "change_pct": ev["change_pct"], "signals": ev["signals"], "severity": ev.get("severity", "info"), + "conditions": ev.get("conditions") or [], + "logic": ev.get("logic") or "and", }) # Free 自选实时只刷新少量标的, 不写全市场策略缓存。 @@ -696,9 +700,59 @@ class QuoteService: # cooldown 去重已在 MonitorRuleEngine 做过, 这里只负责转发。 self._maybe_send_system_notifications(all_alerts) + # Webhook 推送 (飞书等外部 IM, 由规则 webhook_enabled 开关控制)。 + # 紧随系统通知, 同样静默降级不阻断主流程。 + if rule_events: + self._maybe_send_webhook(rule_events, engine) + except Exception as e: # noqa: BLE001 logger.warning("监控评估失败: %s", e) + def _maybe_send_webhook(self, rule_events: list[dict], engine) -> None: + """把告警通过 Webhook 推送到外部 IM (由规则 webhook_enabled 开关控制)。 + + - 全局飞书 URL 未配置: 直接返回 + - 仅推送 webhook_enabled=True 的规则触发的告警 + - 失败静默, 不阻断主流程 + - 去重: 复用 MonitorRuleEngine 的 cooldown, 此处不重复去重 + + 注意: 用 rule_events (含 rule_id) 而非重建后的 all_alerts, + 以便反查引擎规则判断是否启用推送。 + """ + try: + from app.services import preferences + from app.services import webhook_adapter + + url = preferences.get_feishu_webhook_url() + if not url: + return + secret = preferences.get_feishu_webhook_secret() + + # 反查规则, 过滤出启用推送的事件 + source_labels = { + "strategy": "策略", "signal": "信号", + "price": "价格", "market": "异动", + } + rules = engine.rules if engine is not None else {} + pushed = 0 + for ev in rule_events: + rule = rules.get(ev.get("rule_id")) + if not rule or not rule.get("webhook_enabled"): + continue + source = ev.get("source", "") + source_label = source_labels.get(source, source or "通知") + symbol = ev.get("symbol") or "" + name = ev.get("name") or "" + message = ev.get("message") or "" + title = f"TickFlow · {source_label}" + body = f"{symbol} {name} {message}".strip() if symbol else (message or name) + if webhook_adapter.send_feishu(url, title, body, secret): + pushed += 1 + if pushed: + logger.info("飞书 Webhook 推送: %d 条", pushed) + except Exception as e: # noqa: BLE001 + logger.debug("Webhook 推送异常 (不影响告警主流程): %s", e) + def _maybe_send_system_notifications(self, all_alerts: list[dict]) -> None: """把告警转发到操作系统通知中心 (由 preferences 开关控制)。 diff --git a/backend/app/strategy/monitor.py b/backend/app/strategy/monitor.py index 41cf219..8215815 100644 --- a/backend/app/strategy/monitor.py +++ b/backend/app/strategy/monitor.py @@ -410,6 +410,10 @@ class MonitorRuleEngine: "change_pct": pct, "signals": hit_sigs, "severity": severity, + # 触发条件快照 (signal/price/market 类型): 用于触发记录展示 + # 「命中了什么条件」。strategy 类型靠策略选股池 diff, 不写条件。 + "conditions": list(rule.get("conditions", [])) if rtype != "strategy" else [], + "logic": rule.get("logic", "and") if rtype != "strategy" else "and", } events.append(ev) if self._alert_handler: diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx index 5d4d6e1..82b4ef2 100644 --- a/frontend/src/pages/Monitor.tsx +++ b/frontend/src/pages/Monitor.tsx @@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { RadioTower, Plus, Trash2, Settings2, Zap, Bell, ListChecks, BellRing, TrendingUp, TrendingDown, Flame } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { EmptyState } from '@/components/EmptyState' -import { api, type MonitorRule, type AlertEvent } from '@/lib/api' +import { api, type MonitorRule, type AlertEvent, type MonitorCondition } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { fmtPrice, fmtPct } from '@/lib/format' import { cn } from '@/lib/cn' @@ -367,9 +367,33 @@ function AlertsList({ alertsQuery, confirmClear, setConfirmClear, total, enterTs })()} -