FinBuddy 市场监控与弹窗系统

· FinBuddy

今日进展

  1. 新增市场监控服务:异常检测引擎 + 订阅管理 + 通知分发,用户可订阅个股/板块的涨跌异动
  2. 新增弹窗系统:Toast(行情提醒/批量任务/信息提示)、Modal(服务端通知)、弹窗历史面板
  3. 新增自选股静默预取服务:后台预加载自选股数据,打开详情页秒出
  4. 新增 FC 拦截层(interceptor_core):统一的功能计费拦截,所有 API 调用前先检查积分
  5. 对话引擎全面分析与逻辑审查报告:梳理了意图解析到 Swarm 执行的完整链路
  6. 前端新增 Monitor 订阅面板、Popup 上下文、K 线图组件优化

关键代码/伪代码

市场监控服务 — 异常检测 + 订阅推送

# 市场监控是 FinBuddy 从"被动问答"走向"主动推送"的关键一步
# 之前用户得自己问"XX股票今天怎么样"
# 现在可以订阅关注,异动自动推

CLASS MarketMonitorService:
    """市场监控 — 异常检测 + SSE 推送"""

    ASYNC DEF check_anomalies(self):
        # 遍历所有订阅,检查是否有异常
        subscriptions = AWAIT self.subscription_mgr.get_active()
        FOR sub IN subscriptions:
            quote = AWAIT self.get_realtime_quote(sub.stock_code)
            # 异常检测引擎:涨跌幅/成交量/换手率
            anomaly = self.anomaly_engine.detect(quote, sub.rules)
            IF anomaly:
                # 通过 SSE 推送到前端
                AWAIT self.notification_dispatcher.push(
                    user_id=sub.user_id,
                    event="stock_alert",
                    data=anomaly
                )

CLASS AnomalyEngine:
    """异常检测 — 规则引擎,不依赖 LLM"""

    DEF detect(self, quote, rules) -> AnomalyResult | None:
        # 涨跌幅超阈值
        IF abs(quote.change_pct) > rules.change_threshold:
            RETURN AnomalyResult(type="price_surge", ...)
        # 成交量异常放大
        IF quote.volume > quote.avg_volume * rules.volume_ratio:
            RETURN AnomalyResult(type="volume_surge", ...)
        RETURN None

弹窗系统 — 统一事件总线

# 弹窗系统最头疼的是:各种通知来源(行情/任务/系统/服务端)
# 如果每个都自己弹,用户体验一塌糊涂
# 解决方案:统一事件总线 + 优先级队列

CLASS PopupEventBus:
    """弹窗事件总线 — 统一管理所有弹窗"""

    DEF emit(self, event: PopupEvent):
        # 按优先级入队:系统通知 > 行情提醒 > 信息提示
        priority = self._calc_priority(event)
        self.queue.push(event, priority)
        # 如果当前没有弹窗显示,立即弹出
        IF NOT self.is_showing:
            self._show_next()

    DEF _show_next(self):
        # 从队列取出最高优先级的弹窗
        event = self.queue.pop()
        IF event.type == "toast":
            # Toast:3秒自动消失(行情提醒、批量任务进度)
            ToastContainer.show(event)
        ELIF event.type == "modal":
            # Modal:需要用户确认(服务端通知、版本更新)
            BaseModal.show(event)
        # 弹窗关闭后,检查队列是否还有待显示的
        event.on_dismiss = self._show_next

FC 拦截层 — 统一计费

# 之前计费逻辑散落在各个 API route 里
# 有的扣了费,有的忘了扣,有的扣了两次
# 拦截层:所有 API 调用前先过一遍计费检查

CLASS InterceptorCore:
    """FC 拦截层 — 统一功能计费"""

    ASYNC DEF before_request(self, feature: str, user_id: str):
        # 1. 查询功能定价
        pricing = AWAIT self.get_pricing(feature)
        # 2. 检查用户积分是否足够
        balance = AWAIT self.credit_repo.get_balance(user_id)
        IF balance < pricing.cost:
            RAISE InsufficientCreditsError(
                f"需要 {pricing.cost} FC,当前余额 {balance} FC"
            )
        # 3. 冻结积分(防止并发扣费)
        AWAIT self.credit_repo.freeze(user_id, pricing.cost)
        # 4. 记录拦截日志
        logger.info("intercept_pass", feature=feature, cost=pricing.cost)

    ASYNC DEF after_request(self, feature: str, user_id: str, success: bool):
        # 请求成功 → 结算扣费
        IF success:
            AWAIT self.credit_repo.settle(user_id, feature)
        # 请求失败 → 退还冻结积分
        ELSE:
            AWAIT self.credit_repo.refund(user_id, feature)

遇到的问题

  • 弹窗优先级冲突:行情提醒和系统通知同时来的时候,用户容易错过重要的系统通知。解决方案:系统通知走 Modal(必须确认),行情走 Toast(自动消失)
  • 市场监控的 SSE 连接在 Electron 里不稳定:Electron 的渲染进程休眠时会断开 SSE。解决方案:主进程维护 SSE 连接,通过 IPC 转发到渲染进程
  • FC 拦截层的冻结-结算-退款需要事务保证:并发请求时可能出现重复扣费。解决方案:冻结时加分布式锁

明日计划

  • 市场监控增加更多异常规则:均线突破、MACD 金叉等
  • 弹窗系统增加"免打扰"时段设置
  • FinClaw_Web 端错误统一处理架构落地