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
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""
运行时监控模块
统计各环节耗时、API调用量/token消耗、成功率,运行结束输出统计报表
"""
import time
from collections import defaultdict
from contextlib import contextmanager
from .logger import get_logger
logger = get_logger()
STAGE_ORDER = [
"rss_fetch", "time_filter", "deduplicate", "translate_titles",
"keyword_filter", "translate_contents", "ai_analyze",
"webzine_generate", "category_overview", "report_generate", "image_generate"
]
STAGE_LABELS = {
"rss_fetch": "RSS抓取",
"time_filter": "时间过滤",
"deduplicate": "去重",
"translate_titles": "标题翻译",
"keyword_filter": "关键词筛选",
"translate_contents": "正文翻译",
"ai_analyze": "AI评分分类",
"webzine_generate": "网摘生成",
"category_overview": "分类介绍",
"report_generate": "报告生成",
"image_generate": "图片生成",
}
class RunMonitor:
def __init__(self):
self.start_time = time.time()
self.stage_times = {}
self.stage_active = {}
self.api_calls = []
self.source_success = defaultdict(int)
self.source_failure = defaultdict(int)
self.articles_total = 0
self.articles_cached = 0
self.articles_processed = 0
@contextmanager
def stage(self, name):
"""上下文管理器,自动记录阶段耗时"""
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
self.stage_times[name] = elapsed
def record_api_call(self, model, purpose, max_tokens, success, retries=0):
"""记录一次API调用"""
self.api_calls.append({
"model": model,
"purpose": purpose,
"max_tokens": max_tokens,
"success": success,
"retries": retries,
})
def record_article_stats(self, total, cached, processed):
self.articles_total = total
self.articles_cached = cached
self.articles_processed = processed
def record_source_result(self, source, success):
if success:
self.source_success[source] += 1
else:
self.source_failure[source] += 1
def report(self):
total_elapsed = time.time() - self.start_time
lines = []
lines.append("")
lines.append("=" * 64)
lines.append(" 📊 运行监控报告")
lines.append("=" * 64)
lines.append("")
lines.append("── ⏱ 阶段耗时 ──")
stage_total = 0.0
for key in STAGE_ORDER:
if key in self.stage_times:
t = self.stage_times[key]
stage_total += t
label = STAGE_LABELS.get(key, key)
lines.append(f" {label: <10s} {t:6.1f}s")
lines.append(f" {'' * 20}")
lines.append(f" {'合计': <10s} {stage_total:6.1f}s / 总运行 {total_elapsed:.0f}s")
lines.append("")
lines.append("── 📡 文章统计 ──")
lines.append(f" 总计: {self.articles_total} 缓存命中: {self.articles_cached} 本次处理: {self.articles_processed}")
if self.articles_total > 0:
cache_rate = self.articles_cached / self.articles_total * 100
lines.append(f" 缓存命中率: {cache_rate:.0f}%")
lines.append("")
lines.append("── 🤖 API调用统计 ──")
if not self.api_calls:
lines.append(" 无API调用(全部命中缓存)")
else:
by_purpose = defaultdict(lambda: {"count": 0, "tokens": 0, "retries": 0, "failed": 0})
by_model = defaultdict(lambda: {"count": 0, "tokens": 0})
for call in self.api_calls:
p = call["purpose"]
by_purpose[p]["count"] += 1
by_purpose[p]["tokens"] += call["max_tokens"]
by_purpose[p]["retries"] += call["retries"]
if not call["success"]:
by_purpose[p]["failed"] += 1
m = call["model"]
by_model[m]["count"] += 1
by_model[m]["tokens"] += call["max_tokens"]
lines.append(" [按用途]")
for purpose, stats in by_purpose.items():
fail_info = f" 失败{stats['failed']}" if stats['failed'] > 0 else ""
lines.append(f" {purpose}: {stats['count']}次 / tokens≈{stats['tokens']} / 重试{stats['retries']}{fail_info}")
lines.append(" [按模型]")
for model, stats in by_model.items():
lines.append(f" {model}: {stats['count']}次 / tokens≈{stats['tokens']}")
lines.append("")
lines.append("── 📰 源抓取统计 ──")
total_sources = len(self.source_success) + len(self.source_failure)
failed_names = [src for src in self.source_failure if self.source_failure[src] > 0]
success_count = total_sources - len(failed_names)
lines.append(f" 源总数: {total_sources} 成功: {success_count} 失败: {len(failed_names)}")
if failed_names:
lines.append(f" 失败源: {', '.join(failed_names)}")
lines.append("")
lines.append("=" * 64)
report_text = "\n".join(lines)
logger.info(report_text)
return report_text
_monitor = None
def get_monitor():
global _monitor
if _monitor is None:
_monitor = RunMonitor()
return _monitor
def reset_monitor():
global _monitor
_monitor = RunMonitor()