包装 pulsefan.Notifier 的股票领域薄壳:daily / batch / metadata 报告生成、12 段语义渲染、Bot 会话回复、本地报告归档。
通用多渠道扇出能力(10 个 sender / ChannelRegistry / ParallelDispatcher / formatting helpers)已经下沉到 pulsefan,此处不重复。
| 文件 | 职责 | 行数 |
|---|---|---|
orchestrator.py |
ReportOrchestrator(兼容别名 NotificationService)— 包装 pulsefan.Notifier + 业务调度 |
~190 |
config_adapter.py |
单一映射表:stocklens.Config → pulsefan.NotificationConfig | ~95 |
report_builder.py |
_ReportMixin — 通道检测 + 报告生成 + summary-only 处理 |
— |
metadata_report.py |
元数据报告编排入口(仅 setup + 顺序调用 12 个 sub-renderer) | 134 |
metadata_sections/ |
元数据报告的 12 个语义段子包(详见下方子表) | — |
report_generators.py |
多种报告格式生成(dashboard / brief / single / daily) | 920+ |
report_storage.py |
报告文件存储与索引管理 | — |
context_sender.py |
钉钉/飞书 Bot 会话回复 | — |
channel_types.py |
NotificationChannel enum + ChannelDetector + NotificationBuilder |
80 |
__init__.py |
重新导出 ReportOrchestrator + NotificationService 别名 + pulsefan.formatting 工具(chunk_content_by_max_bytes, format_feishu_markdown) | ~30 |
旧的
dispatcher.py已在 pulsefan 拆分(Commit 3)中重命名为orchestrator.py并彻底重构,不留 shim;旧的senders/子包整体搬入pulsefan/senders/。
历史 1394 行单函数 generate_metadata_report 已按 12 个语义段拆分。每段是独立 render_<name>(lines, enhanced_context, _fmt, *, code, stock_name, news_context, report_date) -> None 函数,自行从 enhanced_context 解构需要的字段。metadata_report.py 仅负责 setup 与编排。
| 文件 | 段名 | 内容 | 行数 |
|---|---|---|---|
scoring_overview.py |
量化评分概览 | ScoringService.format_overview_table() 渲染 |
36 |
insider_sector.py |
内部交易+板块 ETF | 内部人交易 + 板块 ETF 比较 | 72 |
dashboard.py |
决策仪表盘 | 关键指标速览 | 98 |
market_overview.py |
行情概览 | realtime + OHLCV + MA 状态 + 筹码 | 120 |
technical.py |
技术分析 | 趋势 + Bollinger + ATR/OBV/VWAP + K 线 | 372 |
fundamentals.py |
基本面 | DB 基本面 + finviz EPS/营收/估值/盈利/派息 | 321 |
industry_capital.py |
行业与资金 | 板块 / 行业 / Peer / 做空 / 持股 / 内部人 | 197 |
macro_env.py |
宏观环境 | 宏观 + 美债 + 情绪 + CNN F&G + PE 百分位 | 219 |
company.py |
公司概况 | 描述 + 公司信息 | 53 |
news.py |
新闻情报 | finviz 英文 + AkshareNews 中文 | 56 |
openbb_extra.py |
OpenBB 增量 | OpenBB 数据扩展 | 49 |
footer.py |
页脚 | 数据采集时间戳 | 28 |
2026-05 实现细节:
_history_compare_cache从无界Dict[Tuple, ...]升级为cachetools.LRUCache(maxsize=128),避免长生命周期实例(FastAPI lifespan singleton) 持续累积旧 batch 的对比结果。
from pulsefan import Notifier
from stocklens.notification import ReportOrchestrator
from stocklens.notification.config_adapter import build_notification_config
# 显式注入 Notifier(推荐 —— api/app.py lifespan 用法)
notifier = Notifier(build_notification_config(get_config()))
orchestrator = ReportOrchestrator(notifier=notifier)
# 兜底懒加载(CLI / Bot 入口未走 lifespan 时)
orchestrator = ReportOrchestrator() # 自动 build_notification_config(get_config())generate_daily_report(results) -> str— 多股汇总 markdowngenerate_metadata_report(code, name, enhanced_context, ...) -> str— 12 段元数据报告is_available() -> bool、get_available_channels()、get_channel_names() -> str
NotificationService = ReportOrchestrator在orchestrator.py末尾保留为别名;旧from stocklens.notification import NotificationService仍可用。- 旧
notifier.send_to_wechat(...)/send_to_feishu(...)等 10 个直通方法通过动态属性委托保留(pipeline 的push_handler.py依赖)。新代码应使用orchestrator.notifier.send_text(...)。
单一映射函数 build_notification_config(config) -> NotificationConfig,把 stocklens 的 130+ 字段 Config 投射成 pulsefan 的 10 个 ChannelConfig。所有 getattr(config, ...) 均带默认值,对 mock config 友好。
业务枚举层 —— NotificationChannel enum + ChannelDetector(业务名称翻译)+ NotificationBuilder(简单告警快捷构造)。保留在 stocklens 因为这些是业务概念(区分"WECHAT" vs "WechatSender" 实例)。
- 依赖:
pulsefan、stocklens.config.settings.Config、stocklens.contracts.BotMessage、stocklens.analyzer.llm_analyzer.AnalysisResult、markdown2、jinja2(可选)、imgkit(可选) - 被依赖:
stocklens/pipeline/push_handler.py— 调用 11 个send_to_<channel>shortcutstocklens/pipeline/orchestrator.py— 构造ReportOrchestrator(source_message=...)stocklens/pipeline/stages/metadata_stage.py— 调用generate_metadata_reportapi/v1/endpoints/agent.py—chat/sendendpointapi/app.pylifespan — 启动时构造pulsefan.Notifier存入app.state.notifier
见 .env.example 中"通知推送"部分。pulsefan 的 NotificationConfig 由 config_adapter.build_notification_config(config) 从这些字段生成;新增渠道字段时,记得同步 config_adapter.py 中的映射表。
关键通用配置:
| 变量 | 默认值 | 说明 |
|---|---|---|
SINGLE_STOCK_NOTIFY |
false | 单股立即推送 |
REPORT_TYPE |
simple | 报告格式 |
REPORT_SUMMARY_ONLY |
false | 仅推送摘要 |
MARKDOWN_TO_IMAGE_CHANNELS |
空 | 渲染为图片的渠道 |
MARKDOWN_TO_IMAGE_MAX_CHARS |
15000 | 触发图片渲染的字符上限 |
-
不要直接
from pulsefan.senders.* import—— CLAUDE.md 已禁止;所有业务推送一律走ReportOrchestrator。 -
pulsefan 不读
.env;所有配置通过config_adapter.build_notification_config(config)注入。新增渠道字段时同步更新 adapter。 -
微信企业号 Webhook 有频率限制(每分钟 20 条);pulsefan 的 dispatcher 不限速,业务层需要自己管。
-
Email SMTP 服务器自动检测:在
pulsefan/senders/email.py的SMTP_CONFIGS表里写死了 10 大邮箱(gmail / qq / 163 等);显式EmailConfig.smtp_server优先生效。 -
飞书 Webhook 和 App Bot 是两种不同的推送方式,pulsefan 的
FeishuSender走 webhook;App Bot 上下文回复在stocklens/notification/context_sender.py。 -
REPORT_DIR可覆盖默认报告输出目录(report_storage.py处理)。 -
2026-05 修复
report_storage.py静态拼写错误:_resolve_stock_dir中_ReportMixin._stock_short_name(code)是真 bug —— 类实际叫_ReportStorageMixin,运行时如果走到该分支会NameError。改正为_ReportStorageMixin._stock_short_name(code)。 -
2026-05 删除死变量:
metadata_sections/fundamentals.py中name = stock_name(赋值后从未使用);metadata_sections/technical.py中realtime = enhanced_context.get("realtime")(同样未使用)。同时把technical.py的"""渲染 技术分析 段..."""docstring 翻译成英文。 -
2026-05 M4 — channel dispatch table(report_builder.send):
_ReportMixin.send()之前 130 行的if channel == X: ... elif channel == Y: ...长链已重写为声明式 dispatch table。每个渠道有专属_dispatch_<channel>(*, content, image_bytes, ...)方法(10 个);_channel_dispatch_table是 lazy-built 的{NotificationChannel: handler}映射;send()主体只剩 dispatch 循环 + 失败计数。新增渠道只需 (i) 写一个_dispatch_xxx(ii) 在_channel_dispatch_table加一行。 另外抽出_convert_markdown_to_image_once(content)集中处理 Markdown→图片转换 + 安装提示日志(先前在send()里内联 ~50 行)。send()行数从 130 降到 ~50。stocklens/pipeline/push_handler.py::_send_notifications同步收敛:原 180 行 if/elif ladder 拆为_convert_to_image_with_hint/_push_wechat/_push_email_groups/_push_other_channel(后者携带 5 个 text-only 渠道的 dispatch table)。Email 单独保留分组路由(Issue #268),WeChat 单独保留 brief 转换(平台 4096 char 限制)。