Skill: wangluo1/rss-reader-skill (version 0.1.0)

--- name: rss-reader description: > RSS 订阅 + AI 汇总分析 + 飞书推送。 定时拉取新文章，AI 生成每日资讯汇总报告（热门话题、关键趋势、推荐阅读），推送到飞书。 当用户提到"RSS"、"订阅"、"RSS阅读器"、"文章订阅"、"资讯订阅"、"资讯汇总"时使用此 Skill。 首次使用自动添加 18 个精选订阅源（中文 + 国外 AI + 科技媒体）。 --- # RSS Reader + AI Summary > 📰 RSS 订阅 + AI 汇总分析 + 飞书推送 > > 首次使用自动添加 18 个精选订阅源，自动生成每日资讯汇总报告！ --- ## ⚠️ 首次使用必读 **🚨 重要：此 Skill 默认无法使用，必须配置 API Key！** 在使用此 Skill 之前，**必须配置以下环境变量**： ### 1️⃣ AI 汇总 API（必需） **默认不支持任何 API，必须手动配置！** 用于生成每日资讯汇总报告（热门话题、关键趋势、推荐阅读）。 #### 推荐方案：智谱 AI（国内稳定） ```bash # 1. 获取 API Key：https://open.bigmodel.cn/ # 2. 在 Gateway 配置中添加： export OPENAI_API_KEY="你的智谱API Key" export OPENAI_BASE_URL="https://open.bigmodel.cn/api/paas/v4" ``` #### 备选方案：OpenAI ```bash export OPENAI_API_KEY="sk-xxx" export OPENAI_BASE_URL="https://api.openai.com/v1" ``` **💡 智谱 AI 优势：** - ✅ 国内访问稳定 - ✅ 价格便宜（glm-4-flash: 0.1元/百万tokens） - ✅ 中文效果好 - ✅ 免费额度充足 **获取智谱 API Key：** https://open.bigmodel.cn/ --- ### 2️⃣ 飞书推送（可选） 用于推送每日汇总报告到飞书群。 ```bash # 国内版 export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" # 国际版（Lark） export FEISHU_WEBHOOK_URL="https://open.larksuite.com/open-apis/bot/v2/hook/xxx" ``` **获取飞书 Webhook：** 1. 在飞书群中添加「自定义机器人」 2. 复制 Webhook 地址 --- ### 3️⃣ 配置方法 在 Gateway 配置文件 `~/.openclaw/gateway/.env` 中添加： ```bash # AI 汇总（智谱） OPENAI_API_KEY=你的API Key OPENAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4 # 飞书推送（可选） FEISHU_WEBHOOK_URL=你的Webhook地址 ``` 然后重启 Gateway： ```bash openclaw gateway restart ``` --- ## 📖 使用方法 ### 命令列表 ```bash # 订阅管理 订阅 <url> [名称] 添加 RSS 订阅 取消订阅 <url> 取消订阅 订阅列表 查看所有订阅 # 刷新和汇总 立即刷新 检查新文章 + 自动生成汇总报告 汇总 手动生成汇总报告 ``` ### 示例 ```bash # 添加订阅 订阅 https://www.ruanyifeng.com/blog/atom.xml 阮一峰 # 查看订阅列表 订阅列表 # 刷新并生成汇总报告 立即刷新 # 手动生成汇总报告 汇总 ``` --- ## ✨ 核心功能 ### 1️⃣ 自动添加订阅源 首次使用自动添加 **18 个精选订阅源**： **中文源（8 个）：** - 阮一峰博客、V2EX、少数派、36氪、虎嗅、InfoQ 中文、IT之家、开源中国 **国外 AI（4 个）：** - Anthropic Blog、OpenAI Blog、Google DeepMind、Google AI Blog **国外科技媒体（4 个）：** - TechCrunch、The Verge、Wired、Ars Technica **国外开发者（3 个）：** - GitHub Blog、Hacker News、Product Hunt **国外商业（2 个）：** - Harvard Business Review、The Economist --- ### 2️⃣ 每日资讯汇总报告 ⭐ 每次刷新后自动生成 AI 分析报告，包含： **📊 报告内容：** - 🔥 **热门话题**：3-5 个最热门的科技话题 - 💡 **关键趋势**：2-3 条技术/行业趋势分析 - 📌 **推荐阅读**：3-5 篇最值得阅读的文章 - 🎯 **一句话总结**：今天的主要资讯概括 - 📚 **原文链接**：所有文章的原始链接列表 **示例报告：** ``` # 📊 每日资讯汇总 ## 🔥 热门话题 1. Apple 宣布下调中国区 App Store 佣金率 2. 小红书率先向AI托管账号出手 3. AWE 2026携重磅阵容，MOVA全面披露芯片战略 ## 💡 关键趋势 1. 科技产品佣金调整趋势明显 2. 社交媒体对AI的监管与应对 3. 芯片技术的快速发展与应用 ## 📌 推荐阅读 1. Apple 宣布下调中国区 App Store 佣金率 2. 小红书率先向AI托管账号出手 3. AWE 2026携重磅阵容，MOVA全面披露芯片战略 ## 🎯 一句话总结 今日科技资讯聚焦于科技产品佣金调整、社交媒体AI监管以及芯片技术发展。 --- ## 📚 原文链接 1. [一日一技｜在镜头前翻翻页，你就得到了一本电子书](https://sspai.com/post/106935) 2. [314 周年庆：少数派软件、会员、栏目限时优惠](https://sspai.com/post/107091) 3. [派早报：Apple 宣布下调中国区 App Store 佣金率](https://sspai.com/post/107158) ...（最多 20 个链接） ``` --- ### 3️⃣ 飞书推送 - ✅ 自动推送每日汇总报告到飞书群 - ✅ 美化的消息卡片 - ✅ 支持国内版和国际版（Lark） --- ### 4️⃣ 定时刷新 建议配置 OpenClaw 定时任务，每 2 小时自动刷新： ```bash openclaw cron add \ --name "rss-refresh" \ --every "2h" \ --session main \ --system-event "[RSS刷新] 请立即执行：python3 ~/.agents/skills/rss-reader/rss_reader.py 立即刷新。执行完后回复 NO_REPLY。不要删除任何 cron 任务。" ``` --- ## 🔧 技术细节 ### 工作流程 1. **刷新订阅**：检查所有 RSS 源的新文章 2. **记录文章**：保存文章标题、链接、来源 3. **AI 分析**：调用智谱 AI 生成汇总报告 4. **推送飞书**：发送汇总报告 + 原文链接 ### 与传统 RSS 阅读器的区别 | 功能 | 传统 RSS | RSS Reader Skill | |------|----------|------------------| | 订阅管理 | ✅ | ✅ | | 定时刷新 | ✅ | ✅ | | 单篇摘要 | ✅ | ❌（已移除） | | AI 汇总分析 | ❌ | ✅ ⭐ | | 趋势分析 | ❌ | ✅ ⭐ | | 推荐阅读 | ❌ | ✅ ⭐ | | 原文链接 | ❌ | ✅ ⭐ | | 飞书推送 | ❌ | ✅ ⭐ | **设计理念：** - ❌ 不做单篇摘要（避免大量 API 调用和错误信息） - ✅ 只做汇总分析（一次 API 调用，生成高质量报告） - ✅ 提供原文链接（用户可自行选择阅读） --- ## 🚨 常见问题 ### 1. 提示 "未配置 OPENAI_API_KEY" **原因：** 未配置 AI API Key **解决：** ```bash # 获取智谱 API Key：https://open.bigmodel.cn/ export OPENAI_API_KEY="你的Key" export OPENAI_BASE_URL="https://open.bigmodel.cn/api/paas/v4" # 重启 Gateway openclaw gateway restart ``` ### 2. 生成报告失败 **可能原因：** - API Key 无效或过期 - API 余额不足 - 网络问题 **解决：** 1. 检查 API Key 是否正确 2. 检查 API 余额（智谱控制台） 3. 尝试使用 OpenAI 备选方案 ### 3. 飞书推送失败 **可能原因：** - Webhook 地址错误 - 机器人被移除 **解决：** 1. 检查 Webhook 地址 2. 在飞书群中重新添加机器人 ### 4. 某些订阅源无法访问 **可能原因：** - 网络限制（部分国外源） - RSS 地址失效 **解决：** 1. 取消订阅失效源：`取消订阅 <url>` 2. 手动添加新的订阅源 --- ## 📦 依赖 **Python 包：** - `feedparser>=6.0.0` - RSS 解析 - `requests>=2.28.0` - HTTP 请求 **安装：** ```bash pip3 install -r requirements.txt ``` --- ## 📄 协议 MIT License --- ## 🙏 致谢 感谢以下项目： - [feedparser](https://github.com/kurtmckee/feedparser) - RSS 解析 - [智谱 AI](https://open.bigmodel.cn/) - AI 分析服务 --- ## 📮 反馈 如有问题或建议，请访问： - GitHub Issues: [项目地址] - ClawHub: https://clawhub.com --- **⚠️ 再次提醒：此 Skill 默认不可用，必须配置 API Key！** # 1. 在飞书群添加「自定义机器人」 # 2. 复制 Webhook 地址 # 3. 在 Gateway 配置中添加： export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" ``` **获取飞书 Webhook：** 1. 打开飞书群聊 2. 点击群设置 → 群机器人 → 添加机器人 3. 选择「自定义机器人」 4. 复制 Webhook 地址 --- ### 3️⃣ 重启 Gateway 配置完成后，**必须重启 Gateway**： ```bash openclaw gateway restart ``` --- ## 🚫 常见错误 ### ❌ 401 错误：令牌已过期或验证不正确 **原因：** 未配置 API Key 或 API Key 无效 **解决：** 1. 确认已在 `~/.openclaw/gateway/.env` 中配置 `OPENAI_API_KEY` 2. 确认 API Key 正确且未过期 3. 重启 Gateway：`openclaw gateway restart` --- ## 功能 | 功能 | 说明 | |------|------| | 订阅管理 | 添加/删除/查看 RSS 订阅 | | 自动刷新 | 定时拉取新文章（每 2 小时） | | AI 摘要 | 智谱 AI 生成文章摘要 | | **每日汇总** | **AI 分析所有文章，生成趋势报告** | | 飞书推送 | 新文章 + 汇总报告自动推送 | | 默认订阅 | 首次使用自动添加 18 个精选源 | --- ## 默认订阅源 首次使用自动添加 **18 个精选订阅源**： ### 中文源（7 个） | 名称 | 分类 | RSS 地址 | |------|------|---------| | 阮一峰博客 | 技术 | https://www.ruanyifeng.com/blog/atom.xml | | V2EX | 社区 | https://www.v2ex.com/index.xml | | 少数派 | 科技 | https://sspai.com/feed | | 36氪 | 商业 | https://36kr.com/feed | | 虎嗅 | 商业 | https://www.huxiu.com/rss/0.xml | | InfoQ 中文 | 技术 | https://www.infoq.cn/feed | | 开源中国 | 技术 | https://www.oschina.net/news/rss | ### 国外源 - AI（4 个） | 名称 | 分类 | RSS 地址 | |------|------|---------| | Anthropic Blog | AI | https://www.anthropic.com/news/rss | | OpenAI Blog | AI | https://openai.com/blog/rss.xml | | Google DeepMind | AI | https://deepmind.google/discover/blog/rss/ | | Google AI Blog | AI | https://blog.google/technology/ai/rss/ | ### 国外源 - 科技媒体（4 个） | 名称 | 分类 | RSS 地址 | |------|------|---------| | TechCrunch | Tech | https://techcrunch.com/feed/ | | The Verge | Tech | https://www.theverge.com/rss/index.xml | | Wired | Tech | https://www.wired.com/feed/rss | | Ars Technica | Tech | https://arstechnica.com/feed/ | ### 国外源 - 开发者（3 个） | 名称 | 分类 | RSS 地址 | |------|------|---------| | GitHub Blog | Dev | https://github.blog/feed/ | | Hacker News | Dev | https://hnrss.org/frontpage | | Product Hunt | Product | https://www.producthunt.com/feed | --- ## 命令 ### 1️⃣ 订阅管理 ```bash # 添加订阅 订阅 <url> [名称] # 取消订阅 取消订阅 <url> # 查看订阅列表 订阅列表 ``` ### 2️⃣ 刷新和汇总 ```bash # 立即刷新 + 自动生成汇总报告 立即刷新 # 手动生成汇总报告 汇总 ``` --- ## 使用示例 ### 示例 1：首次使用 ```bash # 1. 配置 API Key（必须） export OPENAI_API_KEY="your-zhipu-api-key" export OPENAI_BASE_URL="https://open.bigmodel.cn/api/paas/v4" export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" # 2. 重启 Gateway openclaw gateway restart # 3. 运行 Skill（自动添加 18 个默认订阅） python3 ~/.agents/skills/rss-reader/rss_reader.py 订阅列表 ``` ### 示例 2：添加自定义订阅 ```bash python3 ~/.agents/skills/rss-reader/rss_reader.py 订阅 https://example.com/feed.xml "我的博客" ``` ### 示例 3：刷新并生成汇总 ```bash python3 ~/.agents/skills/rss-reader/rss_reader.py 立即刷新 # 输出： # 🔄 开始刷新 18 个订阅... # 📡 检查：阮一峰博客 # 📄 发现 3 篇新文章 # ✅ 已推送到飞书 # ... # ✅ 刷新完成，发现 30 篇新文章 # # 📊 正在生成汇总分析报告... # ✅ 汇总分析报告生成成功 # ✅ 汇总报告已推送到飞书 ``` --- ## 汇总分析报告示例 ```markdown # 📊 每日资讯汇总（2024-03-13） ## 🔥 热门话题 - **AI Agent 生态** - OpenAI、Claude、Gemini 相继推出 Agent 功能 - **开源模型崛起** - Llama 3、Mistral 等开源模型性能逼近闭源 - **AI 编程工具** - Cursor、Copilot、Codeium 竞争激烈 ## 💡 关键趋势 1. **AI Agent 正在从玩具变工具** - 越来越多的企业开始用 Agent 自动化工作流 2. **开源模型质量提升** - 本地部署 AI 成为可能，隐私和成本优势明显 3. **AI 辅助编程成为标配** - 不用 AI 编程工具的程序员将落后 ## 📌 推荐阅读 1. **OpenAI 推出 Agent SDK** - 企业级 Agent 开发平台 2. **Llama 3 性能评测** - 开源模型的新标杆 3. **Cursor vs Copilot 深度对比** - AI 编程工具选型指南 ## 🎯 一句话总结 AI Agent 和开源模型是今天的主旋律，编程工具的 AI 化已成定局。 ``` --- ## 定时任务 默认已配置每 2 小时自动刷新： ```bash # 查看定时任务 openclaw cron list # 手动触发 openclaw cron run rss-refresh ``` --- ## 文件结构 ``` ~/.agents/skills/rss-reader/ ├── SKILL.md # Skill 说明文档 ├── rss_reader.py # 主脚本 ├── requirements.txt # 依赖（feedparser, requests） ├── README.md # 使用说明 └── data/ ├── subscriptions.json # 订阅列表 └── articles.json # 已读文章记录 ``` --- ## 环境变量 | 变量名 | 必需 | 说明 | |--------|------|------| | `OPENAI_API_KEY` | ✅ 必需 | 智谱/OpenAI API Key | | `OPENAI_BASE_URL` | 推荐 | API 地址（默认智谱） | | `FEISHU_WEBHOOK_URL` | 可选 | 飞书推送 Webhook | --- ## 常见问题 ### Q1: 为什么没有 AI 摘要？ **A:** 未配置 API Key。请按上述步骤配置 `OPENAI_API_KEY`。 ### Q2: 为什么推送失败？ **A:** 未配置飞书 Webhook 或 Webhook 地址错误。请检查 `FEISHU_WEBHOOK_URL`。 ### Q3: 某些订阅源解析失败？ **A:** 部分 RSS 源可能无法访问或格式不标准。可以取消订阅该源： ```bash python3 ~/.agents/skills/rss-reader/rss_reader.py 取消订阅 <url> ``` ### Q4: 如何清空已读记录？ ```bash echo '{}' > ~/.agents/skills/rss-reader/data/articles.json ``` --- ## 更新日志 ### v1.1.0 (2024-03-13) - ✨ 新增每日汇总分析功能 - ✨ 刷新后自动生成并推送汇总报告 - 🔧 改进批量摘要逻辑 - 📝 更新文档，强调必须配置 API Key ### v1.0.0 (2024-03-13) - 🎉 初始版本 - ✅ RSS 订阅管理 - ✅ AI 摘要（智谱/OpenAI） - ✅ 飞书推送 - ✅ 18 个默认订阅源 - ✅ 定时任务（每 2 小时） --- ## 技术栈 - **语言：** Python 3 - **RSS 解析：** feedparser - **HTTP 请求：** requests - **AI 模型：** 智谱 glm-4-flash（推荐）/ OpenAI gpt-4o-mini - **推送：** 飞书 Webhook --- ## 反馈与支持 如有问题或建议，请联系 Skill 作者或在 ClawHub 提交 Issue。 --- **享受智能资讯聚合！📰✨**

path: PUBLISH.md
content: # 发布到 ClawHub 指南

## 方法 1：GitHub 仓库（推荐）

1. **创建 GitHub 仓库**
   ```bash
   cd ~/.agents/skills/rss-reader
   git init
   git add .
   git commit -m "Initial commit: RSS Reader Skill"
   git branch -M main
   git remote add origin https://github.com/你的用户名/rss-reader-skill.git
   git push -u origin main
   ```

2. **提交到 ClawHub**
   - 访问：https://clawhub.com
   - 点击 "Submit Skill"
   - 填写 GitHub 仓库地址
   - 等待审核

## 方法 2：打包上传

1. **打包 Skill**
   ```bash
   cd ~/.agents/skills
   tar -czf rss-reader.tar.gz rss-reader/
   ```

2. **上传到 ClawHub**
   - 访问：https://clawhub.com
   - 点击 "Upload Skill"
   - 选择 `rss-reader.tar.gz`
   - 填写 Skill 信息
   - 提交审核

## 文件清单

✅ 必需文件：
- `SKILL.md` - Skill 说明文档
- `rss_reader.py` - 主脚本
- `requirements.txt` - Python 依赖
- `package.json` - Skill 配置
- `LICENSE` - MIT 协议
- `.gitignore` - Git 忽略规则

✅ 可选文件：
- `README.md` - 详细说明文档
- `PUBLISH.md` - 发布指南（本文件）
- `data/.gitkeep` - 数据目录占位符

## 用户安装后需要做的事

1. **配置 API Key**
   ```bash
   # 获取智谱 API Key：https://open.bigmodel.cn/
   export OPENAI_API_KEY="你的Key"
   export OPENAI_BASE_URL="https://open.bigmodel.cn/api/paas/v4"
   ```

2. **配置飞书 Webhook（可选）**
   ```bash
   export FEISHU_WEBHOOK_URL="你的Webhook地址"
   ```

3. **重启 Gateway**
   ```bash
   openclaw gateway restart
   ```

## Skill 特性

- ✅ 18 个精选订阅源（中英文）
- ✅ AI 摘要（智谱/OpenAI）
- ✅ 每日汇总分析报告
- ✅ 飞书推送
- ✅ 定时刷新（每 2 小时）
- ⚠️ 默认不可用，必须配置 API Key

## 审核要点

1. **无硬编码 API Key** ✅
2. **无硬编码 Webhook** ✅
3. **清晰的配置说明** ✅
4. **MIT 协议** ✅
5. **完整的依赖说明** ✅
6. **测试通过** ✅


path: README.md
content: # RSS Reader + AI Summary

> OpenClaw Skill - RSS 订阅 + AI 摘要 + 飞书推送

## 快速开始

### 1. 安装依赖

```bash
cd ~/.agents/skills/rss-reader
pip install -r requirements.txt
```

### 2. 配置环境变量

```bash
# AI 摘要（必需）
export OPENAI_API_KEY="sk-xxx"

# 飞书推送（可选）
export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
```

### 3. 添加订阅

```bash
python3 rss_reader.py 订阅 "https://blog.openai.com/rss"
```

### 4. 查看订阅

```bash
python3 rss_reader.py 订阅列表
```

### 5. 刷新获取新文章

```bash
python3 rss_reader.py 立即刷新
```

## 定时任务

使用 OpenClaw Cron 自动刷新：

```bash
openclaw cron add \
  --name "rss-refresh" \
  --every "2h" \
  --session main \
  --system-event "[RSS刷新] 请立即执行：python3 ~/.agents/skills/rss-reader/rss_reader.py 立即刷新。执行完后回复 NO_REPLY。不要删除任何 cron 任务。"
```

## 飞书机器人配置

1. 在飞书群中添加「自定义机器人」
2. 获取 Webhook 地址
3. 设置环境变量：`FEISHU_WEBHOOK_URL`

## 常见 RSS 源

```bash
# 技术博客
python3 rss_reader.py 订阅 "https://blog.openai.com/rss" "OpenAI Blog"
python3 rss_reader.py 订阅 "https://www.anthropic.com/news/rss" "Anthropic Blog"

# 中文资讯
python3 rss_reader.py 订阅 "https://www.ruanyifeng.com/blog/atom.xml" "阮一峰"
python3 rss_reader.py 订阅 "https://36kr.com/feed" "36氪"

# 开发者社区
python3 rss_reader.py 订阅 "https://www.v2ex.com/index.xml" "V2EX"
```

## 文件说明

```
rss-reader/
├── SKILL.md              # Skill 说明
├── rss_reader.py         # 主脚本
├── requirements.txt      # Python 依赖
├── README.md             # 本文档
└── data/
    ├── subscriptions.json    # 订阅列表
    └── articles.json         # 已读文章
```

## 问题排查

### Q: 为什么没有生成摘要？
A: 检查 `OPENAI_API_KEY` 是否设置正确

### Q: 为什么推送到飞书失败？
A: 检查 `FEISHU_WEBHOOK_URL` 是否正确，确保机器人未被移除

### Q: 为什么有些 RSS 无法订阅？
A: 某些网站可能禁止爬取，或 RSS 格式不标准

## License

MIT


path: package.json
content: {
  "name": "rss-reader",
  "version": "1.0.0",
  "description": "RSS 订阅 + AI 摘要 + 汇总分析 + 飞书推送。首次使用自动添加 18 个精选订阅源。",
  "author": "OpenClaw Community",
  "license": "MIT",
  "keywords": [
    "rss",
    "ai",
    "summary",
    "feishu",
    "news",
    "automation"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/openclaw/rss-reader-skill"
  },
  "skills": [
    {
      "name": "rss-reader",
      "entry": "SKILL.md",
      "commands": [
        "订阅 <url> [名称]",
        "取消订阅 <url>",
        "订阅列表",
        "立即刷新",
        "汇总"
      ]
    }
  ],
  "requirements": {
    "python": ">=3.7",
    "packages": [
      "feedparser>=6.0.0",
      "requests>=2.28.0"
    ]
  },
  "env": {
    "required": [
      {
        "name": "OPENAI_API_KEY",
        "description": "AI 摘要 API Key（推荐使用智谱 AI）",
        "example": "你的智谱API Key"
      }
    ],
    "optional": [
      {
        "name": "OPENAI_BASE_URL",
        "description": "API 地址（默认：智谱）",
        "default": "https://open.bigmodel.cn/api/paas/v4"
      },
      {
        "name": "FEISHU_WEBHOOK_URL",
        "description": "飞书机器人 Webhook 地址",
        "example": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
      }
    ]
  }
}


path: requirements.txt
content: feedparser>=6.0.0
requests>=2.28.0


path: rss_reader.py
content: #!/usr/bin/env python3
"""
RSS Reader + AI Summary
订阅 RSS，检测新文章，AI 生成摘要，推送到飞书
"""

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

try:
    import feedparser
except ImportError:
    print("❌ 缺少依赖：feedparser")
    print("请运行：pip install feedparser")
    sys.exit(1)

try:
    import requests
except ImportError:
    print("❌ 缺少依赖：requests")
    print("请运行：pip install requests")
    sys.exit(1)

# 配置
DATA_DIR = Path(__file__).parent / "data"
SUBSCRIPTIONS_FILE = DATA_DIR / "subscriptions.json"
ARTICLES_FILE = DATA_DIR / "articles.json"

# 从环境变量获取配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://open.bigmodel.cn/api/paas/v4")  # 默认智谱
FEISHU_WEBHOOK_URL = os.getenv("FEISHU_WEBHOOK_URL", "")

# 默认订阅源（首次使用自动添加）
DEFAULT_SUBSCRIPTIONS = {
    # 中文源
    "https://www.ruanyifeng.com/blog/atom.xml": {
        "name": "阮一峰博客",
        "category": "技术"
    },
    "https://www.v2ex.com/index.xml": {
        "name": "V2EX",
        "category": "社区"
    },
    "https://sspai.com/feed": {
        "name": "少数派",
        "category": "科技"
    },
    "https://36kr.com/feed": {
        "name": "36氪",
        "category": "商业"
    },
    "https://www.huxiu.com/rss/0.xml": {
        "name": "虎嗅",
        "category": "商业"
    },
    "https://www.infoq.cn/feed": {
        "name": "InfoQ 中文",
        "category": "技术"
    },
    "https://www.ithome.com/rss/": {
        "name": "IT之家",
        "category": "科技"
    },
    "https://www.oschina.net/news/rss": {
        "name": "开源中国",
        "category": "技术"
    },
    # 国外源 - AI
    "https://www.anthropic.com/news/rss": {
        "name": "Anthropic Blog",
        "category": "AI"
    },
    "https://openai.com/blog/rss.xml": {
        "name": "OpenAI Blog",
        "category": "AI"
    },
    "https://deepmind.google/discover/blog/rss/": {
        "name": "Google DeepMind",
        "category": "AI"
    },
    "https://blog.google/technology/ai/rss/": {
        "name": "Google AI Blog",
        "category": "AI"
    },
    # 国外源 - 科技媒体
    "https://techcrunch.com/feed/": {
        "name": "TechCrunch",
        "category": "Tech"
    },
    "https://www.theverge.com/rss/index.xml": {
        "name": "The Verge",
        "category": "Tech"
    },
    "https://www.wired.com/feed/rss": {
        "name": "Wired",
        "category": "Tech"
    },
    "https://arstechnica.com/feed/": {
        "name": "Ars Technica",
        "category": "Tech"
    },
    # 国外源 - 开发者
    "https://github.blog/feed/": {
        "name": "GitHub Blog",
        "category": "Dev"
    },
    "https://hnrss.org/frontpage": {
        "name": "Hacker News",
        "category": "Dev"
    },
    "https://www.producthunt.com/feed": {
        "name": "Product Hunt",
        "category": "Product"
    },
    # 国外源 - 商业
    "https://hbr.org/feed": {
        "name": "Harvard Business Review",
        "category": "Business"
    },
    "https://www.economist.com/rss": {
        "name": "The Economist",
        "category": "Business"
    }
}

# 确保数据目录存在
DATA_DIR.mkdir(parents=True, exist_ok=True)


def load_json(file_path: Path, default=None):
    """加载 JSON 文件"""
    if not file_path.exists():
        return default if default is not None else {}
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        print(f"⚠️ 加载 {file_path} 失败：{e}")
        return default if default is not None else {}


def save_json(file_path: Path, data):
    """保存 JSON 文件"""
    try:
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    except Exception as e:
        print(f"⚠️ 保存 {file_path} 失败：{e}")


def init_default_subscriptions():
    """初始化默认订阅源（首次使用时自动添加）"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    
    if subscriptions:  # 如果已有订阅，跳过
        return
    
    print("🎁 首次使用，正在添加默认订阅源...")
    
    for url, info in DEFAULT_SUBSCRIPTIONS.items():
        try:
            feed = feedparser.parse(url)
            if not feed.bozo or feed.entries:
                subscriptions[url] = {
                    "name": info["name"],
                    "url": url,
                    "category": info.get("category", "未分类"),
                    "added_at": datetime.now().isoformat(),
                    "last_check": None,
                    "article_count": len(feed.entries) if hasattr(feed, 'entries') else 0,
                    "is_default": True
                }
                print(f"  ✅ {info['name']}")
        except Exception as e:
            print(f"  ⚠️ 跳过 {info['name']}: {e}")
    
    save_json(SUBSCRIPTIONS_FILE, subscriptions)
    print(f"\n✅ 已添加 {len(subscriptions)} 个默认订阅源")


def add_subscription(url: str, name: str = None):
    """添加订阅"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    
    if url in subscriptions:
        return f"⚠️ 已存在订阅：{subscriptions[url]['name']}"
    
    # 解析 RSS 获取名称
    try:
        feed = feedparser.parse(url)
        if feed.bozo and not feed.entries:
            return f"❌ 无效的 RSS 地址：{url}"
        
        feed_name = name or feed.feed.get("title", url)
    except Exception as e:
        return f"❌ 解析 RSS 失败：{e}"
    
    subscriptions[url] = {
        "name": feed_name,
        "url": url,
        "added_at": datetime.now().isoformat(),
        "last_check": None,
        "article_count": len(feed.entries) if hasattr(feed, 'entries') else 0
    }
    
    save_json(SUBSCRIPTIONS_FILE, subscriptions)
    
    # 初始化已读文章
    articles = load_json(ARTICLES_FILE, {})
    if hasattr(feed, 'entries'):
        for entry in feed.entries[:10]:  # 只记录最新的10篇
            article_id = entry.get("id", entry.get("link", ""))
            if article_id:
                articles[article_id] = {
                    "title": entry.get("title", ""),
                    "link": entry.get("link", ""),
                    "read": True,
                    "read_at": datetime.now().isoformat()
                }
        save_json(ARTICLES_FILE, articles)
    
    return f"✅ 已订阅：{feed_name}\n📄 当前文章数：{len(feed.entries) if hasattr(feed, 'entries') else 0}"


def remove_subscription(url: str):
    """取消订阅"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    
    if url not in subscriptions:
        return f"❌ 未找到订阅：{url}"
    
    name = subscriptions[url]["name"]
    del subscriptions[url]
    save_json(SUBSCRIPTIONS_FILE, subscriptions)
    return f"✅ 已取消订阅：{name}"


def list_subscriptions():
    """列出所有订阅"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    
    if not subscriptions:
        return "📭 暂无订阅\n\n使用「订阅 <RSS地址>」添加订阅"
    
    lines = [f"📰 订阅列表（共 {len(subscriptions)} 个）：", ""]
    for i, (url, info) in enumerate(subscriptions.items(), 1):
        last_check = info.get("last_check")
        last_check_str = ""
        if last_check:
            try:
                dt = datetime.fromisoformat(last_check)
                last_check_str = f"\n   最后检查：{dt.strftime('%Y-%m-%d %H:%M')}"
            except:
                pass
        
        lines.append(
            f"{i}. **{info['name']}**\n"
            f"   {url}"
            f"{last_check_str}"
        )
    
    lines.append("")
    lines.append("💡 使用「立即刷新」检查新文章")
    
    return "\n".join(lines)


def fetch_new_articles(url: str):
    """获取新文章"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    articles = load_json(ARTICLES_FILE, {})
    
    try:
        feed = feedparser.parse(url)
        if feed.bozo and not feed.entries:
            print(f"  ⚠️ 解析失败：{url}")
            return []
    except Exception as e:
        print(f"  ❌ 获取失败：{e}")
        return []
    
    new_articles = []
    
    for entry in feed.entries:
        article_id = entry.get("id", entry.get("link", ""))
        
        if not article_id:
            continue
        
        # 跳过已读的文章
        if article_id in articles:
            continue
        
        article = {
            "id": article_id,
            "title": entry.get("title", "无标题"),
            "link": entry.get("link", ""),
            "summary": entry.get("summary", entry.get("description", "")),
            "published": entry.get("published", entry.get("updated", "")),
            "fetched_at": datetime.now().isoformat(),
            "subscription_url": url
        }
        
        new_articles.append(article)
    
    # 更新最后检查时间
    if url in subscriptions:
        subscriptions[url]["last_check"] = datetime.now().isoformat()
        subscriptions[url]["article_count"] = len(feed.entries)
        save_json(SUBSCRIPTIONS_FILE, subscriptions)
    
    return new_articles


def generate_summary(articles: list) -> dict:
    """
    使用 AI 生成摘要
    支持单条和多条文章批量总结
    返回：{article_id: summary}
    """
    if not OPENAI_API_KEY:
        return {a['id']: "（未配置 API Key）" for a in articles}
    
    if not articles:
        return {}
    
    # 单篇文章
    if len(articles) == 1:
        article = articles[0]
        return {article['id']: generate_single_summary(article)}
    
    # 多篇文章批量总结
    return generate_batch_summary(articles)


def generate_single_summary(article: dict) -> str:
    """单篇文章生成摘要"""
    import re
    
    title = article.get('title', '')
    content = article.get('summary', '')
    content_clean = re.sub(r'<[^>]+>', '', content)[:1500]
    
    try:
        response = requests.post(
            f"{OPENAI_BASE_URL}/chat/completions",
            headers={
                "Authorization": f"Bearer {OPENAI_API_KEY}",
                "Content-Type": "application/json"
            },
            json={
                "model": "glm-5",
                "messages": [
                    {
                        "role": "system",
                        "content": "你是一个专业的文章摘要助手。请用简洁的中文总结文章要点，控制在100字以内。"
                    },
                    {
                        "role": "user",
                        "content": f"请总结这篇文章：\n\n标题：{title}\n\n内容：{content_clean}"
                    }
                ],
                "max_tokens": 200,
                "temperature": 0.3
            },
            timeout=30
        )
        
        if response.status_code == 200:
            result = response.json()
            return result["choices"][0]["message"]["content"].strip()
        else:
            print(f"    ⚠️ API 调用失败：{response.status_code}")
            print(f"    响应：{response.text[:200]}")
            return f"（摘要失败：HTTP {response.status_code}）"
    
    except Exception as e:
        print(f"    ⚠️ 摘要生成出错：{e}")
        return f"（摘要出错）"


def generate_batch_summary(articles: list) -> dict:
    """多篇文章批量生成摘要"""
    import re
    
    # 构建批量摘要的 prompt
    articles_text = []
    for i, article in enumerate(articles[:5], 1):  # 最多5篇
        title = article.get('title', '')
        content = article.get('summary', '')
        content_clean = re.sub(r'<[^>]+>', '', content)[:500]
        articles_text.append(f"{i}. {title}\n   {content_clean}")
    
    prompt = "请为以下文章分别生成摘要，每篇摘要控制在50字以内，用序号区分：\n\n"
    prompt += "\n\n".join(articles_text)
    prompt += "\n\n请按以下格式输出：\n1. [摘要内容]\n2. [摘要内容]\n..."
    
    try:
        response = requests.post(
            f"{OPENAI_BASE_URL}/chat/completions",
            headers={
                "Authorization": f"Bearer {OPENAI_API_KEY}",
                "Content-Type": "application/json"
            },
            json={
                "model": "glm-5",
                "messages": [
                    {
                        "role": "system",
                        "content": "你是一个专业的文章摘要助手。请为多篇文章分别生成简洁的中文摘要。"
                    },
                    {
                        "role": "user",
                        "content": prompt
                    }
                ],
                "max_tokens": 1000,
                "temperature": 0.3
            },
            timeout=60
        )
        
        if response.status_code != 200:
            print(f"    ⚠️ 批量摘要失败：{response.status_code}")
            print(f"    响应：{response.text[:200]}")
            return {a['id']: f"（摘要失败：HTTP {response.status_code}）" for a in articles}
        
        result = response.json()
        content = result["choices"][0]["message"]["content"].strip()
        
        # 解析批量摘要结果
        summaries = {}
        lines = content.split('\n')
        current_idx = 0
        
        for line in lines:
            # 匹配 "1. xxx" 或 "1、xxx" 格式
            import re
            match = re.match(r'^(\d+)[.、]\s*(.+)', line.strip())
            if match:
                idx = int(match.group(1)) - 1
                summary = match.group(2).strip()
                if 0 <= idx < len(articles):
                    summaries[articles[idx]['id']] = summary
                    current_idx = idx
        
        # 为未匹配到的文章设置默认值
        for article in articles:
            if article['id'] not in summaries:
                summaries[article['id']] = "（摘要解析失败）"
        
        return summaries
    
    except Exception as e:
        print(f"    ⚠️ 批量摘要出错：{e}")
        return {a['id']: "（摘要出错）" for a in articles}


def send_to_feishu(title: str, summary: str, link: str, source_name: str = ""):
    """推送到飞书"""
    if not FEISHU_WEBHOOK_URL:
        print("  ⚠️ 未配置飞书 Webhook，跳过推送")
        return False
    
    # 构建消息卡片
    message = {
        "msg_type": "interactive",
        "card": {
            "header": {
                "title": {"tag": "plain_text", "content": f"📰 {title}"},
                "template": "blue"
            },
            "elements": [
                {
                    "tag": "div",
                    "text": {"tag": "lark_md", "content": f"**来源：**{source_name}"}
                },
                {
                    "tag": "div",
                    "text": {"tag": "lark_md", "content": summary}
                },
                {
                    "tag": "action",
                    "actions": [
                        {
                            "tag": "button",
                            "text": {"tag": "plain_text", "content": "查看原文 ↗"},
                            "url": link,
                            "type": "primary"
                        }
                    ]
                }
            ]
        }
    }
    
    try:
        response = requests.post(
            FEISHU_WEBHOOK_URL, 
            json=message, 
            timeout=10
        )
        if response.status_code == 200:
            result = response.json()
            if result.get("code") == 0:
                print("  ✅ 已推送到飞书")
                return True
            else:
                print(f"  ❌ 推送失败：{result.get('msg', '未知错误')}")
        else:
            print(f"  ❌ 推送失败：HTTP {response.status_code}")
    except Exception as e:
        print(f"  ❌ 推送出错：{e}")
    
    return False


def refresh_all():
    """刷新所有订阅（不单独推送，等汇总后一起推送）"""
    subscriptions = load_json(SUBSCRIPTIONS_FILE, {})
    articles = load_json(ARTICLES_FILE, {})
    
    if not subscriptions:
        return "📭 暂无订阅\n\n使用「订阅 <RSS地址>」添加订阅"
    
    print(f"🔄 开始刷新 {len(subscriptions)} 个订阅...")
    print()
    
    total_new = 0
    
    for url, info in subscriptions.items():
        print(f"📡 检查：{info['name']}")
        
        new_articles = fetch_new_articles(url)
        
        if not new_articles:
            print("  📭 暂无新文章")
            continue
        
        print(f"  📄 发现 {len(new_articles)} 篇新文章")
        
        # 记录文章（不生成摘要，不单独推送）
        for article in new_articles:
            articles[article['id']] = {
                "title": article['title'],
                "link": article['link'],
                "source": info['name'],
                "read": True,
                "read_at": datetime.now().isoformat()
            }
            total_new += 1
        
        print()
    
    save_json(ARTICLES_FILE, articles)
    
    if total_new > 0:
        return f"✅ 刷新完成，发现 {total_new} 篇新文章"
    else:
        return "📭 刷新完成，暂无新文章"


def generate_daily_report():
    """生成每日汇总分析报告"""
    [REDACTED_SECRET]"OPENAI_API_KEY")
    
    if not [REDACTED_SECRET]"⚠️  未配置 OPENAI_API_KEY，跳过汇总分析")
        print("💡 请在 Gateway 配置中添加：")
        print("   export OPENAI_API_KEY=\"你的智谱API Key\"")
        print("   export OPENAI_BASE_URL=\"https://open.bigmodel.cn/api/paas/v4\"")
        return None
    
    # 收集所有新文章
    articles = load_json(ARTICLES_FILE, {})
    new_articles = [a for a in articles.values() if a.get("read")]
    
    if not new_articles:
        print("📭 暂无新文章，跳过汇总分析")
        return None
    
    print(f"\n📊 正在生成汇总分析报告（共 {len(new_articles)} 篇文章）...")
    
    # 构建文章列表（只用标题和来源）
    articles_text = "\n\n".join([
        f"【{i+1}】{a['title']}\n来源：{a.get('source', '未知')}"
        for i, a in enumerate(new_articles[:30])  # 最多 30 篇
    ])
    
    # 调用智谱 API 生成分析报告
    try:
        base_url = os.getenv("OPENAI_BASE_URL", "https://open.bigmodel.cn/api/paas/v4")
        api_url = base_url if base_url.endswith("/chat/completions") else f"{base_url}/chat/completions"
        
        response = requests.post(
            api_url,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json"
            },
            json={
                "model": "glm-4-flash",
                "messages": [
                    {
                        "role": "system",
                        "content": """你是一个科技资讯分析助手。请根据用户提供的文章列表，生成一份简洁的分析报告。

报告格式：
# 📊 每日资讯汇总

## 🔥 热门话题
（列出 3-5 个最热门的话题，每个话题用一句话说明）

## 💡 关键趋势
（分析这些文章反映的技术/行业趋势，2-3 条）

## 📌 推荐阅读
（推荐 3-5 篇最值得阅读的文章，只写文章标题）

## 🎯 一句话总结
（用一句话总结今天的主要资讯）

要求：
- 简洁明了，避免冗余
- 突出重点，抓住趋势
- 不需要推荐理由"""
                    },
                    {
                        "role": "user",
                        "content": f"以下是今天收集的 {len(new_articles)} 篇科技资讯文章：\n\n{articles_text}"
                    }
                ],
                "temperature": 0.7,
                "max_tokens": 800
            },
            timeout=30
        )
        
        if response.status_code == 200:
            result = response.json()
            report = result["choices"][0]["message"]["content"]
            
            # 添加原文链接列表
            links_section = "\n\n---\n\n## 📚 原文链接\n\n"
            for i, a in enumerate(new_articles[:20], 1):  # 最多 20 个链接
                links_section += f"{i}. [{a['title'][:40]}]({a['link']})\n"
            
            full_report = report + links_section
            print("✅ 汇总分析报告生成成功")
            return full_report
        else:
            print(f"⚠️  生成报告失败：{response.status_code}")
            print(f"   响应：{response.text}")
            return None
    except Exception as e:
        print(f"⚠️  生成报告出错：{e}")
        return None


def push_report_to_feishu(report):
    """推送汇总报告到飞书"""
    webhook_url = os.getenv("FEISHU_WEBHOOK_URL")
    
    if not webhook_url or not report:
        return
    
    print("\n📤 正在推送汇总报告到飞书...")
    
    # 今天的日期
    today = datetime.now().strftime("%Y-%m-%d")
    
    payload = {
        "msg_type": "interactive",
        "card": {
            "config": {
                "wide_screen_mode": True
            },
            "header": {
                "title": {
                    "tag": "plain_text",
                    "content": f"📊 每日资讯汇总（{today}）"
                },
                "template": "blue"
            },
            "elements": [
                {
                    "tag": "markdown",
                    "content": report
                },
                {
                    "tag": "note",
                    "elements": [
                        {
                            "tag": "plain_text",
                            "content": f"由 RSS Reader Skill 自动生成 | {datetime.now().strftime('%H:%M')}"
                        }
                    ]
                }
            ]
        }
    }
    
    try:
        response = requests.post(webhook_url, json=payload, timeout=10)
        if response.status_code == 200:
            result = response.json()
            if result.get("StatusCode") == 0:
                print("✅ 汇总报告已推送到飞书")
            else:
                print(f"⚠️  推送失败：{result}")
        else:
            print(f"⚠️  推送失败：{response.status_code}")
    except Exception as e:
        print(f"⚠️  推送出错：{e}")


def main():
    """主函数"""
    # 首次使用时初始化默认订阅
    if not SUBSCRIPTIONS_FILE.exists() or len(load_json(SUBSCRIPTIONS_FILE, {})) == 0:
        init_default_subscriptions()
    
    if len(sys.argv) < 2:
        print("RSS Reader + AI Summary")
        print()
        print("用法：python rss_reader.py <命令> [参数]")
        print()
        print("命令：")
        print("  订阅 <url> [名称]     添加 RSS 订阅")
        print("  取消订阅 <url>        取消订阅")
        print("  订阅列表              查看所有订阅")
        print("  立即刷新              检查新文章")
        print("  汇总                  生成 AI 汇总报告")
        print()
        print("环境变量：")
        print("  OPENAI_API_KEY       AI 摘要用（必需）")
        print("  OPENAI_BASE_URL      API 地址（默认：智谱）")
        print("  FEISHU_WEBHOOK_URL   飞书推送用（可选）")
        return
    
    command = sys.argv[1]
    
    if command == "订阅":
        if len(sys.argv) < 3:
            print("❌ 请提供 RSS 地址")
            print("用法：python rss_reader.py 订阅 <url> [名称]")
            return
        url = sys.argv[2]
        name = sys.argv[3] if len(sys.argv) >= 4 else None
        print(add_subscription(url, name))
    
    elif command == "取消订阅":
        if len(sys.argv) < 3:
            print("❌ 请提供 RSS 地址")
            return
        print(remove_subscription(sys.argv[2]))
    
    elif command in ["订阅列表", "列表", "list"]:
        print(list_subscriptions())
    
    elif command in ["立即刷新", "刷新", "refresh"]:
        print(refresh_all())
        
        # 刷新后自动生成汇总报告
        print("\n" + "="*60)
        report = generate_daily_report()
        if report:
            print(report)
            print("="*60)
            push_report_to_feishu(report)
    
    elif command in ["汇总", "报告", "report"]:
        # 手动生成汇总报告
        report = generate_daily_report()
        if report:
            print("\n" + "="*60)
            print(report)
            print("="*60)
            push_report_to_feishu(report)
    
    elif command == "help":
        print("RSS Reader + AI Summary")
        print()
        print("命令：")
        print("  订阅 <url> [名称]     添加 RSS 订阅")
        print("  取消订阅 <url>        取消订阅")
        print("  订阅列表              查看所有订阅")
        print("  立即刷新              检查新文章")
        print("  汇总                  生成 AI 汇总报告")
    
    else:
        print(f"❌ 未知命令：{command}")
        print("可用命令：订阅、取消订阅、订阅列表、立即刷新")


if __name__ == "__main__":
    main()


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