feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组

This commit is contained in:
poiuy
2026-07-12 20:01:02 +08:00
commit 54ca4b1b6a
267 changed files with 47047 additions and 0 deletions
+359
View File
@@ -0,0 +1,359 @@
# 军事科技每日摘报系统优化方案指南
## 🎯 优化目标
适配100+RSS订阅源、每日200+文章的处理需求,降低运行成本,提升稳定性,缩短整体处理时间到5分钟以内。
---
## 📋 优化优先级划分
### 🔴 P0级(高优先级,投入极小,收益极大,已完成)
| 优化项 | 预期效果 | 实施难度 | 状态 |
| --- | --- | --- | --- |
| 抓取模块线程池改造+失败重试 | 抓取速度提升60%,成功率提升15% | ⭐ | ✅ 已完成 |
| AI全链路并发化改造 | AI相关环节耗时降低70%,整体流程缩短一半 | ⭐ | ✅ 已完成 |
| 处理顺序前置优化 | 翻译token成本降低40%,翻译时间缩短40% | ⭐ | ✅ 已完成 |
### 🟠 P1级(中优先级,投入小,收益大,优先开展)
| 优化项 | 预期效果 | 实施难度 | 状态 |
| --- | --- | --- | --- |
| 增量处理+SQLite本地缓存 | ~~避免重复处理已抓取/分析的文章,再次运行时间缩短80%,支持断点续跑~~ 已实现文章缓存+网摘缓存+分类介绍缓存+自动过期清理,再次运行几乎零token消耗 | ⭐⭐ | ✅ 已完成 |
| 失败降级机制 | ~~某环节失败自动降级,不会中断整体流程,提升稳定性~~ 全链路10个模块均已覆盖:抓取重试/翻译fallback/AI默认评分/字体多级fallback/缓存连接降级 | ⭐ | ✅ 已完成 |
| 基础监控统计 | ~~各环节耗时统计、API消耗统计、成功率统计,便于排查问题和成本管控~~ 已实现:阶段耗时/API调用量+token/源成功率+缓存命中率一手掌控,运行结束自动打印 | ⭐⭐ | ✅ 已完成 |
### 🟡 P2级(中低优先级,投入中等,长期收益明显)
| 优化项 | 预期效果 | 实施难度 | 状态 |
| --- | --- | --- | --- |
| main()函数拆分重构 | ~~主函数从200+行缩减至~50行,每个子函数15-30行职责单一;可单独测试去重/分类/网摘等步骤~~ 已实现:7个子函数提取完毕,main()从310→115行(-63%),每步可独立测试 | ⭐⭐ | ✅ 已完成 |
| 推送模块完善 | ~~实现完整的微信/飞书webhook推送,支持自定义推送内容~~ 已实现:modules/pusher.py 封装企业微信文本/图片/Markdown + 飞书文本推送,主流程末自动触发,失败优雅降级 | ⭐⭐ | ✅ 已完成 |
| **图片生成模块去重重构** 🔥 | create_webzine_image与create_combined_webzine_image存在~100行重复(文本解析、字体配置、渲染绘制三层重复),提取4个公共函数消除重复,代码量减少~40% | ⭐ | ⏳ 待开展 |
| 差异化抓取调度 | 高频新闻源缩短抓取间隔,低频源延长间隔,降低无效抓取请求 | ⭐⭐ | ⏳ 待开展 |
| 异步流式处理队列 | 抓取和后续处理并行执行,整体耗时再缩短30% | ⭐⭐⭐ | ⏳ 待开展 |
### 🟢 P3级(低优先级,投入大,规模扩大后再考虑)
| 优化项 | 适用场景 | 实施难度 | 状态 |
| --- | --- | --- | --- |
| SimHash相似内容去重 | 源>500、大量转载内容场景,减少重复文章 | ⭐⭐⭐⭐ | 💤 待规划 |
| 微服务拆分+分布式消息队列 | 源>1000、单机器性能不足时,支持水平扩展 | ⭐⭐⭐⭐ | 💤 待规划 |
| 管理后台界面 | 可视化配置源、查看历史报告、统计分析 | ⭐⭐⭐⭐ | 💤 待规划 |
| 本地大模型部署 | 降低API调用成本,实现私有化部署 | ⭐⭐⭐⭐ | 💤 待规划 |
---
## 📊 已完成进度总览
| 阶段 | 完成度 | 核心成果 |
| --- | --- | --- |
| 架构重构 | 100% | 单文件拆分为模块化结构,可维护性大幅提升 |
| P0级优化 | 100% | 抓取+AI环节速度提升2-3倍,成本降低40% |
| P1级优化 | 100% | SQLite缓存+全链路降级+基础监控统计,再次运行几乎零token消耗 |
| 功能完善 | 100% | 推送模块完成,企业微信文本+图片推送+飞书文本推送 |
| 稳定性优化 | 100% | 抓取重试+全链路降级+监控统计 |
---
## 🚀 下一阶段优化实施计划
### 第一阶段 ~~(当前优先开展,预计1-2天完成)~~ ✅ 已完成
> 核心目标:完善可观测性,补齐P1最后一块拼图
1. ~~**统计监控功能**(P1收尾)~~ ✅ 已完成:
- 阶段耗时自动统计,运行结束输出结构化报表
- API调用量/token消耗按用途和模型分组统计
- 源抓取成功/失败率一目了然
2. ~~**SQLite增量缓存系统实现**~~ ✅ 已完成
3. ~~**全链路失败降级机制**~~ ✅ 已完成
### 第二阶段(当前开展,预计2-3天完成)
> 核心目标:提升代码质量,完善功能
**推荐顺序**(按投入产出比排列):
1. ~~**main()函数拆分重构**(建议最先做)~~ ✅ 已完成
2. ~~**推送模块完善**~~ ✅ 已完成:
- `modules/pusher.py` 封装企业微信(文本/图片/Markdown)+ 飞书(文本)webhook 推送
- `config.py` 新增 `load_push_config()` 统一加载推送配置(环境变量 > .env > config.json
- 主流程末自动调用,推送失败优雅降级(warning 日志,不影响 exit 0
- 支持关闭推送(`enable_wechat_push: false` / `enable_feishu_push: false`
3. **异步流式处理改造**
- 需变更架构(引入队列),建议在函数拆分完成、代码结构清晰后再做
4. **差异化抓取调度**
- 为 rss_feeds.txt 增加频率字段,需配合定时调度器改造
### 第三阶段(按需扩展,规模扩大后开展)
> 核心目标:支持更大规模源和更高性能要求
根据实际业务需求,优先开展最急需的优化点。
---
## 📌 实施建议
1. ~~优先完成第一阶段优化~~ ✅ P0+P1 共 6 项全部到位
2. ~~第二阶段 main()拆分 + 推送模块~~ ✅ 已完成
3. 当前优先级:图片生成模块去重重构 → 差异化抓取调度 → 异步流式处理(源扩展后再做)
4. P3 级优化建议在单机器无法满足性能要求,或者有明确功能需求时再实施
---
## 🧪 验证阶段
### 4.1 main() 函数拆分验证
2026-05-14 完成拆分重构,按 `主函数过长问题.md` 方案落地:
| # | 提取函数 | 职责 | 行数 | 可测试性 |
|:---:|---|------|:---:|:---:|
| ① | `merge_from_cache` | 合并缓存+新文章,按评分排序 | 9 | ⭐⭐⭐ |
| ② | `process_new_articles` | 完整管线:排序→标题翻译→筛选→正文翻译→AI分析→写缓存 | 32 | ⭐⭐ |
| ③ | `deduplicate_articles` | 同源标题去重 | 12 | ⭐⭐⭐ |
| ④ | `categorize_articles` | 三类文章分组 | 11 | ⭐⭐⭐ |
| ⑤ | `write_markdown_report` | Markdown报告拼接+写入(含`_append_category`消除三分类重复) | 40 | ⭐⭐ |
| ⑥ | `build_webzine_content` | 网摘文本生成(优先缓存+写回) | 33 | ⭐⭐ |
| ⑦ | `get_or_generate_overview` | 分类介绍查缓存/生成(消除原有4处×8行重复) | 9 | ⭐⭐ |
**拆分效果:** main() 从 ~310 行缩减至 ~115 行(-63%),消除代码重复 60+ 行。拆分后 7 次独立运行全部 exit 0。
**附带修复的 Bug** 发现 `keyword_filter.py` 在正文翻译前硬读 `a['translated_content']` 导致 `KeyError`,已修复为安全 fallback`a.get('translated_content') or a.get('content') or ''`)。
---
### 4.2 SQLite 缓存数据完整性验证
2026-05-14 冷启动全链路运行后,直接查询 `article_cache.db` 验证:
**基本信息:**
| 表 | 行数 | 说明 |
|------|:---:|------|
| `article_cache` | 15 | 与关键词筛选后文章数完全一致 |
| `category_summary_cache` | 4 | 今日必看/装备动态/地区冲突/战略政策全覆盖 |
**逐项检查:**
| 检查项 | 结果 | 判定 |
|------|:---:|:---:|
| id 重复 | 0 条 | ✅ |
| 失败条目 (status≠1) | 0 条 | ✅ |
| translated_title 缺失 | 0/15 | ✅ |
| translated_content 缺失 | 0/15 | ✅ |
| ai_summary 缺失 | 0/15 | ✅ |
| ai_category 缺失 | 0/15 | ✅ |
| published_time 缺失 | 0/15 | ✅ |
| webzine_text 覆盖率 | 3/15 | ✅ (仅TOP3需要) |
| ai_score 范围 | 3.3 ~ 6.5 (avg 5.3) | ✅ 正态分布 |
| 分类分布 | 装备7 / 战略5 / 冲突3 | ✅ 合理 |
**缓存命中验证(连续运行2次):**
| 指标 | 第一次(冷启动) | 第二次(含缓存) |
|------|:---:|:---:|
| 文章缓存命中 | 0 / 15 | **15 / 15** ✅ |
| API 调用次数 | 37 次 | **0 次** ✅ |
| 总耗时 | 153s | **~5s** ✅ |
| 退出码 | 0 | 0 |
> 第二次运行时文章缓存、网摘缓存、分类介绍缓存全部命中,跳过所有 AI 调用,仅做 RSS 抓取 + 文件排版,几乎零 token 消耗。
---
### 4.3 全链路冷启动验证
2026-05-14 清除所有缓存和输出文件后运行,完整链路通过:
| 阶段 | 耗时 | 状态 |
|------|:---:|:---:|
| RSS 抓取(5源) | 7.5s | ✅ |
| 时间过滤 (156→22篇) | 0.0s | ✅ |
| 去重 | 0.0s | ✅ |
| 标题翻译(7篇外文) | 17.9s | ✅ |
| 关键词筛选 (22→15篇) | 0.0s | ✅ |
| 正文翻译(6篇外文) | 9.9s | ✅ |
| AI评分分类(15篇) | 30.7s | ✅ |
| 网摘生成(3篇,含2次重试) | 53.3s | ✅ |
| 分类介绍生成(4类) | 33.5s | ✅ |
| 图片生成 | 0.3s | ✅ |
| 报告生成 | 0.0s | ✅ |
| **总耗时** | **153s** | ✅ |
**API 消耗统计:**
| 用途 | 次数 | tokens 估算 |
|------|:---:|:---:|
| 标题翻译 | 7 | 1,400 |
| 正文翻译 | 6 | 4,800 |
| AI评分分类 | 15 | 12,000 |
| 网摘生成 | 5 (含2次重试) | 4,000 |
| 分类介绍 | 4 | 600 |
| **合计** | **37** | **~22,800** |
**输出文件验证:**
```
military_report_20260514.md 10.6 KB ✅ Markdown 格式完整
military_webzine_20260514.txt 4.9 KB ✅ TOP3 网摘文本完整
military_webzine_20260514.png 868.5 KB ✅ 合并长图渲染正常
article_cache.db 60.0 KB ✅ 数据完整无异常
```
---
### 4.4 当前优化完成总览
| 优先级 | 完成度 | 已实现项目 |
|:---:|:---:|---|
| P0 | 100% | 线程池抓取、AI全链路并发、处理顺序前置 |
| P1 | 100% | SQLite缓存、全链路降级、基础监控统计 |
| P2 | 60% | main()函数拆分 ✅ / 推送模块 ✅ / 图片去重 ⏳ / 差异化调度 ⏳ / 异步流式 ⏳ |
| P3 | 0% | 待规划 |
| **安全** | **100%** | `.gitignore` + 配置分离 + 全项目扫描无硬编码凭据 |
| **配置** | **100%** | `RSS_PROXY_BASE` 提取 + `.env.example` 结构对齐 |
---
### 4.5 推送模块验证
2026-05-21 完成推送模块开发并实际调用验证:
**架构:**
| 文件 | 职责 |
|------|------|
| `modules/pusher.py` | 企业微信文本/图片/Markdown + 飞书文本推送,含失败兜底(5级异常分层捕获) |
| `modules/config.py:load_push_config()` | 三层配置加载(环境变量 > .env > config.json |
| `military_daily_report_v3.py:L334-338` | 主流程末自动触发 |
**配置职责分离:**
| 配置来源 | 内容 | 说明 |
|------|------|------|
| `config.json` | `enable_wechat_push`, `enable_feishu_push` | 仅开关标志,可 git 追踪 |
| `config/.env` | `WECHAT_ACCOUNT`, `WECHAT_TARGET`, `FEISHU_WEBHOOK` | 敏感凭据,.gitignore 排除 |
**错误处理矩阵(5级分层):**
| 异常类型 | 处理方式 | 主流程影响 |
|------|------|:---:|
| `ConnectionError` | warning 日志 + 返回 False | 无 |
| `Timeout` (连接5s/读取15s) | warning 日志 + 返回 False | 无 |
| `HTTPError` (4xx/5xx) | warning 日志 + 返回 False | 无 |
| `JSONDecodeError` | warning 日志 + 返回 False | 无 |
| 其他 `Exception` | warning 日志 + 返回 False | 无 |
**实际运行验证(2026-05-21):**
| 场景 | 配置 | 结果 | 主流程 |
|------|------|------|:---:|
| 双推送关闭 | `enable_wechat_push: false, enable_feishu_push: false` | "未启用任何推送渠道,跳过" | exit 0 ✅ |
| 企业微信(无效key | `enable_wechat_push: true` | 服务器返回 `errcode=93000`warning 降级 | exit 0 ✅ |
| **飞书(真实webhook** | `enable_feishu_push: true` | **飞书推送成功** 🎉 | exit 0 ✅ |
**关键验证点:**
- ✅ 飞书 webhook 真实连接成功,消息送达飞书群
- ✅ 企业微信请求正确送达服务器(无效 key 返回标准 errcode,非网络异常)
- ✅ 所有异常均优雅降级为 warning 日志,不抛异常,不影响主流程
- ✅ 推送代码可通过 config.json 开关一键启用/关闭,webhook 凭据统一在 .env 管理
---
### 4.6 配置安全加固
2026-05-21 完成全项目敏感信息扫描和加固:
**发现的问题:**
| 问题 | 严重程度 | 修复 |
|------|:---:|------|
| 项目缺少 `.gitignore``config/.env` 可能泄露到 Git | 🔴 严重 | 新建 `.gitignore`,首行即排除 `config/.env` |
| `config.json``wechat_target`/`wechat_account` 敏感字段 | 🟡 中等 | 删除,统一迁移到 `.env``WECHAT_ACCOUNT`/`WECHAT_TARGET` |
| `docker-compose.yml` 缺少推送环境变量 | 🟡 中等 | 两个服务均补充 `WECHAT_ACCOUNT`/`WECHAT_TARGET`/`FEISHU_WEBHOOK` |
| `.env` 字段名与代码不一致 | 🟡 中等 | `WECHAT_WEBHOOK_KEY``WECHAT_ACCOUNT``FEISHU_WEBHOOK_URL``FEISHU_WEBHOOK` |
**加固后架构:**
```
config.json ← 仅开关标志,可 git 追踪
config/.env ← 所有敏感凭据,.gitignore 排除
环境变量 ← Docker/CI 场景的最终覆盖
```
**扫描结论:** 所有 `.py` 代码、`Makefile``Dockerfile``config.json` 中均无硬编码凭据残留。`.env.example``.env` 结构完全一致(11 个字段全部对齐)。
---
### 4.7 RSS_PROXY_BASE 配置提取
2026-05-21 将 `default_feeds` 中 8 个硬编码的 `werss.yynnice.top` URL 统一提取为可配置项:
**配置架构:**
| 层级 | 来源 | 效果 |
|:---:|------|------|
| 默认值 | 代码内置 | `https://werss.yynnice.top` |
| 覆盖 | `config/.env``RSS_PROXY_BASE` | 更换代理服务只需改一行 |
| 最终覆盖 | 环境变量 | Docker/CI 注入 |
**涉及文件:**
| 文件 | 改动 |
|------|------|
| `config.py` | `_load_rss_proxy_base()` 三层加载 + 8 个 `default_feeds` 改用 f-string |
| `.env` / `.env.example` | 新增 `RSS_PROXY_BASE` 配置项 |
| `docker-compose.yml` | 两个服务均传递 `RSS_PROXY_BASE` |
> 第三方 RSS 代理服务更换时只需改 `.env` 一行,无需修改 Python 代码或 `rss_feeds.txt`。
---
### 4.8 图片生成模块去重重构
`01.md` 方案提取 4 个公共函数:
| # | 提取函数 | 职责 | 消除重复行 |
|:---:|---|------|:---:|
| ① | `_parse_webzine_line` | 统一文本前缀解析(标题:/正文:/价值点:等) | ~30行 × 2 |
| ② | `_load_webzine_fonts` | 集中字体加载和配置(banner/article_title/bold/content | ~4行 × 2 |
| ③ | `_build_render_commands` | 将网摘文本转为渲染命令列表 | ~40行 × 2 |
| ④ | `_execute_render_commands` | 执行渲染命令,在画布上绘制内容 | ~30行 × 2 |
**重构效果预期:**
| 指标 | 重构前 | 重构后 |
|------|:---:|:---:|
| `create_webzine_image` | 115行 | ~50行 |
| `create_combined_webzine_image` | 106行 | ~45行 |
| 新增公共函数 | 0 | 4个(合计~110行) |
| **净减少代码** | — | **~100行(-45%** |
| 可单独测试的单元 | 0 | 4个公共函数 |
**回归验证方式:** 运行一次完整冷启动,对比重构前后输出的 3 个文件(md/txt/png)的字节级一致性。
---
## 🎯 下一步推荐
P2 剩余 3 项待开展,建议按以下顺序推进:
### 🥇 图片生成模块去重重构(投入 ⭐,收益大,建议最先做)
**当前问题:** `image_generator.py``create_webzine_image`131行)与 `create_combined_webzine_image`106行)存在三层重复:
- **文本解析层**:前缀识别(标题:/正文:/价值点:等)逻辑完全一致
- **字体配置层**:4种字体的加载代码完全相同(banner/article_title/bold/content
- **渲染绘制层**prefix_sameline / article_title / normal 等命令的绘制逻辑高度相似
**方案:** 提取 4 个公共函数(`_parse_webzine_line` / `_load_webzine_fonts` / `_build_render_commands` / `_execute_render_commands`),建立清晰的抽象层次。详见 `01.md`
**涉及改动:**`modules/image_generator.py` 一个文件,纯内部重构,无外部依赖变化。
**推荐理由:**
- 投入最低(⭐),仅涉及同一文件内的函数提取
- 风险极低,两个函数的现有行为完全不变
- 延续 main() 拆分的重构方法论,代码风格一致
- 可作为热身任务,在差异化调度和异步流式之前快速完成
### 🥈 差异化抓取调度(投入 ⭐⭐,收益大)
**当前问题:** 所有源统一 24h 抓一次,高频源(无人机类、战区类)可能错过半天内的重要新闻。
**方案:**`rss_feeds.txt` 增加频率字段,格式从 `名称|URL` 扩展为 `名称|URL|频率(小时)`。兼容现有格式(无频率默认 24h)。
**涉及改动:** `config.py` 解析逻辑 + `rss_fetcher.py` 读取上次抓取时间 + 外部 crontab 调高频率。
### 🥉 异步流式处理队列(投入 ⭐⭐⭐,规模化时收益大)
**当前架构:** 阶段间串行(全部抓取完 → 全部过滤完 → ...)。5 源 153s 完全够用。
**何时做:** 源扩展到 50+、冷启动耗时明显增加时再考虑。当前 5 源 RSS 抓取仅占 7-15s,瓶颈在 AI 调用而非抓取。