Files

14 KiB
Raw Permalink Blame History

tech-spec.md - 技术架构总览

本文档面向未来的 AI Agent 与核心开发者,目的是在阅读源码之前快速建立对项目的整体认识:项目定位、运行模式、核心数据流、模块边界、部署形态、关键约束。

本文写什么:架构层面的"是什么、为什么、边界在哪里";跨模块的数据流和契约;部署/运行环境约束;不读源码就无法获知的设计决策。

本文不写什么:源码摘录、函数签名、字段清单、命令行帮助、变更日志、UI/文案。这些信息以源码、README.mddocs/plan.mdconfig.json 为准。

维护原则:当架构边界、数据流、运行环境、部署形态或核心设计决策发生变化时同步更新本文;普通实现调整、字段增删、文案修改不在维护范围内。

update: 2026-05-17

项目定位

AI 驱动的 RSS 新闻聚合与推送系统:周期性抓取 400+ AI 领域信息源,调用 LLM 评分筛选,按调度规则将高分内容汇总推送到 Discord / 飞书;高分热点条目在 fetch 阶段即时推送。

面向单机部署、单租户使用,所有状态以本地文件(JSON / Markdown)持久化,不依赖外部数据库或队列。

运行模式

模式 触发方 适用场景
生产(推荐) systemd timer 分别触发 fetchpush 单次任务 服务器长期运行,依赖 systemd 提供调度、重启、开机自启
开发 loop 子命令在单进程内并发跑 fetch/push 双循环 本地调试,无需 systemd

CLI 子命令分工(详见 python -m src.main --help):

  • check唯一的 LLM 健康检查入口,仅在部署期由 install.sh 调用
  • fetch / push:单次执行后退出,由 systemd timer 触发;运行期不再做 LLM 健康检查,异常由统一的告警通道兜底
  • loop:开发模式,启动时做一次健康检查,然后并发跑 fetch/push 循环
  • github / hackernews:单板块手动调试入口;只跑对应板块(含 LLM 总结),打印 markdown 到终端,推送、写入 push 文件。仅供 prompt 调优期使用

关键约束:fetch / push 失败时进程退出码非 0,systemd 据此判定 service 失败,下个 timer 周期自动重试。

核心架构

调度模型(生产)

┌─────────────────────────────────────────────────────────────┐
│                     config.json                             │
│  schedule.fetch_interval_minutes  → dnews-fetch.timer       │
│  schedule.push_cron               → dnews-push.timer        │
│  log.retention_days               → journald@dnews retention│
└────────────────────────┬────────────────────────────────────┘
                         │ scripts/install.sh 渲染并安装
            ┌────────────┴────────────┐
            ▼                         ▼
  ┌───────────────────┐     ┌───────────────────┐
  │ dnews-fetch.timer │     │ dnews-push.timer  │
  └─────────┬─────────┘     └─────────┬─────────┘
            ▼                         ▼
  ┌───────────────────┐     ┌───────────────────┐
  │ fetch.service     │     │ push.service      │
  │  抓取+评分+热点推送│     │ 收集+汇总+推送     │
  └─────────┬─────────┘     └─────────┬─────────┘
            └────────────┬────────────┘
                         ▼
                ┌─────────────────┐
                │   news-data/    │
                │   fetch-*.json  │
                │   push-*.md     │
                │   notify-*.md   │
                └─────────────────┘

开发模式(loop)以 asyncio.gather(fetch_loop, push_loop) 并发运行两条循环,push_loop 通过 croniter 计算下次触发时间,行为等价于生产模式但共享单进程。

数据流

flowchart LR
    subgraph Sources ["📡 RSS Sources"]
        RS1[Twitter/X]
        RS2[博客 / 媒体]
        RS3[微信公众号]
    end

    subgraph Fetch ["🔄 fetch job (周期触发)"]
        F1[抓取 RSS] --> F2[HTML→Markdown] --> F3[LLM 批量评分]
        F3 --> HOT{score ≥ hot_threshold?}
        HOT -->|是| IP[即时推送 + notify-*.md]
    end

    subgraph Push ["📅 push job (cron 触发)"]
        P1[读取近 N 天 fetch + 历史 push 上下文] --> P2[LLM 汇总去重] --> P3[生成 push-*.md] --> P4[推送 Discord / 飞书]
    end

    subgraph Storage ["💾 news-data/"]
        DB[(JSON / MD)]
    end

    Sources --> F1
    F3 --> DB
    IP --> DB
    DB --> P1
    P3 --> DB

关键数据契约

  • fetch-YYYY-MM-DD.json:当日抓取与评分结果(含 score / summary / tags / content
  • push-YYYY-MM-DD.md:汇总推送内容(YAML frontmatter + Markdown 正文),同时作为下一次 push 的去重上下文
  • notify-YYYY-MM-DD.md:即时推送记录,作为 LLM 即时推送时的去重上下文

具体字段以源码 src/storage.py 与样例文件为准,README "数据示例" 章节给出了一份示例。

关键模块边界

src/                       运行时代码
├── main.py                CLI 入口;定义 fetch_job / push_job / loop 的编排顺序
├── config.py              加载 config.json,合并 OPML + add/block,做配置校验
├── fetcher.py             RSS 抓取;并发控制、UA 伪装、域名通配符屏蔽;nitter/xcancel 走独立的 requests+Inoreader UA 低并发池
├── processor.py           HTML → Markdown 转换
├── llm.py                 LLM 客户端;批量评分、即时推送生成、汇总生成、错误聚合
├── storage.py             news-data 文件读写;按日期分片;过期清理
└── push/                  推送平台抽象
    ├── base.py            PushPlatform 基类(validate_config / send)
    ├── discord.py
    └── feishu.py

scripts/                   部署脚本(仅生产 systemd 部署使用)
├── install.sh             一键安装:uv sync → LLM check → 渲染单元 → 装入系统 → 启用
├── uninstall.sh           卸载 systemd 单元、daily-news 包装脚本和日志 drop-in;不删数据
├── status.sh              查看 timer/service 状态(daily-news status 包装它)
├── _gen_units.py          从 config.json 渲染 systemd 单元和 daily-news 包装脚本
└── daily-news.tmpl        /usr/local/bin/daily-news 的脚本模板,封装 systemctl/journalctl

systemd/                   systemd 单元模板(由 _gen_units.py 渲染并装入 /etc/systemd/system/)
├── dnews-fetch.service.tmpl    fetch service 单元模板
├── dnews-fetch.timer.tmpl      fetch 定时器(OnUnitActiveSec 间隔触发)
├── dnews-push.service.tmpl     push service 单元模板
├── dnews-push.timer.tmpl       push 定时器(OnCalendar 日历触发)
└── journald-dnews.conf.tmpl    journald 命名空间 dnews 的日志保留 drop-in

config.json                主配置;运行参数 + 调度 + LLM + 推送渠道;唯一可热改的运行配置
prompts/                   LLM 提示词文本;score / immediate_push / digest 各自独立文件
resources/rss.opml         基础 RSS 订阅源(约 420 个),通过 sources.add/block 增量调整
.env                       敏感凭证(API Key / Webhook URL),通过环境变量注入,不入库

板块化扩展 (morning push)

早报推送在 RSS 之上扩展三个板块:GitHub 趋势 / Hacker News 热议 / 跨板块洞察。模块结构、数据流与失败降级详见 docs/extra-sections-design.md。架构层关键约束:

  • 仅在当天 schedule.push_cron 列表里最早那次触发时启用(单条 cron 时每次都启用),其余时段维持纯 RSS 行为
  • 各板块封装为 src/sections/<board>/section.py::run_xxx_section(config, now) -> (markdown, error)
  • push_jobasyncio.gather 并发跑 RSS / GH / HN,串行接 insights;最后用 <!-- SECTION:xxx BEGIN/END --> sentinel 包入 push 文件
  • 仅 RSS 失败会让 push_job 整体退出非 0;其余板块失败 → 板块整段省略 + 告警

新增持久化文件:news-data/trending-history.json(GH 已查阅 repo 索引,按 filter.keep_days 过期)

模块协作的关键约定:

  • fetch 与 push 之间通过文件系统解耦:双方不直接通信,push 只读 fetch 已写入的 JSON
  • LLM 调用的错误处理由调用方决定llm.py 不做 fallback,失败时返回 (空内容, 错误列表);调用方决定是否告警或跳过推送,避免一个批次失败污染整次任务
  • 批量评分按 link 字段对齐:LLM 返回的条目数可能少于输入,按 link 匹配并丢弃无法对齐的结果,错误聚合后由调用方统一上报
  • 推送平台通过基类多态:新增平台只需实现 validate_config()send(),并在工厂函数注册,main.py 无需改动

数据边界与持久化

  • 所有持久化数据落在项目根目录的 news-data/:按日期分片的 fetch-*.json / push-*.md / notify-*.md
  • 过期文件由 fetch job 在每次执行后清理,保留窗口由 filter.keep_days 控制
  • 没有数据库、没有外部缓存、没有跨机器同步;状态完全可由文件系统重建
  • 敏感信息(API Key、Webhook URL)只通过环境变量注入,禁止写入 config.json 或代码

配置与约束

完整配置字段说明见 README.md 的"配置详解"章节,本文只列出对架构有影响的约束。

schedule

字段 约束
fetch_interval_minutes systemd 部署下用 OnUnitActiveSec 实现,从上次任务完成开始计时(非日历对齐)
fetch_lookback_minutes 必须大于 fetch_interval_minutes,用作 RSS 延迟的冗余窗口,依赖 link 去重防止重复入库
push_cron systemd 部署下只支持 minute/hour 字段,其他位必须为 *;不支持范围、列表、*/Nloop 模式下走 croniter,支持完整语法
timezone_hours 整数小时偏移;用于显示和 cron 计算

log

log.retention_days 仅对 systemd 部署生效,由 install.sh 渲染到 journald 命名空间 dnews 的 drop-in 配置;修改后必须重跑 install.sh

LLM

llm.max_prompt_chars 决定批次切分粒度,llm.max_concurrent_batches 决定批次并发数。这两个值同时影响吞吐和单次推送的成本上限。

环境变量

敏感凭证名通过 config.json 的 *.apiKeyName 字段指定环境变量名,由 os.environ 读取;约定通过 .env 提供,systemd 部署时 install.sh 会注入到 service 单元的 EnvironmentFile

systemd 部署形态

文件落点

文件 位置 来源
dnews-{fetch,push}.{service,timer} /etc/systemd/system/ systemd/*.tmpl_gen_units.py 渲染
journald@dnews retention drop-in /etc/systemd/journald@dnews.conf.d/ systemd/journald-dnews.conf.tmpl
daily-news 包装脚本 /usr/local/bin/ scripts/daily-news.tmpl
持久化数据 项目目录下的 news-data/ 运行时生成

cron → OnCalendar 转换

scripts/_gen_units.py 完成:

  • fetch_interval_minutesOnActiveSec + OnUnitActiveSec(间隔触发,跟随上次完成时间)
  • push_cronOnCalendar(日历触发,按指定时刻)
  • 不支持的 cron 语法(范围、列表、*/N 在 minute/hour、非 * 的 day/month/dow)在 install 阶段直接报错

日志

  • 所有 stdout/stderr 进入 journald 命名空间 dnews,与系统其他服务隔离
  • 查询:journalctl --namespace=dnews -u dnews-fetch -f
  • 卸载不会清理历史日志,需要时手动 journalctl --namespace=dnews --vacuum-time=1s

设计决策(重要的"为什么")

决策 原因
调度交给 systemd timer 而非 asyncio 循环 进程崩溃和服务器重启可自愈;调度配置即声明式单元,热更新只需重跑 install.sh
LLM 健康检查只在 check 子命令做 每次 timer 触发都校验会产生无意义的 LLM API 调用;运行期错误由 notify_llm_errors 兜底
LLM 失败时不生成 fallback 内容 避免低质量内容污染推送;由调用方决定告警或跳过
用 journald 命名空间而非文件日志 自动轮转、与系统日志隔离、无需写文件 IO 代码
数据全用本地文件而非数据库 单机单租户场景下足够;可读、可备份、可手动审阅
fetch_lookback_minutes 冗余窗口 RSS 源时间戳常有延迟,仅按时间过滤会漏读;冗余抓取后按 link 去重
Push 上下文带入近 N 天历史 push 文件 避免汇总推送在多个时段重复推同一条目

扩展指南

  • 新推送平台:在 src/push/ 新建文件,继承 PushPlatform,在工厂注册
  • 新评分维度:编辑 prompts/score.txt,调整评分标准
  • 新 RSS 源:编辑 config.jsonsources.add / sources.block / sources.block_domains,无需修改 OPML

测试

测试入口分两类,详细命令参考 README.mdtests/ 目录:

  • tests/pytest/:单元/集成测试,CI 友好,uv run pytest tests/pytest/ 一键跑
  • tests/*.py:交互式实操脚本(fetch_news.py / push_news.py / run_llm_test.py 等),针对真实 RSS 与 LLM 做端到端验证,用于调参和手测

相关文档

  • 用户文档与配置详解:README.md
  • 任务进度与产品决策:docs/plan.md