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
+383
View File
@@ -0,0 +1,383 @@
#!/usr/bin/env python3
"""测试LLM评分和推送功能 - 独立运行脚本
Usage:
# 先激活虚拟环境
source ../.venv/bin/activate
# 测试评分
python tests/run_llm_test.py --score
# 测试即时推送
python tests/run_llm_test.py --immediate-push --push
"""
import argparse
import asyncio
import json
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
# 检查是否在虚拟环境中
if not hasattr(sys, "real_prefix") and not (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
):
print("⚠️ 建议先激活虚拟环境: source .venv/bin/activate")
print("")
# 加载 .env 文件
from dotenv import load_dotenv
load_dotenv()
# 添加项目根目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.config import get_timezone, load_config
from src.llm import compose_digest, generate_immediate_push, score_batch
from src.push import send_to_platforms
from src.storage import read_fetch_data, save_fetch_file
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="测试LLM评分和推送")
parser.add_argument(
"--input",
"-i",
type=str,
default="tests/news-data/fetch-{date}.json",
help="输入文件路径,支持{date}占位符 (默认: tests/news-data/fetch-{date}.json)",
)
parser.add_argument(
"--date",
"-d",
type=str,
default=datetime.now(get_timezone()).strftime("%Y-%m-%d"),
help="日期,格式YYYY-MM-DD (默认: 今天)",
)
parser.add_argument(
"--limit", "-l", type=int, default=0, help="测试的消息数量 (默认: 0表示全部)"
)
# 测试模式选择
parser.add_argument("--score", action="store_true", help="测试评分")
parser.add_argument("--immediate-push", action="store_true", help="测试即时推送")
parser.add_argument("--digest", action="store_true", help="测试汇总推送")
parser.add_argument("--push", action="store_true", help="推送到Discord")
parser.add_argument("--all", action="store_true", help="运行所有测试")
return parser.parse_args()
def should_run(args, mode: str) -> bool:
"""判断是否运行某个模式"""
# 如果没有任何特定模式指定,默认运行评分
if args.all:
return True
# 检查是否指定了任何模式
any_mode = args.score or args.immediate_push or args.digest
if mode == "score":
return args.score or not any_mode # 默认运行评分
elif mode == "immediate_push":
return args.immediate_push
elif mode == "digest":
return args.digest
return False
async def run_llm_test():
"""主函数"""
args = parse_args()
print("=" * 60)
print("🤖 LLM测试脚本")
print("=" * 60)
# 构建输入文件路径
input_path = args.input.format(date=args.date)
print(f"\n📂 输入文件: {input_path}")
# 读取数据
if not Path(input_path).exists():
print(f"❌ 文件不存在: {input_path}")
print("\n💡 提示: 先运行 fetch_news.py 获取新闻数据")
print(" python tests/fetch_news.py --hours 1")
return False
print(input_path)
data = read_fetch_data(input_path)
entries = data.get("entries", [])
meta = data.get("meta", {})
print(f" ✓ 共 {len(entries)}")
if not entries:
print("❌ 没有条目可测试")
return False
# 限制测试数量 (0表示全部)
if args.limit > 0:
test_entries = entries[: args.limit]
print(f" 测试前 {len(test_entries)}")
else:
test_entries = entries
print(f" 测试全部 {len(test_entries)}")
# 显示待评分条目
print(f"\n📄 测试条目:")
for i, e in enumerate(test_entries[:5], 1):
print(f" [{i}] {e.get('title', 'N/A')[:45]}...")
print(f" 来源: {e.get('source', 'N/A')}")
# 加载配置
print("\n⚙️ 加载配置...")
config = load_config()
llm_config = config["llm"]
print(f" ✓ 提供商: {llm_config.get('provider', 'openai')}")
print(f" ✓ 模型: {llm_config.get('model', 'N/A')}")
print(f" ✓ BaseURL: {llm_config.get('baseUrl', 'N/A')}")
# 检查API key
api_key_name = llm_config.get("apiKeyName", "OPENAI_API_KEY")
api_key = os.environ.get(api_key_name)
if not api_key:
print(f"\n❌ 未设置环境变量: {api_key_name}")
return False
print(f" ✓ API Key: {api_key[:10]}...")
# 检查是否启用推送
push_enabled = args.push and config.get("push")
if push_enabled:
print("\n🔌 推送已启用 (将推送到所有已配置的平台)")
# ========== 测试评分 ==========
if should_run(args, "score"):
print("\n" + "-" * 60)
print("🎯 测试: 评分 (score_batch)")
print("-" * 60)
try:
scored, score_errors = await score_batch(test_entries, llm_config)
if score_errors:
print("\n⚠️ 评分存在异常:")
for error in score_errors:
print(f" - {error}")
print("\n✅ 评分完成!")
# 显示评分结果
print("\n📊 评分结果:")
for i, e in enumerate(scored[:5], 1):
print(f"\n [{i}] {e['title'][:40]}...")
print(f" 评分: {e.get('score', 'N/A')}/100")
print(f" 标签: {e.get('tags', [])}")
print(f" 摘要: {e.get('summary', 'N/A')[:60]}...")
# 保存评分结果到JSON文件
print(f"\n💾 保存评分结果到: {input_path}")
# 构建link到评分的映射
score_map = {e.get("link"): e for e in scored if e.get("link")}
# 更新所有entries的评分
all_entries = data.get("entries", [])
for i, entry in enumerate(all_entries):
link = entry.get("link")
if link in score_map:
all_entries[i] = score_map[link]
save_fetch_file(input_path, meta, all_entries)
print(f" ✅ 已保存 {len(scored)} 条评分结果")
# 更新test_entries为评分后的数据
test_entries = scored
except Exception as e:
print(f"\n❌ 评分失败: {e}")
import traceback
traceback.print_exc()
return False
# ========== 测试即时推送 ==========
if should_run(args, "immediate_push"):
print("\n" + "-" * 60)
print("🔥 测试: 即时推送 (generate_immediate_push)")
print("-" * 60)
# 筛选高分条目 (>=80分)用于推送
hot_entries = [e for e in test_entries if e.get("score", 0) >= 90]
if not hot_entries:
hot_entries = test_entries[-3:-1] # 如果没有高分,取前2条
print(f"\n使用 {len(hot_entries)} 条高分消息生成推送...")
# 加载近期推送上下文用于测试
context_days = config.get("filter", {}).get("context_days", 3)
from src.llm import parse_immediate_push_with_metadata
from src.storage import (
get_notify_file,
load_recent_notify_content,
load_recent_push_content,
save_notify_file,
)
recent_notify = load_recent_notify_content(context_days)
recent_push = load_recent_push_content(context_days)
recent_context = (
f"=== 近期即时推送 ===\n{recent_notify}\n\n"
f"=== 近期汇总推送 ===\n{recent_push}"
)
try:
# 传入上下文参数
push_content, immediate_push_error = await generate_immediate_push(
hot_entries, llm_config, recent_push_context=recent_context
)
timestamp = datetime.now(get_timezone()).strftime("%Y-%m-%d")
content_without_title, metadata = parse_immediate_push_with_metadata(
push_content, f"🚨 AI Daily 快讯 | {timestamp}"
)
metadata["pushTime"] = datetime.now(get_timezone()).isoformat()
push_content = content_without_title
if immediate_push_error:
print(f"\n⚠️ 即时推送生成异常: {immediate_push_error}")
push_content = ""
print(f"\n✅ 推送内容生成完成!")
print(f"\n📤 推送内容预览:")
print("-" * 40)
print(
push_content[:500] + "..." if len(push_content) > 500 else push_content
)
print("-" * 40)
# 检查是否有实际内容需要推送
no_content_marker = config.get("filter", {}).get(
"no_content_marker", "[NO_NEW_CONTENT]"
)
if no_content_marker in push_content:
print(f"\nℹ️ 无新内容需要推送 (LLM判定为重复内容)")
else:
# 推送到所有启用的平台
if push_enabled:
print("\n📤 推送消息...")
await send_to_platforms(
push_content,
config["push"],
title="🚨 AI Daily 快讯 | " + metadata["title"],
metadata=metadata,
)
print(" ✅ 推送成功!")
# 保存到 notify 文件
notify_file = get_notify_file()
save_notify_file(notify_file, push_content, metadata)
print(f"\n💾 已保存即时推送到 {notify_file}")
except Exception as e:
print(f"\n❌ 即时推送生成失败: {e}")
import traceback
traceback.print_exc()
# ========== 测试汇总推送 ==========
if should_run(args, "digest"):
print("\n" + "-" * 60)
print("📰 测试: 汇总推送 (compose_digest)")
print("-" * 60)
# 构建上下文(从 fetch 文件读取的历史数据)
context = test_entries[:10] # 使用前10条作为模拟上下文
print(f"\n使用 {len(test_entries)} 条消息生成汇总...")
# 加载近期推送上下文
push_context_days = config.get(
"filter",
).get("push_context_days", 5)
from src.storage import get_push_file, load_recent_push_content, save_push_file
recent_push_context_str = load_recent_push_content(push_context_days)
try:
raw_digest = await compose_digest(
test_entries,
context,
llm_config,
recent_push_context=recent_push_context_str,
)
from src.llm import parse_digest_with_metadata
date_str = datetime.now(get_timezone()).strftime("%Y-%m-%d")
digest_content, metadata = parse_digest_with_metadata(raw_digest, date_str)
metadata["pushTime"] = datetime.now(get_timezone()).isoformat()
print(f"\n✅ 汇总内容生成完成!")
print(f" 标题: {metadata['title']}")
print(f" 导读: {metadata.get('lead', '')[:60]}")
print(f" 重点: {metadata.get('highlights', [])}")
print(f"\n📰 汇总内容预览:")
print("-" * 40)
print(
digest_content[:500] + "..."
if len(digest_content) > 500
else digest_content
)
print("-" * 40)
# 推送到所有启用的平台
if push_enabled:
print("\n📤 推送消息...")
await send_to_platforms(
digest_content,
config["push"],
title="📰 AI Daily 每日精选 | " + metadata["title"],
metadata=metadata,
)
print(" ✅ 推送成功!")
# 保存到 push 文件
push_file = get_push_file()
save_push_file(
push_file,
digest_content,
len(test_entries),
len(test_entries),
profile="default",
metadata=metadata,
)
print(f"\n💾 已保存汇总到 {push_file}")
except Exception as e:
print(f"\n❌ 汇总推送生成失败: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)
print("✅ LLM测试完成!")
print("=" * 60)
return True
if __name__ == "__main__":
try:
success = asyncio.run(run_llm_test())
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\n👋 已取消")
sys.exit(130)
except Exception as e:
print(f"\n❌ 错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)