Skill: freedompixels/rss-content-flow (version 1.2.5)

--- slug: rss-content-flow name: RSS内容流 version: "1.0.0" author: 千策 --- # RSS 内容抓取 将 RSS 订阅转化为结构化内容素材。 ## 核心能力 1. **RSS 订阅管理** — 添加/删除/列出订阅源 2. **内容抓取** — 自动获取最新 N 篇文章（支持 RSS 2.0 和 Atom） 3. **智能过滤** — 去除广告/软文，过滤7天前的旧文章 ## 快速开始 用户说"帮我找今天的选题"时： ``` 1. 读取 ~/.qclaw/skills/rss-content-flow/scripts/config.json（订阅列表） 2. 用 fetch_feed.py 获取各源最新条目 3. 呈现可用选题供用户选择 4. 用户确认后 → 用 AI 改写内容 ``` ## 订阅管理 ### 添加订阅源 ```bash python3 scripts/manage_feeds.py --add <名称> <URL> # 示例：python3 scripts/manage_feeds.py --add 36氪 https://36kr.com/feed ``` ### 列出订阅 ```bash python3 scripts/manage_feeds.py --list ``` ### 删除订阅 ```bash python3 scripts/manage_feeds.py --remove <名称> ``` ### 初始化默认订阅 ```bash python3 scripts/manage_feeds.py --init ``` ### 默认订阅源（预配置） | 名称 | URL | 内容方向 | |------|-----|---------| | 36氪 | https://36kr.com/feed | 科技/商业 | | 虎嗅 | https://www.huxiu.com/rss/0.xml | 商业/创业 | | 少数派 | https://sspai.com/feed | 效率/工具 | | AI研习社 | https://ai.googleblog.com/feed/ | AI/技术 | ## 内容抓取 ### 单源抓取 ```bash python3 scripts/fetch_feed.py --source 36氪 --limit 5 ``` ### 全源抓取 ```bash python3 scripts/fetch_feed.py --all --limit 3 ``` 输出示例： ``` 📰 36氪 | 4篇新文章 ① [AI创业] 标题：xxx ② [商业] 标题：xxx ... 📰 虎嗅 | 2篇新文章 ① [汽车] 标题：xxx ... ``` ### 抓取逻辑 1. 解析 RSS XML，获取 title/link/description/pubDate 2. 过滤：去除广告/软文/太旧的文章（>7天） 3. 按时间倒序排列 4. 返回结构化 JSON ### JSON 输出（供程序处理） ```bash python3 scripts/fetch_feed.py --all --json ``` ## 后续处理 获取文章列表后，用户可将标题和摘要复制给 AI 进行分析和改写。Skill本身只负责数据抓取，不包含自动内容生成功能。 ## 配置项 | 配置 | 说明 | 默认值 | |------|------|--------| | 订阅源 | 监控哪些 RSS | 36氪+虎嗅+少数派 | | 抓取数量 | 每次取几条 | 每源3条 | | 文章有效期 | 过滤几天前的文章 | 7天 | ## 文件结构 ``` rss-content-flow/ ├── SKILL.md # 本文件 ├── scripts/ │ ├── manage_feeds.py # 订阅管理（增删查） │ ├── fetch_feed.py # RSS 抓取脚本 │ └── config.json # 订阅配置（运行时生成） └── assets/ # 封面图模板等（可选） ``` ## 与其他 Skill 的配合 - **cn-auto-publisher**：获取文章后，生成内容可交给 cn-auto-publisher 发布到知乎/小红书 - **cn-trends-ai**：热点 + RSS 结合，获得更完整的内容来源 ## 注意事项 - RSS 源需确保可访问，过期/失效的源自动跳过 - 抓取间隔建议不低于 30 分钟，避免对源站造成压力 - 本 Skill 只负责 RSS 数据抓取，不包含内容分析和改写功能

path: README.md
content: # RSS 内容流 (rss-content-flow)

将 RSS 订阅转化为多平台发布内容。自动抓取最新资讯，AI 分析核心观点，一键生成适配知乎/小红书/公众号的原创文案。

## 功能

- 📡 **RSS 订阅管理** — 添加/删除/列出订阅源
- 🔍 **智能抓取** — 自动获取最新文章，过滤广告和过期内容
- 🧠 **AI 分析** — 提取核心观点、关键数据、金句标签
- ✍️ **平台改写** — 生成知乎/小红书/公众号风格的原创内容
- 💾 **草稿保存** — 写入飞书文档或直接调用发布工具

## 使用方法

### 触发词
"帮我找今天的选题"、"RSS 订阅"、"监控 XX 更新"、"把 XX 改成小红书"、"我需要内容灵感"

### 快速开始

**1. 初始化订阅源**
```bash
python3 scripts/manage_feeds.py --init
```

**2. 查看今日内容选题**
```bash
python3 scripts/fetch_feed.py --all --limit 5
```

**3. 生成内容**
AI 根据选题列表，为选中的文章生成多平台文案。

### 预置订阅源
| 名称 | URL | 方向 |
|------|-----|------|
| 36氪 | https://36kr.com/feed | 科技/商业 |
| 少数派 | https://sspai.com/feed | 效率/工具 |

### 添加自定义订阅
```bash
python3 scripts/manage_feeds.py --add <名称> <RSS_URL>
python3 scripts/fetch_feed.py --source <名称> --limit 3
```

## 工作流程

```
RSS 订阅源
    ↓
fetch_feed.py 抓取最新文章
    ↓
AI 分析：提取观点+评估平台适配度
    ↓
生成内容草稿（知乎/小红书/公众号）
    ↓
保存到飞书 或 直接发布
```

## 系统要求

- Python 3.8+
- OpenClaw + feishu 工具（用于保存草稿）
- 可访问的 RSS 源（部分源需要代理）

## 文件结构

```
rss-content-flow/
├── SKILL.md              # AI 技能定义
├── scripts/
│   ├── manage_feeds.py   # 订阅管理
│   └── fetch_feed.py     # RSS 抓取
└── references/
    └── platform_style.md # 平台风格指南
```

## 与其他 Skill 配合

- **social-media-poster**：生成内容后直接发布
- **feishu-daily-report**：内容草稿归入日报数据源
- **content-repurposer**：同一篇文章改写到不同平台

## License

MIT


path: scripts/config.json
content: 36氪: https://36kr.com/feed
少数派: https://sspai.com/feed
机器之心: https://ai.jiqizhixin.com/rss
量子位: https://www.ijiandao.com/feed

path: scripts/fetch_feed.py
content: #!/usr/bin/env python3
"""
RSS 抓取脚本：从多个订阅源获取最新文章
用法:
  python3 fetch_feed.py --all --limit 5
  python3 fetch_feed.py --source 36氪 --limit 5
  python3 fetch_feed.py --source 36氪 --url "https://36kr.com/feed"
"""

import json
import os
import sys
import ssl
import socket
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
import argparse
import re
import certifi

# 双层 SSL 上下文：macOS Python 需显式加载 certifi 根证书
# 第1层：标准验证（优先）
_SAFE_SSL_CTX = ssl.create_default_context()
_SAFE_SSL_CTX.load_verify_locations(certifi.where())

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.json")

def load_feeds():
    """加载订阅配置"""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

def fetch_rss(url, source_name="未知", limit=5, max_age_days=7):
    """
    获取单个 RSS 源的最新文章
    返回: list of dict {title, link, description, pub_date, source}
    """
    articles = []
    xml_content = None

    # 优先使用标准 SSL 证书验证，失败自动降级
    try:
        with urlopen(url, timeout=15, context=_SAFE_SSL_CTX) as response:
            xml_content = response.read().decode("utf-8", errors="replace")
    except URLError as e:
        # SSL错误被包装在URLError.reason里
        reason = getattr(e, 'reason', None)
        if isinstance(reason, ssl.SSLError):
            fallback_ctx = ssl.create_default_context()
            fallback_ctx.load_verify_locations(certifi.where())
            try:
                with urlopen(url, timeout=15, context=fallback_ctx) as response:
                    xml_content = response.read().decode("utf-8", errors="replace")
            except Exception as e2:
                print(f"  ⚠️ 连接失败: {e2}", file=sys.stderr)
                return []
        else:
            print(f"  ⚠️ 连接失败: {e}", file=sys.stderr)
            return []
    except Exception as e:
        print(f"  ⚠️ 连接失败: {e}", file=sys.stderr)
        return []

    if xml_content is None:
        return []
    
    try:
        root = ET.fromstring(xml_content)
    except ET.ParseError:
        print(f"  ⚠️ XML 解析错误", file=sys.stderr)
        return []
    
    # 判断 RSS 格式 (RSS 2.0 vs Atom)
    ns = {"dc": "http://purl.org/dc/elements/1.1/"}
    if root.tag == "rss":
        items = root.findall(".//item")
        date_format = "%a, %d %b %Y %H:%M:%S %z"
    elif root.tag.endswith("}feed") or root.tag == "feed":
        # Atom 格式
        items = root.findall(".//entry")
        date_format = "%Y-%m-%dT%H:%M:%S"
    else:
        items = root.findall(".//item") or root.findall(".//entry")
        date_format = "%a, %d %b %Y %H:%M:%S %z"
    
    cutoff_date = datetime.now() - timedelta(days=max_age_days)
    
    for item in items[:limit * 3]:  # 先多取一些，过滤后再截取
        title = ""
        link = ""
        description = ""
        pub_date = ""
        
        # 提取标题（兼容 RSS 和 Atom）
        title = ""
        for t in list(item):
            if t.tag in ("title", "{http://purl.org/dc/elements/1.1/}title"):
                if t.text:
                    title = t.text.strip()
                    break
        
        # 提取链接
        link = ""
        for l in list(item):
            if l.tag == "link":
                if l.text and l.text.strip() and l.text.startswith("http"):
                    link = l.text.strip()
                elif l.attrib.get("href"):
                    link = l.attrib["href"]
                break
        
        # Atom 格式的 link
        if not link:
            atom_links = item.findall("link")
            for al in atom_links:
                href = al.attrib.get("href") or (al.text if al.text else "")
                if href and "http" in str(href):
                    link = href
                    break
        
        # 提取描述
        description = ""
        for d in list(item):
            if d.tag in ("description", "summary", "content"):
                if d.text:
                    desc_text = d.text.strip()
                    # 去除 HTML 标签
                    desc_text = re.sub(r"<[^>]+>", "", desc_text)
                    desc_text = re.sub(r"&[^;]+;", "", desc_text)
                    description = desc_text[:300]
                    break
        
        # 提取日期
        pub_date = ""
        for date_elem in list(item):
            if date_elem.tag in ("pubDate", "published", "updated"):
                if date_elem.text:
                    pub_date = date_elem.text.strip()
                    break
        
        # 过滤：去除广告/太旧的
        ad_keywords = ["广告", "推广", "sponsored", "sponsor", "promoted"]
        is_ad = any(kw in title.lower() for kw in ad_keywords)
        
        # 日期过滤
        try:
            if pub_date:
                if "(" in pub_date:
                    pub_date = pub_date.split("(")[0].strip()
                article_date = datetime.strptime(pub_date[:25].strip(), date_format)
                if article_date < cutoff_date:
                    continue
        except (ValueError, TypeError):
            pass  # 日期解析失败时不过滤
        
        if not is_ad and title:
            articles.append({
                "title": title,
                "link": link,
                "description": description,
                "pub_date": pub_date[:16] if pub_date else "",
                "source": source_name
            })
        
        if len(articles) >= limit:
            break
    
    return articles

def fetch_all_feeds(limit=3, max_age_days=7):
    """获取所有订阅源的最新文章"""
    feeds = load_feeds()
    
    if not feeds:
        print("❌ 暂无订阅源，请先运行 --init 或 --add 添加订阅", file=sys.stderr)
        return []
    
    all_articles = []
    results = {}
    
    for name, url in feeds.items():
        print(f"📡 抓取 {name}...", file=sys.stderr)
        articles = fetch_rss(url, name, limit, max_age_days)
        results[name] = articles
        all_articles.extend(articles)
        if articles:
            print(f"  ✅ 获取 {len(articles)} 篇", file=sys.stderr)
        else:
            print(f"  ⚠️ 无新文章或抓取失败", file=sys.stderr)
    
    # 按日期排序
    return all_articles

def print_summary(all_articles):
    """打印内容摘要"""
    if not all_articles:
        print("\n📭 所有订阅源均无新文章（7天内）")
        return
    
    print(f"\n{'='*50}")
    print(f"📬 今日内容速览（共 {len(all_articles)} 篇）")
    print(f"{'='*50}\n")
    
    # 按来源分组
    from collections import defaultdict
    by_source = defaultdict(list)
    for a in all_articles:
        by_source[a["source"]].append(a)
    
    for source, articles in by_source.items():
        print(f"📰 {source} | {len(articles)} 篇新文章")
        for i, a in enumerate(articles, 1):
            print(f"  {i}. {a['title']}")
            if a["description"]:
                desc = a["description"][:80]
                print(f"     {desc}...")
            if a["pub_date"]:
                print(f"     📅 {a['pub_date']}")
            print()
    
    print("-" * 50)
    print("💡 使用「生成知乎内容」或「改写成小红书」来制作发布内容")

def main():
    parser = argparse.ArgumentParser(description="RSS 内容抓取")
    parser.add_argument("--all", action="store_true", help="抓取所有订阅源")
    parser.add_argument("--source", metavar="名称", help="指定订阅源名称")
    parser.add_argument("--url", metavar="URL", help="直接指定 RSS URL（不依赖配置）")
    parser.add_argument("--limit", type=int, default=3, help="每个源抓取文章数量（默认3）")
    parser.add_argument("--days", type=int, default=7, help="文章有效期天数（默认7）")
    parser.add_argument("--json", action="store_true", help="输出 JSON 格式")
    parser.add_argument("--init", action="store_true", help="初始化默认订阅源")
    args = parser.parse_args()
    
    if args.init:
        from manage_feeds import init_default
        init_default()
        return
    
    # 加载订阅
    feeds = load_feeds()
    
    if args.url:
        # 直接指定 URL
        print(f"📡 抓取 {args.url}...", file=sys.stderr)
        articles = fetch_rss(args.url, args.source or "自定义", args.limit, args.days)
    elif args.source:
        # 指定源名
        if args.source not in feeds:
            print(f"❌ 未找到订阅源「{args.source}」", file=sys.stderr)
            print(f"   现有订阅：{', '.join(feeds.keys())}", file=sys.stderr)
            sys.exit(1)
        articles = fetch_rss(feeds[args.source], args.source, args.limit, args.days)
    elif args.all:
        articles = fetch_all_feeds(args.limit, args.days)
    else:
        print("❌ 请指定 --all（所有源）或 --source <名称> 或 --url <URL>", file=sys.stderr)
        sys.exit(1)
    
    if args.json:
        print(json.dumps(articles, ensure_ascii=False, indent=2))
    else:
        print_summary(articles)

if __name__ == "__main__":
    main()


path: scripts/manage_feeds.py
content: #!/usr/bin/env python3
"""
RSS 订阅管理：添加/删除/列出订阅源
用法:
  python3 manage_feeds.py --list
  python3 manage_feeds.py --add 36氪 https://36kr.com/feed
  python3 manage_feeds.py --remove 36氪
"""

import json
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.json")

DEFAULT_FEEDS = {
    "36氪": "https://36kr.com/feed",
    "虎嗅": "https://www.huxiu.com/rss/0.xml",
    "少数派": "https://sspai.com/feed",
    "AI研习社": "https://ai.googleblog.com/feed/",
}

def load_feeds():
    """加载订阅配置"""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

def save_feeds(feeds):
    """保存订阅配置"""
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(feeds, f, ensure_ascii=False, indent=2, sort_keys=True)
    print(f"✅ 已保存 {len(feeds)} 个订阅源")

def list_feeds():
    """列出所有订阅"""
    feeds = load_feeds()
    if not feeds:
        print("📭 暂无订阅源（使用 --add 添加）")
        print("\n默认推荐订阅：")
        for name, url in DEFAULT_FEEDS.items():
            print(f"  {name}: {url}")
        return
    
    print(f"📡 当前订阅（共 {len(feeds)} 个）：\n")
    for name, url in feeds.items():
        print(f"  • {name}")
        print(f"    {url}")
        print()

def add_feed(name, url):
    """添加订阅源"""
    if not name or not url:
        print("❌ 请提供名称和 URL：--add <名称> <URL>")
        sys.exit(1)
    
    feeds = load_feeds()
    
    if name in feeds:
        print(f"⚠️  已存在同名订阅源「{name}」，将覆盖原 URL")
    
    feeds[name] = url
    save_feeds(feeds)
    print(f"✅ 已添加/更新：{name}")
    print(f"   URL: {url}")

def remove_feed(name):
    """删除订阅源"""
    feeds = load_feeds()
    
    if name not in feeds:
        print(f"❌ 未找到订阅源「{name}」")
        available = list(feeds.keys())
        if available:
            print(f"   现有订阅：{', '.join(available)}")
        sys.exit(1)
    
    removed_url = feeds.pop(name)
    save_feeds(feeds)
    print(f"🗑️  已删除：{name}")
    print(f"   原URL: {removed_url}")

def init_default():
    """初始化默认订阅"""
    feeds = load_feeds()
    if not feeds:
        feeds = DEFAULT_FEEDS.copy()
        save_feeds(feeds)
        print("✅ 已初始化默认订阅源（36氪、虎嗅、少数派、AI研习社）")
    else:
        print(f"📡 已有 {len(feeds)} 个订阅源，跳过默认初始化")

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="RSS 订阅管理")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--list", action="store_true", help="列出所有订阅")
    group.add_argument("--add", nargs=2, metavar=("名称", "URL"), help="添加订阅源")
    group.add_argument("--remove", metavar="名称", help="删除订阅源")
    group.add_argument("--init", action="store_true", help="初始化默认订阅源")
    args = parser.parse_args()
    
    if args.list:
        list_feeds()
    elif args.add:
        add_feed(args.add[0], args.add[1])
    elif args.remove:
        remove_feed(args.remove)
    elif args.init:
        init_default()


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