Skill: kern1x/ai-news-pusher (version 2.2.2)

--- name: ai-news-pusher-v2 description: AI新闻自动获取与推送Skill v2.2。新增智能产品价值评分、高质量信源过滤、三级分类机制和人工反馈迭代。支持Tavily、Brave、RSS多新闻源聚合，无需API Key即可使用RSS源。当用户需要获取AI行业最新动态、自动化新闻推送、多源新闻聚合或智能内容过滤时触发此Skill。 optional_envs: - name: FEISHU_WEBHOOK_URL description: 飞书群聊自定义机器人的Webhook URL，用于推送新闻消息（需要此变量才能实际推送） - name: OPENAI_API_KEY description: OpenAI API Key，用于LLM评分功能（可选，无Key时使用规则评分） - name: ANTHROPIC_API_KEY description: Anthropic API Key，用于LLM评分功能（可选，与OpenAI二选一） - name: TAVILY_API_KEY description: Tavily API Key，用于Tavily新闻源（可选） - name: BRAVE_API_KEY description: Brave Search API Key，用于Brave新闻源（可选） - name: OPENCLAW_GATEWAY_URL description: OpenClaw网关地址（用于定时任务，可选） - name: OPENCLAW_GATEWAY_TOKEN description: OpenClaw网关令牌（用于定时任务，⚠️ 高度敏感：可允许调度/外部控制，仅在完全理解此功能并信任运行环境时使用） --- # AI News Pusher v2.2 - AI新闻推送 ## 🎉 v2.2 重大更新！ **v2.2 全新升级：** - ✅ **产品价值评分系统** - 基于LLM的四维度智能评分 - ✅ **高质量信源过滤** - 新增Arxiv、OpenAI博客等权威信源 - ✅ **三级分类机制** - 自动推送、待阅池、彻底拦截 - ✅ **人工反馈迭代** - 通过标记持续优化评分系统 - ✅ **数据持久化** - 完整的历史记录和反馈管理 - ✅ **管理工具** - 交互式审核、分析和配置 ## 概述 本Skill提供AI新闻的自动获取和智能推送功能，支持多新闻源聚合和价值评分： - **多源聚合** - Tavily API、Brave Search API、RSS订阅源 - **智能评分** - 基于LLM的产品价值四维度评分 - **灵活分类** - 80分以上自动推送，60-80分待审核，60分以下拦截 - **持续优化** - 通过人工反馈和分析不断改进 ## 🚀 快速开始 ### 1. 仅使用RSS源（无需任何API Key） ```bash # 获取AI新闻（仅RSS源） python3 scripts/fetch_ai_news.py --source rss --limit 10 ``` ### 2. 启用智能评分（推荐） ```bash # 设置OpenAI API Key（用于评分） export OPENAI_API_KEY=your_api_key_here # 获取新闻并启用评分 python3 scripts/fetch_ai_news.py --source all --limit 20 --enable-scoring ``` ### 3. 审核待阅池 ```bash # 交互式审核待阅池内容 python3 scripts/news_manager.py review # 查看统计数据 python3 scripts/news_manager.py stats ``` ### 4. 推送到Feishu ```bash # 设置Feishu Webhook export FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx # 推送新闻 python3 scripts/push_to_feishu.py --limit 8 ``` ## 📖 详细使用说明 ### 获取AI新闻（带评分） ```bash python3 scripts/fetch_ai_news.py [选项] 选项: --limit N 返回N条新闻 (默认10) --days N 搜索最近N天的新闻 (默认3) --source SOURCE 新闻源: tavily|brave|rss|all (默认all) --format FORMAT 输出格式: json|text|markdown (默认json) --output FILE 输出到文件 --query QUERY 搜索关键词 (默认: AI artificial intelligence) --enable-scoring 启用智能价值评分 --llm-provider LLM提供商: openai|anthropic (默认openai) ``` ### 多源新闻聚合 ```python from scripts.news_sources import get_default_aggregator # 获取聚合器（自动检测可用的API Key） aggregator = get_default_aggregator( include_tavily=True, include_brave=True, include_rss=True ) # 获取新闻 news = aggregator.fetch_all( query="AI artificial intelligence", limit=10, days=3 ) print(f"共获取 {len(news)} 条新闻") ``` ### 智能价值评分 ```python from scripts.news_scorer import NewsScorer from scripts.data_storage import DataStorage # 初始化评分器 scorer = NewsScorer(llm_provider="openai") # 批量评分 categorized = scorer.score_batch(news_list) print(f"自动推送: {len(categorized['auto_push'])}") print(f"待阅池: {len(categorized['gray_zone'])}") print(f"已过滤: {len(categorized['filtered'])}") # 保存到存储 storage = DataStorage() storage.batch_save_gray_zone(categorized['gray_zone']) storage.save_pushed(categorized['auto_push']) storage.save_filtered(categorized['filtered']) ``` ### 新闻管理工具 ```bash # 查看所有可用命令 python3 scripts/news_manager.py --help # 常用命令 python3 scripts/news_manager.py review # 审核待阅池 python3 scripts/news_manager.py stats # 查看统计 python3 scripts/news_manager.py filtered # 查看被过滤内容 python3 scripts/news_manager.py feedback # 查看反馈记录 python3 scripts/news_manager.py analyze # 分析反馈数据 python3 scripts/news_manager.py config # 更新配置 ``` ### 推送到Feishu ```bash python3 scripts/push_to_feishu.py [选项] 选项: --limit N 推送新闻数量 (默认8) --input FILE 从文件加载新闻(JSON格式) --multi-source 使用多源聚合 --channel CHANNEL 推送渠道 (默认feishu) --dry-run 仅格式化消息，不实际发送 --use-webhook 使用Webhook发送 ``` ### 配置定时推送任务 ```bash # 创建每天9点推送任务 python3 scripts/schedule_push.py create --time "0 9 * * *" --limit 8 # 使用简写时间格式 python3 scripts/schedule_push.py create --time "09:00" --limit 10 # 测试推送 python3 scripts/schedule_push.py test --limit 5 # 列出所有任务 python3 scripts/schedule_push.py list # 删除任务 python3 scripts/schedule_push.py delete --job-id <任务ID> ``` ## 🔧 环境变量配置 ### ⚠️ 安全提示 - **所有API Keys和Webhook URL都是敏感信息，请妥善保管，不要提交到代码仓库** - **OPENCLAW_GATEWAY_TOKEN特别敏感：此令牌可能允许调度/外部控制，仅在完全理解此功能后使用** ### 新闻源API Key（可选） ```bash # Tavily API Key（可选，推荐） export TAVILY_API_KEY=your_tavily_api_key # Brave Search API Key（可选） export BRAVE_API_KEY=your_brave_api_key ``` ### LLM评分配置（可选） ```bash # OpenAI API Key（用于评分，推荐） export OPENAI_API_KEY=your_openai_api_key # 或使用 Anthropic export ANTHROPIC_API_KEY=your_anthropic_api_key ``` ### Feishu推送配置（可选，但需要设置才能推送） ```bash # Feishu Webhook URL export FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx ``` ### OpenClaw Gateway配置（用于定时任务，可选） ```bash # OpenClaw Gateway地址 export OPENCLAW_GATEWAY_URL=http://localhost:8080 # ⚠️ OpenClaw Gateway令牌（敏感：仅在理解此功能时使用） export OPENCLAW_GATEWAY_TOKEN=your_gateway_token ``` ## 📦 依赖项 - Python 3.8+ - tavily-python（可选） - feedparser（必需） - requests（必需） - openai（可选，用于LLM评分） - anthropic（可选，用于LLM评分） 安装依赖： ```bash # 核心依赖 pip install feedparser requests # 完整依赖（推荐） pip install tavily-python feedparser requests openai anthropic ``` ## 🎯 评分机制说明 ### 四维度评分 1. **时效性 (25%)** - 6小时内：+25分 - 12小时内：+20分 - 24小时内：+15分 - 48小时内：+10分 - 72小时内：+5分 2. **源头权重 (25%)** - 官方/学术机构（权重≥9）：+25分 - 高质量媒体（权重≥7）：+15分 - 普通来源：+5分 3. **产业关联度 (25%)** - 底层技术突破：+25分 - 应用层创新：+15分 - 资讯八卦：+5分 4. **信息增量 (25%)** - 新技术参数/融资额/落地场景：+25分 - 有新信息：+15分 - 旧闻重发：+5分 ### 三级分类 - **≥80分**：自动推送（auto_push） - **60-80分**：待阅池（gray_zone）- 人工审核 - **<60分**：彻底拦截（filtered） ## 🔒 安全注意事项 1. **API Key安全**：不要将API Key硬编码在代码中，使用环境变量。所有API Keys和Webhook URL都是敏感信息，请妥善保管，不要提交到代码仓库。 2. **Feishu Webhook**：保护好Webhook URL，不要泄露给他人 3. **网络连接**：RSS源和API调用需要访问外网，请确保网络畅通 4. **定时任务**：使用OpenClaw Cron系统时需要配置Gateway URL。⚠️ OPENCLAW_GATEWAY_TOKEN特别敏感：此令牌可能允许调度/外部控制，仅在完全理解此功能后使用。 5. **数据存储**：data目录包含敏感信息，注意保密 ## 🐛 故障排除 ### Tavily API错误 - 检查 `TAVILY_API_KEY` 是否正确设置 - 确认API Key是否过期或额度用完 - 如果没有Tavily Key，使用 `--source rss` 仅使用RSS源 ### LLM评分失败 - 检查 `OPENAI_API_KEY` 或 `ANTHROPIC_API_KEY` 是否设置 - 确认API Key额度充足 - 评分器会自动降级到规则评分模式 ### RSS源获取失败 - 检查网络连接 - 确认RSS URL是否有效（可在浏览器中打开测试） - 某些RSS源可能有访问频率限制 ### Feishu推送失败 - 检查 `FEISHU_WEBHOOK_URL` 是否正确设置 - 确认Webhook URL是否有效 - 检查Feishu机器人是否有发送消息权限 ## 📚 版本历史 ### v2.2.0 (2026-03-07) - **重大升级**：智能产品价值评分系统 - 新增Arxiv、OpenAI博客等高质量信源 - 实现三级分类机制（自动推送、待阅池、拦截） - 新增人工反馈和迭代优化系统 - 新增数据持久化模块 - 新增管理工具脚本 ### v2.1.0 (2026-03-02) - 修复v2.0的所有问题 - Tavily API Key现在是可选的 - 新增Brave Search API支持 - RSS源无需API Key，始终可用 - 移除所有硬编码路径 - 支持真正的Feishu推送 ### v1.0.0 (2026-03-02) - 初始版本 - 支持Tavily API和RSS源 - 支持定时任务配置 - 支持推送到Feishu ## 📞 获取帮助 如果在使用过程中遇到问题，可以： 1. 查看故障排除指南 2. 检查环境变量配置 3. 查看脚本输出错误信息 4. 查看 `OPTIMIZATION_GUIDE.md` 获取详细优化指南 5. 联系Skill作者获取支持 --- **🎉 感谢使用 AI News Pusher v2.2！**

path: requirements.txt
content: # AI News Pusher v3.0 - Dependencies

# Core dependencies (required)
feedparser>=6.0.0
requests>=2.25.0

# Optional dependencies for news sources
tavily-python>=0.3.0

# Optional dependencies for LLM scoring
openai>=1.0.0
anthropic>=0.20.0


path: scripts/data_storage.py
content: #!/usr/bin/env python3
"""
数据持久化模块 - 存储待阅池、标记数据和反馈
"""

import os
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional


class DataStorage:
    """数据存储管理器"""
    
    def __init__(self, data_dir: Optional[str] = None):
        """
        初始化数据存储
        
        Args:
            data_dir: 数据存储目录，默认为项目根目录下的 data 文件夹
        """
        if data_dir is None:
            script_dir = Path(__file__).parent.absolute()
            data_dir = str(script_dir.parent / 'data')
        
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(exist_ok=True)
        
        self.gray_zone_file = self.data_dir / 'gray_zone.json'
        self.feedback_file = self.data_dir / 'feedback.json'
        self.filtered_file = self.data_dir / 'filtered.json'
        self.pushed_file = self.data_dir / 'pushed.json'
        self.config_file = self.data_dir / 'config.json'
        
        self._init_files()
    
    def _init_files(self):
        """初始化数据文件"""
        for file_path in [self.gray_zone_file, self.feedback_file, 
                         self.filtered_file, self.pushed_file]:
            if not file_path.exists():
                with open(file_path, 'w', encoding='utf-8') as f:
                    json.dump([], f)
        
        if not self.config_file.exists():
            default_config = {
                'auto_push_threshold': 80,
                'gray_zone_min': 60,
                'gray_zone_max': 80,
                'created_at': datetime.now().isoformat()
            }
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(default_config, f, ensure_ascii=False, indent=2)
    
    def _load_json(self, file_path: Path) -> List[Dict]:
        """加载JSON文件"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            return []
    
    def _save_json(self, file_path: Path, data: List[Dict]):
        """保存JSON文件"""
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    
    def _get_news_id(self, news: Dict) -> str:
        """获取新闻唯一ID（使用URL）"""
        return news.get('url', '') or f"{news.get('title', '')}_{news.get('source', '')}"
    
    def save_to_gray_zone(self, news: Dict):
        """
        保存新闻到待阅池
        
        Args:
            news: 新闻字典（已评分）
        """
        gray_zone = self._load_json(self.gray_zone_file)
        
        news_id = self._get_news_id(news)
        existing = next((n for n in gray_zone if self._get_news_id(n) == news_id), None)
        
        if not existing:
            news['saved_at'] = datetime.now().isoformat()
            news['reviewed'] = False
            gray_zone.append(news)
            self._save_json(self.gray_zone_file, gray_zone)
    
    def batch_save_gray_zone(self, news_list: List[Dict]):
        """批量保存到待阅池"""
        for news in news_list:
            self.save_to_gray_zone(news)
    
    def get_gray_zone(self, limit: Optional[int] = None, only_unreviewed: bool = True) -> List[Dict]:
        """
        获取待阅池新闻
        
        Args:
            limit: 限制返回数量
            only_unreviewed: 只返回未审核的
        
        Returns:
            新闻列表
        """
        gray_zone = self._load_json(self.gray_zone_file)
        
        if only_unreviewed:
            gray_zone = [n for n in gray_zone if not n.get('reviewed', False)]
        
        gray_zone.sort(key=lambda x: x.get('score', 0), reverse=True)
        
        if limit:
            gray_zone = gray_zone[:limit]
        
        return gray_zone
    
    def review_gray_zone_news(self, news_id: str, action: str, manual_score: Optional[int] = None, 
                              notes: str = "") -> bool:
        """
        审核待阅池新闻
        
        Args:
            news_id: 新闻ID
            action: 操作类型 (approve, reject, escalate)
            manual_score: 人工评分
            notes: 备注
        
        Returns:
            是否成功
        """
        gray_zone = self._load_json(self.gray_zone_file)
        
        news_index = None
        for i, news in enumerate(gray_zone):
            if self._get_news_id(news) == news_id:
                news_index = i
                break
        
        if news_index is None:
            return False
        
        news = gray_zone.pop(news_index)
        news['reviewed'] = True
        news['review_action'] = action
        news['reviewed_at'] = datetime.now().isoformat()
        news['manual_score'] = manual_score
        news['review_notes'] = notes
        
        if action == 'approve':
            self._append_to_list(self.pushed_file, news)
            if manual_score is not None:
                self.save_feedback(news_id, news.get('score', 0), manual_score, notes)
        elif action == 'reject':
            self._append_to_list(self.filtered_file, news)
        elif action == 'escalate':
            news['score'] = manual_score if manual_score else news.get('score', 0) + 20
            self._append_to_list(self.pushed_file, news)
            if manual_score is not None:
                self.save_feedback(news_id, news.get('score', 0), manual_score, notes)
        
        self._save_json(self.gray_zone_file, gray_zone)
        return True
    
    def _append_to_list(self, file_path: Path, item: Dict):
        """向列表文件追加数据"""
        data = self._load_json(file_path)
        data.append(item)
        self._save_json(file_path, data)
    
    def save_feedback(self, news_id: str, original_score: int, manual_score: int, notes: str = ""):
        """保存反馈数据"""
        feedback = self._load_json(self.feedback_file)
        feedback.append({
            'news_id': news_id,
            'original_score': original_score,
            'manual_score': manual_score,
            'notes': notes,
            'timestamp': datetime.now().isoformat()
        })
        self._save_json(self.feedback_file, feedback)
    
    def get_feedback(self, limit: Optional[int] = None) -> List[Dict]:
        """获取反馈数据"""
        feedback = self._load_json(self.feedback_file)
        feedback.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
        if limit:
            feedback = feedback[:limit]
        return feedback
    
    def save_filtered(self, news_list: List[Dict]):
        """保存被过滤的新闻"""
        for news in news_list:
            news['filtered_at'] = datetime.now().isoformat()
            self._append_to_list(self.filtered_file, news)
    
    def get_filtered(self, days: int = 7, limit: Optional[int] = None) -> List[Dict]:
        """获取被过滤的新闻（用于抽检）"""
        filtered = self._load_json(self.filtered_file)
        
        cutoff_date = datetime.now() - datetime.timedelta(days=days)
        recent_filtered = [
            n for n in filtered 
            if datetime.fromisoformat(n.get('filtered_at', '2000-01-01')) > cutoff_date
        ]
        
        recent_filtered.sort(key=lambda x: x.get('filtered_at', ''), reverse=True)
        
        if limit:
            recent_filtered = recent_filtered[:limit]
        
        return recent_filtered
    
    def save_pushed(self, news_list: List[Dict]):
        """保存已推送的新闻"""
        for news in news_list:
            news['pushed_at'] = datetime.now().isoformat()
            self._append_to_list(self.pushed_file, news)
    
    def get_pushed(self, days: int = 7, limit: Optional[int] = None) -> List[Dict]:
        """获取已推送的新闻"""
        pushed = self._load_json(self.pushed_file)
        
        cutoff_date = datetime.now() - datetime.timedelta(days=days)
        recent_pushed = [
            n for n in pushed 
            if datetime.fromisoformat(n.get('pushed_at', '2000-01-01')) > cutoff_date
        ]
        
        recent_pushed.sort(key=lambda x: x.get('pushed_at', ''), reverse=True)
        
        if limit:
            recent_pushed = recent_pushed[:limit]
        
        return recent_pushed
    
    def get_stats(self) -> Dict:
        """获取统计数据"""
        return {
            'gray_zone_count': len(self._load_json(self.gray_zone_file)),
            'pushed_count': len(self._load_json(self.pushed_file)),
            'filtered_count': len(self._load_json(self.filtered_file)),
            'feedback_count': len(self._load_json(self.feedback_file))
        }
    
    def get_config(self) -> Dict:
        """获取配置"""
        try:
            with open(self.config_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            return {}
    
    def update_config(self, config_updates: Dict):
        """更新配置"""
        config = self.get_config()
        config.update(config_updates)
        config['updated_at'] = datetime.now().isoformat()
        with open(self.config_file, 'w', encoding='utf-8') as f:
            json.dump(config, f, ensure_ascii=False, indent=2)
    
    def clear_old_data(self, days: int = 30):
        """清理旧数据"""
        cutoff_date = datetime.now() - datetime.timedelta(days=days)
        
        for file_path in [self.pushed_file, self.filtered_file]:
            data = self._load_json(file_path)
            date_field = 'pushed_at' if file_path == self.pushed_file else 'filtered_at'
            
            filtered_data = [
                item for item in data
                if datetime.fromisoformat(item.get(date_field, '2000-01-01')) > cutoff_date
            ]
            
            self._save_json(file_path, filtered_data)


path: scripts/fetch_ai_news.py
content: #!/usr/bin/env python3
"""
AI新闻获取脚本 - 使用Tavily API或Brave API搜索最新AI资讯
支持仅RSS模式（无需API Key）
集成新闻价值评分和分类功能
"""

import os
import sys
import json
import argparse
from datetime import datetime, timedelta
from pathlib import Path

script_dir = Path(__file__).parent.absolute()
sys.path.insert(0, str(script_dir))

from news_scorer import NewsScorer
from data_storage import DataStorage


def get_api_key(service="tavily"):
    """
    获取API Key - 可选，不是必须的
    
    Args:
        service: "tavily" 或 "brave"
    
    Returns:
        str or None: API Key或None
    """
    if service == "tavily":
        return os.environ.get('TAVILY_API_KEY')
    elif service == "brave":
        return os.environ.get('BRAVE_API_KEY')
    return None


def search_ai_news_tavily(api_key, days=3, limit=10, query="AI artificial intelligence"):
    """
    使用Tavily搜索AI新闻
    
    Args:
        [REDACTED_SECRET] API Key
        days: 搜索最近N天的新闻
        limit: 返回新闻数量
        query: 搜索关键词
    
    Returns:
        list: 新闻列表
    """
    try:
        from tavily import TavilyClient
        client = TavilyClient([REDACTED_SECRET])
    except ImportError:
        print("错误: 请安装tavily-python: pip install tavily-python", file=sys.stderr)
        return []
    
    # 计算日期范围
    end_date = datetime.now()
    start_date = end_date - timedelta(days=days)
    
    # 搜索查询
    search_query = f"{query} {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
    
    # 执行搜索
    try:
        response = client.search(
            query=search_query,
            search_depth="advanced",
            include_answer=False,
            max_results=limit * 2,
            topic="news"
        )
    except Exception as e:
        print(f"Tavily搜索失败: {e}", file=sys.stderr)
        return []
    
    # 处理结果
    news_list = []
    for result in response.get('results', []):
        news_item = {
            'title': result.get('title', ''),
            'url': result.get('url', ''),
            'content': result.get('content', ''),
            'published_date': result.get('published_date', ''),
            'source': result.get('source', 'Tavily'),
            'source_type': 'tavily'
        }
        news_list.append(news_item)
        
        if len(news_list) >= limit:
            break
    
    return news_list


def search_ai_news_brave(api_key, days=3, limit=10, query="AI artificial intelligence"):
    """
    使用Brave Search API搜索AI新闻
    
    Args:
        api_key: Brave API Key
        days: 搜索最近N天的新闻
        limit: 返回新闻数量
        query: 搜索关键词
    
    Returns:
        list: 新闻列表
    """
    import requests
    
    # 计算日期范围
    end_date = datetime.now()
    start_date = end_date - timedelta(days=days)
    
    # 添加时间范围到查询
    search_query = f"{query} after:{start_date.strftime('%Y-%m-%d')} before:{end_date.strftime('%Y-%m-%d')}"
    
    headers = {
        "X-Subscription-Token": api_key,
        "Accept": "application/json"
    }
    
    params = {
        "q": search_query,
        "count": min(limit * 2, 20),  # Brave限制
        "search_lang": "en",
        "freshness": f"{days}d"  # 最近N天
    }
    
    try:
        response = requests.get(
            "https://api.search.brave.com/res/v1/web/search",
            headers=headers,
            params=params,
            timeout=30
        )
        response.raise_for_status()
        data = response.json()
        
        news_list = []
        for result in data.get("web", {}).get("results", []):
            news_item = {
                'title': result.get('title', ''),
                'url': result.get('url', ''),
                'content': result.get('description', ''),
                'published_date': result.get('age', ''),  # Brave返回的是相对时间
                'source': result.get('meta', {}).get('domain', 'Brave'),
                'source_type': 'brave'
            }
            news_list.append(news_item)
            
            if len(news_list) >= limit:
                break
        
        return news_list
        
    except Exception as e:
        print(f"Brave搜索失败: {e}", file=sys.stderr)
        return []


def format_news_output(news_list, output_format='json'):
    """
    格式化新闻输出
    
    Args:
        news_list: 新闻列表
        output_format: 输出格式 (json, text, markdown)
    """
    if output_format == 'json':
        print(json.dumps(news_list, ensure_ascii=False, indent=2))
    elif output_format == 'text':
        for i, news in enumerate(news_list, 1):
            print(f"\n[{i}] {news['title']}")
            print(f"    来源: {news['source']}")
            print(f"    链接: {news['url']}")
            if news.get('published_date'):
                print(f"    日期: {news['published_date']}")
            print(f"    摘要: {news['content'][:200]}...")
    elif output_format == 'markdown':
        print("# AI新闻资讯\n")
        for i, news in enumerate(news_list, 1):
            print(f"## {i}. {news['title']}\n")
            print(f"- **来源**: {news['source']}")
            print(f"- **链接**: [{news['url']}]({news['url']})")
            if news.get('published_date'):
                print(f"- **日期**: {news['published_date']}")
            print(f"\n{news['content']}\n")
            print("---\n")


def main():
    parser = argparse.ArgumentParser(
        description='AI新闻获取工具 - 支持Tavily、Brave、RSS多源，集成价值评分',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
使用示例:
  # 使用Tavily API（需要TAVILY_API_KEY）
  python3 fetch_ai_news.py --source tavily --limit 10
  
  # 使用Brave API（需要BRAVE_API_KEY）
  python3 fetch_ai_news.py --source brave --limit 10
  
  # 仅使用RSS源（无需API Key）
  python3 fetch_ai_news.py --source rss --limit 10
  
  # 多源聚合（Tavily + Brave + RSS）
  python3 fetch_ai_news.py --source all --limit 10
  
  # 启用评分和分类（需要OPENAI_API_KEY）
  python3 fetch_ai_news.py --source rss --limit 10 --enable-scoring
        '''
    )
    
    parser.add_argument('--limit', type=int, default=10, help='返回新闻数量 (默认10)')
    parser.add_argument('--days', type=int, default=3, help='搜索最近N天的新闻 (默认3)')
    parser.add_argument('--source', choices=['tavily', 'brave', 'rss', 'all'], default='all',
                        help='新闻源选择 (默认all，聚合所有可用源)')
    parser.add_argument('--format', choices=['json', 'text', 'markdown'], default='json',
                        help='输出格式 (默认json)')
    parser.add_argument('--output', type=str, help='输出到文件')
    parser.add_argument('--query', type=str, default='AI artificial intelligence',
                        help='搜索关键词 (默认: AI artificial intelligence)')
    parser.add_argument('--enable-scoring', action='store_true',
                        help='启用新闻价值评分和分类 (需要OPENAI_API_KEY)')
    parser.add_argument('--llm-provider', choices=['openai', 'anthropic'], default='openai',
                        help='LLM提供商 (默认openai)')
    
    args = parser.parse_args()
    
    news_list = []
    
    # 根据source参数选择新闻源
    if args.source in ['tavily', 'all']:
        [REDACTED_SECRET]'tavily')
        if [REDACTED_SECRET]"正在从 Tavily 获取新闻...")
            tavily_news = search_ai_news_tavily(api_key, days=args.days, limit=args.limit, query=args.query)
            news_list.extend(tavily_news)
            print(f"  Tavily: 获取了 {len(tavily_news)} 条新闻")
        else:
            if args.source == 'tavily':
                print("错误: 未设置 TAVILY_API_KEY 环境变量", file=sys.stderr)
                sys.exit(1)
            else:
                print("  Tavily: 跳过 (未设置 TAVILY_API_KEY)")
    
    if args.source in ['brave', 'all']:
        [REDACTED_SECRET]'brave')
        if [REDACTED_SECRET]"正在从 Brave 获取新闻...")
            brave_news = search_ai_news_brave(api_key, days=args.days, limit=args.limit, query=args.query)
            news_list.extend(brave_news)
            print(f"  Brave: 获取了 {len(brave_news)} 条新闻")
        else:
            if args.source == 'brave':
                print("错误: 未设置 BRAVE_API_KEY 环境变量", file=sys.stderr)
                sys.exit(1)
            else:
                print("  Brave: 跳过 (未设置 BRAVE_API_KEY)")
    
    if args.source in ['rss', 'all']:
        print("正在从 RSS 源获取新闻...")
        try:
            from news_sources import RSSSource
            rss_source = RSSSource()
            rss_news = rss_source.fetch(query=args.query, limit=args.limit, days=args.days)
            news_list.extend(rss_news)
            print(f"  RSS: 获取了 {len(rss_news)} 条新闻")
        except Exception as e:
            print(f"  RSS: 获取失败 - {e}", file=sys.stderr)
    
    if not news_list:
        print("错误: 未能从任何源获取新闻", file=sys.stderr)
        sys.exit(1)
    
    # 去重
    seen_urls = set()
    unique_news = []
    for news in news_list:
        url = news.get('url', '')
        if url and url not in seen_urls:
            seen_urls.add(url)
            unique_news.append(news)
    
    # 按日期排序
    def parse_date(news):
        date_str = news.get('published_date', '')
        if date_str:
            try:
                from datetime import datetime
                return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
            except:
                pass
        from datetime import datetime
        return datetime.min
    
    unique_news.sort(key=parse_date, reverse=True)
    
    print(f"\n总共获取 {len(unique_news)} 条不重复新闻")
    
    # 评分和分类
    categorized = None
    if args.enable_scoring:
        print("\n📊 开始新闻价值评分...")
        scorer = NewsScorer(llm_provider=args.llm_provider)
        categorized = scorer.score_batch(unique_news)
        
        storage = DataStorage()
        
        print(f"\n📋 评分结果:")
        print(f"  自动推送 (≥80分): {len(categorized['auto_push'])} 条")
        print(f"  待阅池 (60-80分): {len(categorized['gray_zone'])} 条")
        print(f"  已过滤 (<60分): {len(categorized['filtered'])} 条")
        
        # 保存到数据存储
        if categorized['gray_zone']:
            storage.batch_save_gray_zone(categorized['gray_zone'])
            print(f"  待阅池已保存")
        
        if categorized['filtered']:
            storage.save_filtered(categorized['filtered'])
            print(f"  已过滤新闻已保存")
        
        if categorized['auto_push']:
            storage.save_pushed(categorized['auto_push'])
            print(f"  自动推送新闻已保存")
        
        # 输出高质量新闻
        if categorized['auto_push']:
            print(f"\n🌟 高质量新闻 (自动推送):")
            for i, news in enumerate(categorized['auto_push'][:5], 1):
                print(f"  [{i}] {news['title'][:60]}... (评分: {news['score']})")
        
        # 使用评分后的新闻进行输出
        unique_news = categorized['auto_push'] + categorized['gray_zone']
    
    # 输出结果
    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            if args.format == 'json':
                json.dump(unique_news, f, ensure_ascii=False, indent=2)
            else:
                import io
                from contextlib import redirect_stdout
                
                output_buffer = io.StringIO()
                with redirect_stdout(output_buffer):
                    format_news_output(unique_news, args.format)
                f.write(output_buffer.getvalue())
        print(f"\n结果已保存到: {args.output}")
    else:
        format_news_output(unique_news, args.format)


if __name__ == '__main__':
    main()


path: scripts/news_manager.py
content: #!/usr/bin/env python3
"""
新闻管理脚本 - 用于人工审核待阅池、标记反馈和分析
"""

import os
import sys
import json
import argparse
from pathlib import Path

script_dir = Path(__file__).parent.absolute()
sys.path.insert(0, str(script_dir))

from data_storage import DataStorage
from news_scorer import NewsScorer


def print_news_summary(news, index=None):
    """打印新闻摘要"""
    prefix = f"[{index}] " if index is not None else ""
    print(f"\n{prefix}{'=' * 80}")
    print(f"标题: {news.get('title', '无标题')}")
    print(f"来源: {news.get('source', '未知')} (权重: {news.get('source_weight', 5)}, 分类: {news.get('source_category', 'unknown')})")
    print(f"链接: {news.get('url', '无链接')}")
    print(f"评分: {news.get('score', '未评分')} | 理由: {news.get('rationale', '')}")
    print(f"发布时间: {news.get('published_date', '未知')}")
    print(f"{'-' * 80}")
    print(f"内容: {news.get('content', '无内容')[:500]}...")
    print(f"{'=' * 80}\n")


def review_gray_zone(storage: DataStorage, limit: int = 10):
    """交互式审核待阅池"""
    gray_zone = storage.get_gray_zone(limit=limit, only_unreviewed=True)
    
    if not gray_zone:
        print("✅ 待阅池为空，没有需要审核的新闻")
        return
    
    print(f"📋 待审核新闻数量: {len(gray_zone)}")
    
    for i, news in enumerate(gray_zone, 1):
        print_news_summary(news, i)
        
        while True:
            action = input("操作 (a=批准, r=拒绝, e=提升, s=跳过, q=退出): ").strip().lower()
            
            if action == 'q':
                print("👋 退出审核")
                return
            elif action == 's':
                print("⏭️  跳过")
                break
            elif action in ['a', 'r', 'e']:
                manual_score = None
                notes = ""
                
                get_score = input("是否输入人工评分？(y/n, 默认n): ").strip().lower()
                if get_score == 'y':
                    try:
                        manual_score = int(input("请输入人工评分 (0-100): "))
                    except ValueError:
                        print("⚠️  无效的评分，使用默认值")
                
                get_notes = input("是否添加备注？(y/n, 默认n): ").strip().lower()
                if get_notes == 'y':
                    notes = input("请输入备注: ").strip()
                
                action_map = {'a': 'approve', 'r': 'reject', 'e': 'escalate'}
                news_id = news.get('url', '') or f"{news.get('title', '')}_{news.get('source', '')}"
                
                if storage.review_gray_zone_news(news_id, action_map[action], manual_score, notes):
                    print(f"✅ 操作成功: {action_map[action]}")
                else:
                    print("❌ 操作失败")
                break
            else:
                print("⚠️  无效操作，请重新输入")


def view_stats(storage: DataStorage):
    """查看统计数据"""
    stats = storage.get_stats()
    print("\n📊 统计数据")
    print(f"待审核: {stats['gray_zone_count']}")
    print(f"已推送: {stats['pushed_count']}")
    print(f"已过滤: {stats['filtered_count']}")
    print(f"反馈记录: {stats['feedback_count']}")
    
    config = storage.get_config()
    print(f"\n⚙️ 配置阈值")
    print(f"自动推送: ≥{config.get('auto_push_threshold', 80)}分")
    print(f"待阅池: {config.get('gray_zone_min', 60)}-{config.get('gray_zone_max', 80)}分")


def view_filtered(storage: DataStorage, days: int = 7, limit: int = 10):
    """查看被过滤的新闻（用于抽检）"""
    filtered = storage.get_filtered(days=days, limit=limit)
    
    if not filtered:
        print("✅ 最近没有被过滤的新闻")
        return
    
    print(f"🔍 最近 {days} 天被过滤的新闻 (显示前 {limit} 条):")
    for i, news in enumerate(filtered, 1):
        print_news_summary(news, i)


def view_feedback(storage: DataStorage, limit: int = 20):
    """查看反馈记录"""
    feedback = storage.get_feedback(limit=limit)
    
    if not feedback:
        print("✅ 暂无反馈记录")
        return
    
    print(f"\n💬 反馈记录 (显示前 {limit} 条):")
    for fb in feedback:
        print(f"\n{'-' * 60}")
        print(f"新闻ID: {fb.get('news_id', '')}")
        print(f"原始评分: {fb.get('original_score', 0)} → 人工评分: {fb.get('manual_score', 0)}")
        print(f"备注: {fb.get('notes', '')}")
        print(f"时间: {fb.get('timestamp', '')}")


def analyze_feedback(storage: DataStorage):
    """分析反馈并给出优化建议"""
    feedback = storage.get_feedback()
    
    if not feedback:
        print("⚠️  需要先收集一些反馈数据才能分析")
        return
    
    scorer = NewsScorer()
    for fb in feedback:
        scorer.add_feedback(fb['news_id'], fb['original_score'], fb['manual_score'], fb.get('notes', ''))
    
    analysis = scorer.analyze_feedback()
    
    print("\n📈 反馈分析")
    print(f"总反馈数: {analysis['total_feedback']}")
    print(f"平均评分差: {analysis['avg_score_diff']:.1f}")
    print(f"高估次数: {analysis['over_estimates']}")
    print(f"低估次数: {analysis['under_estimates']}")
    print(f"\n💡 建议: {analysis['recommendation']}")


def update_thresholds(storage: DataStorage):
    """更新评分阈值"""
    config = storage.get_config()
    
    print(f"当前配置:")
    print(f"自动推送阈值: {config.get('auto_push_threshold', 80)}")
    print(f"待阅池下限: {config.get('gray_zone_min', 60)}")
    print(f"待阅池上限: {config.get('gray_zone_max', 80)}")
    
    updates = {}
    
    try:
        val = input("\n新的自动推送阈值 (直接回车保持当前): ").strip()
        if val:
            updates['auto_push_threshold'] = int(val)
        
        val = input("新的待阅池下限 (直接回车保持当前): ").strip()
        if val:
            updates['gray_zone_min'] = int(val)
        
        val = input("新的待阅池上限 (直接回车保持当前): ").strip()
        if val:
            updates['gray_zone_max'] = int(val)
        
        if updates:
            storage.update_config(updates)
            print("✅ 配置已更新")
        else:
            print("⏭️  未做更改")
    except ValueError:
        print("❌ 无效的数值")


def main():
    parser = argparse.ArgumentParser(
        description='AI新闻管理工具 - 审核待阅池、管理反馈和分析',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  # 交互式审核待阅池
  python3 news_manager.py review
  
  # 查看统计数据
  python3 news_manager.py stats
  
  # 查看被过滤的新闻（抽检）
  python3 news_manager.py filtered --days 3 --limit 20
  
  # 查看反馈记录
  python3 news_manager.py feedback
  
  # 分析反馈
  python3 news_manager.py analyze
        '''
    )
    
    subparsers = parser.add_subparsers(dest='command', help='可用命令')
    
    # review命令
    review_parser = subparsers.add_parser('review', help='交互式审核待阅池')
    review_parser.add_argument('--limit', type=int, default=10, help='限制审核数量')
    
    # stats命令
    subparsers.add_parser('stats', help='查看统计数据')
    
    # filtered命令
    filtered_parser = subparsers.add_parser('filtered', help='查看被过滤的新闻（抽检）')
    filtered_parser.add_argument('--days', type=int, default=7, help='查看最近N天')
    filtered_parser.add_argument('--limit', type=int, default=10, help='限制显示数量')
    
    # feedback命令
    feedback_parser = subparsers.add_parser('feedback', help='查看反馈记录')
    feedback_parser.add_argument('--limit', type=int, default=20, help='限制显示数量')
    
    # analyze命令
    subparsers.add_parser('analyze', help='分析反馈数据')
    
    # config命令
    subparsers.add_parser('config', help='更新评分阈值配置')
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        return
    
    storage = DataStorage()
    
    if args.command == 'review':
        review_gray_zone(storage, args.limit)
    elif args.command == 'stats':
        view_stats(storage)
    elif args.command == 'filtered':
        view_filtered(storage, args.days, args.limit)
    elif args.command == 'feedback':
        view_feedback(storage, args.limit)
    elif args.command == 'analyze':
        analyze_feedback(storage)
    elif args.command == 'config':
        update_thresholds(storage)


if __name__ == '__main__':
    main()


path: scripts/news_scorer.py
content: #!/usr/bin/env python3
"""
新闻价值评分模块 - 基于 LLM 的产品价值打分系统
"""

import os
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple


class NewsScorer:
    """新闻价值评分器"""
    
    # Few-shot 学习样例
    FEW_SHOT_EXAMPLES = [
        {
            "news": {
                "title": "GPT-5 Technical Report: Multimodal Reasoning at Scale",
                "content": "OpenAI今日发布GPT-5技术报告，新增1T参数MoE架构，在MMLU上达到95.2%准确率...",
                "source": "openai_blog",
                "source_weight": 10,
                "published_date": (datetime.now() - timedelta(hours=2)).isoformat()
            },
            "expected_score": 95,
            "rationale": "OpenAI官方发布，24小时内首发，技术细节丰富，产业影响重大"
        },
        {
            "news": {
                "title": "RAG技术在企业知识库中的应用实践",
                "content": "本文详细介绍了检索增强生成在企业级知识库中的部署方案，包括向量数据库选型、提示词优化等...",
                "source": "huggingface_blog",
                "source_weight": 9,
                "published_date": (datetime.now() - timedelta(days=1)).isoformat()
            },
            "expected_score": 82,
            "rationale": "高质量社区来源，技术实践干货，有落地场景，信息增量大"
        },
        {
            "news": {
                "title": "AI创业公司获1000万美元种子轮融资",
                "content": "某AI创业公司今日宣布完成1000万美元种子轮融资，投资方包括多家知名VC...",
                "source": "techcrunch_ai",
                "source_weight": 8,
                "published_date": (datetime.now() - timedelta(hours=12)).isoformat()
            },
            "expected_score": 75,
            "rationale": "知名科技媒体，时效性强，有融资信息，但技术深度有限"
        }
    ]
    
    # 评分阈值
    AUTO_PUSH_THRESHOLD = 80
    GRAY_ZONE_MIN = 60
    GRAY_ZONE_MAX = 80
    
    def __init__(self, llm_provider: str = "openai"):
        """
        初始化评分器
        
        Args:
            llm_provider: LLM 提供商 (openai, anthropic, etc.)
        """
        self.llm_provider = llm_provider
        self.[REDACTED_SECRET])
        self.few_shot_examples = self.FEW_SHOT_EXAMPLES.copy()
        self.feedback_data: List[Dict] = []
        
    def _get_api_key(self) -> Optional[str]:
        """获取 API Key"""
        if self.llm_provider == "openai":
            return os.environ.get('OPENAI_API_KEY')
        elif self.llm_provider == "anthropic":
            return os.environ.get('ANTHROPIC_API_KEY')
        return None
    
    def _calculate_source_bonus(self, source_weight: int, source_category: str) -> int:
        """计算信源加分"""
        bonus = 0
        if source_weight >= 9:
            bonus += 15
        elif source_weight >= 7:
            bonus += 10
        elif source_weight >= 5:
            bonus += 5
        
        if source_category == 'official':
            bonus += 10
        elif source_category == 'academic':
            bonus += 8
        
        return min(bonus, 25)
    
    def _calculate_recency_score(self, published_date_str: str) -> int:
        """计算时效性得分"""
        try:
            if not published_date_str:
                return 0
            
            published_date = datetime.fromisoformat(published_date_str.replace('Z', '+00:00'))
            now = datetime.now()
            hours_diff = (now - published_date).total_seconds() / 3600
            
            if hours_diff <= 6:
                return 25
            elif hours_diff <= 12:
                return 20
            elif hours_diff <= 24:
                return 15
            elif hours_diff <= 48:
                return 10
            elif hours_diff <= 72:
                return 5
            return 0
        except:
            return 0
    
    def _extract_keywords(self, content: str) -> List[str]:
        """提取技术关键词"""
        tech_keywords = [
            'transformer', 'moe', 'rag', 'fine-tuning', 'pretrain',
            'llama', 'gpt', 'claude', 'mistral', 'gemma',
            'mmlu', 'humaneval', 'benchmark', 'token', 'context window',
            'quantization', 'inference', 'training', 'dataset',
            'api', 'sdk', 'framework', 'model', 'architecture'
        ]
        content_lower = content.lower()
        found = [kw for kw in tech_keywords if kw in content_lower]
        return found
    
    def _build_scoring_prompt(self, news: Dict) -> str:
        """构建评分提示词"""
        few_shot_text = ""
        for i, example in enumerate(self.few_shot_examples, 1):
            few_shot_text += f"""
示例 {i}:
新闻标题: {example['news']['title']}
新闻内容: {example['news']['content'][:200]}...
信源: {example['news']['source']}
发布时间: {example['news']['published_date']}
预期得分: {example['expected_score']}
评分理由: {example['rationale']}
"""
        
        return f"""你是一位专业的AI行业资讯分析师，请为以下AI新闻进行"产品价值"打分（0-100分）。

评分维度：
1. 时效性 (25%) - 是否为24小时内首发消息
2. 源头权重 (25%) - 来自官方/学术机构还是普通媒体
3. 产业关联度 (25%) - 是底层技术突破还是应用层八卦
4. 信息增量 (25%) - 是否提供新参数/融资额/落地场景

{few_shot_text}

请评估以下新闻，只返回JSON格式，包含"score"（0-100整数）和"rationale"（评分理由）字段：

新闻标题: {news.get('title', '')}
新闻内容: {news.get('content', '')}
信源: {news.get('source', 'unknown')}
信源权重: {news.get('source_weight', 5)}
信源分类: {news.get('source_category', 'unknown')}
发布时间: {news.get('published_date', '')}
链接: {news.get('url', '')}
"""
    
    def _score_with_llm(self, news: Dict) -> Tuple[int, str]:
        """使用 LLM 评分"""
        if not self.[REDACTED_SECRET] self._score_with_rules(news)
        
        try:
            prompt = self._build_scoring_prompt(news)
            
            if self.llm_provider == "openai":
                from openai import OpenAI
                client = OpenAI([REDACTED_SECRET])
                response = client.chat.completions.create(
                    model="gpt-3.5-turbo",
                    messages=[
                        {"role": "system", "content": "你是一位专业的AI行业资讯分析师，只返回JSON格式。"},
                        {"role": "user", "content": prompt}
                    ],
                    temperature=0.3,
                    max_tokens=500
                )
                result_text = response.choices[0].message.content.strip()
            
            elif self.llm_provider == "anthropic":
                import anthropic
                client = anthropic.Anthropic([REDACTED_SECRET])
                response = client.messages.create(
                    model="claude-3-haiku-20240307",
                    max_tokens=500,
                    temperature=0.3,
                    messages=[{"role": "user", "content": prompt}]
                )
                result_text = response.content[0].text.strip()
            
            else:
                return self._score_with_rules(news)
            
            try:
                result = json.loads(result_text)
                score = max(0, min(100, int(result.get('score', 50))))
                rationale = result.get('rationale', '')
                return score, rationale
            except:
                return self._score_with_rules(news)
                
        except Exception as e:
            print(f"LLM 评分失败，使用规则评分: {e}")
            return self._score_with_rules(news)
    
    def _score_with_rules(self, news: Dict) -> Tuple[int, str]:
        """基于规则的备用评分方法"""
        score = 50
        rationale_parts = []
        
        recency_score = self._calculate_recency_score(news.get('published_date', ''))
        score += recency_score
        if recency_score > 0:
            rationale_parts.append(f"时效性+{recency_score}")
        
        source_bonus = self._calculate_source_bonus(
            news.get('source_weight', 5),
            news.get('source_category', 'unknown')
        )
        score += source_bonus
        if source_bonus > 0:
            rationale_parts.append(f"信源+{source_bonus}")
        
        keywords = self._extract_keywords(news.get('content', '') + news.get('title', ''))
        keyword_bonus = min(len(keywords) * 3, 15)
        score += keyword_bonus
        if keyword_bonus > 0:
            rationale_parts.append(f"技术关键词+{keyword_bonus}")
        
        score = max(0, min(100, score))
        rationale = " | ".join(rationale_parts) if rationale_parts else "规则评分"
        
        return score, rationale
    
    def score_news(self, news: Dict) -> Dict:
        """
        对单条新闻进行评分
        
        Args:
            news: 新闻字典
        
        Returns:
            包含评分信息的新闻字典
        """
        score, rationale = self._score_with_llm(news)
        
        category = self._classify_by_score(score)
        
        return {
            **news,
            'score': score,
            'rationale': rationale,
            'category': category,
            'scored_at': datetime.now().isoformat()
        }
    
    def _classify_by_score(self, score: int) -> str:
        """根据分数分类"""
        if score >= self.AUTO_PUSH_THRESHOLD:
            return 'auto_push'
        elif self.GRAY_ZONE_MIN <= score < self.GRAY_ZONE_MAX:
            return 'gray_zone'
        else:
            return 'filtered'
    
    def score_batch(self, news_list: List[Dict]) -> Dict[str, List[Dict]]:
        """
        批量评分并分类
        
        Args:
            news_list: 新闻列表
        
        Returns:
            分类后的新闻字典
        """
        auto_push = []
        gray_zone = []
        filtered = []
        
        for news in news_list:
            scored_news = self.score_news(news)
            category = scored_news['category']
            
            if category == 'auto_push':
                auto_push.append(scored_news)
            elif category == 'gray_zone':
                gray_zone.append(scored_news)
            else:
                filtered.append(scored_news)
        
        return {
            'auto_push': auto_push,
            'gray_zone': gray_zone,
            'filtered': filtered
        }
    
    def add_feedback(self, news_id: str, original_score: int, manual_score: int, notes: str = ""):
        """
        添加人工反馈，用于后续迭代
        
        Args:
            news_id: 新闻ID（可用URL）
            original_score: 原始评分
            manual_score: 人工评分
            notes: 备注
        """
        self.feedback_data.append({
            'news_id': news_id,
            'original_score': original_score,
            'manual_score': manual_score,
            'notes': notes,
            'timestamp': datetime.now().isoformat()
        })
    
    def add_few_shot_example(self, news: Dict, score: int, rationale: str):
        """
        添加新的 Few-shot 示例
        
        Args:
            news: 新闻内容
            score: 评分
            rationale: 理由
        """
        self.few_shot_examples.append({
            'news': news,
            'expected_score': score,
            'rationale': rationale
        })
    
    def analyze_feedback(self) -> Dict:
        """分析反馈数据，找出优化方向"""
        if not self.feedback_data:
            return {'status': 'no_feedback'}
        
        total_diff = 0
        over_estimates = 0
        under_estimates = 0
        
        for fb in self.feedback_data:
            diff = fb['manual_score'] - fb['original_score']
            total_diff += diff
            if diff > 10:
                under_estimates += 1
            elif diff < -10:
                over_estimates += 1
        
        avg_diff = total_diff / len(self.feedback_data)
        
        return {
            'total_feedback': len(self.feedback_data),
            'avg_score_diff': avg_diff,
            'over_estimates': over_estimates,
            'under_estimates': under_estimates,
            'recommendation': self._generate_recommendation(avg_diff, over_estimates, under_estimates)
        }
    
    def _generate_recommendation(self, avg_diff: float, over: int, under: int) -> str:
        """生成优化建议"""
        if abs(avg_diff) < 5:
            return "评分系统表现良好，继续保持"
        elif avg_diff > 10:
            return "建议：模型评分偏保守，可增加正向Few-shot示例"
        elif avg_diff < -10:
            return "建议：模型评分偏宽松，可增加负向Few-shot示例"
        elif over > under:
            return "建议：存在较多高估，检查信源权重或提示词"
        elif under > over:
            return "建议：存在较多低估，扩充技术关键词库"
        return "持续收集更多反馈数据"


path: scripts/push_to_feishu.py
content: #!/usr/bin/env python3
"""
AI新闻推送到Feishu脚本 - 支持真正的Webhook推送
"""

import os
import sys
import json
import argparse
import requests
from datetime import datetime
from pathlib import Path


def get_script_dir():
    """获取脚本所在目录（支持相对路径）"""
    return Path(__file__).parent.absolute()


def load_news_from_file(file_path):
    """从文件加载新闻"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        print(f"加载文件失败: {e}", file=sys.stderr)
        return []


def fetch_news_from_script(limit=8):
    """通过调用fetch_ai_news脚本获取新闻"""
    import subprocess
    try:
        script_dir = get_script_dir()
        result = subprocess.run(
            ['python3', str(script_dir / 'fetch_ai_news.py'), '--limit', str(limit), '--format', 'json'],
            capture_output=True,
            text=True,
            cwd=str(script_dir)
        )
        if result.returncode == 0:
            return json.loads(result.stdout)
        else:
            print(f"获取新闻失败: {result.stderr}", file=sys.stderr)
            return []
    except Exception as e:
        print(f"获取新闻失败: {e}", file=sys.stderr)
        return []


def fetch_news_multi_source(limit=8):
    """使用多源聚合获取新闻"""
    try:
        from news_sources import get_default_aggregator
        aggregator = get_default_aggregator()
        news = aggregator.fetch_all(query="AI artificial intelligence", limit=limit, days=3)
        return news
    except Exception as e:
        print(f"多源聚合失败，回退到单源: {e}", file=sys.stderr)
        return fetch_news_from_script(limit)


def format_feishu_message(news_list):
    """
    格式化Feishu消息
    
    Args:
        news_list: 新闻列表
    
    Returns:
        str: 格式化后的消息
    """
    if not news_list:
        return "暂无AI新闻"
    
    lines = []
    lines.append("📰 **AI新闻资讯**")
    lines.append(f"更新时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append(f"共 {len(news_list)} 条新闻\n")
    lines.append("---\n")
    
    for i, news in enumerate(news_list, 1):
        title = news.get('title', '无标题')
        url = news.get('url', '')
        content = news.get('content', '')[:200]
        source = news.get('source', '未知来源')
        date = news.get('published_date', '')
        source_type = news.get('source_type', '')
        
        # 来源标签
        source_tag = f"[{source}]"
        if source_type:
            source_tag = f"[{source_type.upper()}] {source_tag}"
        
        lines.append(f"**{i}. {title}**")
        lines.append(f"🔗 {url}")
        lines.append(f"📌 {source_tag}")
        if date:
            lines.append(f"📅 {date}")
        if content:
            lines.append(f"> {content}...")
        lines.append("")  # 空行
    
    lines.append("---")
    lines.append("🤖 由 OpenClaw AI 助手推送")
    
    return "\n".join(lines)


def send_to_feishu_webhook(message, webhook_url=None):
    """
    发送消息到Feishu Webhook
    
    Args:
        message: 消息内容（支持Markdown）
        webhook_url: Feishu Webhook URL，如果不提供则从环境变量获取
    
    Returns:
        bool: 是否发送成功
    """
    webhook_url = webhook_url or os.environ.get('FEISHU_WEBHOOK_URL')
    
    if not webhook_url:
        print("❌ 错误: 未设置 FEISHU_WEBHOOK_URL 环境变量", file=sys.stderr)
        print("提示: 在Feishu群聊中添加'自定义机器人'，获取Webhook URL", file=sys.stderr)
        return False
    
    # 构造Feishu消息
    payload = {
        "msg_type": "text",
        "content": {
            "text": message
        }
    }
    
    try:
        print(f"正在发送消息到Feishu...")
        response = requests.post(
            webhook_url,
            json=payload,
            headers={"Content-Type": "application/json"},
            timeout=30
        )
        response.raise_for_status()
        
        result = response.json()
        if result.get("code") == 0:
            print(f"✅ 消息发送成功！")
            return True
        else:
            print(f"❌ 发送失败: {result.get('msg')}", file=sys.stderr)
            return False
            
    except requests.exceptions.RequestException as e:
        print(f"❌ 网络请求失败: {e}", file=sys.stderr)
        return False
    except Exception as e:
        print(f"❌ 发送失败: {e}", file=sys.stderr)
        return False


def send_to_feishu(message, channel='feishu', use_webhook=True):
    """
    发送消息到Feishu
    
    Args:
        message: 消息内容
        channel: 渠道名称（默认feishu）
        use_webhook: 是否使用webhook发送（默认True）
    
    Returns:
        bool: 是否发送成功
    """
    print(f"消息已格式化，准备发送到 {channel}")
    print(f"消息长度: {len(message)} 字符")
    
    # 输出消息内容（调试用）
    print("\n=== 消息预览 ===")
    print(message[:500] + "..." if len(message) > 500 else message)
    print("=== 消息结束 ===\n")
    
    if use_webhook:
        # 使用webhook发送
        return send_to_feishu_webhook(message)
    else:
        # 仅打印消息（调试模式）
        print("提示: 使用 --use-webhook 参数或设置 FEISHU_WEBHOOK_URL 环境变量以实际发送消息")
        return True


def main():
    parser = argparse.ArgumentParser(
        description='AI新闻推送到Feishu - 支持真正的Webhook推送',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
使用示例:
  # 获取并推送新闻到Feishu（需要设置FEISHU_WEBHOOK_URL）
  python3 push_to_feishu.py --limit 8
  
  # 使用多源聚合
  python3 push_to_feishu.py --limit 10 --multi-source
  
  # 从文件加载新闻并推送
  python3 push_to_feishu.py --input news.json
  
  # 仅格式化消息，不发送（干运行模式）
  python3 push_to_feishu.py --limit 5 --dry-run
  
环境变量:
  FEISHU_WEBHOOK_URL    Feishu Webhook URL（必须设置才能实际发送）
        '''
    )
    
    parser.add_argument('--limit', type=int, default=8,
                        help='推送新闻数量 (默认8)')
    parser.add_argument('--input', type=str,
                        help='从文件加载新闻(JSON格式)')
    parser.add_argument('--multi-source', action='store_true',
                        help='使用多源聚合(Tavily+Brave+RSS)')
    parser.add_argument('--channel', type=str, default='feishu',
                        help='推送渠道 (默认feishu)')
    parser.add_argument('--dry-run', action='store_true',
                        help='仅格式化消息，不实际发送')
    parser.add_argument('--use-webhook', action='store_true',
                        help='使用Webhook发送（默认如果设置了FEISHU_WEBHOOK_URL则自动使用）')
    
    args = parser.parse_args()
    
    # 检查环境变量
    webhook_url = os.environ.get('FEISHU_WEBHOOK_URL')
    if not webhook_url and not args.dry_run:
        print("⚠️ 警告: 未设置 FEISHU_WEBHOOK_URL 环境变量", file=sys.stderr)
        print("提示: 使用 --dry-run 参数进行测试，或设置 FEISHU_WEBHOOK_URL 环境变量", file=sys.stderr)
        print("     在Feishu群聊中添加'自定义机器人'获取Webhook URL", file=sys.stderr)
    
    # 加载新闻
    if args.input:
        print(f"从文件加载新闻: {args.input}")
        news_list = load_news_from_file(args.input)
    else:
        print(f"获取新闻 (limit={args.limit}, multi-source={args.multi_source})...")
        if args.multi_source:
            news_list = fetch_news_multi_source(limit=args.limit)
        else:
            news_list = fetch_news_from_script(limit=args.limit)
    
    if not news_list:
        print("❌ 没有获取到新闻", file=sys.stderr)
        sys.exit(1)
    
    print(f"✓ 共获取 {len(news_list)} 条新闻")
    
    # 格式化消息
    message = format_feishu_message(news_list)
    
    if args.dry_run:
        print("\n=== 消息内容 (干运行模式) ===")
        print(message)
        print("\n=== 消息未发送 ===")
        print("提示: 使用 --use-webhook 参数或设置 FEISHU_WEBHOOK_URL 环境变量以实际发送消息")
    else:
        # 发送消息
        use_webhook = args.use_webhook or bool(webhook_url)
        success = send_to_feishu(message, channel=args.channel, use_webhook=use_webhook)
        if success:
            print(f"\n✅ 成功推送 {len(news_list)} 条新闻到 {args.channel}")
        else:
            print(f"\n❌ 推送失败", file=sys.stderr)
            sys.exit(1)


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)