
News Aggregator Skill
- 5.8k installs
- 1.2k repo stars
- Updated May 27, 2026
- cclank/news-aggregator-skill
news-aggregator-skill is an agent skill for Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face
About
Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI, Import AI, BBC, The Guardian, Al Jazeera, France 24, Reuters fallback, AI Newsletters, WallStreetCN, Weibo, 少数派, InfoQ 中文, Podcasts, and user-defined OPML feeds. Use when user requests 'daily scans', --- name: news-aggregator-skill description: "Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI, Import AI, BBC, The Guardian, Al Jazeera, France 24, Reuters fallback, AI Newsletters, WallStreetCN, Weibo, 少数派, InfoQ 中文, Podcasts, and user-defined OPML feeds. Use when user requests 'daily scans', 'tech news', 'finance updates', 'AI briefings', 'international news', 'deep analysis', or says '如意如意' to open the interactive menu." --- # News Aggregator Skill Fetch real-time hot news from 44+ sources (including international news + AI curated aggregators + user-defined OPML feeds), generate deep analysis reports in Chinese.
- News Aggregator Skill
- Multiple sources (comma-separated)
- All sources (broad scan)
- With keyword filter (auto-expand: "AI" → "AI,LLM,GPT,Claude,Agent,RAG")
- **Source**: 源名 | **Time**: 时间 | **Heat**: 🔥 热度值
News Aggregator Skill by the numbers
- 5,800 all-time installs (skills.sh)
- +30 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #125 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
news-aggregator-skill capabilities & compatibility
- Capabilities
- news aggregator skill · multiple sources (comma separated) · all sources (broad scan) · with keyword filter (auto expand: "ai" → "ai,llm · **source**: 源名 | **time**: 时间 | **heat**: 🔥 热度值
- Use cases
- documentation
What news-aggregator-skill says it does
Translate all content to **Simplified Chinese**.
### Step 3: Save & Present Save the report to `reports/YYYY-MM-DD/<source>_report.md`, then display the full content to the user.
--- ## 📰 Unified Report Template **All sources use this single template.** Show/hide optional fields based on data availability.
npx skills add https://github.com/cclank/news-aggregator-skill --skill news-aggregator-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.8k |
|---|---|
| repo stars | ★ 1.2k |
| Security audit | 2 / 3 scanners passed |
| Last updated | May 27, 2026 |
| Repository | cclank/news-aggregator-skill ↗ |
When should developers use news-aggregator-skill and what problem does it solve?
Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI,
Who is it for?
Developers working with news-aggregator-skill patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI,
What you get
Grounded guidance and workflows from SKILL.md for news-aggregator-skill.
- filtered news briefings
- deep analysis summaries
By the numbers
- Covers 44+ news and feed sources
- Includes OPML support for user-defined feeds
Files
News Aggregator Skill
Fetch real-time hot news from 44+ sources (including international news + AI curated aggregators + user-defined OPML feeds), generate deep analysis reports in Chinese.
---
🔄 Universal Workflow (3 Steps)
Every news request follows the same workflow, regardless of source or combination:
Step 1: Fetch Data
# Single source
python3 scripts/fetch_news.py --source <source_key> --no-save
# Multiple sources (comma-separated)
python3 scripts/fetch_news.py --source hackernews,github,wallstreetcn --no-save
# All sources (broad scan)
python3 scripts/fetch_news.py --source all --limit 15 --deep --no-save
# With keyword filter (auto-expand: "AI" → "AI,LLM,GPT,Claude,Agent,RAG")
python3 scripts/fetch_news.py --source hackernews --keyword "AI,LLM,GPT" --deep --no-saveStep 2: Generate Report
Read the output JSON and format every item using the Unified Report Template below. Translate all content to Simplified Chinese.
Step 3: Save & Present
Save the report to reports/YYYY-MM-DD/<source>_report.md, then display the full content to the user.
---
📰 Unified Report Template
All sources use this single template. Show/hide optional fields based on data availability.
#### N. [标题 (中文翻译)](https://original-url.com)
- **Source**: 源名 | **Time**: 时间 | **Heat**: 🔥 热度值
- **Links**: [Discussion](hn_url) | [GitHub](gh_url) ← 仅在数据存在时显示
- **Summary**: 一句话中文摘要。
- **Deep Dive**: 💡 **Insight**: 深度分析(背景、影响、技术价值)。Source-Specific Adaptations
Only the differences from the universal template:
| Source | Adaptation |
|---|---|
| Hacker News | MUST include [Discussion](hn_url) link |
| GitHub | Use 🌟 Stars for Heat, add Lang field, add #Tags in Deep Dive |
| Hugging Face | Use 🔥 +N upvotes for Heat, include [GitHub](url) if present, write 深度解读 (not just translate abstract) |
| Preserve exact heat text (e.g. "108万") | |
| AIHOT | summary 已是中文编辑稿,直接引用不要再翻译;Heat 字段为空也别造数据;保留 推荐理由 风格的一句话点评 |
| TLDR AI | 单条标题往往是多主题混合(Topic A 💻, Topic B ⚡, Topic C ⛪),拆成 bullet 列出每个主题;summary 是 HTML 段落,需要拆出每个主题对应的一两句概述 |
| Import AI | 周刊长文,标题形如 Import AI 458: 主题1; 主题2; 主题3。建议默认配 `--deep`,否则 RSS summary 只是开头几句;Deep Dive 直接提炼 Jack Clark 的核心观点而非平铺事实 |
| International News | MUST use the Unified Report Template for every item;只使用最近 24h RSS 条目,不用更早新闻 Smart Fill;英文标题与摘要翻译成简体中文,保留原始媒体名与链接;同一事件多家媒体重复时可合并观点但不能合并链接 |
| Reuters | reuters 使用 Google News RSS 的 site:reuters.com fallback;报告里保留 Reuters (Google News fallback) source,不要写成官方公开 RSS |
---
🛠️ Tools
fetch_news.py
| Arg | Description | Default |
|---|---|---|
--source | Source key(s), comma-separated. See table below. | all |
--limit | Max items per source | 15 |
--keyword | Comma-separated keyword filter | None |
--deep | Download article text for richer analysis | Off |
--save | Force save to reports dir | Auto for single source |
--outdir | Custom output directory | reports/YYYY-MM-DD/ |
Available Sources (44+ with user OPML)
| Category | Key | Name |
|---|---|---|
| Global News | hackernews | Hacker News |
36kr | 36氪 | |
wallstreetcn | 华尔街见闻 | |
tencent | 腾讯新闻 | |
weibo | 微博热搜 | |
v2ex | V2EX | |
producthunt | Product Hunt | |
github | GitHub Trending | |
| Tech Community (v2) | lobsters | Lobsters |
devto | Dev.to | |
| AI/Tech | huggingface | HF Daily Papers |
arxiv | arXiv (cs.AI/cs.CL/cs.LG, v2) | |
ai_newsletters | All AI Newsletters (aggregate) | |
bensbites | Ben's Bites | |
interconnects | Interconnects (Nathan Lambert) | |
oneusefulthing | One Useful Thing (Ethan Mollick) | |
chinai | ChinAI (Jeffrey Ding) | |
memia | Memia | |
aitoroi | AI to ROI | |
kdnuggets | KDnuggets | |
| Chinese (v2) | sspai | 少数派 |
infoq_cn | InfoQ 中文站(RSS 只给标题,推荐配 `--deep` 拿正文) | |
| AI Curated (v3) | aihot | AIHOT 中文 AI 精选(跨源 + 中文编辑稿) |
tldr_ai | TLDR AI 英文日刊 | |
import_ai | Import AI by Jack Clark 周刊(推荐 `--deep`) | |
| International News | international | 最近 24h 国际新闻聚合(BBC / Guardian / Al Jazeera / France 24 / Reuters fallback) |
bbc_top | BBC Top News (24h) | |
bbc_world | BBC World (24h) | |
bbc_chinese | BBC 中文 (24h) | |
guardian_world | The Guardian World (24h) | |
aljazeera | Al Jazeera (24h) | |
france24 | France 24 (24h) | |
reuters | Reuters via Google News RSS fallback (24h) | |
| Podcasts | podcasts | All Podcasts (aggregate) |
lexfridman | Lex Fridman | |
80000hours | 80,000 Hours | |
latentspace | Latent Space | |
| Essays | essays | All Essays (aggregate) |
paulgraham | Paul Graham | |
waitbutwhy | Wait But Why | |
jamesclear | James Clear | |
farnamstreet | Farnam Street | |
scottyoung | Scott Young | |
dankoe | Dan Koe | |
| Custom (v2) | user | Your OPML feeds (see below) |
自定义订阅源 (User OPML)
把你常看的 RSS/Atom 源写进 OPML,--source user 即可统一抓取。
1. 放置 OPML 文件(按优先级查找):
~/.config/news-aggregator/user_sources.opml(推荐,跨 skill 复用)<skill_root>/user_sources.opml(本仓库内)
2. 文件格式:标准 OPML 2.0,可直接从 Feedly / Inoreader / NetNewsWire 导出。参考 user_sources.opml.example:
<outline type="rss" text="Simon Willison" title="Simon Willison"
xmlUrl="https://simonwillison.net/atom/everything/" />只 xmlUrl 必填,其它可选。
3. 运行:python3 scripts/fetch_news.py --source user --limit 15
daily_briefing.py (Morning Routines)
Pre-configured multi-source profiles:
python3 scripts/daily_briefing.py --profile <profile>| Profile | Sources | Instruction File |
|---|---|---|
general | HN, 36Kr, GitHub, Weibo, PH, WallStreetCN | instructions/briefing_general.md |
finance | WallStreetCN, 36Kr, Tencent | instructions/briefing_finance.md |
tech | GitHub, HN, Product Hunt | instructions/briefing_tech.md |
social | Weibo, V2EX, Tencent | instructions/briefing_social.md |
ai_daily | HF Papers, AI Newsletters | instructions/briefing_ai_daily.md |
reading_list | Essays, Podcasts | (Use universal template) |
Workflow: Execute script → Read corresponding instruction file → Generate report following both the instruction file AND the universal template.
---
⚠️ Rules (Strict)
1. Language: ALL output in Simplified Chinese (简体中文). Keep well-known English proper nouns (ChatGPT, Python, etc.). 2. Time: MANDATORY field. Never skip. If missing in JSON, mark as "Unknown Time". Preserve "Real-time" / "Today" / "Hot" as-is. 3. Anti-Hallucination: Only use data from the JSON. Never invent news items. Use simple SVO sentences. Do not fabricate causal relationships. 4. Smart Keyword Expansion: When user says "AI" → auto-expand to "AI,LLM,GPT,Claude,Agent,RAG,DeepSeek". Similar expansions for other domains. 5. Smart Fill: If results < 5 items in a time window, supplement with high-value items from wider range. Mark supplementary items with ⚠️. Exception: International News sources are a hard 24h window; do not supplement with older items. 6. Save: Always save report to reports/YYYY-MM-DD/ before displaying.
---
📋 Interactive Menu
When the user says "如意如意" or asks for "menu/help":
1. Read templates.md 2. Display the menu 3. Execute the user's selection using the Universal Workflow above
---
Requirements
- Python 3.8+,
pip install -r requirements.txt - Playwright (for HF Papers & Ben's Bites):
playwright install chromium
1. Execute the daily briefing script for the specified profile (general, finance, tech, social, ai_daily, reading_list): // turbo python3 scripts/daily_briefing.py --profile {{profile}}
2. Read the generated JSON file from the default output directory reports/YYYY-MM-DD.
3. Read the relevant instruction file in instructions/ (e.g., briefing_general.md).
4. Generate the final Markdown report as per SKILL.md:
- Translate content to Simplified Chinese.
- Provide Deep Dive insights.
- Format using the strict template.
- Save to
reports/YYYY-MM-DD/{{profile}}_briefing_report.md.
5. Notify the user of completion.
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files from skill execution
*_stderr.log
*.log
*.png
*_debug.txt
# Generated Data and Reports
*.json
reports/
test_results/
# Playwright / Testing
playwright-report/
test-results/
# Environments
.env
.env.*
!.env.example
# IDEs
.idea/
.vscode/
*.swp
*.swo
# User-specific OPML feeds (keep the .example committed)
user_sources.opml
# Local agent helper links. Keep .agent/workflows committed.
.agent/skills/
Schedule Morning Global Scan
The goal is to automate a global news scan at 6:30 AM daily. Since the AI agent cannot wake up to process the data, we will implement a "headless" reporting pipeline that fetches data and generates a readable Markdown report automatically.
User Review Required
[!NOTE]
AI Analysis Limitation: The automated report will contained "raw" summaries fetched from the source. It will not contain the "Deep Dive" insights (Core Value, Inspiration, Scenarios) that I usually provide, as that requires my active intelligence.
Action Required: You currently need to approve the crontab command to install the schedule.Proposed Changes
Scripts
[NEW] daily_scan.sh
- A bash script acting as the entry point.
- Activates the Python virtual environment.
- Runs
fetch_news.pywith--source all --limit 15 --deep. - Pipes the output to a temporary JSON file.
- Calls
generate_basic_report.pyto convert JSON to Markdown. - Saves the report in a date-based subdirectory
reports/YYYY-MM-DD/with a timestamped filename.
[NEW] generate_basic_report.py
- A simple Python script.
- Reads JSON input from stdin or file.
- Formats it into the standard
SKILL.mdMarkdown style (excluding the AI Deep Dive section). - Outputs to stdout or file.
System Configuration
- Use
crontabto schedulescripts/daily_scan.shat 06:30 daily.
Verification Plan
Automated Execution Test
- Run
bash scripts/daily_scan.shmanually. - Verify a new file
reports/YYYY-MM-DD/global_scan_HHMM.mdis created. - Verify the content of the markdown file is readable and contains news from multiple sources.
Manual Verification
- Check
crontab -lafter installation to confirm the job is listed.
🧠 AI Deep Dive Instructions (AI 深度日报)
INPUT: JSON object withnewsletter_picks,huggingface_paperssections.
OUTPUT: A deep-dive AI report focusing on research, industry analysis, and key tech updates.
---
🎯 Focus Areas
1. SOTA Research: Deep analysis of top papers from Hugging Face. Focus on methodology and results. 2. Industry Analysis: Strategic insights from top AI newsletters (ChinAI, Memia, etc.).
⚠️ Anti-Laziness Protocol
1. Volume: Output MUST contain at least 15 items across all sections. 2. Depth: For papers and newsletters, provide 2-3 bullet points of analysis (Why it matters, Key takeaway).
📝 Report Structure
Part 1: 🔬 SOTA Research (Hugging Face Papers)
- Data Source: Hugging Face Daily Papers
- Format:
#### 1. [Title (Translated)](url)
- **Source**: Hugging Face Papers | **Time**: Today
- **Summary**: One sentence summary of the paper's contribution.
- **Deep Dive**:
* **Innovation**: Key technical novelty (e.g., "New attention mechanism").
* **Impact**: Potential applications or performance gains.Part 2: 📧 Industry Insights (Newsletters)
- Data Source: AI Newsletters (ChinAI, Memia, etc.)
- Focus: Strategic shifts, policy changes, major product launches.
- Format:
#### 1. [Title (Translated)](url)
- **Source**: Newsletter Name | **Time**: X hours ago
- **Summary**: Concise overview of the newsletter topic.
- **Insight**: 💡 Strategic implication or key takeaway.💰 Daily Finance Briefing Instructions (今日财经早报)
INPUT: JSON object withmarket_overview,china_finance,global_finance,cryptosections.
OUTPUT: A professional financial analysis report.
---
🎯 Focus Areas
1. Market Sentiment: Bull/Bear signals, VIX, fear/greed indicators. 2. Policy Shifts: Central bank decisions (Fed/PBOC), extensive regulations. 3. Big Tech Earnings: FAANG, BAT earnings reports impacting the market. 4. Macro Trends: Inflation (CPI/PPI), Employment, GDP.
⚠️ Anti-Laziness Protocol
1. Volume: Output MUST contain at least 20 distinct items. 1. Volume: Output MUST contain at least 20 distinct items. 2. Synthesis: Do not just list headlines. Group them by "Macro", "Sector", "Crypto". 3. Deep Insight (CRITICAL): "Market Insight" must go beyond surface level.
- Bad: "This is good for AI."
- Good: "Signals a capital rotation from hardware to software layers; likely to boost SaaS valuations by 10-15% in Q3."
- Bad: "Fed raised rates."
- Good: "25bps hike was priced in, but the hawkish tone on 2025 cuts triggered a 2% drop in 10Y Treasuries, pressuring growth stocks."
📝 Report Structure
Part 1: 🌏 Macro & Market Overview (宏观与大盘)
- Data Source: WallstreetCN, Global Finance
- Format:
- Format (Strict 4-Line Block):
#### 1. [Title (Translated)](original_url)
> **Time**: 09:48 | **Impact**: 🔴 Bearish / 🟢 Bullish | **Heat**: 🔥 999
> **Summary**: Concise summary in Chinese.
> **Deep Dive**: 💡 **Insight**: [Deep analysis of market implications, quantitative impact, or connecting dots].Part 2: 🇨🇳 China Market Deep Dive (A股/港股/中概)
- Data Source: 36Kr Finance, Tencent Finance
- Focus: Regulatory changes, IPOs, Sector rotations.
Part 3: ₿ Crypto & Future Tech (加密与前沿)
- Data Source: Hacker News (Crypto keywords), WallstreetCN
- Focus: Bitcoin ETF, Web3 policy, major hacks or upgrades.
🌄 Daily Morning Briefing Instructions
This file defines how the AI Agent should construct the Daily Morning Briefing report.
INPUT: A large JSON object containingglobal_scan,hn_ai, andgithub_trendinglists.
OUTPUT: A single, comprehensive Markdown report.
---
⚠️ Anti-Laziness Protocol (STRICT)
1. Volume Target (REAL ITEMS ONLY): The input JSON usually contains ~60+ items.
- Target: Aim for 20-25 distinct items, but NEVER invent items to meet this number.
- Global Scan: Pick top 10-15 real items.
- HN AI: Pick top 5-8 real items.
- GitHub: Pick top 8-10 real items.
2. No Aggregation: Do NOT summarize multiple distinct news items into one bullet point. One Item = One Section. 3. Deep Dive & Linking:
- Hacker News: You MUST include
[Discussion](hn_url)next to the Source. - Context: Use the
contentfield for deep analysis.
---
📝 Report Structure
Part 1: 🌍 Global Scan (全网速览)
- Goal: Broad coverage of the most important news across all sources.
- Selection Logic:
- Prioritize "Breaking News" and "High Heat" items.
- Ensure diversity: Include at least 1 item from Finance, 1 from China Tech, 1 from Silicon Valley.
- Format:
- Format (Strict 4-Line List):
#### 1. [Title (Translated)](original_url)
- **Source**: XYZ | **Time**: 2h ago | **Heat**: 🔥 High
- **Hacker News**: [Discussion](hn_url) (REQUIRED if Source is HN)
- **Summary**: Concise summary in Chinese.
- **Deep Dive**: 💡 **Insight**: Context, impact, or why this matters.Part 2: 🦄 Hacker News AI Deep Dive (AI 深度读)
- Goal: Specific focus on AI/LLM technical discussions.
- Selection Logic:
- Focus on heavy technical discussions (HN comments link is mandatory).
- Look for "Show HN", "Launch", or controversial AI policy/ethics discussions.
- Format:
- Format (Strict 4-Line List):
#### 1. [Title](url)
- **Hacker News**: [Discussion](hn_url) | **Time**: 4h ago
- **Summary**: One sentence technical summary.
- **Deep Dive**: 💡 **Insight**: Technical breakdown or impact analysis.Part 3: 🐙 GitHub Trending (开源精选)
- Goal: Discovery of new, high-quality open source tools.
- Selection Logic:
- Prioritize new tools over established ones (e.g.
FlashMLA>React). - Format:
- Format (Strict 4-Line List):
#### 1. [Repo/Name](url)
- **Stats**: 🌟 Stars | **Lang**: Python | **Time**: Today
- **Summary**: What problem does it solve?
- **Deep Dive**: 💡 **Insight**: Why is it trending? (e.g. #RAG #LocalFirst)---
🎨 Tone & Style
- Language: Simplified Chinese (简体中文).
- Style: Professional, Insightful, "Tech Magazine" vibe.
- Objective: Make the user feel they have completely absorbed the world's most important tech information in 5 minutes.
👨💻 GitHub Trending Briefing Instructions
INPUT: JSON object with github_trending section.OUTPUT: A detailed report on top GitHub projects.
---
🎯 Focus Areas
1. Innovation: Why is this project trending? What problem does it solve? 2. Engineering: Code quality, architecture, performance (e.g. implementation details). 3. Impact: How does this affect the developer ecosystem?
⚠️ Protocol
1. Select Top 10: Filter the list to the most interesting 10 projects. 2. Verify: Ensure the summary accurately reflects the project's README content.
📝 Report Structure
🔝 Top Projects (热门项目)
For each project, use the following format:
N. Title
- Heat: 🔥 {stars} | Lang: {language}
- Summary: Concise description of the tool/library.
- Deep Dive:
- Core Value (核心价值): What makes this project unique or valuable? (e.g. Performance, DX, New Paradigm)
- Inspiration (启发思考): What can we learn from this? (e.g. Rust rewrite, Agentic workflow, Local-first)
- Scenarios (场景):
#{Keyword1}#{Keyword2}#{Keyword3}
🍉 Daily Social & Gossip Briefing Instructions (今日吃瓜早报)
INPUT: JSON object withweibo_hot,v2ex_hot,hn_culturesections.
OUTPUT: A fun, engaging summary of what the internet is discussing.
---
🎯 Focus Areas
1. Trending Topics: What is everyone arguing about today? (War, Politics, Gender, Work culture). 2. Big Dramas: Company scandals, Influencer drama, Public outcries. 3. Viral Memes: What's the new meme stock or internet slang?
⚠️ Anti-Laziness Protocol
1. Volume: Output MUST contain at least 25 items. The more the merrier. 2. Tone: Casual, witty, internet-native language (but keep it objective).
- Constraint: Logic > Wit. Do not sacrifice factual accuracy for a "smooth" sentence. Avoid "Although/But" unless there is a clear contradiction in the source.
3. Context: Explain why it's controversial for readers who are out of the loop.
📝 Report Structure
Part 1: 🔴 Weibo Hot Search (微博热搜)
- Focus: Social news, Entertainment, National events.
- Format (Strict 4-Line List):
#### 1. [Title](url)
- **Source**: Weibo | **Time**: Real-time | **Heat**: 🔥 2.5m
- **Summary**: Quick summary of the drama/event.
- **Deep Dive**: 💡 **Context**: Why are people angry/excited? (Background info).Part 2: 🤓 Geek Drama (极客圈吃瓜)
- Focus: V2EX debates (e.g., "Salary", "Layoffs", "Marriage").
- Goal: Show the human side of the tech industry.
🤖 Daily Tech Briefing Instructions (今日科技早报)
INPUT: JSON object withai_frontier,hard_tech,dev_tools,startupssections.
OUTPUT: A deep-tech newsletter for engineers and founders.
---
🎯 Focus Areas
1. AI/LLM SOTA: New models, papers, benchmarks (DeepSeek, OpenAI, Anthropic). 2. Dev Ecosystem: Major framework updates (React, Next.js, Rust, Python). 3. Hardcore Engineering: CUDA optimization, Distributed systems, Kernel updates.
⚠️ Anti-Laziness Protocol
1. Volume: Output MUST contain at least 15 Deep-Dive items. 2. No Fluff: Skip generic "AI is the future" PR articles. Focus on code, releases, and benchmarks.
📝 Report Structure
Part 1: 🚨 AI Frontier (AI 前沿)
- Data Source: Hacker News (AI keywords), GitHub (AI repos)
- This section requires the deepest analysis. Explain the innovation behind the news.
Part 2: 🛠️ Developer Tooling (开发者工具)
- Data Source: GitHub Trending, Product Hunt (DevTools)
- Format (Strict 4-Line List):
#### 1. [Title](url)
- **Source**: SourceName | **Time**: 1h ago
- **Summary**: Concise description of the tool/update.
- **Deep Dive**: 💡 **Insight**: Innovation point or developer impact.Part 3: 🦄 Startup Radar (硅谷与国内创投)
- Data Source: Product Hunt, 36Kr, Hacker News (Launch)
- New interesting products or funding rounds.
📝 Engineering Mistake Log (犯错笔记)
📌 Issue: WallStreetCN Timestamp Ambiguity (2026-01-24)
1. The Error (错误现场)
- Problem: The AI report stated "OpenAI Revenue... 1h ago", but the actual event was ~12h ago.
- My Code:
time_str = datetime.fromtimestamp(ts).strftime('%H:%M') if ts else ""
# Output: "09:35"- The Result: The AI received "Time: 09:35" at 21:30. Lacking the date, it possibly interpreted it as "09:35 PM" (Future?) or just "21:35" (Recent?), or generalized incorrectly. It calculated "1h ago" erroneously.
2. The Root Cause (根本原因)
- Data Loss: Converting a Unix timestamp (absolute) to
HH:MM(relative/cyclic) destroys crucial date information. - Context Dependent: "09:35" is only meaningful if you know it's today. If the news was from yesterday 09:35, the string is identical, making it impossible to distinguish "12h ago" from "36h ago".
3. The Fix (修复方案)
- Absolute Time: ALWAYS use full date-time format for machine processing.
time_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
# Output: "2026-01-24 09:35"- Lesson: Don't format for "Human Readability" in the raw data layer. Let the UI/Presentation layer (Markdown generation) decide how to display it ("1h ago"). The data layer must remain precise.
---
📌 Issue: Algolia API Query Failure (2026-01-24)
1. The Error (错误现场)
I attempted to construct a search query for the Algolia HN API by simply joining keywords with OR.
- My Code:
query_str = " OR ".join(keywords)
# Resulted in: "AI OR LLM OR Github Copilot OR OpenAI"- The Result: The API returned 0 hits for this query, despite plenty of data existing.
2. The Root Cause (根本原因)
- Syntax Ignorance: I assumed Algolia's search engine would intelligently handle multi-word phrases (like "Github Copilot") within a boolean string without quotes.
- Tokenization Ambiguity: In many search engines,
A OR B Cis interpreted asA OR (B AND C). The phraseGithub Copilot(with a space) broke theORchain logic or was malformed for the specificsearch_by_dateendpoint parser. - Lack of Verification: I deployed this change without a dedicated unit test for the query string construction, assuming "it's just a string join".
3. The Fix (修复方案)
- Quoted Phrases: Explicitly wrap multi-word terms in quotes.
passed_keywords = [f'"{k}"' if ' ' in k else k for k in keywords]
query_str = " OR ".join(passed_keywords)
# Result: "AI OR LLM OR \"Github Copilot\" OR OpenAI"- Fallback Mechanism: Added a "retry with simple query" logic. If the complex boolean query returns 0 hits, the script now automatically tries searching for just the first keyword (e.g., "AI") to ensure we get some relevant data rather than empty json.
4. Lessons Learned (教训)
1. Don't Guess APIs: Search API syntaxes (Solr, Elasticsearch, Algolia) vary subtly. Always verify complex boolean queries in a debug script first. 2. Empty Data is Dangerous: For an AI Agent, "Empty Data" is the mother of Hallucination. If a fetcher returns 0 items, the AI will often try to "help" by inventing them. 3. Fail Gracefully: Hard filters (complex queries) usually need a soft fallback (broad queries).
---
📌 Issue: AI Logical Phrasing Failure (2026-01-24)
1. The Error (错误现场)
- Text: "U23国足这次杀入决赛圈(虽然半决赛赢了)..."
- Problem: The connector "虽然" (although) creates a nonsensical contrast. Winning a semi-final is the cause of entering the final, not a contradiction.
- Why: The AI likely tried to be "chatty" or "nuanced" but failed, possibly attempting to say "Although it's just U23..." or "Although the final hasn't happened yet..." but truncated the thought.
2. The Fix (修复方案)
- Immediate: Manually corrected to "半决赛大胜晋级决赛" (Won semis, advanced to finals).
- Verification: User challenged "How do you know they won big?".
- Fact Check: Performed web search. Validated: China U23 beat Vietnam U23 3-0 on Jan 20th. (3-0 qualifies as "Big Win").
- Lesson: Grammar fixes are also Fact claims. When changing "Although A, B" to "Because A (Big), B", I added the qualifier "Big". Even if it turns out true, I should have verified before writing it to avoid being questioned.
3. Prevention (预防)
- Prompting: Direct the model to use simple, declarative sentences ("SVO") for news summaries to avoid logic traps like "Although/However" when the causal link is weak.
75: 76: --- 77: 78: ## 📌 Issue: Stale Cache Hallucination | 陈旧缓存误导 (2026-02-17) 79: 80: ### 1. The Error (错误现场) 81: - Problem: The AI generated a WallStreetCN report with dates from 2026-01-27, while the current date was 2026-02-17. 82: - Step: I ran the fetcher, but the report generator script was missing. I pro-actively searched the filesystem for "raw data" and found wallstreetcn_raw.json in the root directory. 83: - Result: The file was a legacy artifact from a month ago. I assumed it was fresh and used it for the report. 84: 85: ### 2. The Root Cause (根本原因) 86: - Heuristic Bias: "If a file exists in the root with a matching name, it's probably the current output." This assumption is dangerous in long-lived or shared workspaces. 87: - Ignoring Primary Evidence: The CLI tool output actually contained the correct 02-17 data, but I preferred reading a "file" for stability during Markdown generation and chose the wrong one. 88: - SKILL.md Deviation: SKILL.md specifies that reports/data go to reports/YYYY-MM-DD/. I looked in the project root instead, which is not the standard output path for this skill. 89: 90: ### 3. Prevention (预防) 91: 1. CLI Output First: Always prioritze stdout of the current tool execution as the ultimate source of truth. 92: 2. Verify Timestamp: Before reading any "raw" or "cache" file, check its last modified time or internal date fields. 93: 3. Strict Path Adherence: Follow the data storage patterns defined in documentation (reports/ folder) rather than guessing where temporary files might be.
🗞️ News Aggregator Skill
   
全网科技/金融/AI深度新闻聚合助手,专为智能 Agent 打造的高效信息引擎。
---
✨ 核心特性
- 🌍 全网多源聚合:一站式覆盖跨越硅谷科技、中国创投、开源社区、金融市场、国际新闻以及顶级 AI 播客/硬核推文的 44+ 个高价值信源。
- 🔧 OPML 自定义订阅:内置 44+ 源覆盖不全时,在
user_sources.opml里新增一条带xmlUrl的 RSS/Atom 订阅项即可接入,兼容 Feedly / Inoreader 等 RSS 阅读器导出格式。未内置的媒体、机构博客和个人订阅源(如 NYT 中文)都可走这条路,详见user_sources.opml.example。 - 🚀 完美支持 OpenClaw:专为原生大模型 Agent 平台(如 OpenClaw、Code Agent)深度定制,即插即用,沉浸式体验信息流。
- 🆓 开箱即用 (Zero-Config):纯净抓取,无需配置任何第三方 API Key,告别繁琐的环境变量和额度焦虑。
- 🧠 AI 智能深度阅读 (Deep Fetch):智能穿透防爬虫机制(内置 Playwright 绕过 Cloudflare),抓取完整正文内容交给大模型过滤、提炼与总结。
- 📰 场景化早报 (Daily Briefings):内置多套场景预设(综合早报、财经早报、科技早报、吃瓜早报、AI深度日报),一键生成杂志级排版的 Markdown 中文报告。
- 🪄 魔法交互菜单:支持通过专属口令唤醒全局交互式菜单,告别繁琐长难句,只需输入序号即可指哪打哪。
---
📚 聚合信源图谱
系统现已覆盖全球 44+ 个主流高价值信息渠道,随取随用:
🎯 核心新闻源
- 全球科技:🦄 Hacker News (
hackernews), 🐱 Product Hunt (producthunt) - 开源进展:🐙 GitHub Trending (
github), 🤓 V2EX (v2ex) - 国内风控:🚀 36Kr (
36kr), 🐧 腾讯科技 (tencent) - 社会金融:🔴 微博热搜 (
weibo), 📈 华尔街见闻 (wallstreetcn) - AI 论文:🤗 Hugging Face Papers (
huggingface)
🆕 扩展源 (v2)
- 技术社区:🦞 Lobsters (
lobsters), 👩💻 Dev.to (devto) - 学术原文:📜 arXiv (
arxiv) —— cs.AI/CL/LG 最新提交 - 中文深度:📕 少数派 (
sspai), 💻 InfoQ 中文 (infoq_cn)
🎯 AI 精选聚合 (v3) —— 单源信息密度顶 5-10 个原始源
- 中文 AI 跨源精选:🔥 AIHOT (
aihot) —— 跨 X / IT 之家 / DeepMind / Anthropic / 各 AI 公司 newsroom 的中文编辑稿日更 - 英文日刊:📨 TLDR AI (
tldr_ai) —— 每日 5-10 主题摘要 - 英文周刊深度:📜 Import AI (
import_ai) —— Jack Clark(前 OpenAI/Anthropic 联创)独立思考
🌍 国际新闻源
- 国际聚合:🌍 International (
international) —— BBC / Guardian / Al Jazeera / France 24 / Reuters fallback 最近 24h 混合扫描 - BBC:BBC Top News (
bbc_top), BBC World (bbc_world), BBC 中文 (bbc_chinese) —— 最近 24h - 全球媒体:The Guardian World (
guardian_world), Al Jazeera (aljazeera), France 24 (france24) —— 最近 24h - Reuters fallback:Reuters (
reuters) —— 使用 Google News RSS 的site:reuters.com最近 24h 检索结果,适合无 Reuters Connect 账号时轻量追踪
🔧 自定义订阅
- 通用 OPML (
user) —— 在 OPML 里添加带xmlUrl的 RSS/Atom 订阅项即可接入
📧 AI 行业内参 (Newsletters & Creators)
- 🧪 Latent Space AINews (
latentspace_ainews) - (近期新增) - ChinAI (Jeffrey Ding) (
chinai) - Memia (Ben Reid) (
memia) - Ben's Bites (
bensbites) - One Useful Thing (Ethan Mollick) (
oneusefulthing) - Interconnects (Nathan Lambert) (
interconnects) - AI to ROI & KDnuggets 等...
✍️ 深度思考 & 播客
- 行业泰斗专栏: Paul Graham, James Clear, Wait But Why, Scott Young...
- 顶级硬核播客: Lex Fridman Podcast, Latent Space (swyx), 80,000 Hours...
---
📥 安装指南
第一步:安装到 Code Agent
选择以下任一方式将 Skill 添加到您的 Agent:
方法 A:使用 Openskills CLI (推荐)
会自动处理路径依赖和配置同步。
# 安装 skill
openskills install git@github.com:cclank/news-aggregator-skill.git
# 同步配置到 Agent
openskills sync方法 B:使用 NPX
直接从远程仓库添加。
npx skills add https://github.com/cclank/news-aggregator-skill方法 C:手动集成
git clone git@github.com:cclank/news-aggregator-skill.git YourProject/.claude/skills/news-aggregator-skill第二步:安装 Python 依赖
进入已安装的 Skill 目录,执行依赖安装(如果您的 Agent 足够聪明,可要求其自动配置):
cd YourProject/.claude/skills/news-aggregator-skill
pip install -r requirements.txt
playwright install chromium---
🚀 如何使用
1. 🔮 唤醒交互菜单 (推荐)
最简单、最迷人的使用方式,来自专属交互彩蛋,直接召唤智能指令菜单:
"news-aggregator-skill 如意如意"
系统将立即为您展示多达 53 个功能选项的精美列表,直接回复数字序号即可生成完美排版的今日大盘!
2. 🗣️ 自然语言触发
您也可以直接通过对话指定需求:
- 场景日报:"帮我跑一份 💰财经早报,看看今天华市有什么动静。"
- 深度穿透:"抓取 5 条最新的 GitHub 趋势,记得开启 Deep Fetch 深入阅读下他们的 README。"
- 硬核科研:"看看今天 HuggingFace 有什么新发的神仙论文?"
- 国际新闻:"抓取 BBC、Reuters 和 Al Jazeera 的今日国际新闻。"
- 自由组合:"帮我把 Hacker News, 华尔街见闻 和 微博热搜 今天的前十条揉在一起生成一个早报。"
- 自定义订阅:拷一份
user_sources.opml.example到user_sources.opml(或~/.config/news-aggregator/user_sources.opml),加自己想看的 RSS,运行python scripts/fetch_news.py --source user --limit 15
---
💡 开发与扩展
欢迎提交 PR 为框架接入新的全球优质信源。我们期望共建一个最纯净、最高效、抗干扰的防降智信息获取舱。
⭐ Star History

📝 License: MIT License
requests
beautifulsoup4
import json
import concurrent.futures
import sys
import os
from fetch_news import (
fetch_hackernews, fetch_github, fetch_producthunt,
fetch_weibo, fetch_36kr, fetch_tencent, fetch_v2ex, fetch_wallstreetcn,
fetch_huggingface_papers, fetch_ai_newsletters, fetch_podcasts, fetch_essays,
enrich_items_with_content
)
import argparse
# --- Profile Configurations ---
PROFILES = {
# 1. 综合早报 (General Morning Routine)
"general": {
"global_scan": {
"sources": [
(fetch_hackernews, 5, None),
(fetch_producthunt, 5, None),
(fetch_github, 5, None),
(fetch_weibo, 5, None),
(fetch_36kr, 5, None),
(fetch_tencent, 5, None),
(fetch_wallstreetcn, 5, None),
(fetch_v2ex, 5, None)
],
"enrich": True
},
"hn_ai": {
"sources": [(fetch_hackernews, 20, "AI,LLM,GPT,DeepSeek,Github Copilot,Claude,OpenAI")],
"enrich": True
},
"github_trending": {
"sources": [(fetch_github, 15, None)],
"enrich": True
}
},
# 2. 财经早报 (Finance)
"finance": {
"market_overview": {
"sources": [
(fetch_wallstreetcn, 30, None),
(fetch_hackernews, 10, "Economy,Inflation,Fed,Stock,Finance")
],
"enrich": True
},
"china_finance": {
"sources": [
(fetch_36kr, 20, "财报,营收,上市,IPO,基金,投资"),
(fetch_tencent, 15, "财经,股票,基金")
],
"enrich": True
},
"crypto": {
"sources": [
(fetch_hackernews, 15, "Bitcoin,Crypto,Ethereum,Blockchain,Web3,DeFi"),
(fetch_wallstreetcn, 10, "比特币,加密货币")
],
"enrich": True
}
},
# 3. 科技早报 (Tech)
"tech": {
"ai_frontier": {
"sources": [
(fetch_hackernews, 25, "AI,LLM,Transformer,Diffusion,Model,RAG"),
(fetch_github, 10, "AI,LLM,GPT")
],
"enrich": True
},
"dev_tools": {
"sources": [
(fetch_producthunt, 20, "Developer Tools,Coding,API"),
(fetch_github, 15, None)
],
"enrich": True
},
"startups": {
"sources": [(fetch_36kr, 20, "融资,首发,独角兽,创投"), (fetch_producthunt, 10, None)],
"enrich": True
}
},
# 4. 吃瓜早报 (Social/Gossip)
"social": {
"weibo_hot": {
"sources": [(fetch_weibo, 40, None)],
"enrich": False # No need for deep verify, just title/heat
},
"v2ex_hot": {
"sources": [(fetch_v2ex, 30, None)],
"enrich": True # Content is fun
}
},
# 5. GitHub Trending (Github Only)
"github": {
"github_trending": {
"sources": [(fetch_github, 20, None)],
"enrich": True
}
},
# 6. AI Daily (AI Deep Dive)
"ai_daily": {
"newsletter_picks": {
"sources": [(fetch_ai_newsletters, 100, None)], # Capture all (approx 30-40)
"enrich": True
},
"huggingface_papers": {
"sources": [(fetch_huggingface_papers, 20, None)],
"enrich": True
}
},
# 7. Reading List (Podcasts & Essays)
"reading_list": {
"essays": {
"sources": [(fetch_essays, 50, None)], # Capture all
"enrich": True
},
"podcasts": {
"sources": [(fetch_podcasts, 50, None)],
"enrich": False
},
"hn_deep": {
"sources": [(fetch_hackernews, 20, "blog,essay,philosophy,book")],
"enrich": True
}
}
}
def fetch_section(section_name, config):
print(f"[{section_name}] Starting fetch...", file=sys.stderr)
results = []
# Run source fetchers for this section in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
future_map = {}
for func, limit, kw in config["sources"]:
future = executor.submit(func, limit, kw)
future_map[future] = f"{func.__name__}"
for future in concurrent.futures.as_completed(future_map):
fname = future_map[future]
try:
items = future.result()
results.extend(items)
print(f"[{section_name}] {fname} returned {len(items)} items", file=sys.stderr)
except Exception as e:
print(f"[{section_name}] {fname} failed: {e}", file=sys.stderr)
# Enrich if requested
if config["enrich"] and results:
print(f"[{section_name}] Enriching content for {len(results)} items...", file=sys.stderr)
results = enrich_items_with_content(results, max_workers=10)
return results
def save_individual_sources(data, base_dir):
"""
Splits the aggregated data by 'source' and saves individual JSON files.
"""
if not os.path.exists(base_dir):
os.makedirs(base_dir)
source_map = {}
total_count = 0
# Flatten and grouping
for section, items in data.items():
for item in items:
src = item.get('source', 'Unknown')
# Sanitize filename
safe_name = "".join([c if c.isalnum() else "_" for c in src])
if safe_name not in source_map:
source_map[safe_name] = []
source_map[safe_name].append(item)
total_count += 1
# Save
print(f"Saving {len(source_map)} individual source files to {base_dir}...", file=sys.stderr)
for src, items in source_map.items():
fpath = os.path.join(base_dir, f"{src}.json")
with open(fpath, 'w', encoding='utf-8') as f:
json.dump(items, f, indent=2, ensure_ascii=False)
return list(source_map.keys())
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--profile', default='general', choices=PROFILES.keys(), help='Briefing Profile')
parser.add_argument('--outdir', help='Optional output directory for individual files')
parser.add_argument('--no-save', action='store_true', help='Skip saving JSON files to disk (only output to stdout)')
args = parser.parse_args()
config = PROFILES.get(args.profile, PROFILES['general'])
final_data = {}
# Fetch all sections
for section, sec_config in config.items():
final_data[section] = fetch_section(section, sec_config)
# Determine Output Directory
# Default to reports/YYYY-MM-DD
if args.outdir:
out_dir = args.outdir
else:
from datetime import datetime
today = datetime.now().strftime('%Y-%m-%d')
out_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'reports', today)
# Validating existence or creating is handled in save logic, but let's be explicitly safe
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Output result to stdout (for agent to read)
print(json.dumps(final_data, indent=2, ensure_ascii=False))
if not args.no_save:
# Save Unified JSON
unified_path = os.path.join(out_dir, f"{args.profile}_briefing_unified.json")
with open(unified_path, 'w', encoding='utf-8') as f:
json.dump(final_data, f, indent=2, ensure_ascii=False)
# Save Individual Sources
sources_saved = save_individual_sources(final_data, out_dir)
print(f"Saved unified report and {len(sources_saved)} individual source files to {out_dir}", file=sys.stderr)
else:
print(f"JSON output sent to stdout only (--no-save mode)", file=sys.stderr)
if __name__ == "__main__":
main()
import sys
from playwright.sync_api import sync_playwright
def inspect():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://huggingface.co/papers/2602.07026", timeout=30000)
# Print Abstract
# Look for abstract in common structures
print("--- PAGE TEXT ---")
# Find potential abstract
# Try generic selector for paragraphs
for p in page.query_selector_all("p"):
txt = p.inner_text()
if len(txt) > 200:
print(f"POTENTIAL ABSTRACT (p) Found (len {len(txt)})")
# Look for GitHub link
# Usually a button with text "GitHub" or href containing github.com
print("--- LINKS ---")
for a in page.query_selector_all("a[href*='github.com']"):
href = a.get_attribute('href')
text = a.inner_text()
print(f"GITHUB LINK: {text} -> {href}")
print("--- BUTTONS ---")
for btn in page.query_selector_all("button"):
txt = btn.inner_text()
print(f"BUTTON: {txt}")
# Look for upvotes (usually near title or top right)
# Often has an icon (heart or like) and a number
print("--- NUMBERS ---")
# Try finding elements with numbers
# This is tricky without specific class, but let's dump text of top elements
# Try specific heat selector if known (often div.rounded-xl containing number?)
browser.close()
# Try finding elements with class 'abstract'
for el in page.query_selector_all(".abstract, [class*='abstract']"):
print(f"ABSTRACT CLASS: {el.inner_text()}")
# Try specific HF structure often used
# Often it is a div covering the abstract
browser.close()
if __name__ == "__main__":
inspect()
import sys
import json
import time
from playwright.sync_api import sync_playwright
def fetch_bensbites():
results = []
with sync_playwright() as p:
try:
# Launch Chromium (headless)
browser = p.chromium.launch(headless=True)
# Use specific context with real UA
context = browser.new_context(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
)
page = context.new_page()
# Go to new Substack homepage
url = "https://www.bensbites.com/"
print(f"Navigating to {url}...", file=sys.stderr)
# Use the API endpoint for archive - much more reliable
api_url = "https://www.bensbites.com/api/v1/archive?sort=new&limit=12"
print(f"Fetching archive from API: {api_url}", file=sys.stderr)
try:
page.goto(api_url, timeout=60000)
# Substack API returns JSON often wrapped in a <pre> or just raw
content = page.inner_text("body")
data = json.loads(content)
# Substack API can return a list directly or a dict with 'posts'
posts = data if isinstance(data, list) else data.get('posts', [])
for post in posts:
results.append({
"source": "Ben's Bites",
"title": post.get('title'),
"url": f"https://www.bensbites.com/p/{post.get('slug')}",
"time": post.get('post_date', 'Recent').split('T')[0],
"summary": post.get('subtitle', 'AI News & Tools'),
})
if len(results) >= 5: break
except Exception as e:
print(f"API Fetch error: {e}. Falling back to DOM scraping...", file=sys.stderr)
# Fallback to homepage DOM scraping if API fails
try:
page.goto(url, timeout=60000, wait_until="domcontentloaded")
page.wait_for_timeout(5000)
links = page.query_selector_all('a')
seen_urls = set()
for link in links:
href = link.get_attribute('href')
if not href or "/p/" not in href: continue
full_url = href if href.startswith("http") else f"https://www.bensbites.com{href}"
if full_url in seen_urls: continue
seen_urls.add(full_url)
title_el = link.query_selector("h1, h2, h3, h4, .post-title")
link_text = title_el.inner_text().strip() if title_el else link.inner_text().strip()
if len(link_text) > 10:
results.append({
"source": "Ben's Bites",
"title": link_text,
"url": full_url,
"time": "Recent",
"summary": "AI News & Tools",
})
if len(results) >= 5: break
except: pass
if not results:
results.append({
"source": "Ben's Bites",
"title": "Ben's Bites (Latest)",
"url": url,
"time": "Today",
"summary": "Daily AI Digest. Content protected, visit link.",
})
browser.close()
except Exception as e:
# sys.stderr.write(f"Playwright Error: {str(e)}\n")
# Return a safe fallback instead of failing
if not results:
results.append({
"source": "Ben's Bites",
"title": "Ben's Bites (Visit Site)",
"url": "https://bensbites.beehiiv.com/",
"time": "Today",
"summary": "Unable to fetch content. Please verify on site.",
})
try:
if 'browser' in locals(): browser.close()
except: pass
print(json.dumps(results, indent=2))
if __name__ == "__main__":
fetch_bensbites()
import sys
import json
import argparse
from playwright.sync_api import sync_playwright
def fetch_content(url):
results = []
with sync_playwright() as p:
try:
# Launch Chromium (headless)
# Add args to look more like a real browser
browser = p.chromium.launch(
headless=True,
args=["--disable-blink-features=AutomationControlled"]
)
# Use specific context with real UA
context = browser.new_context(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
)
page = context.new_page()
# Go to the URL
# Wait until network is idle which might indicate challenge is solved or feed loaded
response = page.goto(url, timeout=30000, wait_until="networkidle")
# Get content
content = page.content()
# If it's an RSS feed rendered in browser, it might be wrapped in <pre> or just text
# Chrome often wraps XML in a style.
# Let's try to get innerText of body
body_text = page.inner_text("body")
# Print raw content for the caller (rss_parser) to handle?
# Or if we want to return JSON directly?
# The caller expects items.
# But here we are just bypassing the firewall.
# It's better if this script returns the RAW HTML/XML content to stdout,
# and let the python script parse it using the robust rss_parser logic.
print(content)
# Debugging info to stderr
title = page.title()
sys.stderr.write(f"Page Title: {title}\n")
if "Just a moment" in title or "Challenge" in title:
sys.stderr.write("Cloudflare Challenge Detected! Waiting longer...\n")
page.wait_for_timeout(10000)
content = page.content()
print(content) # Print again after wait? No, let's just update content variable.
browser.close()
except Exception as e:
sys.stderr.write(f"Playwright Error: {str(e)}\n")
# Don't exit 1, just let it return empty so we don't crash main script
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("url", help="URL to fetch")
args = parser.parse_args()
fetch_content(args.url)
import sys
import json
from playwright.sync_api import sync_playwright
import argparse
def fetch_papers(limit=10):
results = []
with sync_playwright() as p:
try:
# Launch Chromium (headless)
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Go to the Daily Papers page
try:
page.goto("https://huggingface.co/papers", timeout=60000, wait_until="domcontentloaded")
except:
print("Warning: Page Load Timeout. Attempting to extract partial content.", file=sys.stderr)
# Wait for articles to load
page.wait_for_selector("article", timeout=15000)
# Extract basic data first
articles = page.query_selector_all("article")
candidates = []
for art in articles:
if len(candidates) >= limit:
break
try:
# Title is usually in an h3
title_el = art.query_selector("h3")
if not title_el: continue
title = title_el.inner_text().strip()
# Link is in the h3 > a
link_el = title_el.query_selector("a")
if not link_el: continue
href = link_el.get_attribute("href")
if href.startswith("/"):
href = "https://huggingface.co" + href
candidates.append({
"title": title,
"url": href,
"heat": "Trending",
"summary": ""
})
except: continue
# Now fetch details for each candidate
for item in candidates:
try:
sys.stderr.write(f" Visiting details: {item['url']}\n")
try:
page.goto(item['url'], timeout=60000, wait_until="domcontentloaded")
except:
sys.stderr.write(" -> Detail Page Timeout (Partial/Continue)\n")
# 1. Abstract
abstract_text = ""
paragraphs = page.query_selector_all("p")
for p in paragraphs:
txt = p.inner_text()
if len(txt) > 200:
abstract_text = txt
break
if abstract_text:
item['summary'] = abstract_text
# 2. GitHub Link
gh_el = page.query_selector("a[href*='github.com']")
if gh_el:
item['github'] = gh_el.get_attribute("href")
sys.stderr.write(f" -> Found GitHub: {item['github']}\n")
# 3. Heat (Upvotes)
# Use JS to find button with +Number
heat_text = page.evaluate("""() => {
const btns = Array.from(document.querySelectorAll('button'));
const heatBtn = btns.find(b => /^\\+\\d+$/.test(b.innerText.trim()));
return heatBtn ? heatBtn.innerText.trim() : null;
}""")
if heat_text:
item['heat'] = heat_text
sys.stderr.write(f" -> Found Heat: {heat_text}\n")
except Exception as e:
item['summary'] = f"Failed to fetch details: {str(e)}"
results.append(item)
browser.close()
except Exception as e:
sys.stderr.write(f"Playwright Error: {str(e)}\n")
sys.exit(1)
print(json.dumps(results))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--limit", type=int, default=10, help="Number of papers to fetch")
args = parser.parse_args()
fetch_papers(limit=args.limit)
import argparse
import json
import requests
from bs4 import BeautifulSoup
import sys
import io
import time
import re
import concurrent.futures
import os
from datetime import datetime, timezone, timedelta
from email.utils import parsedate_to_datetime
import subprocess
# Windows console defaults to cp936/GBK; force UTF-8 so Chinese JSON output isn't mangled.
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except Exception:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# Headers for scraping to avoid basic bot detection
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
from bs4 import XMLParsedAsHTMLWarning
import warnings
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
def filter_by_hours(items, hours=24):
"""Keep only items published within the last N hours.
Items whose time cannot be parsed are kept (fail-open)."""
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
result = []
for item in items:
t = item.get('time', '')
try:
pub = parsedate_to_datetime(str(t))
if pub.tzinfo is None:
pub = pub.replace(tzinfo=timezone.utc)
if pub >= cutoff:
result.append(item)
except Exception:
result.append(item) # unparseable → keep
return result
def filter_items(items, keyword=None):
if not keyword:
return items
keywords = [k.strip() for k in keyword.split(',') if k.strip()]
pattern = '|'.join([r'\b' + re.escape(k) + r'\b' for k in keywords])
regex = r'(?i)(' + pattern + r')'
return [item for item in items if re.search(regex, item['title'])]
def fetch_url_content(url):
"""
Fetches the content of a URL and extracts text from paragraphs.
Truncates to 3000 characters.
"""
if not url or not url.startswith('http'):
return ""
try:
response = requests.get(url, headers=HEADERS, timeout=5)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# Remove script and style elements
for script in soup(["script", "style", "nav", "footer", "header"]):
script.extract()
# Get text
text = soup.get_text(separator=' ', strip=True)
# Simple cleanup
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = ' '.join(chunk for chunk in chunks if chunk)
return text[:3000]
except Exception:
return ""
def enrich_items_with_content(items, max_workers=10):
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_item = {executor.submit(fetch_url_content, item['url']): item for item in items}
for future in concurrent.futures.as_completed(future_to_item):
item = future_to_item[future]
try:
content = future.result()
if content:
item['content'] = content
except Exception:
item['content'] = ""
return items
# --- Source Fetchers ---
def fetch_hackernews(limit=5, keyword=None):
if keyword:
# Use Algolia API for keyword search (Much better recall for specific topics like "AI")
try:
# 24h window
timestamp_24h = int(time.time() - 24 * 3600)
# Query builder strategy
raw_keywords = [k.strip() for k in keyword.split(',')]
# 1. Try Complex Query with Quoted Phrases
# "Github Copilot" needs quotes in Algolia search string if mixed with OR
quoted_keywords = [f'"{k}"' if ' ' in k else k for k in raw_keywords]
query_str = " OR ".join(quoted_keywords)
api_url = f"http://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=created_at_i>{timestamp_24h}&hitsPerPage={limit*2}&query={requests.utils.quote(query_str)}"
data = requests.get(api_url, timeout=10).json()
hits = data.get('hits', [])
# 2. Level 2 Fallback: If 0 results, try just the first keyword (usually the most broad, e.g. "AI")
if not hits and raw_keywords:
simple_query = raw_keywords[0]
api_url_simple = f"http://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=created_at_i>{timestamp_24h}&hitsPerPage={limit*2}&query={requests.utils.quote(simple_query)}"
data = requests.get(api_url_simple, timeout=10).json()
hits = data.get('hits', [])
items = []
for hit in hits:
items.append({
"source": "Hacker News",
"title": hit.get('title'),
"url": hit.get('url') or f"https://news.ycombinator.com/item?id={hit['objectID']}",
"hn_url": f"https://news.ycombinator.com/item?id={hit['objectID']}",
"heat": f"{hit.get('points', 0)} points",
"time": "Today" # Algolia return is recent by definition of filter
})
# Only return if we actually found something.
# If we found nothing after all attempts, we might want to fall back to scraping frontpage
# but frontpage is unlikely to have keyword matches if deep search failed.
# However, returning [] is better than hallucinating.
return items[:limit]
except Exception as e:
print(f"HN Algolia failed: {e}", file=sys.stderr)
# Fallback to scraping logic below if API completely errors out (e.g. network/timeout)
pass
# Fallback / Default: Scrape Front Page
base_url = "https://news.ycombinator.com"
news_items = []
page = 1
max_pages = 5
while len(news_items) < limit and page <= max_pages:
url = f"{base_url}/news?p={page}"
try:
response = requests.get(url, headers=HEADERS, timeout=10)
if response.status_code != 200: break
except: break
soup = BeautifulSoup(response.text, 'html.parser')
rows = soup.select('.athing')
if not rows: break
page_items = []
for row in rows:
try:
id_ = row.get('id')
title_line = row.select_one('.titleline a')
if not title_line: continue
title = title_line.get_text()
link = title_line.get('href')
# Metadata
score_span = soup.select_one(f'#score_{id_}')
score = score_span.get_text() if score_span else "0 points"
# Age/Time
age_span = soup.select_one(f'.age a[href="item?id={id_}"]')
time_str = age_span.get_text() if age_span else ""
if link and link.startswith('item?id='): link = f"{base_url}/{link}"
page_items.append({
"source": "Hacker News",
"title": title,
"url": link,
"hn_url": f"{base_url}/item?id={id_}",
"heat": score,
"time": time_str
})
except: continue
news_items.extend(filter_items(page_items, keyword))
if len(news_items) >= limit: break
page += 1
time.sleep(0.5)
return news_items[:limit]
def fetch_weibo(limit=5, keyword=None):
# Use the PC Ajax API which returns JSON directly and is less rate-limited than scraping s.weibo.com
url = "https://weibo.com/ajax/side/hotSearch"
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://weibo.com/"
}
try:
response = requests.get(url, headers=headers, timeout=10)
data = response.json()
items = data.get('data', {}).get('realtime', [])
all_items = []
for item in items:
# key 'note' is usually the title, sometimes 'word'
title = item.get('note', '') or item.get('word', '')
if not title: continue
# 'num' is the heat value
heat = item.get('num', 0)
# Construct URL (usually search query)
# Web UI uses: https://s.weibo.com/weibo?q=%23TITLE%23&Refer=top
full_url = f"https://s.weibo.com/weibo?q={requests.utils.quote(title)}&Refer=top"
all_items.append({
"source": "Weibo Hot Search",
"title": title,
"url": full_url,
"heat": f"{heat}",
"time": "Real-time"
})
return filter_items(all_items, keyword)[:limit]
except Exception:
return []
def fetch_github(limit=5, keyword=None):
if keyword:
# Use GitHub Search for keywords
query = f"{keyword.split(',')[0]} sort:updated" # Use first kw as primary
url = f"https://github.com/search?q={requests.utils.quote(query)}&type=repositories"
# Note: GitHub Search page is hard to scrape due to login requirements (often).
# Fallback strat: Topics? "https://github.com/topics/{kw}?o=desc&s=updated"
topic_url = f"https://github.com/topics/{keyword.split(',')[0].strip()}?o=desc&s=updated"
try:
response = requests.get(topic_url, headers=HEADERS, timeout=10)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
items = []
for article in soup.select('article.border'):
# Topic page structure changes often, but let's try generic selector
h3 = article.select_one('h3 a')
# Actually standard topic page: <h3 class="f3"><a href="/user/repo">...
if not h3: continue
repo_link = h3['href'] # /user/repo
title = repo_link.strip('/')
link = "https://github.com" + repo_link
desc = ""
desc_div = article.select_one('.color-fg-muted')
if desc_div: desc = desc_div.get_text(strip=True)
items.append({
"source": "GitHub Trending",
"title": f"{title} - {desc}",
"url": link,
"heat": "Topic Match",
"time": "Updated recently"
})
if items: return items[:limit]
except: pass
# Default Trending
try:
response = requests.get("https://github.com/trending", headers=HEADERS, timeout=10)
except: return []
soup = BeautifulSoup(response.text, 'html.parser')
items = []
for article in soup.select('article.Box-row'):
try:
h2 = article.select_one('h2 a')
if not h2: continue
title = h2.get_text(strip=True).replace('\n', '').replace(' ', '')
link = "https://github.com" + h2['href']
desc = article.select_one('p')
desc_text = desc.get_text(strip=True) if desc else ""
# Stars (Heat)
# usually the first 'Link--muted' with a SVG star
stars_tag = article.select_one('a[href$="/stargazers"]')
stars = stars_tag.get_text(strip=True) if stars_tag else ""
items.append({
"source": "GitHub Trending",
"title": f"{title} - {desc_text}",
"url": link,
"heat": f"{stars} stars",
"time": "Today"
})
except: continue
return filter_items(items, keyword)[:limit]
def fetch_36kr(limit=5, keyword=None):
try:
response = requests.get("https://36kr.com/newsflashes", headers=HEADERS, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
items = []
for item in soup.select('.newsflash-item'):
title = item.select_one('.item-title').get_text(strip=True)
href = item.select_one('.item-title')['href']
time_tag = item.select_one('.time')
time_str = time_tag.get_text(strip=True) if time_tag else ""
items.append({
"source": "36Kr",
"title": title,
"url": f"https://36kr.com{href}" if not href.startswith('http') else href,
"time": time_str,
"heat": ""
})
return filter_items(items, keyword)[:limit]
except: return []
def fetch_v2ex(limit=5, keyword=None):
try:
# Hot topics json
data = requests.get("https://www.v2ex.com/api/topics/hot.json", headers=HEADERS, timeout=10).json()
items = []
for t in data:
# V2EX API fields: created, replies (heat)
replies = t.get('replies', 0)
created = t.get('created', 0)
# convert epoch to readable if possible, simpler to just leave as is or basic format
# Let's keep it simple
items.append({
"source": "V2EX",
"title": t['title'],
"url": t['url'],
"heat": f"{replies} replies",
"time": "Hot"
})
return filter_items(items, keyword)[:limit]
except: return []
def fetch_tencent(limit=5, keyword=None):
try:
url = "https://i.news.qq.com/web_backend/v2/getTagInfo?tagId=aEWqxLtdgmQ%3D"
data = requests.get(url, headers={"Referer": "https://news.qq.com/"}, timeout=10).json()
items = []
for news in data['data']['tabs'][0]['articleList']:
items.append({
"source": "Tencent News",
"title": news['title'],
"url": news.get('url') or news.get('link_info', {}).get('url'),
"time": news.get('pub_time', '') or news.get('publish_time', '')
})
return filter_items(items, keyword)[:limit]
except: return []
def fetch_wallstreetcn(limit=5, keyword=None):
try:
url = "https://api-one.wallstcn.com/apiv1/content/information-flow?channel=global-channel&accept=article&limit=30"
data = requests.get(url, timeout=10).json()
items = []
for item in data['data']['items']:
res = item.get('resource')
if res and (res.get('title') or res.get('content_short')):
ts = res.get('display_time', 0)
time_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M') if ts else ""
items.append({
"source": "Wall Street CN",
"title": res.get('title') or res.get('content_short'),
"url": res.get('uri'),
"time": time_str
})
return filter_items(items, keyword)[:limit]
except: return []
def fetch_producthunt(limit=5, keyword=None):
try:
# Using RSS for speed and reliability without API key
response = requests.get("https://www.producthunt.com/feed", headers=HEADERS, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
items = []
for entry in soup.find_all(['item', 'entry']):
title = entry.find('title').get_text(strip=True)
link_tag = entry.find('link')
url = link_tag.get('href') or link_tag.get_text(strip=True) if link_tag else ""
pubBox = entry.find('pubDate') or entry.find('published')
pub = pubBox.get_text(strip=True) if pubBox else ""
items.append({
"source": "Product Hunt",
"title": title,
"url": url,
"time": pub,
"heat": "Top Product" # RSS implies top rank
})
return filter_items(items, keyword)[:limit]
except: return []
# --- New Fetchers (RSS/API) ---
from rss_parser import fetch_rss_feed
# fetch_tldr_ai removed: all known feed URLs (feed.tldr.tech/ai, tldr.tech/ai/rss) return 404.
def fetch_huggingface_papers(limit=5, keyword=None):
items = []
# User requested a "Good Solution" without fallback.
# We use Playwright (which is installed) to bypass the SSL/fingerprinting connection issues.
# Logic extracted to scripts/fetch_hf_papers_playwright.py for reusability
try:
import subprocess
import sys
import os
# Locate the standalone script
script_path = os.path.join(os.path.dirname(__file__), "fetch_hf_papers_playwright.py")
# Run the playwright script in a subprocess
cmd = [sys.executable, script_path, "--limit", str(limit)]
# Increase timeout for detail fetch (10 pages * 5s = 50s + startup)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout)
for paper in data:
items.append({
"source": "HF Papers",
"title": paper['title'],
"url": paper['url'],
"github": paper.get('github', ''),
"heat": paper.get('heat', ''),
"time": datetime.now().strftime("%Y-%m-%d"), # Daily Papers are today's papers
"summary": paper.get('summary', '')
})
else:
print(f"HF Playwright Failed: {result.stderr}", file=sys.stderr)
except Exception as e:
print(f"HF Playwright Exception: {e}", file=sys.stderr)
return filter_items(items[:limit], keyword)
def fetch_latentspace_ainews(limit=5, keyword=None):
"""Fetch AINews daily roundups from Latent Space Substack RSS.
Filters for posts with [AINews] title prefix, separating them from podcast episodes."""
items = []
try:
response = requests.get("https://www.latent.space/feed", headers=HEADERS, timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
for entry in soup.find_all('item'):
title_tag = entry.find('title')
if not title_tag:
continue
title = title_tag.get_text(strip=True)
# Filter: only AINews posts (title starts with [AINews])
if not title.startswith('[AINews]'):
continue
# Extract link from guid (Substack RSS has link text empty, guid has the URL)
guid_tag = entry.find('guid')
link = guid_tag.get_text(strip=True) if guid_tag else ""
# Fallback: try link tag
if not link:
link_tag = entry.find('link')
if link_tag:
link = link_tag.get_text(strip=True) or (link_tag.get('href') or '')
# Publication date
pub_tag = entry.find('pubdate') or entry.find('published')
pub_date = pub_tag.get_text(strip=True) if pub_tag else ""
# Simplify date if possible
try:
from email.utils import parsedate_to_datetime
dt = parsedate_to_datetime(pub_date)
pub_date = dt.strftime('%Y-%m-%d')
except Exception:
pass
# Content snippet from description
desc_tag = entry.find('description')
content = ""
if desc_tag:
desc_html = desc_tag.get_text(strip=True)
desc_soup = BeautifulSoup(desc_html, 'html.parser')
content = desc_soup.get_text(separator=' ', strip=True)[:2000]
items.append({
"source": "Latent Space AINews",
"title": title,
"url": link,
"time": pub_date,
"heat": "Daily Roundup",
"content": content
})
except Exception as e:
print(f"Latent Space AINews fetch error: {e}", file=sys.stderr)
return filter_items(items[:limit], keyword)
# --- Extended Sources (v2): Lobsters / Dev.to / arXiv / Papers with Code / 少数派 / 即刻 / User OPML ---
def fetch_lobsters(limit=5, keyword=None):
"""Lobsters hottest stories via official JSON API."""
items = []
try:
data = requests.get("https://lobste.rs/hottest.json", headers=HEADERS, timeout=10).json()
for story in data:
tags = ",".join(story.get('tags', []))
items.append({
"source": "Lobsters",
"title": story.get('title', ''),
"url": story.get('url') or story.get('short_id_url') or story.get('comments_url', ''),
"comments_url": story.get('comments_url', ''),
"heat": f"{story.get('score', 0)} points",
"time": story.get('created_at', '')[:10] if story.get('created_at') else '',
"tags": tags,
})
except Exception as e:
print(f"Lobsters fetch error: {e}", file=sys.stderr)
return filter_items(items[:limit], keyword)
def fetch_devto(limit=5, keyword=None):
"""Dev.to top articles of the past 24h via official JSON API."""
items = []
try:
url = "https://dev.to/api/articles?top=1&per_page=30"
data = requests.get(url, headers=HEADERS, timeout=10).json()
for art in data:
tag_list = art.get('tag_list', [])
tags = ",".join(tag_list) if isinstance(tag_list, list) else str(tag_list)
items.append({
"source": "Dev.to",
"title": art.get('title', ''),
"url": art.get('url', ''),
"heat": f"{art.get('positive_reactions_count', 0)} reactions",
"time": (art.get('published_at') or '')[:10],
"summary": art.get('description', ''),
"tags": tags,
})
except Exception as e:
print(f"Dev.to fetch error: {e}", file=sys.stderr)
return filter_items(items[:limit], keyword)
def fetch_sspai(limit=5, keyword=None):
"""少数派 latest articles via RSS."""
return filter_items(fetch_rss_feed("https://sspai.com/feed", "少数派", limit * 2)[:limit], keyword)
# NOTE: Papers with Code (paperswithcode.com) was acquired/merged by Hugging Face
# and now 302-redirects to huggingface.co/papers/trending. Duplicates `huggingface` source.
# Removed from v2 sources. Use --source huggingface for trending papers.
def fetch_arxiv(limit=5, keyword=None, categories=None):
"""arXiv latest submissions in given CS categories via official Atom API.
arXiv 接口偶尔较慢,最多重试 3 次(timeout=45s,退避 2s/4s)。"""
cats = categories or ['cs.AI', 'cs.CL', 'cs.LG']
cat_query = '+OR+'.join(f'cat:{c}' for c in cats)
url = (
f"http://export.arxiv.org/api/query"
f"?search_query={cat_query}"
f"&sortBy=submittedDate&sortOrder=descending"
f"&max_results={limit * 2}"
)
items = []
from rss_parser import parse_rss_content
for attempt in range(3):
try:
response = requests.get(url, headers=HEADERS, timeout=45)
response.encoding = response.apparent_encoding or 'utf-8'
items = parse_rss_content(response.content, "arXiv", limit * 2)
if items:
break
except Exception as e:
print(f"arXiv attempt {attempt + 1}/3 error: {e}", file=sys.stderr)
if attempt < 2:
time.sleep(2 * (attempt + 1)) # 2s, 4s
return filter_items(items[:limit], keyword)
def fetch_infoq_cn(limit=5, keyword=None):
"""InfoQ 中文站最新文章 via RSS。"""
return filter_items(fetch_rss_feed("https://www.infoq.cn/feed.xml", "InfoQ 中文", limit * 2)[:limit], keyword)
def fetch_aihot(limit=15, keyword=None):
"""AIHOT (aihot.virxact.com) AI 精选聚合,跨源中文编辑稿,日更 ~50 条。
默认拉最近 24h 内容(日更源,50 条/天,取最多 limit 条)。"""
raw = fetch_rss_feed("https://aihot.virxact.com/rss", "AIHOT", max(limit * 4, 50))
items = filter_by_hours(raw, hours=24)
return filter_items(items[:limit], keyword)
def fetch_tldr_ai(limit=3, keyword=None):
"""TLDR AI 英文每日 AI 摘要,5-10 主题/期。
默认拉最近 48h(日刊时间戳为午夜 UTC,48h 确保任意时段都能拿到最新 1-2 期)。"""
raw = fetch_rss_feed("https://tldr.tech/api/rss/ai", "TLDR AI", limit * 4)
items = filter_by_hours(raw, hours=48)
return filter_items(items[:limit], keyword)
def fetch_import_ai(limit=2, keyword=None):
"""Import AI by Jack Clark(前 OpenAI/Anthropic 联创)周更深度评论。
默认拉最近 7 天(周刊,1 条 = 1 期 = 1 周),通常返回最新 1 期。"""
raw = fetch_rss_feed("https://importai.substack.com/feed", "Import AI", limit * 4)
items = filter_by_hours(raw, hours=168) # 7 days
return filter_items(items[:limit], keyword)
INTERNATIONAL_NEWS_SOURCES = [
("BBC Top News", "https://feeds.bbci.co.uk/news/rss.xml"),
("BBC World", "https://feeds.bbci.co.uk/news/world/rss.xml"),
("BBC Chinese", "https://feeds.bbci.co.uk/zhongwen/simp/rss.xml"),
("The Guardian World", "https://www.theguardian.com/world/rss"),
("Al Jazeera", "https://www.aljazeera.com/xml/rss/all.xml"),
("France 24", "http://www.france24.com/en/rss"),
]
REUTERS_GOOGLE_NEWS_RSS = (
"https://news.google.com/rss/search?"
"q=site%3Areuters.com%20when%3A1d&hl=en-US&gl=US&ceid=US%3Aen"
)
INTERNATIONAL_NEWS_MAX_AGE_HOURS = 24
def fetch_recent_rss_feed(url, source_name, limit=10, keyword=None, hours=INTERNATIONAL_NEWS_MAX_AGE_HOURS):
"""Fetch an RSS feed and keep only items from the recent time window."""
raw = fetch_rss_feed(url, source_name, max(limit * 4, 30))
items = filter_by_hours(raw, hours=hours)
return filter_items(items, keyword)[:limit]
def create_recent_rss_fetcher(url, name, hours=INTERNATIONAL_NEWS_MAX_AGE_HOURS):
def fetcher(limit=5, keyword=None):
return fetch_recent_rss_feed(url, name, limit, keyword, hours)
return fetcher
def fetch_reuters(limit=10, keyword=None):
"""Reuters public fallback via Google News RSS.
Reuters.com no longer exposes a reliable unauthenticated public RSS feed."""
items = fetch_rss_feed(
REUTERS_GOOGLE_NEWS_RSS,
"Reuters (Google News fallback)",
max(limit * 4, 30),
)
items = filter_by_hours(items, hours=INTERNATIONAL_NEWS_MAX_AGE_HOURS)
for item in items:
title = item.get('title', '')
if title.endswith(' - Reuters'):
item['title'] = title[:-10]
item['fallback'] = "Google News RSS search for site:reuters.com"
return filter_items(items, keyword)[:limit]
def fetch_international(limit=15, keyword=None):
"""Aggregate official international RSS feeds plus Reuters fallback."""
per_source = max(2, min(5, limit // 3))
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
tasks = [
(name, executor.submit(fetch_recent_rss_feed, url, name, per_source, keyword))
for name, url in INTERNATIONAL_NEWS_SOURCES
]
tasks.append(("Reuters", executor.submit(fetch_reuters, per_source, keyword)))
item_groups = []
for name, future in tasks:
try:
items = future.result()
if items:
item_groups.append(items)
except Exception as e:
print(f"International fetch error for {name}: {e}", file=sys.stderr)
merged = []
max_group_len = max((len(group) for group in item_groups), default=0)
for index in range(max_group_len):
for group in item_groups:
if index < len(group):
merged.append(group[index])
if len(merged) >= limit:
return merged
return merged
def fetch_user_feeds(limit=5, keyword=None):
"""Fetch user-defined RSS feeds from an OPML file.
Looks at ~/.config/news-aggregator/user_sources.opml first,
then <skill_root>/user_sources.opml."""
try:
# Add scripts/ to path for direct import
sys.path.insert(0, os.path.dirname(__file__))
from fetch_user_feeds import find_opml_file, parse_opml, fetch_all_feeds
opml_path = find_opml_file()
if not opml_path:
print("No OPML configured. See user_sources.opml.example", file=sys.stderr)
return []
feeds = parse_opml(opml_path)
if not feeds:
return []
items = fetch_all_feeds(feeds, limit_per_feed=3)
return filter_items(items[:limit], keyword)
except Exception as e:
print(f"User feeds error: {e}", file=sys.stderr)
return []
# --- Source Definitions (Global for Access) ---
AI_NEWSLETTER_SOURCES = [
# Bens Bites is protected by Cloudflare -> Use Playwright
("Ben's Bites", "https://www.bensbites.com/feed"),
("Interconnects", "https://www.interconnects.ai/feed"), # Fixed: needs www.
("One Useful Thing", "https://www.oneusefulthing.org/feed"),
# Removed: The Rundown (beehiiv feed 404), The Neuron (403 Forbidden)
("ChinAI", "https://chinai.substack.com/feed"),
("Memia", "https://memia.substack.com/feed"),
("AI to ROI", "https://ai2roi.substack.com/feed"),
("KDnuggets", "https://www.kdnuggets.com/feed"),
]
# ... (rest of sources)
def fetch_rss_with_playwright(url, source_name, limit=5):
"""Fallback fetcher using Playwright to bypass Cloudflare"""
try:
# Special handling for Ben's Bites which uses custom Homepage Scraper
if "Ben's Bites" in source_name:
script_path = os.path.join(os.path.dirname(__file__), "fetch_bensbites.py")
# No arguments needed, script hardcodes URL
cmd = [sys.executable, script_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
if result.returncode == 0:
try:
data = json.loads(result.stdout)
if not data: raise ValueError("Empty JSON")
return data
except Exception:
# Fallback for Ben's Bites if parsing fails
return [{
"source": "Ben's Bites",
"title": "Ben's Bites (Visit Site)",
"url": "https://bensbites.beehiiv.com/",
"time": "Today",
"summary": "Auto-fetch failed. Please verify on site.",
}]
else:
return [{
"source": "Ben's Bites",
"title": "Ben's Bites (Check Site)",
"url": "https://bensbites.beehiiv.com/",
"time": "Today",
"summary": "Fetch process failed.",
}]
# User generic Playwright script for all OTHER protected feeds
if result.returncode == 0:
from rss_parser import parse_rss_content
# Result stdout should be the HTML/XML content
return parse_rss_content(result.stdout, source_name, limit)
else:
print(f"Playwright fetch failed for {source_name}: {result.stderr}", file=sys.stderr)
return []
except Exception as e:
print(f"Playwright exception for {source_name}: {e}", file=sys.stderr)
return []
PODCAST_SOURCES = [
("Lex Fridman", "https://lexfridman.com/feed/podcast"),
# Removed: Cognitive Rev (megaphone.fm feed 404)
("80000 Hours", "https://feeds.transistor.fm/80-000-hours-podcast"),
("Latent Space", "https://latent.space/feed"),
]
ESSAY_SOURCES = [
("Wait But Why", "https://waitbutwhy.com/feed"),
("James Clear", "https://jamesclear.com/feed"),
("Farnam Street", "https://fs.blog/feed"),
("Paul Graham", "http://www.aaronsw.com/2002/feeds/pgessays.rss"),
("Scott Young", "https://www.scotthyoung.com/blog/feed/"),
("Dan Koe", "https://thedankoe.com/feed/"),
]
def fetch_ai_newsletters(limit=5, keyword=None):
"""Aggregate Fetcher for AI Newsletters"""
all_items = []
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(fetch_rss_feed, url, name, 3): name for name, url in AI_NEWSLETTER_SOURCES}
for future in concurrent.futures.as_completed(futures):
all_items.extend(future.result())
return filter_items(all_items, keyword)[:limit]
def fetch_podcasts(limit=5, keyword=None):
all_items = []
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(fetch_rss_feed, url, name, 3): name for name, url in PODCAST_SOURCES}
for future in concurrent.futures.as_completed(futures):
all_items.extend(future.result())
return filter_items(all_items, keyword)[:limit]
def fetch_essays(limit=5, keyword=None):
all_items = []
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(fetch_rss_feed, url, name, 3): name for name, url in ESSAY_SOURCES}
for future in concurrent.futures.as_completed(futures):
all_items.extend(future.result())
return filter_items(all_items, keyword)[:limit]
def create_single_rss_fetcher(url, name):
def fetcher(limit=5, keyword=None):
return filter_items(fetch_rss_feed(url, name, limit), keyword)[:limit]
return fetcher
def save_report(data, source_name, out_dir):
"""
Saves JSON and generates a simple Markdown report.
"""
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Sanitize source name for filename
safe_name = "".join([c if c.isalnum() else "_" for c in source_name]).lower()
timestamp = datetime.now().strftime("%H%M")
# 1. Save JSON
json_path = os.path.join(out_dir, f"{safe_name}_{timestamp}.json")
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return json_path
def main():
parser = argparse.ArgumentParser()
sources_map = {
'hackernews': fetch_hackernews, 'weibo': fetch_weibo, 'github': fetch_github,
'36kr': fetch_36kr, 'v2ex': fetch_v2ex, 'tencent': fetch_tencent,
'wallstreetcn': fetch_wallstreetcn, 'producthunt': fetch_producthunt,
# Aggregates
'huggingface': fetch_huggingface_papers,
'ai_newsletters': fetch_ai_newsletters, 'podcasts': fetch_podcasts,
'essays': fetch_essays,
# Standalone AI Sources
'latentspace_ainews': fetch_latentspace_ainews,
# Extended (v2): tech community / academic / Chinese deep-content / user OPML
'lobsters': fetch_lobsters,
'devto': fetch_devto,
'sspai': fetch_sspai,
'infoq_cn': fetch_infoq_cn,
'arxiv': fetch_arxiv,
# Curated AI aggregators (v3): one feed = many original sources, pre-curated
'aihot': fetch_aihot,
'tldr_ai': fetch_tldr_ai,
'import_ai': fetch_import_ai,
# International news (official RSS where available; Reuters uses a marked fallback)
'international': fetch_international,
'bbc_top': create_recent_rss_fetcher("https://feeds.bbci.co.uk/news/rss.xml", "BBC Top News"),
'bbc_world': create_recent_rss_fetcher("https://feeds.bbci.co.uk/news/world/rss.xml", "BBC World"),
'bbc_chinese': create_recent_rss_fetcher("https://feeds.bbci.co.uk/zhongwen/simp/rss.xml", "BBC Chinese"),
'guardian_world': create_recent_rss_fetcher("https://www.theguardian.com/world/rss", "The Guardian World"),
'aljazeera': create_recent_rss_fetcher("https://www.aljazeera.com/xml/rss/all.xml", "Al Jazeera"),
'france24': create_recent_rss_fetcher("http://www.france24.com/en/rss", "France 24"),
'reuters': fetch_reuters,
'user': fetch_user_feeds,
}
# Dynamic Registration of Sub-sources
# AI Newsletters
for name, url in AI_NEWSLETTER_SOURCES:
key = name.lower().replace(' ', '').replace("'", "")
# Check if this source needs Playwright
if "Ben's Bites" in name or "The Rundown" in name:
sources_map[key] = lambda limit=10, k=None, u=url, n=name: filter_items(fetch_rss_with_playwright(u, n, limit), k)[:limit]
else:
sources_map[key] = create_single_rss_fetcher(url, name)
# Podcasts
for name, url in PODCAST_SOURCES:
key = name.lower().replace(' ', '')
sources_map[key] = create_single_rss_fetcher(url, name)
# Essays
for name, url in ESSAY_SOURCES:
key = name.lower().replace(' ', '')
sources_map[key] = create_single_rss_fetcher(url, name)
parser.add_argument('--source', default='all', help='Source(s) to fetch from (comma-separated). Now supports sub-sources like "chinai", "paulgraham"')
parser.add_argument('--limit', type=int, default=10, help='Limit per source. Default 10')
parser.add_argument('--keyword', help='Comma-sep keyword filter')
parser.add_argument('--deep', action='store_true', help='Download article content for detailed summarization')
parser.add_argument('--save', action='store_true', help='Save output to reports directory (JSON + MD)')
parser.add_argument('--no-save', action='store_true', dest='no_save', help='Skip saving JSON files to disk (only output to stdout)')
parser.add_argument('--outdir', help='Custom output directory for saved reports')
parser.add_argument('--list-sources', action='store_true', help='List all available source keys')
args = parser.parse_args()
if args.list_sources:
print(f"{'Source Key':<20} | {'Source Name'}")
print("-" * 40)
for key in sorted(sources_map.keys()):
print(f"{key:<20}")
return
to_run = []
if args.source == 'all':
to_run = list(sources_map.values())
else:
requested_sources = [s.strip() for s in args.source.split(',')]
for s in requested_sources:
if s in sources_map: to_run.append(sources_map[s])
results = []
def run_fetchers(fetchers, limit, kw):
res = []
for func in fetchers:
try:
res.extend(func(limit, kw))
except: pass
return res
# Primary Fetch
results = run_fetchers(to_run, args.limit, args.keyword)
# Smart Fill Logic (Only if keyword is used and results are sparse)
MIN_ITEMS = 5
if args.keyword and len(results) < MIN_ITEMS:
sys.stderr.write(f"Smart Fill triggered: Found {len(results)} items, filling gaps...\n")
# Secondary Fetch (Broad, no keyword)
# We fetch enough to potentially fill the gap, limit=MIN_ITEMS is a safe bet for each source
fill_limit = MIN_ITEMS
fill_results = run_fetchers(to_run, limit=fill_limit, kw=None)
# Deduplicate and Append
existing_urls = {item.get('url') for item in results}
existing_titles = {item.get('title') for item in results}
for item in fill_results:
if len(results) >= MIN_ITEMS:
break
u = item.get('url')
t = item.get('title')
if u not in existing_urls and t not in existing_titles:
# Mark as smart fill
item['smart_fill'] = True
# Add warning to time field as per SKILL.md
if 'time' in item:
item['time'] = f"⚠️ {item['time']}"
results.append(item)
existing_urls.add(u)
existing_titles.add(t)
if args.deep and results:
sys.stderr.write(f"Deep fetching content for {len(results)} items...\n")
results = enrich_items_with_content(results)
print(json.dumps(results, indent=2, ensure_ascii=False))
# Save Report if requested or if running a single source (implicit convenience)
# Skip saving when --no-save is set (agent reads from stdout)
if not getattr(args, 'no_save', False) and (args.save or args.source != 'all'):
if args.outdir:
out_dir = args.outdir
else:
today = datetime.now().strftime('%Y-%m-%d')
out_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'reports', today)
md_file = save_report(results, args.source, out_dir)
sys.stderr.write(f"\n[Saved] Raw Data: {md_file} (Agent to process)\n")
if __name__ == "__main__":
main()
"""
用户自定义 RSS/Atom 订阅源聚合器。
读取 OPML 文件(兼容 Feedly / Inoreader / NetNewsWire 导出格式),
并发抓取每个订阅源,输出统一 JSON 到 stdout。
OPML 查找优先级:
1. ~/.config/news-aggregator/user_sources.opml
2. <skill_root>/user_sources.opml
用法:
python fetch_user_feeds.py --limit 15 --per-feed 3
"""
import sys
import os
import json
import argparse
import concurrent.futures
from xml.etree import ElementTree as ET
# 复用 rss_parser
sys.path.insert(0, os.path.dirname(__file__))
from rss_parser import fetch_rss_feed
def find_opml_file():
"""按优先级查找 OPML 文件,返回第一个存在的路径或 None。"""
candidates = [
os.path.expanduser("~/.config/news-aggregator/user_sources.opml"),
os.path.join(os.path.dirname(os.path.dirname(__file__)), "user_sources.opml"),
]
for path in candidates:
if os.path.exists(path):
return path
return None
def parse_opml(path):
"""解析 OPML,返回 [(name, xml_url), ...] 列表。"""
tree = ET.parse(path)
root = tree.getroot()
feeds = []
for outline in root.iter("outline"):
xml_url = outline.get("xmlUrl")
if not xml_url:
continue
name = outline.get("title") or outline.get("text") or xml_url
feeds.append((name, xml_url))
return feeds
def fetch_all_feeds(feeds, limit_per_feed=3):
"""并发抓取所有 feed,返回合并后的 items 列表。"""
all_items = []
if not feeds:
return all_items
max_workers = min(8, len(feeds))
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_name = {
executor.submit(fetch_rss_feed, url, name, limit_per_feed): name
for name, url in feeds
}
for future in concurrent.futures.as_completed(future_to_name):
name = future_to_name[future]
try:
items = future.result()
all_items.extend(items)
except Exception as e:
print(f"User feed '{name}' fetch error: {e}", file=sys.stderr)
return all_items
def main():
parser = argparse.ArgumentParser(
description="Fetch user-defined RSS feeds from OPML and emit JSON."
)
parser.add_argument("--opml", help="Override OPML file path")
parser.add_argument("--limit", type=int, default=15, help="Total max items returned")
parser.add_argument("--per-feed", type=int, default=3, help="Max items per individual feed")
args = parser.parse_args()
opml_path = args.opml or find_opml_file()
if not opml_path:
print("[]")
print(
"No OPML file found. Copy user_sources.opml.example to "
"~/.config/news-aggregator/user_sources.opml or to skill root.",
file=sys.stderr,
)
return
try:
feeds = parse_opml(opml_path)
except Exception as e:
print(f"Failed to parse OPML {opml_path}: {e}", file=sys.stderr)
print("[]")
return
if not feeds:
print("[]")
print(f"No <outline xmlUrl=...> entries in {opml_path}", file=sys.stderr)
return
print(f"Fetching {len(feeds)} feeds from {opml_path}...", file=sys.stderr)
items = fetch_all_feeds(feeds, args.per_feed)
print(json.dumps(items[:args.limit], ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
import json
import sys
def process_data():
try:
with open('reports/2026-02-02/general_data_raw.json', 'r') as f:
data = json.load(f)
# Global Scan
print("=== GLOBAL SCAN (Top 20) ===")
global_scan = data.get('global_scan', [])
# Sort by heat if available? The fetcher usually returns them in some order.
# Let's just take the first 20.
for i, item in enumerate(global_scan[:20]):
print(f"{i+1}. Source: {item.get('source')} | Time: {item.get('time')} | Heat: {item.get('heat')}")
print(f" Title: {item.get('title')}")
print(f" URL: {item.get('url')}")
if item.get('hn_url'):
print(f" HN URL: {item.get('hn_url')}")
print(f" Content Snippet: {item.get('content', '')[:200]}...")
print()
# HN AI
print("\n=== HN AI (Top 10) ===")
hn_ai = data.get('hn_ai', [])
for i, item in enumerate(hn_ai[:10]):
print(f"{i+1}. Time: {item.get('time')}")
print(f" Title: {item.get('title')}")
print(f" URL: {item.get('url')}")
print(f" HN URL: {item.get('hn_url')}")
print(f" Content Snippet: {item.get('content', '')[:200]}...")
print()
# GitHub Trending
print("\n=== GITHUB TRENDING (Top 12) ===")
gh_trend = data.get('github_trending', [])
for i, item in enumerate(gh_trend[:12]):
print(f"{i+1}. Time: {item.get('time')}")
print(f" Title: {item.get('title')}")
print(f" URL: {item.get('url')}")
# Stats are usually in title or content for github fetcher?
# Let's inspect content.
print(f" Content Snippet: {item.get('content', '')[:300]}...")
print()
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
process_data()
import sys
import requests
from bs4 import BeautifulSoup
import urllib3
import re
import time
from datetime import datetime
# Suppress insecure request warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def clean_text(text):
if not text: return ""
text = text.strip()
# html.parser leaves CDATA markers intact; strip them
text = re.sub(r'^\s*<!\[CDATA\[|\]\]>\s*$', '', text).strip()
return text
def parse_rss_content(content, source_name, limit=5):
"""
Parses RSS/Atom content string (XML or HTML) and returns items.
"""
try:
# Use html.parser which is built-in and lenient
soup = BeautifulSoup(content, 'html.parser')
items = []
entries = soup.find_all(['item', 'entry'])
for entry in entries:
# --- Title ---
title_tag = entry.find('title')
if not title_tag: continue
title = clean_text(title_tag.get_text())
# --- Link ---
link = ""
link_tag = entry.find('link')
if link_tag:
if link_tag.has_attr('href'):
link = link_tag['href']
elif link_tag.get_text(strip=True):
link = link_tag.get_text(strip=True)
if not link:
link = str(link_tag.next_sibling).strip()
if not link:
guid = entry.find('guid')
if guid and guid.get_text(strip=True).startswith('http'):
link = guid.get_text(strip=True)
# --- Time ---
pub = entry.find(['pubdate', 'published', 'updated', 'dc:date'])
time_str = clean_text(pub.get_text()) if pub else ""
# --- Summary / Content ---
content_encoded = entry.find('content:encoded')
description = entry.find('description')
summary = entry.find('summary')
content = entry.find('content')
raw_summary = ""
if content_encoded: raw_summary = content_encoded.get_text()
elif description: raw_summary = description.get_text()
elif summary: raw_summary = summary.get_text()
elif content: raw_summary = content.get_text()
soup_desc = BeautifulSoup(raw_summary, 'html.parser')
_summary_text = soup_desc.get_text(separator=' ', strip=True)
clean_summary = (_summary_text[:300] + "...") if len(_summary_text) > 300 else _summary_text
# --- Heat ---
heat = ""
comments = entry.find('slash:comments')
if comments:
heat = f"{comments.get_text(strip=True)} comments"
items.append({
"source": source_name,
"title": title,
"url": link,
"time": time_str,
"heat": heat,
"summary": clean_summary
})
if len(items) >= limit: break
return items
except Exception as e:
print(f"Content Parse failed: {e}", file=sys.stderr)
return []
def fetch_rss_feed(url, source_name, limit=5):
"""
Robust RSS/Atom fetcher using BeautifulSoup.
Handles various feed formats (RSS 2.0, Atom, etc.)
"""
# User-Agent is critical
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
}
last_error = None
for attempt in range(3):
try:
response = requests.get(url, headers=headers, timeout=15, verify=False)
response.raise_for_status()
response.encoding = response.apparent_encoding or 'utf-8'
return parse_rss_content(response.content, source_name, limit)
except Exception as e:
last_error = e
if attempt < 2:
time.sleep(1 + attempt)
print(f"RSS Fetch failed for {url}: {last_error}", file=sys.stderr)
return []
import subprocess
import os
import sys
import shutil
from datetime import datetime
def get_all_sources():
"""Retrieve all source keys from fetch_news.py --list-sources"""
cmd = [sys.executable, 'scripts/fetch_news.py', '--list-sources']
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error listing sources: {result.stderr}")
return []
lines = result.stdout.strip().split('\n')
sources = []
start_collecting = False
for line in lines:
if line.startswith('---'):
start_collecting = True
continue
if start_collecting and line.strip():
key = line.split('|')[0].strip()
if key:
sources.append(key)
return sources
def test_source(source, out_dir):
"""Run fetch_news.py for a single source"""
print(f"Testing {source}...", end=' ', flush=True)
cmd = [
sys.executable,
'scripts/fetch_news.py',
'--source', source,
'--limit', '1',
'--save',
'--outdir', out_dir
]
start_time = datetime.now()
try:
# Timeout after 30s to prevent hanging
result = subprocess.run(cmd, capture_output=True, text=True, timeout=45)
duration = (datetime.now() - start_time).total_seconds()
if result.returncode == 0:
# Check if file was actually created
# fetch_news prints "[Saved] Raw Data: ...JSON"
if "Raw Data:" in result.stderr:
print(f"✅ PASS ({duration:.1f}s)")
return True, duration, None
else:
# It might have returned empty list [] and not saved anything?
# fetch_news only saves if data is not empty?
# Let's check logic: "if args.save or ... md_file = save_report..."
# save_report writes file even if data is empty?
# "if not data: f.write('No items found.')" -> Yes.
# So if it didn't save, something else happened.
print(f"⚠️ EMPTY/NO-SAVE ({duration:.1f}s)")
return False, duration, "Script ran but no save message found"
else:
print(f"❌ FAIL ({duration:.1f}s)")
return False, duration, result.stderr.strip()
except subprocess.TimeoutExpired:
print("❌ TIMEOUT (45s)")
return False, 45, "Timeout"
except Exception as e:
print(f"❌ ERROR: {e}")
return False, 0, str(e)
def main():
# Setup
test_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'test_results')
if os.path.exists(test_dir):
shutil.rmtree(test_dir)
os.makedirs(test_dir)
print(f"Starting systematic source test...")
print(f"Output directory: {test_dir}\n")
sources = get_all_sources()
print(f"Found {len(sources)} sources to test.\n")
results = []
for source in sources:
success, duration, error = test_source(source, test_dir)
results.append({
'source': source,
'success': success,
'duration': duration,
'error': error
})
# Generate Summary
summary_path = os.path.join(test_dir, 'summary.md')
with open(summary_path, 'w', encoding='utf-8') as f:
f.write("# 🧪 News Source Test Report\n")
f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
passed = [r for r in results if r['success']]
failed = [r for r in results if not r['success']]
f.write(f"## Summary\n")
f.write(f"- **Total**: {len(results)}\n")
f.write(f"- **Passed**: {len(passed)}\n")
f.write(f"- **Failed**: {len(failed)}\n\n")
f.write("## ❌ Failures\n")
if not failed:
f.write("None! 🎉\n")
else:
for r in failed:
f.write(f"- **{r['source']}**: {r['error']}\n")
f.write("\n## 📋 Detailed Results\n")
f.write("| Source | Status | Duration | Note |\n")
f.write("|---|---|---|---|\n")
for r in results:
status = "✅ PASS" if r['success'] else "❌ FAIL"
error_msg = r['error'] if r['error'] else ""
f.write(f"| {r['source']} | {status} | {r['duration']:.1f}s | {error_msg} |\n")
print(f"\nTest passed: {len(passed)}/{len(results)}")
print(f"Report saved to: {summary_path}")
if __name__ == "__main__":
main()
🗞️ News Aggregator 指令菜单
请回复 序号 执行任务。所有报告均自动保存到 reports/YYYY-MM-DD/ 并以中文呈现。
---
🎯 核心新闻源
| # | 名称 | 命令 |
|---|---|---|
| 1 | 🦄 硅谷热点 (Hacker News) | --source hackernews |
| 2 | 🐙 开源趋势 (GitHub Trending) | --source github |
| 3 | 🚀 创投快讯 (36Kr) | --source 36kr |
| 4 | 🐱 产品猎人 (Product Hunt) | --source producthunt |
| 5 | 🤓 极客社区 (V2EX) | --source v2ex |
| 6 | 🐧 腾讯科技 (Tencent News) | --source tencent |
| 7 | 📈 华尔街见闻 (WallStreetCN) | --source wallstreetcn |
| 8 | 🔴 微博热搜 (Weibo) | --source weibo |
| 9 | 🤗 HF 每日论文 (Hugging Face) | --source huggingface |
---
📧 AI 行业内参
| # | 名称 | 命令 |
|---|---|---|
| 10 | 🧪 Latent Space AINews (swyx) | --source latentspace_ainews |
| 11 | ChinAI (Jeffrey Ding) | --source chinai |
| 12 | Memia (Ben Reid) | --source memia |
| 13 | Ben's Bites | --source bensbites |
| 14 | One Useful Thing (Ethan Mollick) | --source oneusefulthing |
| 15 | Interconnects (Nathan Lambert) | --source interconnects |
| 16 | AI to ROI | --source aitoroi |
| 17 | KDnuggets | --source kdnuggets |
| 18 | 🧠 全部 AI 内参聚合 | --source ai_newsletters --limit 3 |
---
✍️ 深度思考 & 播客
| # | 名称 | 命令 |
|---|---|---|
| 19 | Paul Graham | --source paulgraham |
| 20 | Wait But Why | --source waitbutwhy |
| 21 | James Clear | --source jamesclear |
| 22 | Farnam Street | --source farnamstreet |
| 23 | Scott Young | --source scottyoung |
| 24 | Dan Koe | --source dankoe |
| 25 | 📚 全部文章聚合 | --source essays --limit 3 |
| 26 | Lex Fridman Podcast | --source lexfridman |
| 27 | Latent Space (swyx) | --source latentspace |
| 28 | 80,000 Hours | --source 80000hours |
| 29 | 🎧 全部播客聚合 | --source podcasts --limit 3 |
---
☕️ 每日早报 (Daily Briefings)
| # | 名称 | 命令 |
|---|---|---|
| 30 | 🌅 综合早报 (General) | daily_briefing.py --profile general --no-save |
| 31 | 💰 财经早报 (Finance) | daily_briefing.py --profile finance --no-save |
| 32 | 🤖 科技早报 (Tech) | daily_briefing.py --profile tech --no-save |
| 33 | 🍉 吃瓜早报 (Social) | daily_briefing.py --profile social --no-save |
| 34 | 🧠 AI 深度日报 (AI Daily) | daily_briefing.py --profile ai_daily --no-save |
| 35 | 📚 深度阅读清单 | daily_briefing.py --profile reading_list --no-save |
---
🆕 扩展源 (v2)
| # | 名称 | 命令 |
|---|---|---|
| 36 | 🦞 Lobsters 技术深度 | --source lobsters |
| 37 | 👩💻 Dev.to 开发者热门 | --source devto |
| 38 | 📜 arXiv AI 最新论文 (cs.AI/CL/LG) | --source arxiv |
| 39 | 📕 少数派 (sspai) | --source sspai |
| 40 | 💻 InfoQ 中文 (软件工程/AI) | --source infoq_cn --deep |
---
🎯 AI 精选聚合 (v3) —— 二次精选,密度最高
这一档是别人的编辑团队替你筛过的 AI 高价值内容,单源信息密度顶 5-10 个原始信源。
| # | 名称 | 命令 |
|---|---|---|
| 41 | 🔥 AIHOT 中文 AI 精选(跨源 + 中文编辑稿) | --source aihot |
| 42 | 📨 TLDR AI(英文日刊,每天 5-10 主题摘要) | --source tldr_ai |
| 43 | 📜 Import AI by Jack Clark(英文周刊深度评论) | --source import_ai --deep |
| 44 | 🌐 AI 精选三件套(一次拉全) | --source aihot,tldr_ai,import_ai |
---
🔧 自定义订阅源
| # | 名称 | 命令 |
|---|---|---|
| 45 | 🔧 我的订阅源 (OPML) | --source user |
💡 首次使用 OPML(45):先cp user_sources.opml.example user_sources.opml,编辑里面的<outline xmlUrl="...">加自己的源;或从 Feedly/Inoreader 导出 OPML 覆盖即可。
---
🌍 国际新闻源
| # | 名称 | 命令 |
|---|---|---|
| 46 | 🌍 国际新闻聚合 (最近 24h) | --source international --limit 20 |
| 47 | 📰 BBC Top News (最近 24h) | --source bbc_top |
| 48 | 🌐 BBC World (最近 24h) | --source bbc_world |
| 49 | 🈶 BBC 中文 (最近 24h) | --source bbc_chinese |
| 50 | 🗞️ The Guardian World (最近 24h) | --source guardian_world |
| 51 | 🛰️ Al Jazeera (最近 24h) | --source aljazeera |
| 52 | 🇫🇷 France 24 (最近 24h) | --source france24 |
| 53 | 🧭 Reuters fallback (Google News RSS, 最近 24h) | --source reuters |
💡 输出与时间窗口:国际新闻源必须按统一报告模板输出;抓取只保留最近 24 小时内容,不用旧闻补位。reuters使用 Google News RSS 的site:reuters.com检索结果。Reuters 官方公开 RSS 不稳定;如有 Reuters Connect 账号,可把 authenticated RSS 放进 OPML。
---
🔀 自由组合
直接指定多个源,用逗号分隔:
hackernews,github,wallstreetcn例如:"帮我看看 HN 和 GitHub 今天有什么热点" → Agent 自动执行 --source hackernews,github
---
✨ 请输入序号 (1-53) 或源名组合来执行
<?xml version="1.0" encoding="UTF-8"?>
<!--
user_sources.opml.example - User-defined RSS/Atom feeds template.
Usage:
1. Copy this file to user_sources.opml in skill root,
or to ~/.config/news-aggregator/user_sources.opml
2. Edit the outline list below to add your own feeds
3. Run: python scripts/fetch_news.py source user limit 15
Or export an OPML from Feedly / Inoreader / NetNewsWire to replace this file.
Only xmlUrl is required; title/text are for display.
-->
<opml version="2.0">
<head>
<title>My News Aggregator Feeds</title>
</head>
<body>
<!-- ===== Enabled by default (verified reachable) ===== -->
<!-- Simon Willison's Weblog (English, LLM and engineering) -->
<outline type="rss" text="Simon Willison" title="Simon Willison"
xmlUrl="https://simonwillison.net/atom/everything/"
htmlUrl="https://simonwillison.net/" />
<!-- 阮一峰的网络日志 (Chinese tech weekly digest) -->
<outline type="rss" text="阮一峰的网络日志" title="阮一峰的网络日志"
xmlUrl="https://www.ruanyifeng.com/blog/atom.xml"
htmlUrl="https://www.ruanyifeng.com/blog/" />
<!-- ===== Disabled by default (require VPN from mainland China) ===== -->
<!-- Uncomment the ones you want. All confirmed live but blocked in CN. -->
<!--
BBC 中文 (already built in as --source bbc_chinese; keep as a custom-feed example only)
<outline type="rss" text="BBC 中文" title="BBC 中文"
xmlUrl="https://feeds.bbci.co.uk/zhongwen/simp/rss.xml"
htmlUrl="https://www.bbc.com/zhongwen/simp" />
纽约时报中文网 (NYT Chinese)
<outline type="rss" text="纽约时报中文网" title="纽约时报中文网"
xmlUrl="https://cn.nytimes.com/rss.html"
htmlUrl="https://cn.nytimes.com/" />
Hacker News Front Page (high-signal English tech)
<outline type="rss" text="Hacker News Front Page" title="Hacker News Front Page"
xmlUrl="https://hnrss.org/frontpage"
htmlUrl="https://news.ycombinator.com/" />
-->
</body>
</opml>
Related skills
How it compares
Pick this over single-site scrapers when you need multi-source tech, AI, and finance briefings with OPML extensibility rather than one publication's API.
FAQ
What does news-aggregator-skill do?
Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI, Import AI, BBC, The
When should I invoke news-aggregator-skill?
Comprehensive news aggregator that fetches, filters, and deeply analyzes real-time content from 44+ sources including Hacker News, Lobsters, Dev.to, GitHub, arXiv, Hugging Face Papers, AIHOT, TLDR AI, Import AI, BBC, The
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is News Aggregator Skill safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.