Skill: renchengxiang/rss-daily-digest (version 1.0.0)

--- name: rss-daily-digest description: >- Fetch RSS feeds, parse articles, generate AI summaries, and compile a daily digest report in Markdown. Use when user asks for RSS summary, news digest, daily briefing, feed reader, morning news, or tech news roundup. version: 1.0.0 homepage: https://github.com/yourname/rss-daily-digest user-invocable: true metadata: {"openclaw":{"emoji":"📰","requires":{"bins":["python3","curl"],"env":["RSS_FEEDS_CONFIG"]},"primaryEnv":"RSS_FEEDS_CONFIG"}} --- # RSS Daily Digest Generate a structured daily news digest from configured RSS feeds. ## Prerequisites - Python 3 with `feedparser` and `datetime` modules - Feed sources configured in `{baseDir}/references/feed-sources.md` - Output directory: `~/openclaw-output/digests/` ## Workflow When the user asks for a daily digest or RSS summary: 1. **Read feed sources**: Read the file `{baseDir}/references/feed-sources.md` to get the list of RSS feed URLs and their categories. 2. **Fetch and parse feeds**: For each feed URL, run: python3 {baseDir}/scripts/fetch_feeds.py \ --feeds-file {baseDir}/references/feed-sources.md --hours 24 This outputs a JSON array of articles published in the last 24 hours. 3. **Summarize articles**: For each article in the JSON output: - Read the title and description - Generate a one-sentence summary (max 30 words) in the same language as the article - Assign a relevance score (1-5) based on the user's interests if known 4. **Format the digest**: Run: python3 {baseDir}/scripts/format_digest.py \ --input /tmp/openclaw-rss-articles.json \ --template {baseDir}/assets/digest-template.md This produces the final Markdown report. 5. **Save the report**: - Create directory if needed: `mkdir -p ~/openclaw-output/digests/` - Save as: `~/openclaw-output/digests/digest-YYYY-MM-DD.md` - Use today's date for the filename 6. **Report to user**: Confirm completion with: - Total number of articles found - Number per category - File path of the saved digest - Offer to send to a messaging channel if configured ## Error Handling - If a feed URL returns HTTP error or times out (>10s), skip it and note in output - If `feedparser` is not installed, run: `pip3 install feedparser` - If zero articles found in 24h window, inform user and suggest expanding timeframe - Never silently fail — always report which feeds succeeded and which failed ## Output Format Use this structure for the digest: # Daily Digest — {DATE} > {TOTAL_COUNT} articles from {FEED_COUNT} sources ## Tech - **[Article Title](url)** — One-sentence AI summary. *(Source Name)* ## Business - ... ## World - ... --- *Generated by RSS Daily Digest Skill at {TIMESTAMP}* ## Rules - Never fabricate article titles or URLs — only use data from actual RSS feeds - Never modify or summarize in a way that changes the factual content - If the user specifies a language preference, translate summaries accordingly - Always preserve the original article URL - Maximum 50 articles per digest to keep it readable

path: references/feed-sources.md
content: # RSS Feed Sources


## Tech
- https://hnrss.org/frontpage — Hacker News Front Page
- https://www.ruanyifeng.com/blog/atom.xml — 阮一峰的网络日志
- https://feeds.feedburner.com/TheHackersNews — The Hacker News (Security)


## Business
- https://feeds.bloomberg.com/technology/news.rss — Bloomberg Tech
- https://rss.nytimes.com/services/xml/rss/nyt/Business.xml — NYT Business


## World
- https://feeds.bbci.co.uk/news/world/rss.xml — BBC World News


## AI
- https://buttondown.com/ainews/rss — AI News Newsletter


path: scripts/fetch_feeds.py
content: #!/usr/bin/env python3
"""
fetch_feeds.py — 从 feed-sources.md 中读取 RSS 源并获取最近 N 小时的文章
输出: JSON 数组到 stdout（供 Agent 或下游脚本消费）
"""


import argparse
import json
import re
import sys
from datetime import datetime, timedelta, timezone




def parse_feed_sources(filepath):
    """解析 feed-sources.md，提取分类和 URL"""
    feeds = []
    current_category = "Uncategorized"
    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            # 匹配 ## Tech 格式的分类标题
            cat_match = re.match(r'^##\s+(.+)$', line)
            if cat_match:
                current_category = cat_match.group(1).strip()
                continue
            # 匹配 - URL — Description 格式的源
            feed_match = re.match(
                r'^-\s+(https?://\S+)\s+[—–-]\s+(.+)$', line
            )
            if feed_match:
                feeds.append({
                    "url": feed_match.group(1),
                    "source": feed_match.group(2).strip(),
                    "category": current_category
                })
    return feeds




def fetch_feed(url, hours=24):
    """获取单个 RSS 源的文章"""
    try:
        import feedparser
    except ImportError:
        print(
            "ERROR: feedparser not installed. Run: pip3 install feedparser",
            file=sys.stderr
        )
        sys.exit(1)


    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
    articles = []


    try:
        feed = feedparser.parse(url)
        if feed.bozo and not feed.entries:
            return {"error": f"Parse error: {feed.bozo_exception}", "url": url}


        for entry in feed.entries[:20]:  # 每个源最多 20 篇
            published = None
            for time_field in ['published_parsed', 'updated_parsed']:
                t = getattr(entry, time_field, None)
                if t:
                    from calendar import timegm
                    published = datetime.fromtimestamp(
                        timegm(t), tz=timezone.utc
                    )
                    break


            if published and published < cutoff:
                continue


            articles.append({
                "title": getattr(entry, 'title', 'Untitled'),
                "url": getattr(entry, 'link', ''),
                "description": getattr(entry, 'summary', '')[:300],
                "published": published.isoformat() if published else None,
            })
    except Exception as e:
        return {"error": str(e), "url": url}


    return {"articles": articles}




def main():
    parser = argparse.ArgumentParser(description='Fetch RSS feeds')
    parser.add_argument(
        '--feeds-file', required=True, help='Path to feed-sources.md'
    )
    parser.add_argument(
        '--hours', type=int, default=24, help='Lookback hours'
    )
    args = parser.parse_args()


    sources = parse_feed_sources(args.feeds_file)
    results = []
    errors = []


    for source in sources:
        result = fetch_feed(source["url"], args.hours)
        if "error" in result:
            errors.append({
                "source": source["source"], "error": result["error"]
            })
        else:
            for article in result.get("articles", []):
                article["source"] = source["source"]
                article["category"] = source["category"]
                results.append(article)


    output = {
        "articles": results,
        "errors": errors,
        "total": len(results),
        "sources_checked": len(sources),
        "sources_failed": len(errors),
        "generated_at": datetime.now(timezone.utc).isoformat()
    }


    # 同时写到临时文件（方便下游脚本读取）
    tmp_path = "/tmp/openclaw-rss-articles.json"
    with open(tmp_path, 'w', encoding='utf-8') as f:
        json.dump(output, f, ensure_ascii=False, indent=2)


    # 输出到 stdout（Agent 可以直接读取）
    print(json.dumps(output, ensure_ascii=False, indent=2))




if __name__ == "__main__":
    main()


path: scripts/format_digest.py
content: #!/usr/bin/env python3
"""
format_digest.py — 将 JSON 格式的文章数据格式化为 Markdown 日报
"""
import argparse
import json
from datetime import datetime, timezone
from collections import defaultdict




EMOJI_MAP = {
    "Tech": "🔧", "Business": "💼", "World": "🌍",
    "AI": "🤖", "Science": "🔬", "Other": "📌"
}




def format_digest(articles_data, template_path=None):
    """将文章按分类整理成 Markdown 格式日报"""
    articles = articles_data.get("articles", [])
    errors = articles_data.get("errors", [])
    now = datetime.now(timezone.utc)


    # 按分类分组
    by_category = defaultdict(list)
    for a in articles:
        by_category[a.get("category", "Other")].append(a)


    # 构建 Markdown
    lines = []
    lines.append(
        f"# Daily Digest — {now.strftime('%Y-%m-%d')}\n"
    )
    lines.append(
        f"> **{len(articles)}** articles from "
        f"**{articles_data.get('sources_checked', '?')}** sources\n"
    )


    for category, items in sorted(by_category.items()):
        emoji = EMOJI_MAP.get(category, "📌")
        lines.append(f"\n## {emoji} {category}\n")
        for item in items[:15]:  # 每分类最多 15 篇
            title = item.get("title", "Untitled")
            url = item.get("url", "#")
            source = item.get("source", "Unknown")
            desc = item.get(
                "ai_summary", item.get("description", "")[:80]
            )
            lines.append(
                f"- **[{title}]({url})** — {desc} *({source})*"
            )


    # 错误报告
    if errors:
        lines.append(f"\n## Feed Errors ({len(errors)})\n")
        for err in errors:
            lines.append(f"- {err['source']}: {err['error']}")


    lines.append(
        f"\n---\n*Generated by RSS Daily Digest Skill "
        f"at {now.strftime('%Y-%m-%d %H:%M UTC')}*"
    )
    return "\n".join(lines)




def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--input', required=True, help='JSON articles file')
    parser.add_argument('--template', help='Optional template path')
    args = parser.parse_args()


    with open(args.input, 'r', encoding='utf-8') as f:
        data = json.load(f)


    digest = format_digest(data, args.template)
    print(digest)




if __name__ == "__main__":
    main()


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