# 军事科技每日资讯推送系统 基于大模型的军事/科技新闻采集、评分、整合与推送系统。自动抓取 RSS 订阅源,经 LLM 评分筛选与实体聚类后,生成日报并推送到飞书群。 ## 系统架构 ``` ┌───────────────────────────────────────────────────────────────┐ │ Fetch 循环(每 30 分钟) │ │ RSS 抓取 → 中英翻译 → LLM 评分 → 硬约束过滤 → 动态阈值 │ │ → 实体提取 + 角度分类 → 实体归一化 → 实体聚类 │ │ → 广告过滤 → 缓存 → 簇热度计算 → 即时推送(≥90 分) │ └──────────────────┬────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────────┐ │ Push 循环(每日 08:00) │ │ 推送 1: 🔥 热点速览(簇视图) │ │ 今日速览 + 热点洞察(含[持续跟踪])+ 跨日追踪 + 趋势分析 │ │ 推送 2: 📰 军事科技每日精选(单篇视图) │ │ 网摘 Top3 + 分类概览(5 类 × 5 条) │ │ 推送 3: 🖼️ 网摘图片(PNG 合并长图,图床上传后推送) │ └───────────────────────────────────────────────────────────────┘ ``` | 核心功能 | 说明 | |---------|------| | 订阅源管理 | OPML 文件管理 137 个 RSS 源(英文军事 + 中文微信公众号) | | 双语翻译 | 中英自动分离(汉字占比 > 20%),中文零 API 调用,外文并发翻译 | | 三层评分 | 硬约束(非目标 ≤79 / KOL ≤89)+ 时间衰减 + 动态阈值 | | 实体提取 | 5 类实体(人物/机构/装备/事件/地域)+ 6 类角度分类,双层归一化(规则 200+ 别名 + LLM 消歧) | | 实体聚类 | 加权实体重叠 + 并查集,细粒度聚类保留多角度报道,自动命名簇 | | 簇热度 | 四维评分(最高分 + 传播 + 角度 + 优先)+ LLM 精评融合,热度簇提升为热点 | | 簇记忆 | SQLite 持久化已推送簇 ID,24h 内同簇不重复推送 | | 广告过滤 | 四层防御纵深(tag 黑名单 → 动态阈值 → 空角度 → 空实体) | | 增量缓存 | SQLite 三级缓存(文章/网摘/分类摘要),二次运行几乎零 token 消耗 | | 飞书推送 | 即时热点推送(≥90 分)+ 每日早报三波(热点速览 + 每日精选 + 网摘图片) | | 双视角洞察 | LLM 一次调用输出精炼摘要 + 结构化洞察(事件脉络/多角度分析/影响与展望) | | 分类概览 | 5 类军事新闻分类(装备技术/地区安全/军事改革/产业观察/其他热点),每类 top 5 | | 网摘图片 | PIL 渲染网摘为 PNG 合并长图,自适应高度 + 动态文本换行,图床 API 上传后推送飞书 | | 反馈闭环 | 用户评分修正记录 + 规则自动生成,修正后的评分自动应用到后续批次 | | 运行监控 | 12 阶段耗时 + API 调用统计 + 源抓取成功率 + 缓存命中率 | ## 快速开始 ### 环境要求 - Python 3.10+ - 能访问 OpenAI 兼容 API(DeepSeek / 火山引擎 / OpenAI 等) ### 安装 ```bash # 1. 进入项目目录 cd my-daily # 2. 安装依赖 pip install -r requirements.txt # 3. 配置环境变量 cp .env.example .env # 编辑 .env,填入 API Key 和 Webhook 地址 # 4. 配置系统参数(可选,已有默认值) cp config.json.example config.json # config.json 已存在时跳过,按需修改调度/评分/源过滤参数 ``` ### 环境变量(.env) ```env # OpenAI 兼容接口(必填) OPENAI_API_KEY=your_api_key OPENAI_API_BASE=https://api.deepseek.com OPENAI_MODEL=deepseek-v4-flash # LLM 消歧 + 网摘专用模型(可选,不填则复用 OPENAI_MODEL) WEBZINE_MODEL=deepseek-v4-pro # 飞书推送(可选,不填则跳过推送) FEISHU_WEBHOOK=your_webhook_url # 图床上传配置(可选,网摘图片通过图床 API 上传) IMAGE_HOSTING_PASSWORD=your_image_hosting_password_here # ── 定时计划配置(可选,覆盖 config.json)── # FETCH_INTERVAL_MINUTES=30 # RSS 拉取间隔(分钟) # FETCH_LOOKBACK_MINUTES=60 # 回溯时间窗口(分钟) # PUSH_CRON="0 8 * * *" # 推送定时计划(cron 表达式,多个用逗号分隔) # TIMEZONE_HOURS=8 # 时区 # ── 评价管线参数覆盖(可选,最高优先级)── # HEAT_THRESHOLD=30 # 热度阈值固定值 # HOT_THRESHOLD=90 # 即时推送分数线 # CLUSTER_MERGE_THRESHOLD=4 # 聚类合并阈值 # PRECISE_BLEND_RATIO=0.4 # LLM 精评融合权重 # MEMORY_WINDOW_HOURS=24 # 簇记忆去重时间窗口 # CROSS_DAY_MIN_OVERLAP=0.05 # 跨日关联实体重叠阈值 ``` > **优先级规则**:`.env` 环境变量 > `config.json` > 源码默认值。详见 [CONFIG.md](CONFIG.md)。 ### 配置项速查 所有参数及调优指南见 **[CONFIG.md](CONFIG.md)**。 | 配置节 | 控制环节 | 关键参数 | |--------|----------|----------| | **`.env`** | 接口/推送/定时/图床/评价覆盖 | `OPENAI_API_KEY`, `FEISHU_WEBHOOK`, `IMAGE_HOSTING_PASSWORD`, `FETCH_INTERVAL_MINUTES`, `PUSH_CRON` | | `schedule` | 定时计划 | `fetch_interval_minutes`(30), `push_cron`(0 8 * * *) | | `filter` | 过滤推送 | `hot_threshold`(90), `cluster_promotion_offset`(15) | | `scoring` | 单篇评分 | `non_target_max_score`(79), `half_life_hours`(12), `min_age_hours`(6) | | `cluster` | 聚类成簇 | `merge_threshold`(4), `entity_weights`, `generic_entities` | | `heat` | 簇热度 | `score_weight`(0.4), `propagation_cap`(30), `precise_blend_ratio`(0.4) | | `memory` | 去重记忆 | `window_hours`(24), `cleanup_days`(7) | ## 命令用法 ### 一键启动(推荐) ```bash python start.py ``` 同时启动数据管线后端(Fetch + Push 循环)和 Web 归档站点(http://0.0.0.0:8080)。所有终端输出实时写入 `logs/YYYYMMDD-HHmmSS.log`,按 Ctrl+C 安全停止所有服务。 ### main.py 命令 ```bash python main.py [options] ``` | 命令 | 说明 | 示例 | |------|------|------| | `check` | 校验 LLM 接口可达性 | `python main.py check` | | `fetch` | 单次抓取 → 翻译 → 评分 → 缓存 | `python main.py fetch --lookback 120` | | `test-fetch` | 测试抓取(限定源数量) | `python main.py test-fetch --max 5 --lookback 300` | | `push` | 单次推送(含早报/默认两种模式) | `python main.py push` | | `rss` | 仅生成 RSS Digest(不推送,打印到终端) | `python main.py rss` | | `loop` | 长跑模式(Fetch + Push 双循环并行) | `python main.py loop` | | `feedback` | 提交评分修正反馈 | `python main.py feedback --link URL --score 85 --reason "理由"` | ### test-fetch / fetch 参数 | 参数 | 默认值 | 说明 | |------|:---:|------| | `--max N` | 5(仅 test-fetch) | 仅拉取前 N 个源 | | `--lookback N` | 来自 config | 时间回溯窗口(分钟) | ### feedback 参数 | 参数 | 说明 | |------|------| | `--link URL` | 需要修正评分的文章链接 | | `--score N` | 修正后的分数(0-100) | | `--reason "..."` | 修正理由 | | `--list` | 查看所有已记录的修正 | | `--rules` | 查看已生成的修正规则 | | `--stats` | 查看修正统计数据 | ## 部署 ### Linux(systemd) ```bash sudo bash scripts/setup_systemd.sh # 一键安装 bash scripts/status.sh # 查看运行状态 sudo journalctl -u my-daily-fetch -f # 实时日志 sudo bash scripts/uninstall_systemd.sh # 卸载 ``` ### Windows(计划任务) ```batch scripts\setup_service.bat install # 安装为计划任务 scripts\setup_service.bat start # 手动运行一次 scripts\setup_service.bat status # 查看任务状态 scripts\setup_service.bat loop # 前台调试 scripts\setup_service.bat uninstall # 卸载计划任务 ``` ### Docker(推荐,适用于绿联云 NAS / 任何 Linux 主机) ```bash # 1. 准备目录和配置 mkdir -p docker/data docker/cache cp .env docker/.env # 2. 构建并启动 docker compose up -d --build # ── 运维 ────────────────────────────────────── docker compose logs -f # 查看实时日志 docker compose restart # 重启 docker compose down # 停止并删除容器 docker compose up -d --build # 更新代码后重建并启动 ``` #### 目录结构(Docker 部署) ``` my-daily/ ├── docker/ # Docker 持久化目录 │ ├── .env # 配置文件(API Key, Webhook 等) │ ├── data/ # 运行时数据(fetch/push/image 文件) │ └── cache/ # SQLite 缓存(避免重复消费 API) ├── Dockerfile # 镜像构建文件 ├── docker-compose.yml # Docker Compose 配置 └── .dockerignore # 构建忽略文件 ``` #### 在绿联云 NAS 上的操作 ```bash # 方案 A:SSH 直接操作(NAS 开启 SSH 后) # 将 my-daily 目录上传到 NAS,进入目录执行: docker compose up -d --build # 方案 B:绿联云 Docker 管理器(图形界面) # 1. 在本机构建镜像并导出: docker save -o military-digest.tar military-digest:latest # 2. 将 tar 文件上传到 NAS # 3. 在绿联云 Docker > 镜像管理 > 导入镜像 # 4. 创建容器时挂载卷: # - /path/to/docker/.env → /app/.env(只读) # - /path/to/docker/data → /app/data # - /path/to/docker/cache → /app/cache # 5. 设置重启策略:always # 6. 启动容器 ``` ## 运维工具 | 脚本 | 平台 | 功能 | |------|:---:|------| | [scripts/check_sources.py](scripts/check_sources.py) | 通用 | 源连通性检测:HTTP 可达性 + RSS 解析 + 条目时效分布 | | [scripts/test_entity_extract.py](scripts/test_entity_extract.py) | 通用 | 实体提取 + 角度分类独立测试 | | [scripts/test_normalizer.py](scripts/test_normalizer.py) | 通用 | 实体归一化效果验证 | | [scripts/test_llm_disambiguate.py](scripts/test_llm_disambiguate.py) | 通用 | LLM 消歧层独立测试 | | [scripts/status.sh](scripts/status.sh) | Linux | 项目状态查看 | | [scripts/install.sh](scripts/install.sh) | Linux | 依赖安装 + systemd 服务文件生成 | | [scripts/uninstall.sh](scripts/uninstall.sh) | Linux | systemd 服务卸载 | ### 源连通性检测 ```bash python scripts/check_sources.py # 全部源 python scripts/check_sources.py --max 50 # 前 50 个源 python scripts/check_sources.py --failed-only # 仅显示失败源 python scripts/check_sources.py --show-urls # 显示 URL python scripts/check_sources.py --concurrency 20 # 自定义并发数 ``` ## 数据与输出 | 目录/文件 | 说明 | 保留策略 | |-----------|------|:---:| | `data/fetch-YYYY-MM-DD.json` | 当日 Fetch 结果(含评分、摘要) | 7 天 | | `data/hotalert-YYYYMMDD-HHmmSS.md` | 军事科技快讯(即时推送) | 7 天 | | `data/cluster-YYYYMMDD-HHmmSS.md` | 热点速览(簇视图推送) | 7 天 | | `data/digest-YYYYMMDD-HHmmSS.md` | 军事科技每日精选(单篇视图推送) | 7 天 | | `data/images/YYYYMMDD/` | 网摘 PNG 图片 | 7 天 | | `data/article_cache.db` | SQLite 三级缓存(文章/网摘/分类摘要) | 7 天自动清理 | | `logs/YYYYMMDD-HHmmSS.log` | 运行日志(终端输出完整记录) | 手动清理 | ## 评价管线详解 从 RSS 抓取到即时推送,文章经历 **12 个步骤**,分属两个层面:单篇评分层和簇级评价层。 ### 第一层:单篇评分 #### 步骤 1 — LLM 批量评分 调用 `score_batch()` 对每篇文章打分(0-100),同时产出 `tags` 和 `summary`。 | 配置项 | 位置 | 默认值 | 说明 | |--------|------|:---:|------| | `max_prompt_chars` | `config.json → llm` | 10000 | 单批次 Prompt 上限,超限自动拆分 | | `max_concurrent_batches` | `config.json → llm` | 3 | LLM 并发批次数 | | Prompt | `prompts/score_batch.md` | — | 含核心约束:非军事≤79、KOL≤89、90+需官方首发 | #### 步骤 2 — 硬约束过滤 `apply_hard_constraints()` 对 LLM 评分施加上限: | 约束 | 阈值 | 作用 | |------|:---:|------| | 非目标域名(YouTube/Twitter/B站/微博等) | **≤79** | 社交媒体来源不可信 | | KOL 域名(同上) | **≤89** | 个人账号非官方首发 | > 配置:`config.json → scoring.non_target_max_score` / `kol_max_score` #### 步骤 3 — 时间衰减 `apply_time_decay()` 指数衰减旧文章分数: ``` 有效年龄 = max(0, 当前时间 - 发布时间 - min_age_hours) 衰减因子 = 0.5 ^ (有效年龄 / half_life_hours) 最终分数 = 原分数 × 衰减因子 ``` | 参数 | 默认值 | 说明 | |------|:---:|------| | `half_life_hours` | 12 | 半衰期 | | `min_age_hours` | 6 | 6 小时内不衰减 | #### 步骤 4 — 动态阈值 `calculate_dynamic_threshold()` 筛选合格文章: ``` 阈值 = 均值 + 1.5 × 标准差 clamp[40, 90] ``` 只有 `score ≥ 阈值` 的文章进入下一步实体提取。 --- ### 第二层:簇级评价 #### 步骤 5 — 实体提取 + 角度分类 `batch_extract_all()` 对合格文章提取实体和角度,两个 LLM 调用并发执行。 | 类别 | Prompt | 输出 | |------|--------|------| | 实体(5 类) | `prompts/extract_entities.md` | `entities: {person, org, equipment, event, location}` | | 角度(6 类) | `prompts/classify_angle.md` | `reporting_angle: 政策发布/技术突破/战略分析/舆论反应/冲突事件/人物动态` | #### 步骤 6 — 实体归一化 `normalize_entries_async()` 双层归一化: | 层 | 方式 | 成本 | 示例 | |----|------|:---:|------| | 规则映射 | `entity_aliases.json`(200+ 别名) | 零 | `PL-21 → 霹雳-21`、`DARPA → 美国国防高级研究计划局` | | LLM 消歧 | `WEBZINE_MODEL` 批量判断未知别名 | API | `Strait of Hormuz → 霍尔木兹海峡` | #### 步骤 7 — 聚类成簇 `cluster_articles()` 加权实体重叠 + 并查集: | 实体类别 | 权重 | 合并条件 | |----------|:---:|------| | equipment / event | **3**(核心) | 共享即直接合并(泛化实体黑名单内的除外) | | org / person | **2** | 需累计权重 ≥ 4 | | location | **1** | 需累计权重 ≥ 4 | > 泛化黑名单(不触发直接合并):`无人机`、`导弹`、`雷达`、`坦克`、`战机`、`军舰` 等 23 个类别词 #### 步骤 8 — 启发式热度计算 `calculate_cluster_heat()` 四维评分(0-100): ``` 热度 = min(100, 最高分 × 0.4 × 时间衰减 + min(报道数 × log(来源多样性) × 3, 30) + 角度数/6 × 20 + min(90+文章数 × 5, 10) ) ``` #### 步骤 9 — 热度阈值 `calculate_hotspot_threshold()` 动态阈值: ``` 热度阈值 = 均值 + multiplier × σ clamp[clamp_min, clamp_max] ``` | 配置 | 位置 | 默认值 | 效果 | |------|------|:---:|------| | `threshold_clamp_min` | `config.json → heat` | 35 | 阈值下限 | | `threshold_clamp_max` | `config.json → heat` | 80 | 阈值上限 | | `threshold_multiplier` | `config.json → heat` | 1.5 | σ 乘数 | | `HEAT_THRESHOLD` | `.env` | — | **固定覆盖**(最高优先级) | #### 步骤 10 — LLM 精评融合 `precise_score_clusters()` 对 Top 30 簇调用 LLM 重新精评(军事价值40% + 时效25% + 信息密度20% + 传播15%),与启发式 6:4 融合: ``` 融合热度 = 启发式 × 0.6 + 精评分 × 0.4 ``` > Prompt:`prompts/precise_heat.md`,失败时回退纯启发式热度。 #### 步骤 11 — 热点识别 热点文章 = **三类并集**: | 来源 | 条件 | |------|------| | 直接热点 | 单篇 score ≥ `hot_threshold`(90) | | 簇提升 | 所在簇热度 ≥ 阈值,且单篇 ≥ `hot_threshold - 15`(75) | #### 步骤 12 — 簇记忆去重 `cluster_memory.is_recently_pushed()` 检查簇是否在 24h 内已推送,已推送则跳过。 > 存储:SQLite 表 `cluster_memory`,7 天自动清理。 --- ### 配置速查 | 参数 | 位置 | 默认值 | 作用环节 | |------|------|:---:|------| | `hot_threshold` | `config.json → filter` | 90 | 步骤 11:直接热点线 | | `non_target_max_score` | `config.json → scoring` | 79 | 步骤 2:非目标封顶 | | `kol_max_score` | `config.json → scoring` | 89 | 步骤 2:KOL 封顶 | | `half_life_hours` | `config.json → scoring` | 12 | 步骤 3:衰减半衰期 | | `dynamic_threshold_multiplier` | `config.json → scoring` | 1.5 | 步骤 4:σ 乘数 | | `threshold_clamp_min` | `config.json → heat` | 35 | 步骤 9:热度下限 | | `threshold_multiplier` | `config.json → heat` | 1.5 | 步骤 9:热度 σ 乘数 | | `HEAT_THRESHOLD` | `.env` | — | 步骤 9:固定覆盖 | ## 项目文件说明 ```text my-daily/ ├── main.py # 入口 + 双循环架构 ├── start.py # 一键启动(含日志记录) ├── config.json # 用户配置(不入库) ├── .env # API 密钥(不入库) ├── config.json.example # 配置模板 ├── .env.example # 环境变量模板 ├── requirements.txt # Python 依赖 ├── CONFIG.md # 配置参数完整说明 ├── Dockerfile # Docker 镜像构建 ├── docker-compose.yml # Docker Compose 配置 ├── .dockerignore # Docker 构建忽略 ├── logs/ # 运行日志(YYYYMMDD-HHmmSS.log) ├── prompts/ # LLM 提示词(10 个) │ ├── score_batch.md # 批量评分 │ ├── digest.md # RSS Digest 生成 │ ├── immediate_push.md # 即时推送快讯 │ ├── webzine.md # 参考消息风格网摘 │ ├── category_overview.md # 分类概览 │ ├── extract_entities.md # 实体提取(5 类) │ ├── classify_angle.md # 角度分类(6 类) │ ├── precise_heat.md # 热点精评(四维评分) │ ├── cluster_insight.md # 双视角洞察 │ └── cluster_summary.md # 轻量簇摘要 ├── resources/ │ └── rss_feeds.opml # RSS 订阅源(137 个源) ├── templates/ # 推送模板 │ ├── cluster_view.md # 簇视图模板 │ ├── article_view.md # 单篇视图模板 │ └── immediate_push.md # 即时推送模板 ├── scripts/ # 运维 + 测试脚本 │ ├── check_sources.py # 源连通性检测 │ ├── setup_systemd.sh # Linux systemd 安装 │ ├── setup_service.bat # Windows 计划任务管理 │ ├── install.sh / uninstall.sh │ └── status.sh ├── systemd/ # systemd 模板 ├── src/ # 源代码 │ ├── config.py # 配置加载 + OPML 解析 + 环境变量覆盖 │ ├── fetcher.py # RSS 异步抓取 │ ├── translator.py # 中英分离翻译管线 │ ├── llm.py # LLM 调用(评分/摘要/洞察/精评) │ ├── scoring.py # 评分后处理(约束/衰减/阈值) │ ├── cache.py # SQLite 三级缓存 │ ├── storage.py # 文件存储 + Sentinel 分段 │ ├── monitor.py # 运行监控统计 │ ├── logger.py # 日志系统 │ ├── processor.py # HTML → Markdown │ ├── utils.py # 通用工具函数 │ ├── markdown_utils.py # Markdown 解析工具 │ ├── renderer.py # 模板渲染 │ ├── generators/ # 图片生成模块 │ │ ├── image_renderer.py # 网摘 PNG 图片渲染 │ │ └── get_chinese_font.py # 中文字体管理 │ ├── processors/ # 处理管线 │ │ ├── entity_extractor.py # 实体提取 + 角度分类 │ │ ├── entity_normalizer.py # 实体归一化(双层) │ │ ├── entity_aliases.json # 别名映射表 │ │ ├── cluster_engine.py # 实体聚类引擎 │ │ ├── cluster_memory.py # 簇记忆系统 │ │ ├── heat_calculator.py # 簇热度计算 │ │ ├── rank_engine.py # 排序引擎 │ │ ├── trend_analyzer.py # 趋势分析 │ │ ├── feedback.py # 反馈闭环 │ │ └── cross_day.py # 跨日关联 │ ├── integrators/ │ │ └── summary_integrator.py # 摘要整合编排 │ ├── utils/ │ │ └── prompt_checker.py # Prompt 防退化检查器 │ ├── push/ # 推送模块 │ │ ├── feishu.py # 飞书 Webhook 推送 │ │ └── image_hosting.py # 图床上传模块 │ └── sections/ # 内容板块 │ ├── rss/section.py # RSS Digest 板块 │ └── webzine/section.py # 网摘生成板块 ├── CHANGELOG.md # 更新日志 ├── PROGRESS.md # 开发进展追踪 └── README.md # 本文件 ``` ## 常见问题 **Q: 为什么英文源抓不到文章?** A: 英文军事源(如 The War Zone、Defense One)更新频率低,可能 2-3 天无新文章。中文微信公众号源更新频繁,扩大 `--lookback` 可覆盖更多时间窗口。 **Q: 如何添加/屏蔽订阅源?** A: OPML 文件管理为主。临时追加用 `config.json` 的 `sources.add`,屏蔽用 `sources.block` 或 `sources.block_domains`。支持 `*.domain.com` 通配。 **Q: 如何修改定时计划?** A: 在 `.env` 中设置 `FETCH_INTERVAL_MINUTES`(拉取间隔)和 `PUSH_CRON`(推送定时,cron 表达式),重启服务即可生效。 **Q: 内存/CPU 占用如何?** A: 异步抓取 + 并发控制(默认 10 并发),翻译/评分均分批处理。137 源全量抓取峰值内存约 200MB,日常运行稳定在 100MB 以内。