Files
my-daily/README.md
T
2026-07-12 20:11:56 +08:00

517 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 军事科技每日资讯推送系统
基于大模型的军事/科技新闻采集、评分、整合与推送系统。自动抓取 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 兼容 APIDeepSeek / 火山引擎 / 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 <command> [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` | 查看修正统计数据 |
## 部署
### Linuxsystemd
```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
# 方案 ASSH 直接操作(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 | 步骤 2KOL 封顶 |
| `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 以内。