Add 39 shared skills from local agent inventory
Sources: ~/.openclaw/skills, ~/.agents/skills, workshop-skills, workspace/skills
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
.__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Data files (user data)
|
||||
data/
|
||||
*.json
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# OpenClaw specific
|
||||
.clawdhub/
|
||||
|
||||
# Personal milestone (local only)
|
||||
MILESTONE.md
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpenClaw Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,360 @@
|
||||
<div align="center">
|
||||
|
||||
# 🔥 每日热榜 - OpenClaw Skill
|
||||
|
||||
[](https://github.com/one-box-u/openclaw-daily-hot-news/stargazers)
|
||||
[](https://github.com/one-box-u/openclaw-daily-hot-news/network)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
**📖 [中文说明](README.md)** | **🇺🇸 [English Readme](README_EN.md)**
|
||||
|
||||
---
|
||||
|
||||
一个基于 DailyHotApi 的热榜聚合查询技能,支持 54 个平台热榜查询、跨平台聚合、舆情监控等功能。
|
||||
|
||||
</div>
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
### 核心功能
|
||||
- **热榜查询**:查询任意 54 个平台的热榜数据
|
||||
- **分类浏览**:按类别快速定位特定平台
|
||||
- **实时获取**:每次请求都获取最新热榜数据
|
||||
- **历史记录**:自动保存每日热榜数据到本地
|
||||
- **智能清理**:启动时自动检查并提示清理7天前的旧数据
|
||||
|
||||
### 扩展功能
|
||||
- **热点摘要**:15 种标签分类,AI 引导选择
|
||||
- **行业垂直**:十大行业分类,科技/游戏/金融等
|
||||
- **个性化订阅**:自定义关键词和平台偏好
|
||||
- **跨平台聚合**:全网 TOP10 热点榜单
|
||||
- **舆情监控**:关键词监控和热度告警
|
||||
|
||||
## 📊 支持平台(54 个)
|
||||
|
||||
### 🎬 视频/直播平台(5 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 哔哩哔哩 | bilibili | 热门榜 |
|
||||
| AcFun | acfun | 排行榜 |
|
||||
| 抖音 | douyin | 热点榜 |
|
||||
| 快手 | kuaishou | 热点榜 |
|
||||
| 酷安 | coolapk | 热榜 |
|
||||
|
||||
### 💬 社交媒体(8 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 微博 | weibo | 热搜榜 |
|
||||
| 知乎 | zhihu | 热榜 |
|
||||
| 知乎日报 | zhihu-daily | 推荐榜 |
|
||||
| 百度贴吧 | tieba | 热议榜 |
|
||||
| 豆瓣讨论小组 | douban-group | 讨论精选 |
|
||||
| V2EX | v2ex | 主题榜 |
|
||||
| NGA | ngabbs | 热帖 |
|
||||
| 虎扑 | hupu | 步行街热帖 |
|
||||
|
||||
### 📰 新闻资讯(10 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 百度 | baidu | 热搜榜 |
|
||||
| 澎湃新闻 | thepaper | 热榜 |
|
||||
| 今日头条 | toutiao | 热榜 |
|
||||
| 36氪 | 36kr | 热榜 |
|
||||
| 腾讯新闻 | qq-news | 热点榜 |
|
||||
| 新浪网 | sina | 热榜 |
|
||||
| 新浪新闻 | sina-news | 热点榜 |
|
||||
| 网易新闻 | netease-news | 热点榜 |
|
||||
| 虎嗅 | huxiu | 24小时 |
|
||||
| 爱范儿 | ifanr | 快讯 |
|
||||
|
||||
### 💻 科技/技术社区(8 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| IT之家 | ithome | 热榜 |
|
||||
| IT之家「喜加一」 | ithome-xijiayi | 最新动态 |
|
||||
| 少数派 | sspai | 热榜 |
|
||||
| CSDN | csdn | 排行榜 |
|
||||
| 稀土掘金 | juejin | 热榜 |
|
||||
| 51CTO | 51cto | 推荐榜 |
|
||||
| NodeSeek | nodeseek | 最新动态 |
|
||||
| HelloGitHub | hellogithub | Trending |
|
||||
|
||||
### 🎮 游戏/ACG(5 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 原神 | genshin | 最新消息 |
|
||||
| 米游社 | miyoushe | 最新消息 |
|
||||
| 崩坏3 | honkai | 最新动态 |
|
||||
| 崩坏:星穹铁道 | starrail | 最新动态 |
|
||||
| 英雄联盟 | lol | 更新公告 |
|
||||
|
||||
### 📚 阅读/文化(4 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 简书 | jianshu | 热门推荐 |
|
||||
| 果壳 | guokr | 热门文章 |
|
||||
| 微信读书 | weread | 飙升榜 |
|
||||
| 豆瓣电影 | douban-movie | 新片榜 |
|
||||
|
||||
### 🔧 工具/其他(5 个)
|
||||
| 平台 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 吾爱破解 | 52pojie | 榜单 |
|
||||
| 全球主机交流 | hostloc | 榜单 |
|
||||
| 中央气象台 | weatheralarm | 全国气象预警 |
|
||||
| 中国地震台 | earthquake | 地震速报 |
|
||||
| 历史上的今天 | history | 月-日 |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 部署后端服务
|
||||
|
||||
```bash
|
||||
cd daily-hot-api
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
```bash
|
||||
export DAILY_HOT_API_URL=http://localhost:6688
|
||||
```
|
||||
|
||||
### 4. 运行
|
||||
|
||||
```bash
|
||||
python3 daily_hot_news.py --query "微博热搜"
|
||||
```
|
||||
|
||||
## 💬 使用案例
|
||||
|
||||
### 案例 1:查询单个平台热榜
|
||||
|
||||
```bash
|
||||
# 查询微博热搜
|
||||
python3 daily_hot_news.py -q "微博热搜"
|
||||
|
||||
# 查询知乎热榜
|
||||
python3 daily_hot_news.py -q "知乎热榜"
|
||||
|
||||
# 查询 B站热门
|
||||
python3 daily_hot_news.py -q "B站热门"
|
||||
|
||||
# 查询原神最新消息
|
||||
python3 daily_hot_news.py -q "原神"
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
🔥 **微博热搜**
|
||||
更新时间: 2026-02-05T19:00:00.000Z
|
||||
|
||||
1. 王一博 中山装 520万
|
||||
2. 肖战 害羞笑 480万
|
||||
3. 微博之夜红毯 450万
|
||||
...
|
||||
```
|
||||
|
||||
### 案例 2:浏览所有平台
|
||||
|
||||
```bash
|
||||
# 查看所有支持的热榜源
|
||||
python3 daily_hot_news.py --list
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
📊 **支持的热榜源(共 54 个)**
|
||||
|
||||
🎬 视频/直播
|
||||
• 哔哩哔哩 (bilibili)
|
||||
• 抖音 (douyin)
|
||||
• 快手 (kuaishou)
|
||||
• AcFun (acfun)
|
||||
• 酷安 (coolapk)
|
||||
|
||||
💬 社交媒体
|
||||
• 微博 (weibo)
|
||||
• 知乎 (zhihu)
|
||||
• V2EX (v2ex)
|
||||
• NGA (ngabbs)
|
||||
...
|
||||
```
|
||||
|
||||
### 案例 3:跨平台聚合
|
||||
|
||||
```bash
|
||||
# 查看全网热点 TOP10
|
||||
python3 daily_hot_news.py --cross-platform
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
🌐 全网热点 TOP10
|
||||
|
||||
🥇 原神 · 热度 952万 【B站最高】
|
||||
评分: 98分 - 🔥 超级爆款
|
||||
跨 5 平台讨论
|
||||
|
||||
🥈 微博热搜 · 热度 876万 【微博最高】
|
||||
评分: 95分 - 全网讨论
|
||||
|
||||
🥉 特朗普 · 热度 654万 【微博最高】
|
||||
评分: 89分 - 国际话题
|
||||
...
|
||||
```
|
||||
|
||||
### 案例 4:设置舆情监控
|
||||
|
||||
```bash
|
||||
# 设置监控:AI 话题,热度超过 500 万通知
|
||||
python3 daily_hot_news.py --monitor "AI,500万"
|
||||
|
||||
# 查看监控配置
|
||||
python3 daily_hot_news.py -q "查看我的监控"
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
✅ **已设置监控!**
|
||||
|
||||
监控关键词:AI
|
||||
热度阈值:500万
|
||||
监控平台:全部 54 个
|
||||
|
||||
当有话题热度超过阈值时,我会立即通知您!
|
||||
```
|
||||
|
||||
### 案例 5:定时推送配置
|
||||
|
||||
```bash
|
||||
# 配置每天早上 8 点推送微博热搜
|
||||
"每天早上 8 点推送微博热搜"
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
⏰ **定时推送已设置!**
|
||||
|
||||
推送时间:每天 08:00
|
||||
热榜源:微博热搜
|
||||
推送方式:飞书消息
|
||||
|
||||
每天早上 8 点会自动推送微博热榜到您的飞书!
|
||||
```
|
||||
|
||||
## 🎮 高级用法
|
||||
|
||||
### 按类别查询
|
||||
|
||||
```bash
|
||||
# 查看科技类热榜
|
||||
"有什么科技热榜"
|
||||
|
||||
# 查看游戏类热榜
|
||||
"游戏有什么热点"
|
||||
```
|
||||
|
||||
### 搜索特定话题
|
||||
|
||||
```bash
|
||||
# 搜索包含某个关键词的热榜
|
||||
"搜索 AI 相关热榜"
|
||||
"查找 ChatGPT 热点"
|
||||
```
|
||||
|
||||
### 查看历史数据
|
||||
|
||||
```bash
|
||||
# 查看昨天的微博热榜
|
||||
"微博昨天"
|
||||
|
||||
# 查看历史记录
|
||||
"微博历史"
|
||||
```
|
||||
|
||||
### 查看已保存数据
|
||||
|
||||
```bash
|
||||
# 查看已保存的热榜统计
|
||||
"已保存了哪些数据"
|
||||
```
|
||||
|
||||
### 清理旧数据
|
||||
|
||||
```bash
|
||||
# 回复"清理"或"是"可删除7天前的旧热榜数据
|
||||
# Skill启动时会自动检测并提示
|
||||
```
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
daily-hot-news/
|
||||
├── daily_hot_news.py # 主入口
|
||||
├── news_digest.py # 热点摘要
|
||||
├── industry_hot.py # 行业垂直
|
||||
├── personalized.py # 个性化订阅
|
||||
├── cross_platform.py # 跨平台聚合
|
||||
├── sentiment_monitor.py # 舆情监控
|
||||
├── api_client.py # API 客户端
|
||||
├── formatter.py # 格式化器
|
||||
├── storage.py # 数据存储
|
||||
├── config.py # 配置
|
||||
├── requirements.txt # 依赖列表
|
||||
└── README.md # 本说明
|
||||
```
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `DAILY_HOT_API_URL` | http://localhost:6688 | 后端 API 地址 |
|
||||
| `DAILY_HOT_CACHE_TTL` | 3600 | 缓存时间(秒) |
|
||||
| `DAILY_HOT_MAX_ITEMS` | 20 | 返回最大条数 |
|
||||
| `DAILY_HOT_TIMEOUT` | 10 | 请求超时(秒) |
|
||||
|
||||
### 🔧 技术架构
|
||||
|
||||
- **API 调用**:每次用户请求都实时调用 DailyHotApi,确保数据最新
|
||||
- **历史记录**:自动保存到 `data/{平台}/{日期}.json`
|
||||
- **旧数据清理**:启动时自动检测 7 天前数据,提示用户清理
|
||||
|
||||
## 🛡️ 安全说明
|
||||
|
||||
- ✅ 不包含任何 API 密钥或密码
|
||||
- ✅ 所有敏感配置通过环境变量管理
|
||||
- ✅ 用户数据存储在本地,不上传云端
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
**v2.0.1** (2026-02-06)
|
||||
- ✨ 优化数据获取策略:每次请求都获取最新数据
|
||||
- ✨ 自动保存历史记录到本地
|
||||
- ✨ 启动时自动检查并提示清理7天前旧数据
|
||||
- 🔧 修复 API 客户端兼容性问题
|
||||
|
||||
**v2.0.0** (2026-02-05)
|
||||
- ✨ 新增 5 个扩展功能
|
||||
- ✨ 支持 15 种热点标签分类
|
||||
- ✨ 支持十大行业垂直热榜
|
||||
- ✨ 新增个性化订阅功能
|
||||
- ✨ 新增跨平台 TOP10 聚合
|
||||
- ✨ 新增舆情监控告警
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🤝 致谢
|
||||
|
||||
- [DailyHotApi](https://github.com/imsyy/DailyHotApi) - 提供 54 个热榜源 API
|
||||
- [OpenClaw](https://github.com/openclaw/openclaw) - AI 助手平台
|
||||
@@ -0,0 +1,360 @@
|
||||
<div align="center">
|
||||
|
||||
# 🔥 Daily Hot News - OpenClaw Skill
|
||||
|
||||
[](https://github.com/one-box-u/openclaw-daily-hot-news/stargazers)
|
||||
[](https://github.com/one-box-u/openclaw-daily-hot-news/network)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
**🇺🇸 English Readme** | **📖 [中文说明](README.md)**
|
||||
|
||||
---
|
||||
|
||||
A hot news aggregation skill based on DailyHotApi, supporting 54 platform hot search queries, cross-platform aggregation, and sentiment monitoring.
|
||||
|
||||
</div>
|
||||
|
||||
## 🎯 Features
|
||||
|
||||
### Core Features
|
||||
- **Hot Search Query**: Query hot search data from any of 54 platforms
|
||||
- **Category Browse**: Quickly locate specific platforms by category
|
||||
- **Real-time Fetch**: Always get the latest hot search data on each request
|
||||
- **History**: Automatically save daily hot search data to local storage
|
||||
- **Smart Cleanup**: Automatically check and prompt to clean data older than 7 days on startup
|
||||
|
||||
### Extended Features
|
||||
- **Hot News Digest**: 15-tag classification, AI-guided selection
|
||||
- **Industry Vertical**: 10 industry categories (tech, gaming, finance, etc.)
|
||||
- **Personalized Subscription**: Custom keywords and platform preferences
|
||||
- **Cross-Platform Aggregation**: Top 10 hot search榜单 nationwide
|
||||
- **Sentiment Monitoring**: Keyword monitoring and hot alerts
|
||||
|
||||
## 📊 Supported Platforms (54)
|
||||
|
||||
### 🎬 Video/Live Streaming (5)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| Bilibili | bilibili | Hot Ranking |
|
||||
| AcFun | acfun | Ranking List |
|
||||
| Douyin | douyin | Hot Topics |
|
||||
| Kuaishou | kuaishou | Hot Topics |
|
||||
| Coolapk | coolapk | Hot Ranking |
|
||||
|
||||
### 💬 Social Media (8)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| Weibo | weibo | Hot Search |
|
||||
| Zhihu | zhihu | Hot List |
|
||||
| Zhihu Daily | zhihu-daily | Recommended |
|
||||
| Tieba | tieba | Hot Discussion |
|
||||
| Douban Group | douban-group | Discussion Picks |
|
||||
| V2EX | v2ex | Topic Ranking |
|
||||
| NGA | ngabbs | Hot Posts |
|
||||
| Hupu | hupu | Street Hot Posts |
|
||||
|
||||
### 📰 News & Media (10)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| Baidu | baidu | Hot Search |
|
||||
| The Paper | thepaper | Hot List |
|
||||
| Toutiao | toutiao | Hot List |
|
||||
| 36kr | 36kr | Hot List |
|
||||
| QQ News | qq-news | Hot Topics |
|
||||
| Sina | sina | Hot List |
|
||||
| Sina News | sina-news | Hot Topics |
|
||||
| NetEase News | netease-news | Hot Topics |
|
||||
| Huxiu | huxiu | 24 Hours |
|
||||
| Ifanr | ifanr | Quick News |
|
||||
|
||||
### 💻 Tech/Developer Communities (8)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| IT Home | ithome | Hot List |
|
||||
| IT Home Xijiayi | ithome-xijiayi | Latest Updates |
|
||||
| Sspai | sspai | Hot List |
|
||||
| CSDN | csdn | Ranking List |
|
||||
| Juejin | juejin | Hot List |
|
||||
| 51CTO | 51cto | Recommended |
|
||||
| NodeSeek | nodeseek | Latest Updates |
|
||||
| HelloGitHub | hellogithub | Trending |
|
||||
|
||||
### 🎮 Gaming/ACG (5)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| Genshin | genshin | Latest News |
|
||||
| MiyouShe | miyoushe | Latest News |
|
||||
| Honkai 3 | honkai | Latest Updates |
|
||||
| StarRail | starrail | Latest Updates |
|
||||
| LOL | lol | Update Notice |
|
||||
|
||||
### 📚 Reading/Culture (4)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| Jianshu | jianshu | Popular Recommendations |
|
||||
| Guokr | guokr | Popular Articles |
|
||||
| WeRead | weread | Rising List |
|
||||
| Douban Movie | douban-movie | New Movies |
|
||||
|
||||
### 🔧 Tools/Other (5)
|
||||
| Platform | API | Description |
|
||||
|----------|------|------|
|
||||
| 52pojie | 52pojie | Ranking List |
|
||||
| HostLoc | hostloc | Ranking List |
|
||||
| Weather Alarm | weatheralarm | National Warning |
|
||||
| Earthquake | earthquake | Earthquake Report |
|
||||
| History Today | history | Month-Day |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Deploy Backend Service
|
||||
|
||||
```bash
|
||||
cd daily-hot-api
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Configure Environment Variables
|
||||
|
||||
```bash
|
||||
export DAILY_HOT_API_URL=http://localhost:6688
|
||||
```
|
||||
|
||||
### 4. Run
|
||||
|
||||
```bash
|
||||
python3 daily_hot_news.py --query "weibo hot"
|
||||
```
|
||||
|
||||
## 💬 Usage Examples
|
||||
|
||||
### Example 1: Query Single Platform
|
||||
|
||||
```bash
|
||||
# Query Weibo hot search
|
||||
python3 daily_hot_news.py -q "微博热搜"
|
||||
|
||||
# Query Zhihu hot list
|
||||
python3 daily_hot_news.py -q "知乎热榜"
|
||||
|
||||
# Query Bilibili trending
|
||||
python3 daily_hot_news.py -q "B站热门"
|
||||
|
||||
# Query Genshin latest
|
||||
python3 daily_hot_news.py -q "原神"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
🔥 **Weibo Hot Search**
|
||||
Update Time: 2026-02-05T19:00:00.000Z
|
||||
|
||||
1. Wang Yibo Zhongshan Suit 5.2M
|
||||
2. Xiao Zhan Shy Smile 4.8M
|
||||
3. Weibo Night Red Carpet 4.5M
|
||||
...
|
||||
```
|
||||
|
||||
### Example 2: Browse All Platforms
|
||||
|
||||
```bash
|
||||
# View all supported hot search sources
|
||||
python3 daily_hot_news.py --list
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
📊 **Supported Hot Search Sources (54)**
|
||||
|
||||
🎬 Video/Live Streaming
|
||||
• Bilibili (bilibili)
|
||||
• Douyin (douyin)
|
||||
• Kuaishou (kuaishou)
|
||||
• AcFun (acfun)
|
||||
• Coolapk (coolapk)
|
||||
|
||||
💬 Social Media
|
||||
• Weibo (weibo)
|
||||
• Zhihu (zhihu)
|
||||
• V2EX (v2ex)
|
||||
• NGA (ngabbs)
|
||||
...
|
||||
```
|
||||
|
||||
### Example 3: Cross-Platform Aggregation
|
||||
|
||||
```bash
|
||||
# View nationwide Top 10
|
||||
python3 daily_hot_news.py --cross-platform
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
🌐 Nationwide Hot Topics TOP10
|
||||
|
||||
🥇 Genshin Update · 9.52M 【Highest on Bilibili】
|
||||
Score: 98 - 🔥 Super Popular
|
||||
Discussed on 5 platforms
|
||||
|
||||
🥈 Weibo Hot · 8.76M 【Highest on Weibo】
|
||||
Score: 95 - Nationwide Discussion
|
||||
|
||||
🥉 Trump · 6.54M 【Highest on Weibo】
|
||||
Score: 89 - International Topic
|
||||
...
|
||||
```
|
||||
|
||||
### Example 4: Set Sentiment Monitoring
|
||||
|
||||
```bash
|
||||
# Set monitoring: AI topic, alert when exceeds 5M
|
||||
python3 daily_hot_news.py --monitor "AI,500万"
|
||||
|
||||
# View monitoring configuration
|
||||
python3 daily_hot_news.py -q "查看我的监控"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
✅ **Monitoring Set!**
|
||||
|
||||
Monitoring Keyword: AI
|
||||
Hot Threshold: 5M
|
||||
Platforms: All 54
|
||||
|
||||
I'll notify you when any topic exceeds the threshold!
|
||||
```
|
||||
|
||||
### Example 5: Scheduled Push Configuration
|
||||
|
||||
```bash
|
||||
# Configure daily 8 AM Weibo hot push
|
||||
"每天早上 8 点推送微博热搜"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
⏰ **Scheduled Push Configured!**
|
||||
|
||||
Push Time: Daily 08:00
|
||||
Hot Source: Weibo Hot Search
|
||||
Push Method: Feishu Message
|
||||
|
||||
I'll automatically push Weibo hot to your Feishu every morning at 8!
|
||||
```
|
||||
|
||||
## 🎮 Advanced Usage
|
||||
|
||||
### Query by Category
|
||||
|
||||
```bash
|
||||
# View tech hot searches
|
||||
"有什么科技热榜"
|
||||
|
||||
# View gaming hot topics
|
||||
"游戏有什么热点"
|
||||
```
|
||||
|
||||
### Search Specific Topics
|
||||
|
||||
```bash
|
||||
# Search for topics containing keywords
|
||||
"搜索 AI 相关热榜"
|
||||
"查找 ChatGPT 热点"
|
||||
```
|
||||
|
||||
### View Historical Data
|
||||
|
||||
```bash
|
||||
# View yesterday's Weibo hot
|
||||
"微博昨天"
|
||||
|
||||
# View history records
|
||||
"微博历史"
|
||||
```
|
||||
|
||||
### View Saved Data
|
||||
|
||||
```bash
|
||||
# View saved hot search statistics
|
||||
"已保存了哪些数据"
|
||||
```
|
||||
|
||||
### Cleanup Old Data
|
||||
|
||||
```bash
|
||||
# Reply "清理" or "是" to delete data older than 7 days
|
||||
# Skill will automatically detect and prompt on startup
|
||||
```
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
daily-hot-news/
|
||||
├── daily_hot_news.py # Main Entry
|
||||
├── news_digest.py # Hot News Digest
|
||||
├── industry_hot.py # Industry Vertical
|
||||
├── personalized.py # Personalized Subscription
|
||||
├── cross_platform.py # Cross-Platform Aggregation
|
||||
├── sentiment_monitor.py # Sentiment Monitoring
|
||||
├── api_client.py # API Client
|
||||
├── formatter.py # Formatter
|
||||
├── storage.py # Data Storage
|
||||
├── config.py # Configuration
|
||||
├── requirements.txt # Dependencies
|
||||
└── README.md # This Document
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
|---------------------|---------|-------------|
|
||||
| `DAILY_HOT_API_URL` | http://localhost:6688 | Backend API URL |
|
||||
| `DAILY_HOT_CACHE_TTL` | 3600 | Cache Time (seconds) |
|
||||
| `DAILY_HOT_MAX_ITEMS` | 20 | Max Items Returned |
|
||||
| `DAILY_HOT_TIMEOUT` | 10 | Request Timeout (seconds) |
|
||||
|
||||
### 🔧 Technical Architecture
|
||||
|
||||
- **API Calls**: Each user request triggers a real-time call to DailyHotApi for the latest data
|
||||
- **History**: Automatically saved to `data/{platform}/{date}.json`
|
||||
- **Old Data Cleanup**: Automatically detects data older than 7 days on startup and prompts for cleanup
|
||||
|
||||
## 🛡️ Security Note
|
||||
|
||||
- ✅ No API keys or passwords included
|
||||
- ✅ All sensitive configurations managed via environment variables
|
||||
- ✅ User data stored locally, not uploaded to cloud
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
**v2.0.1** (2026-02-06)
|
||||
- ✨ Optimized data fetching: always fetch latest data on each request
|
||||
- ✨ Auto-save history records to local storage
|
||||
- ✨ Auto-check and prompt cleanup for data older than 7 days on startup
|
||||
- 🔧 Fixed API client compatibility issues
|
||||
|
||||
**v2.0.0** (2026-02-05)
|
||||
- ✨ Added 5 extended features
|
||||
- ✨ Support 15 hot tag classifications
|
||||
- ✨ Support 10 industry vertical hot searches
|
||||
- ✨ Added personalized subscription feature
|
||||
- ✨ Added cross-platform TOP10 aggregation
|
||||
- ✨ Added sentiment monitoring alerts
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
|
||||
## 🤝 Acknowledgements
|
||||
|
||||
- [DailyHotApi](https://github.com/imsyy/DailyHotApi) - Providing 54 hot search source APIs
|
||||
- [OpenClaw](https://github.com/openclaw/openclaw) - AI Assistant Platform
|
||||
@@ -0,0 +1,323 @@
|
||||
---
|
||||
name: daily-hot-news
|
||||
description: 每日热榜技能 - 查询微博、知乎、B站、抖音等54个平台的热榜数据,支持定时推送和分类浏览。
|
||||
categories:
|
||||
- information-aggregation
|
||||
- daily-utility
|
||||
- news
|
||||
emoji: 🔥
|
||||
metadata:
|
||||
openclaw:
|
||||
requires:
|
||||
bins: ["python3"]
|
||||
install:
|
||||
- id: python-deps
|
||||
kind: exec
|
||||
command: "cd /root/.openclaw/workspace/skills/daily-hot-news && python3 -m pip install requests aiohttp"
|
||||
label: "安装Python依赖"
|
||||
---
|
||||
|
||||
# 🔥 每日热榜
|
||||
|
||||
## 🎯 概述
|
||||
|
||||
提供 **54 个热榜源** 的本地化查询服务,基于 [DailyHotApi](https://github.com/imsyy/DailyHotApi) 项目。
|
||||
|
||||
**核心功能**:
|
||||
- 📊 热榜查询 - 查询任意平台的热榜数据
|
||||
- 📋 分类浏览 - 列出所有支持的热榜源
|
||||
- 💾 历史记录 - 自动保存每日热榜数据
|
||||
- ⏰ 定时推送 - 自动推送热榜到飞书
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
```
|
||||
用户请求 → DailyHotApi Skill → 本地 DailyHotApi 服务 → 返回格式化结果
|
||||
```
|
||||
|
||||
### 组件说明
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| **DailyHotApi 服务** | 独立运行,抓取/聚合热榜数据 |
|
||||
| **DailyHotApi Skill** | OpenClaw 插件,处理用户请求 |
|
||||
|
||||
---
|
||||
|
||||
## 📡 支持的热榜源(54个)
|
||||
|
||||
### 🎬 视频/直播平台
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| bilibili | 哔哩哔哩 |
|
||||
| acfun | AcFun |
|
||||
| douyin | 抖音 |
|
||||
| kuaishou | 快手 |
|
||||
| coolapk | 酷安 |
|
||||
|
||||
### 💬 社交媒体
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| weibo | 微博 |
|
||||
| zhihu | 知乎 |
|
||||
| zhihu-daily | 知乎日报 |
|
||||
| tieba | 百度贴吧 |
|
||||
| douban-group | 豆瓣讨论小组 |
|
||||
| v2ex | V2EX |
|
||||
| ngabbs | NGA |
|
||||
| hupu | 虎扑 |
|
||||
|
||||
### 📰 新闻资讯
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| baidu | 百度热搜 |
|
||||
| thepaper | 澎湃新闻 |
|
||||
| toutiao | 今日头条 |
|
||||
| 36kr | 36氪 |
|
||||
| qq-news | 腾讯新闻 |
|
||||
| sina | 新浪网 |
|
||||
| sina-news | 新浪新闻 |
|
||||
| netease-news | 网易新闻 |
|
||||
| huxiu | 虎嗅 |
|
||||
| ifanr | 爱范儿 |
|
||||
|
||||
### 💻 科技/技术社区
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| ithome | IT之家 |
|
||||
| ithome-xijiayi | IT之家「喜加一」 |
|
||||
| sspai | 少数派 |
|
||||
| csdn | CSDN |
|
||||
| juejin | 稀土掘金 |
|
||||
| 51cto | 51CTO |
|
||||
| nodeseek | NodeSeek |
|
||||
| hellogithub | HelloGitHub |
|
||||
|
||||
### 🎮 游戏/ACG
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| genshin | 原神 |
|
||||
| miyoushe | 米游社 |
|
||||
| honkai | 崩坏3 |
|
||||
| starrail | 崩坏:星穹铁道 |
|
||||
| lol | 英雄联盟 |
|
||||
|
||||
### 📚 阅读/文化
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| jianshu | 简书 |
|
||||
| guokr | 果壳 |
|
||||
| weread | 微信读书 |
|
||||
| douban-movie | 豆瓣电影 |
|
||||
|
||||
### 🔧 工具/其他
|
||||
| 接口 | 名称 |
|
||||
|------|------|
|
||||
| 52pojie | 吾爱破解 |
|
||||
| hostloc | 全球主机交流 |
|
||||
| weatheralarm | 中央气象台 |
|
||||
| earthquake | 中国地震台 |
|
||||
| history | 历史上的今天 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 部署说明
|
||||
|
||||
### 1. PM2 方式管理(推荐)
|
||||
|
||||
DailyHotApi 服务使用 PM2 管理,确保稳定运行。
|
||||
|
||||
```bash
|
||||
cd /root/.openclaw/workspace/skills/daily-hot-api
|
||||
|
||||
# 部署并启动服务
|
||||
./deploy.sh
|
||||
|
||||
# 查看状态
|
||||
./deploy.sh status
|
||||
|
||||
# 重启服务
|
||||
./deploy.sh restart
|
||||
|
||||
# 停止服务
|
||||
./deploy.sh stop
|
||||
|
||||
# 查看日志
|
||||
./deploy.sh logs
|
||||
```
|
||||
|
||||
**服务地址**: `http://localhost:6688`
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
```bash
|
||||
export DAILY_HOT_API_URL=http://localhost:6688
|
||||
```
|
||||
|
||||
### 3. 安装 Skill 依赖
|
||||
|
||||
```bash
|
||||
cd /root/.openclaw/workspace/skills/daily-hot-news
|
||||
pip install requests aiohttp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 使用示例
|
||||
|
||||
### 查询热榜
|
||||
```
|
||||
用户: 微博热搜
|
||||
Skill: 调用 /weibo → 返回 Top 10 热榜
|
||||
```
|
||||
|
||||
### 查看所有热榜
|
||||
```
|
||||
用户: 有什么热榜
|
||||
Skill: 返回 54 个热榜源列表
|
||||
```
|
||||
|
||||
### 查询历史热榜
|
||||
```
|
||||
用户: 微博历史
|
||||
Skill: 显示之前保存的微博热榜记录
|
||||
```
|
||||
|
||||
### 查看已保存数据
|
||||
```
|
||||
用户: 已保存了哪些数据
|
||||
Skill: 返回所有已保存的热榜数据统计
|
||||
```
|
||||
|
||||
### 定时推送
|
||||
```
|
||||
用户: 每天早上8点推送B站热门
|
||||
Skill: 设置 cron 任务 → 每日调用 /bilibili → 推送到飞书
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 数据存储
|
||||
|
||||
### 存储位置
|
||||
所有热榜数据保存在:
|
||||
```
|
||||
/root/.openclaw/workspace/skills/daily-hot-news/data/
|
||||
```
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
data/
|
||||
├── weibo/
|
||||
│ ├── 2026-02-05.json
|
||||
│ └── 2026-02-04.json
|
||||
├── zhihu/
|
||||
│ └── 2026-02-05.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 配置项
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `DAILY_HOT_DATA_DIR` | data/ | 数据存储目录 |
|
||||
| `DAILY_HOT_AUTO_SAVE` | true | 是否自动保存热榜数据 |
|
||||
|
||||
### 管理命令
|
||||
```bash
|
||||
# 查看已保存的数据统计
|
||||
python3 storage.py
|
||||
|
||||
# 清理 30 天前的旧数据
|
||||
python3 storage.py --clear 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 响应格式
|
||||
|
||||
### 热榜列表响应
|
||||
```json
|
||||
{
|
||||
"platform": "微博",
|
||||
"updateTime": "2026-02-05 17:00:00",
|
||||
"data": [
|
||||
{
|
||||
"rank": 1,
|
||||
"title": "热搜标题",
|
||||
"hot": "1234万",
|
||||
"url": "https://..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置项
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `DAILY_HOT_API_URL` | http://localhost:6688 | DailyHotApi 服务地址 |
|
||||
| `DAILY_HOT_CACHE_TTL` | 3600 | 缓存时间(秒) |
|
||||
| `DAILY_HOT_MAX_ITEMS` | 20 | 返回最大条数 |
|
||||
|
||||
---
|
||||
|
||||
## 📊 资源占用
|
||||
|
||||
| 组件 | 内存 | CPU |
|
||||
|------|------|-----|
|
||||
| DailyHotApi 服务 | ~200MB | 极低 |
|
||||
| DailyHotApi Skill | <10MB | 可忽略 |
|
||||
|
||||
**总计**: <250MB,对服务器无压力
|
||||
|
||||
---
|
||||
|
||||
## 🔧 故障排查
|
||||
|
||||
### 问题: 服务无法连接
|
||||
```bash
|
||||
# 检查 PM2 状态
|
||||
./deploy.sh status
|
||||
|
||||
# 查看日志
|
||||
./deploy.sh logs
|
||||
|
||||
# 重启服务
|
||||
./deploy.sh restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
daily-hot-news/
|
||||
├── SKILL.md # 本说明书
|
||||
├── daily_hot_news.py # 核心 Skill 脚本
|
||||
├── api_client.py # API 客户端封装
|
||||
├── formatter.py # 响应格式化
|
||||
├── config.py # 配置管理
|
||||
├── storage.py # 数据存储模块
|
||||
├── data/ # 热榜数据存储目录
|
||||
├── README.md # 快速开始
|
||||
└── requirements.txt # 依赖列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
**v1.1.0** (2026-02-05)
|
||||
- ✨ 新增数据存储功能
|
||||
- ✨ 支持自动保存每日热榜
|
||||
- ✨ 支持查询历史记录
|
||||
- ✨ 新增数据统计命令
|
||||
|
||||
**v1.0.0** (2026-02-05)
|
||||
- 初始版本
|
||||
- 支持 54 个热榜源
|
||||
- 基础查询和定时推送功能
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
每日热榜技能扩展模块
|
||||
|
||||
整合了以下功能:
|
||||
- news_digest: 热点新闻摘要(15种标签分类)
|
||||
- industry_hot: 行业热榜垂直(十大行业分类)
|
||||
- personalized: 个性化订阅(关键词/平台/排除项配置)
|
||||
- cross_platform: 跨平台聚合(TOP10聚合)
|
||||
- sentiment_monitor: 舆情监控(关键词过滤)
|
||||
"""
|
||||
|
||||
from .news_digest import (
|
||||
NewsDigest,
|
||||
DigestConfig,
|
||||
DigestMode,
|
||||
TAG_MAPPING,
|
||||
ALL_TAGS,
|
||||
create_digest as create_news_digest
|
||||
)
|
||||
|
||||
from .industry_hot import (
|
||||
IndustryHot,
|
||||
IndustryConfig,
|
||||
IndustryMode,
|
||||
INDUSTRIES,
|
||||
ALL_INDUSTRIES,
|
||||
create_industry_hot
|
||||
)
|
||||
|
||||
from .personalized import (
|
||||
PersonalizedSubscription,
|
||||
UserPreferences,
|
||||
SubscriptionMode,
|
||||
KEYWORD_OPTIONS,
|
||||
PLATFORM_OPTIONS,
|
||||
EXCLUDE_OPTIONS,
|
||||
create_personalized
|
||||
)
|
||||
|
||||
from .cross_platform import (
|
||||
CrossPlatformAggregator,
|
||||
CrossPlatformConfig,
|
||||
create_cross_platform
|
||||
)
|
||||
|
||||
from .sentiment_monitor import (
|
||||
SentimentMonitor,
|
||||
SentimentConfig,
|
||||
SentimentType,
|
||||
create_sentiment_monitor
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# News Digest
|
||||
"NewsDigest",
|
||||
"DigestConfig",
|
||||
"DigestMode",
|
||||
"TAG_MAPPING",
|
||||
"ALL_TAGS",
|
||||
"create_news_digest",
|
||||
|
||||
# Industry Hot
|
||||
"IndustryHot",
|
||||
"IndustryConfig",
|
||||
"IndustryMode",
|
||||
"INDUSTRIES",
|
||||
"ALL_INDUSTRIES",
|
||||
"create_industry_hot",
|
||||
|
||||
# Personalized
|
||||
"PersonalizedSubscription",
|
||||
"UserPreferences",
|
||||
"SubscriptionMode",
|
||||
"KEYWORD_OPTIONS",
|
||||
"PLATFORM_OPTIONS",
|
||||
"EXCLUDE_OPTIONS",
|
||||
"create_personalized",
|
||||
|
||||
# Cross Platform
|
||||
"CrossPlatformAggregator",
|
||||
"CrossPlatformConfig",
|
||||
"create_cross_platform",
|
||||
|
||||
# Sentiment Monitor
|
||||
"SentimentMonitor",
|
||||
"SentimentConfig",
|
||||
"SentimentType",
|
||||
"create_sentiment_monitor"
|
||||
]
|
||||
@@ -0,0 +1,432 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DailyHotApi Skill - API 客户端封装
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import subprocess
|
||||
import os
|
||||
from typing import Optional, Dict, List, Any
|
||||
from datetime import datetime
|
||||
from config import config
|
||||
from storage import storage # 导入存储模块
|
||||
|
||||
# 部署状态存储
|
||||
_deployment_status = {
|
||||
"is_deploying": False,
|
||||
"last_check": None,
|
||||
"needs_deployment": False,
|
||||
"message": ""
|
||||
}
|
||||
|
||||
|
||||
class HotSource:
|
||||
"""热榜源定义"""
|
||||
|
||||
def __init__(self, id: str, name: str, category: str, description: str = ""):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.category = category
|
||||
self.description = description
|
||||
|
||||
|
||||
# 54 个热榜源定义
|
||||
HOT_SOURCES = {
|
||||
# 视频/直播平台
|
||||
"bilibili": HotSource("bilibili", "哔哩哔哩", "video", "热门榜"),
|
||||
"acfun": HotSource("acfun", "AcFun", "video", "排行榜"),
|
||||
"douyin": HotSource("douyin", "抖音", "video", "热点榜"),
|
||||
"kuaishou": HotSource("kuaishou", "快手", "video", "热点榜"),
|
||||
"coolapk": HotSource("coolapk", "酷安", "video", "热榜"),
|
||||
|
||||
# 社交媒体
|
||||
"weibo": HotSource("weibo", "微博", "social", "热搜榜"),
|
||||
"zhihu": HotSource("zhihu", "知乎", "social", "热榜"),
|
||||
"zhihu-daily": HotSource("zhihu-daily", "知乎日报", "social", "推荐榜"),
|
||||
"tieba": HotSource("tieba", "百度贴吧", "social", "热议榜"),
|
||||
"douban-group": HotSource("douban-group", "豆瓣讨论小组", "social", "讨论精选"),
|
||||
"v2ex": HotSource("v2ex", "V2EX", "social", "主题榜"),
|
||||
"ngabbs": HotSource("ngabbs", "NGA", "social", "热帖"),
|
||||
"hupu": HotSource("hupu", "虎扑", "social", "步行街热帖"),
|
||||
|
||||
# 新闻资讯
|
||||
"baidu": HotSource("baidu", "百度", "news", "热搜榜"),
|
||||
"thepaper": HotSource("thepaper", "澎湃新闻", "news", "热榜"),
|
||||
"toutiao": HotSource("toutiao", "今日头条", "news", "热榜"),
|
||||
"36kr": HotSource("36kr", "36氪", "news", "热榜"),
|
||||
"qq-news": HotSource("qq-news", "腾讯新闻", "news", "热点榜"),
|
||||
"sina": HotSource("sina", "新浪网", "news", "热榜"),
|
||||
"sina-news": HotSource("sina-news", "新浪新闻", "news", "热点榜"),
|
||||
"netease-news": HotSource("netease-news", "网易新闻", "news", "热点榜"),
|
||||
"huxiu": HotSource("huxiu", "虎嗅", "news", "24小时"),
|
||||
"ifanr": HotSource("ifanr", "爱范儿", "news", "快讯"),
|
||||
|
||||
# 科技/技术社区
|
||||
"ithome": HotSource("ithome", "IT之家", "tech", "热榜"),
|
||||
"ithome-xijiayi": HotSource("ithome-xijiayi", "IT之家「喜加一」", "tech", "最新动态"),
|
||||
"sspai": HotSource("sspai", "少数派", "tech", "热榜"),
|
||||
"csdn": HotSource("csdn", "CSDN", "tech", "排行榜"),
|
||||
"juejin": HotSource("juejin", "稀土掘金", "tech", "热榜"),
|
||||
"51cto": HotSource("51cto", "51CTO", "tech", "推荐榜"),
|
||||
"nodeseek": HotSource("nodeseek", "NodeSeek", "tech", "最新动态"),
|
||||
"hellogithub": HotSource("hellogithub", "HelloGitHub", "tech", "Trending"),
|
||||
|
||||
# 游戏/ACG
|
||||
"genshin": HotSource("genshin", "原神", "game", "最新消息"),
|
||||
"miyoushe": HotSource("miyoushe", "米游社", "game", "最新消息"),
|
||||
"honkai": HotSource("honkai", "崩坏3", "game", "最新动态"),
|
||||
"starrail": HotSource("starrail", "崩坏:星穹铁道", "game", "最新动态"),
|
||||
"lol": HotSource("lol", "英雄联盟", "game", "更新公告"),
|
||||
|
||||
# 阅读/文化
|
||||
"jianshu": HotSource("jianshu", "简书", "reading", "热门推荐"),
|
||||
"guokr": HotSource("guokr", "果壳", "reading", "热门文章"),
|
||||
"weread": HotSource("weread", "微信读书", "reading", "飙升榜"),
|
||||
"douban-movie": HotSource("douban-movie", "豆瓣电影", "reading", "新片榜"),
|
||||
|
||||
# 工具/其他
|
||||
"52pojie": HotSource("52pojie", "吾爱破解", "tool", "榜单"),
|
||||
"hostloc": HotSource("hostloc", "全球主机交流", "tool", "榜单"),
|
||||
"weatheralarm": HotSource("weatheralarm", "中央气象台", "tool", "全国气象预警"),
|
||||
"earthquake": HotSource("earthquake", "中国地震台", "tool", "地震速报"),
|
||||
"history": HotSource("history", "历史上的今天", "tool", "月-日"),
|
||||
}
|
||||
|
||||
|
||||
class DailyHotApiClient:
|
||||
"""DailyHotApi 客户端"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_url = config.api_url
|
||||
self.timeout = config.timeout
|
||||
|
||||
async def fetch_hot_list(self, source_id: str, use_cache: bool = False) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取热榜数据
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID(如 weibo, zhihu, bilibili)
|
||||
use_cache: 是否使用缓存(默认False,每次获取最新数据)
|
||||
|
||||
Returns:
|
||||
热榜数据字典或 None(失败时)
|
||||
"""
|
||||
# 获取热榜源信息
|
||||
source = HOT_SOURCES.get(source_id)
|
||||
if not source:
|
||||
return None
|
||||
|
||||
# 构建请求 URL
|
||||
url = config.get_api_url(source_id)
|
||||
|
||||
# 步骤1:检查API是否可用
|
||||
print(f"[DailyHotApi] 正在连接 {source.name}...")
|
||||
api_available = await check_api_availability(url)
|
||||
|
||||
if not api_available:
|
||||
# API不可用,尝试部署
|
||||
print(f"[DailyHotApi] ⚠️ 后端服务不可用,尝试自动部署...")
|
||||
|
||||
# 触发部署
|
||||
deploy_result = await deploy_daily_hot_api()
|
||||
|
||||
if deploy_result["success"]:
|
||||
# 部署成功,等待服务启动
|
||||
print(f"[DailyHotApi] ⏳ 等待服务启动 (5秒)...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 再次检查API
|
||||
api_available = await check_api_availability(url)
|
||||
|
||||
if not api_available:
|
||||
# 仍然不可用,可能需要更多时间
|
||||
print(f"[DailyHotApi] ⏳ 服务可能需要更多时间启动,再次等待 (10秒)...")
|
||||
await asyncio.sleep(10)
|
||||
api_available = await check_api_availability(url)
|
||||
|
||||
# 返回部署状态
|
||||
if deploy_result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"deploy_message": deploy_result["message"],
|
||||
"is_deployed": True,
|
||||
"data": None,
|
||||
"message": "🎉 后端服务已部署成功!请稍后再次尝试获取热榜数据。"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"deploy_message": "❌ 自动部署失败",
|
||||
"steps": deploy_result.get("steps", []),
|
||||
"data": None,
|
||||
"message": f"⚠️ 无法连接后端服务\n\n自动部署失败:{deploy_result['message']}\n\n请手动部署:\n1. cd /root/.openclaw\n2. git clone https://github.com/imsyy/DailyHotApi.git\n3. cd DailyHotApi\n4. bash deploy.sh"
|
||||
}
|
||||
|
||||
# API可用,获取数据
|
||||
try:
|
||||
timeout_obj = aiohttp.ClientTimeout(total=self.timeout)
|
||||
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status != 200:
|
||||
return None
|
||||
|
||||
result = await response.json()
|
||||
|
||||
# 检查返回格式
|
||||
if result.get("code") != 200:
|
||||
return None
|
||||
|
||||
# 格式化数据
|
||||
data = {
|
||||
"platform": source.name,
|
||||
"category": source.category,
|
||||
"source_id": source_id,
|
||||
"update_time": result.get("updateTime"),
|
||||
"from_cache": result.get("fromCache", False),
|
||||
"total": result.get("total", 0),
|
||||
"data": self._format_items(result.get("data", [])),
|
||||
}
|
||||
|
||||
# 每次获取最新数据后,保存到本地历史记录
|
||||
save_result = storage.save_hot_list(source_id, data)
|
||||
if save_result:
|
||||
print(f"[DailyHotApi] ✅ 已保存 {source.name} 热榜数据到历史记录")
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DailyHotApi] Error fetching {source_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_hot榜单(self, source_id: str, limit: int = 10) -> Optional[List[Dict]]:
|
||||
"""
|
||||
获取热榜条目列表(兼容旧接口)
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID
|
||||
limit: 返回条目数限制
|
||||
|
||||
Returns:
|
||||
热榜条目列表或 None
|
||||
"""
|
||||
data = await self.fetch_hot_list(source_id)
|
||||
if data:
|
||||
return data.get("data", [])[:limit]
|
||||
return None
|
||||
|
||||
def _format_items(self, items: List[Dict]) -> List[Dict]:
|
||||
"""格式化热榜条目"""
|
||||
formatted = []
|
||||
for i, item in enumerate(items[:config.max_items], 1):
|
||||
formatted.append({
|
||||
"rank": i,
|
||||
"title": item.get("title", ""),
|
||||
"desc": item.get("desc", ""),
|
||||
"hot": item.get("hot", ""),
|
||||
"url": item.get("url", ""),
|
||||
"mobile_url": item.get("mobileUrl", ""),
|
||||
})
|
||||
return formatted
|
||||
|
||||
def get_all_sources(self) -> Dict[str, Dict]:
|
||||
"""获取所有热榜源"""
|
||||
return {
|
||||
source_id: {
|
||||
"id": source_id,
|
||||
"name": source.name,
|
||||
"category": source.category,
|
||||
"description": source.description,
|
||||
}
|
||||
for source_id, source in HOT_SOURCES.items()
|
||||
}
|
||||
|
||||
def get_sources_by_category(self) -> Dict[str, List[Dict]]:
|
||||
"""按类别获取热榜源"""
|
||||
categories = {}
|
||||
for source_id, source in HOT_SOURCES.items():
|
||||
if source.category not in categories:
|
||||
categories[source.category] = []
|
||||
categories[source.category].append({
|
||||
"id": source_id,
|
||||
"name": source.name,
|
||||
"description": source.description,
|
||||
})
|
||||
return categories
|
||||
|
||||
def search_sources(self, query: str) -> List[Dict]:
|
||||
"""搜索热榜源"""
|
||||
query = query.lower()
|
||||
results = []
|
||||
for source_id, source in HOT_SOURCES.items():
|
||||
if (query in source.name.lower() or
|
||||
query in source.id.lower() or
|
||||
query in source.category.lower()):
|
||||
results.append({
|
||||
"id": source_id,
|
||||
"name": source.name,
|
||||
"category": source.category,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
# 全局客户端实例
|
||||
api_client = DailyHotApiClient()
|
||||
|
||||
|
||||
# ============================================
|
||||
# 自动检测和部署功能
|
||||
# ============================================
|
||||
|
||||
async def check_api_availability(url: str, timeout: int = 3) -> bool:
|
||||
"""
|
||||
检查API是否可用
|
||||
|
||||
Args:
|
||||
url: API地址
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
True表示可用,False表示不可用
|
||||
"""
|
||||
try:
|
||||
timeout_obj = aiohttp.ClientTimeout(total=timeout)
|
||||
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
|
||||
async with session.get(url, allow_redirects=True) as response:
|
||||
return response.status == 200
|
||||
except Exception as e:
|
||||
print(f"[DailyHotApi] API不可用: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def deploy_daily_hot_api() -> Dict[str, Any]:
|
||||
"""
|
||||
自动部署DailyHotApi后端服务
|
||||
|
||||
Returns:
|
||||
部署结果字典
|
||||
"""
|
||||
global _deployment_status
|
||||
|
||||
if _deployment_status["is_deploying"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "部署正在进行中,请稍候...",
|
||||
"is_deploying": True
|
||||
}
|
||||
|
||||
_deployment_status["is_deploying"] = True
|
||||
_deployment_status["message"] = "🚀 正在自动部署DailyHotApi后端服务..."
|
||||
|
||||
result = {
|
||||
"success": False,
|
||||
"message": "",
|
||||
"steps": []
|
||||
}
|
||||
|
||||
try:
|
||||
# 检查是否已安装
|
||||
daily_hot_path = "/root/.openclaw/DailyHotApi"
|
||||
if not os.path.exists(daily_hot_path):
|
||||
# 步骤1:克隆仓库
|
||||
step_msg = "📦 正在克隆DailyHotApi仓库..."
|
||||
print(step_msg)
|
||||
result["steps"].append(step_msg)
|
||||
|
||||
clone_cmd = ["git", "clone", "https://github.com/imsyy/DailyHotApi.git", daily_hot_path]
|
||||
clone_proc = await asyncio.create_subprocess_exec(
|
||||
*clone_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await clone_proc.communicate()
|
||||
|
||||
if clone_proc.returncode != 0:
|
||||
error_msg = f"❌ 克隆失败: {stderr.decode()}"
|
||||
print(error_msg)
|
||||
result["steps"].append(error_msg)
|
||||
result["message"] = "部署失败:无法克隆仓库"
|
||||
_deployment_status["is_deploying"] = False
|
||||
return result
|
||||
|
||||
result["steps"].append("✅ 克隆成功")
|
||||
else:
|
||||
result["steps"].append("✅ DailyHotApi已存在,跳过克隆")
|
||||
|
||||
# 步骤2:部署服务
|
||||
if os.path.exists(daily_hot_path):
|
||||
step_msg = "🔧 正在部署DailyHotApi服务..."
|
||||
print(step_msg)
|
||||
result["steps"].append(step_msg)
|
||||
|
||||
deploy_script = os.path.join(daily_hot_path, "deploy.sh")
|
||||
deploy_cmd = ["bash", deploy_script]
|
||||
|
||||
deploy_proc = await asyncio.create_subprocess_exec(
|
||||
*deploy_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await deploy_proc.communicate()
|
||||
|
||||
deploy_output = stdout.decode() + stderr.decode()
|
||||
|
||||
if deploy_proc.returncode == 0:
|
||||
result["steps"].append("✅ 部署成功")
|
||||
result["success"] = True
|
||||
result["message"] = "🎉 DailyHotApi后端服务部署成功!正在启动..."
|
||||
_deployment_status["message"] = result["message"]
|
||||
else:
|
||||
error_msg = f"❌ 部署失败: {deploy_output}"
|
||||
print(error_msg)
|
||||
result["steps"].append(error_msg)
|
||||
result["message"] = "部署失败,请手动检查"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"❌ 部署异常: {str(e)}"
|
||||
print(error_msg)
|
||||
result["steps"].append(error_msg)
|
||||
result["message"] = f"部署异常: {str(e)}"
|
||||
|
||||
finally:
|
||||
_deployment_status["is_deploying"] = False
|
||||
_deployment_status["last_check"] = datetime.now().isoformat()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_deployment_status() -> Dict[str, Any]:
|
||||
"""
|
||||
获取当前部署状态
|
||||
|
||||
Returns:
|
||||
部署状态字典
|
||||
"""
|
||||
return {
|
||||
"is_deploying": _deployment_status["is_deploying"],
|
||||
"last_check": _deployment_status["last_check"],
|
||||
"message": _deployment_status["message"]
|
||||
}
|
||||
|
||||
|
||||
async def test_connection() -> bool:
|
||||
"""测试 API 连接"""
|
||||
try:
|
||||
timeout_obj = aiohttp.ClientTimeout(total=5)
|
||||
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
|
||||
async with session.get(f"{config.api_url}/") as response:
|
||||
return response.status == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
print("Testing DailyHotApi connection...")
|
||||
|
||||
if asyncio.run(test_connection()):
|
||||
print("✓ Service is running")
|
||||
else:
|
||||
print("✗ Service not available")
|
||||
print(f" URL: {config.api_url}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
每日热榜 Skill - 配置管理
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""配置管理类"""
|
||||
|
||||
def __init__(self):
|
||||
# 每日热榜服务地址
|
||||
self.api_url: str = os.getenv(
|
||||
"DAILY_HOT_API_URL",
|
||||
"http://localhost:6688"
|
||||
)
|
||||
|
||||
# 缓存时间(秒)
|
||||
self.cache_ttl: int = int(os.getenv("DAILY_HOT_CACHE_TTL", "3600"))
|
||||
|
||||
# 返回最大条数
|
||||
self.max_items: int = int(os.getenv("DAILY_HOT_MAX_ITEMS", "20"))
|
||||
|
||||
# 请求超时时间(秒)
|
||||
self.timeout: int = int(os.getenv("DAILY_HOT_TIMEOUT", "10"))
|
||||
|
||||
# 启用调试模式
|
||||
self.debug: bool = os.getenv("DAILY_HOT_DEBUG", "false").lower() == "true"
|
||||
|
||||
# 数据存储路径
|
||||
self.data_dir: str = os.getenv(
|
||||
"DAILY_HOT_DATA_DIR",
|
||||
"/root/.openclaw/workspace/skills/daily-hot-news/data"
|
||||
)
|
||||
|
||||
# 是否自动保存每日热榜
|
||||
self.auto_save: bool = os.getenv("DAILY_HOT_AUTO_SAVE", "true").lower() == "true"
|
||||
|
||||
def get_api_url(self, endpoint: str = "") -> str:
|
||||
"""获取完整的 API 地址"""
|
||||
# 移除末尾斜杠
|
||||
base_url = self.api_url.rstrip("/")
|
||||
if endpoint:
|
||||
return f"{base_url}/{endpoint}"
|
||||
return base_url
|
||||
|
||||
def is_service_available(self) -> bool:
|
||||
"""检查服务是否配置"""
|
||||
return bool(self.api_url)
|
||||
|
||||
def get_data_path(self, source_id: str = "", date_str: str = "") -> str:
|
||||
"""获取数据文件路径"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
if not date_str:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
data_dir = self.data_dir
|
||||
|
||||
if source_id:
|
||||
# 按热榜源分目录存储
|
||||
source_dir = os.path.join(data_dir, source_id)
|
||||
os.makedirs(source_dir, exist_ok=True)
|
||||
return os.path.join(source_dir, f"{date_str}.json")
|
||||
else:
|
||||
# 主目录
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
return os.path.join(data_dir, f"all_{date_str}.json")
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
跨平台聚合 - Cross Platform Aggregation Module
|
||||
|
||||
功能:
|
||||
- 直接用全量数据做TOP10聚合
|
||||
- 跨平台热点排行
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
import asyncio
|
||||
|
||||
|
||||
@dataclass
|
||||
class CrossPlatformConfig:
|
||||
"""跨平台聚合配置"""
|
||||
total_items: int = 10 # 总条目数限制(默认TOP10)
|
||||
min_hot_score: float = 0 # 最小热度阈值
|
||||
merge_strategy: str = "score" # 合并策略:score(按热度), time(按时间)
|
||||
include_platforms: List[str] = None # 包含的平台(None表示全部)
|
||||
exclude_platforms: List[str] = None # 排除的平台
|
||||
|
||||
|
||||
class CrossPlatformAggregator:
|
||||
"""跨平台聚合类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None):
|
||||
"""
|
||||
初始化跨平台聚合
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(可选)
|
||||
formatter: 格式化器实例(可选)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
self.all_platforms = self._get_default_platforms()
|
||||
|
||||
def _get_default_platforms(self) -> List[str]:
|
||||
"""获取默认的全部平台列表"""
|
||||
return [
|
||||
"weibo", "zhihu", "douban-group", "douban-movie",
|
||||
"ithome", "36kr", "sspai", "csdn", "juejin",
|
||||
"genshin", "miyoushe", "bilibili", "hupu",
|
||||
"sina-news", "netease-news", "qq-news",
|
||||
"sina-money", "eastmoney", "xueqiu",
|
||||
"autohome", "懂车帝", "mafengwo", "ctrip",
|
||||
"dianping", "xiaohongshu", "weibo"
|
||||
]
|
||||
|
||||
async def fetch_all_hot_data(self, limit_per_platform: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
一次性获取全部平台的热榜数据
|
||||
|
||||
Args:
|
||||
limit_per_platform: 每个平台获取的条目数
|
||||
|
||||
Returns:
|
||||
全部平台的热榜数据列表
|
||||
"""
|
||||
all_items = []
|
||||
|
||||
if self.api_client:
|
||||
for platform in self.all_platforms:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform, limit=limit_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform}: {e}")
|
||||
continue
|
||||
|
||||
return all_items
|
||||
|
||||
async def aggregate_top_hot(self, config: Optional[CrossPlatformConfig] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
聚合跨平台TOP热点(新版:先全部获取,再聚合)
|
||||
|
||||
Args:
|
||||
config: 配置对象(可选)
|
||||
|
||||
Returns:
|
||||
聚合后的热榜数据
|
||||
"""
|
||||
if config is None:
|
||||
config = CrossPlatformConfig()
|
||||
|
||||
# 步骤1:全部获取
|
||||
all_items = await self.fetch_all_hot_data()
|
||||
|
||||
# 步骤2:聚合处理
|
||||
# 平台过滤
|
||||
if config.include_platforms:
|
||||
all_items = [item for item in all_items if item.get("source_platform") in config.include_platforms]
|
||||
if config.exclude_platforms:
|
||||
all_items = [item for item in all_items if item.get("source_platform") not in config.exclude_platforms]
|
||||
|
||||
# 热度过滤
|
||||
if config.min_hot_score > 0:
|
||||
all_items = [item for item in all_items if (item.get("hot", 0) or item.get("score", 0)) >= config.min_hot_score]
|
||||
|
||||
# 去重和排序
|
||||
merged_items = self._merge_and_sort(all_items, config.merge_strategy)
|
||||
|
||||
# 限制总数
|
||||
merged_items = merged_items[:config.total_items]
|
||||
|
||||
return {
|
||||
"total_items": len(merged_items),
|
||||
"items": merged_items
|
||||
}
|
||||
|
||||
def _merge_and_sort(self, items: List[Dict], strategy: str = "score") -> List[Dict]:
|
||||
"""
|
||||
合并、去重和排序
|
||||
|
||||
Args:
|
||||
items: 条目列表
|
||||
strategy: 排序策略
|
||||
|
||||
Returns:
|
||||
处理后的条目列表
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
# 按标题去重
|
||||
seen_titles = set()
|
||||
unique_items = []
|
||||
|
||||
for item in items:
|
||||
title = item.get("title", "").strip().lower()
|
||||
if title and title not in seen_titles:
|
||||
seen_titles.add(title)
|
||||
unique_items.append(item)
|
||||
|
||||
# 根据策略排序
|
||||
if strategy == "score":
|
||||
unique_items.sort(key=lambda x: x.get("hot", 0) or x.get("score", 0), reverse=True)
|
||||
elif strategy == "time":
|
||||
unique_items.sort(key=lambda x: x.get("time", "") or "", reverse=True)
|
||||
|
||||
return unique_items
|
||||
|
||||
def format_aggregation_response(self, agg_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化聚合响应
|
||||
|
||||
Args:
|
||||
agg_data: 聚合数据
|
||||
|
||||
Returns:
|
||||
格式化的响应文本
|
||||
"""
|
||||
items = agg_data.get("items", [])
|
||||
|
||||
if not items:
|
||||
return "❌ 暂无热点数据"
|
||||
|
||||
response = "🏆 **跨平台热点TOP10**\n"
|
||||
response += f"共 {agg_data['total_items']} 条热点\n"
|
||||
response += "-" * 40 + "\n\n"
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "无标题")
|
||||
hot = item.get("hot", item.get("score", ""))
|
||||
platform = item.get("source_platform", "")
|
||||
|
||||
response += f"{i}. {title}\n"
|
||||
if hot:
|
||||
response += f" 🔥 热度: {hot}"
|
||||
if platform:
|
||||
response += f" | 📱 {platform}"
|
||||
response += "\n\n"
|
||||
|
||||
return response
|
||||
|
||||
async def process_user_request(self, user_input: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入(可选)
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
# 获取聚合数据
|
||||
agg_data = await self.aggregate_top_hot()
|
||||
|
||||
# 格式化响应
|
||||
response_text = self.format_aggregation_response(agg_data)
|
||||
|
||||
return {
|
||||
"action": "show_top_hot",
|
||||
"data": agg_data,
|
||||
"message": response_text
|
||||
}
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_cross_platform(api_client=None, formatter=None) -> CrossPlatformAggregator:
|
||||
"""创建跨平台聚合实例"""
|
||||
return CrossPlatformAggregator(api_client=api_client, formatter=formatter)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
async def test():
|
||||
aggregator = await create_cross_platform()
|
||||
|
||||
print("获取跨平台热点TOP10...")
|
||||
result = await aggregator.process_user_request()
|
||||
print(result["message"])
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
每日热榜技能主入口示例
|
||||
|
||||
这个文件展示如何将三个扩展功能集成到主Skill中。
|
||||
在实际使用时,可以根据需要调整和整合。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# 支持相对导入(作为包的一部分)和绝对导入(直接运行)
|
||||
try:
|
||||
from .news_digest import NewsDigest, DigestConfig, create_digest as create_news_digest
|
||||
from .industry_hot import IndustryHot, IndustryConfig, create_industry_hot
|
||||
from .personalized import PersonalizedSubscription, UserPreferences, create_personalized
|
||||
from .storage import storage
|
||||
from .api_client import api_client, check_api_availability, deploy_daily_hot_api, get_deployment_status
|
||||
from .config import config
|
||||
except ImportError:
|
||||
from news_digest import NewsDigest, DigestConfig, create_digest as create_news_digest
|
||||
from industry_hot import IndustryHot, IndustryConfig, create_industry_hot
|
||||
from personalized import PersonalizedSubscription, UserPreferences, create_personalized
|
||||
from storage import storage
|
||||
from api_client import api_client, check_api_availability, deploy_daily_hot_api, get_deployment_status
|
||||
from config import config
|
||||
|
||||
|
||||
class DailyHotNewsSkill:
|
||||
"""每日热榜技能主类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None):
|
||||
"""
|
||||
初始化技能
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(用于获取热榜数据)
|
||||
formatter: 格式化器实例(用于格式化输出)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
|
||||
# 初始化各功能模块
|
||||
self.news_digest: Optional[NewsDigest] = None
|
||||
self.industry_hot: Optional[IndustryHot] = None
|
||||
self.personalized: Optional[PersonalizedSubscription] = None
|
||||
|
||||
# 标记是否已检查过旧数据(避免每次都提示)
|
||||
self._old_data_checked = False
|
||||
self._old_data_notification = None
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化各模块"""
|
||||
self.news_digest = await create_news_digest(
|
||||
api_client=self.api_client,
|
||||
formatter=self.formatter
|
||||
)
|
||||
self.industry_hot = await create_industry_hot(
|
||||
api_client=self.api_client,
|
||||
formatter=self.formatter
|
||||
)
|
||||
self.personalized = await create_personalized(
|
||||
api_client=self.api_client,
|
||||
formatter=self.formatter
|
||||
)
|
||||
|
||||
# 启动时检查是否有7天前的旧数据
|
||||
await self._check_old_data()
|
||||
|
||||
async def _check_old_data(self):
|
||||
"""检查并提示用户清理7天前的旧数据"""
|
||||
if self._old_data_checked:
|
||||
return
|
||||
|
||||
old_files = storage.get_old_data_files(days=7)
|
||||
if old_files:
|
||||
# 统计
|
||||
sources = set(f["source_id"] for f in old_files)
|
||||
print(f"\n⚠️ 发现 {len(old_files)} 个旧数据文件(7天前)")
|
||||
print(f" 涉及平台: {', '.join(list(sources)[:5])}...")
|
||||
print(f" 示例文件: {old_files[0]['date_str']} - {old_files[0]['source_id']}")
|
||||
|
||||
# 返回提示信息给用户
|
||||
self._old_data_notification = {
|
||||
"has_old_data": True,
|
||||
"count": len(old_files),
|
||||
"sources": list(sources),
|
||||
"message": f"""🗑️ **发现旧热榜数据**
|
||||
|
||||
检测到 {len(old_files)} 个热榜数据文件已超过7天未清理,涉及 {len(sources)} 个平台。
|
||||
|
||||
是否需要清理这些旧数据?
|
||||
- 回复"**清理**"或"**是**":删除7天前的所有旧数据
|
||||
- 回复"**跳过**"或"**否**":保留数据,下次启动不再提醒"""
|
||||
}
|
||||
else:
|
||||
self._old_data_notification = None
|
||||
|
||||
self._old_data_checked = True
|
||||
|
||||
def get_old_data_notification(self) -> Optional[Dict[str, Any]]:
|
||||
"""获取旧数据清理提示(如果有)"""
|
||||
return self._old_data_notification
|
||||
|
||||
async def handle_old_data_cleanup(self, confirm: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
处理旧数据清理
|
||||
|
||||
Args:
|
||||
confirm: 是否确认清理
|
||||
|
||||
Returns:
|
||||
清理结果
|
||||
"""
|
||||
if not confirm:
|
||||
return {
|
||||
"action": "ask_confirm",
|
||||
"message": "请确认是否清理7天前的旧数据?\n回复\"清理\"确认,回复\"跳过\"取消。"
|
||||
}
|
||||
|
||||
old_files = storage.get_old_data_files(days=7)
|
||||
if not old_files:
|
||||
return {
|
||||
"action": "show_message",
|
||||
"message": "✅ 没有需要清理的旧数据"
|
||||
}
|
||||
|
||||
deleted = storage.cleanup_old_files(old_files)
|
||||
return {
|
||||
"action": "show_message",
|
||||
"message": f"✅ 成功清理 {deleted} 个旧数据文件"
|
||||
}
|
||||
|
||||
async def handle_request(self, user_input: str, intent: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
intent: 意图(可选,用于路由到对应功能)
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
user_input_lower = user_input.lower()
|
||||
|
||||
# 检查是否是清理旧数据命令
|
||||
cleanup_keywords = ["清理", "删除旧数据", "清除缓存", "clean"]
|
||||
if any(kw in user_input_lower for kw in cleanup_keywords):
|
||||
return await self.handle_old_data_cleanup(confirm=True)
|
||||
|
||||
# 检查是否是获取热榜的请求
|
||||
is_hot_request = any([
|
||||
intent == "news_digest",
|
||||
self._is_news_digest_request(user_input_lower),
|
||||
intent == "industry_hot",
|
||||
self._is_industry_hot_request(user_input_lower),
|
||||
])
|
||||
|
||||
if is_hot_request:
|
||||
# 先检查API是否可用
|
||||
api_url = config.api_url
|
||||
print(f"\n[DailyHotSkill] 检查后端服务可用性: {api_url}")
|
||||
|
||||
api_available = await check_api_availability(api_url)
|
||||
|
||||
if not api_available:
|
||||
print(f"[DailyHotSkill] ⚠️ 后端服务不可用,触发自动部署...")
|
||||
deploy_result = await deploy_daily_hot_api()
|
||||
|
||||
# 返回部署状态给用户
|
||||
if deploy_result["success"]:
|
||||
# 构建部署步骤消息
|
||||
steps_text = ""
|
||||
for step in deploy_result.get("steps", []):
|
||||
steps_text += f"{step}\n"
|
||||
|
||||
return {
|
||||
"action": "show_deploying",
|
||||
"message": f"""🚀 **正在自动部署后端服务**
|
||||
|
||||
{steps_text}
|
||||
|
||||
⏳ **请稍候...**
|
||||
|
||||
后端服务正在启动,预计需要1-2分钟。
|
||||
|
||||
**部署完成后**,请再次发送请求获取热榜数据。
|
||||
|
||||
---
|
||||
💡 如果自动部署失败,请手动执行:
|
||||
1. `cd /root/.openclaw`
|
||||
2. `git clone https://github.com/imsyy/DailyHotApi.git`
|
||||
3. `cd DailyHotApi`
|
||||
4. `bash deploy.sh`""",
|
||||
"deploy_success": True,
|
||||
"steps": deploy_result.get("steps", [])
|
||||
}
|
||||
else:
|
||||
# 部署失败,返回错误和手动部署指南
|
||||
steps_text = ""
|
||||
for step in deploy_result.get("steps", []):
|
||||
steps_text += f"{step}\n"
|
||||
|
||||
return {
|
||||
"action": "show_deploy_failed",
|
||||
"message": f"""⚠️ **后端服务不可用**
|
||||
|
||||
自动部署失败,需要手动部署。
|
||||
|
||||
**部署步骤:**
|
||||
```bash
|
||||
cd /root/.openclaw
|
||||
git clone https://github.com/imsyy/DailyHotApi.git
|
||||
cd DailyHotApi
|
||||
bash deploy.sh
|
||||
```""",
|
||||
"deploy_success": False,
|
||||
"steps": deploy_result.get("steps", [])
|
||||
}
|
||||
|
||||
# 路由到对应功能
|
||||
if intent == "news_digest" or self._is_news_digest_request(user_input_lower):
|
||||
return await self._handle_news_digest(user_input)
|
||||
|
||||
elif intent == "industry_hot" or self._is_industry_hot_request(user_input_lower):
|
||||
return await self._handle_industry_hot(user_input)
|
||||
|
||||
elif intent == "personalized" or self._is_personalized_request(user_input_lower):
|
||||
return await self._handle_personalized(user_input)
|
||||
|
||||
else:
|
||||
# 默认返回功能选择引导
|
||||
return await self._show_main_menu()
|
||||
|
||||
def _is_news_digest_request(self, user_input: str) -> bool:
|
||||
"""判断是否为新闻摘要请求"""
|
||||
keywords = ["热点", "摘要", "标签", "科技", "游戏", "娱乐", "财经", "新闻"]
|
||||
return any(kw in user_input for kw in keywords)
|
||||
|
||||
def _is_industry_hot_request(self, user_input: str) -> bool:
|
||||
"""判断是否为行业热榜请求"""
|
||||
keywords = ["行业", "汽车", "金融", "医疗", "旅游", "餐饮", "房产"]
|
||||
return any(kw in user_input for kw in keywords)
|
||||
|
||||
def _is_personalized_request(self, user_input: str) -> bool:
|
||||
"""判断是否为个性化请求"""
|
||||
keywords = ["配置", "设置", "偏好", "关注", "个性化", "订阅"]
|
||||
return any(kw in user_input for kw in keywords)
|
||||
|
||||
async def _handle_news_digest(self, user_input: str) -> Dict[str, Any]:
|
||||
"""处理新闻摘要请求"""
|
||||
if not self.news_digest:
|
||||
return {"error": "模块未初始化"}
|
||||
|
||||
return await self.news_digest.process_user_request(user_input)
|
||||
|
||||
async def _handle_industry_hot(self, user_input: str) -> Dict[str, Any]:
|
||||
"""处理行业热榜请求"""
|
||||
if not self.industry_hot:
|
||||
return {"error": "模块未初始化"}
|
||||
|
||||
return await self.industry_hot.process_user_request(user_input)
|
||||
|
||||
async def _handle_personalized(self, user_input: str) -> Dict[str, Any]:
|
||||
"""处理个性化订阅请求"""
|
||||
if not self.personalized:
|
||||
return {"error": "模块未初始化"}
|
||||
|
||||
return await self.personalized.process_user_request(user_input)
|
||||
|
||||
async def _show_main_menu(self) -> Dict[str, Any]:
|
||||
"""显示主菜单"""
|
||||
# 检查是否有旧数据清理提示
|
||||
old_data = self.get_old_data_notification()
|
||||
|
||||
menu_text = """🎯 **每日热榜 - 功能选择**
|
||||
|
||||
请选择您想使用的功能:
|
||||
|
||||
1. **📰 热点新闻摘要**
|
||||
按标签浏览热点新闻(科技、游戏、娱乐、财经等)
|
||||
|
||||
2. **🏭 行业热榜垂直**
|
||||
按行业分类查看热榜(汽车、金融、医疗、旅游等)
|
||||
|
||||
3. **⚙️ 个性化订阅**
|
||||
配置您的偏好,获取定制化热榜
|
||||
|
||||
💡 您可以直接告诉我您想做什么,例如:
|
||||
- "今天有什么科技热点"
|
||||
- "看看汽车行业热榜"
|
||||
- "配置个性化热榜"
|
||||
"""
|
||||
|
||||
result = {
|
||||
"action": "show_menu",
|
||||
"message": menu_text
|
||||
}
|
||||
|
||||
# 如果有旧数据,添加提示
|
||||
if old_data:
|
||||
result["old_data_prompt"] = old_data["message"]
|
||||
|
||||
return result
|
||||
|
||||
# 便捷方法
|
||||
|
||||
async def get_news_digest_tags(self) -> str:
|
||||
"""获取新闻摘要标签选项"""
|
||||
if self.news_digest:
|
||||
return await self.news_digest.get_tag_options()
|
||||
return "模块未初始化"
|
||||
|
||||
async def get_industry_options(self) -> str:
|
||||
"""获取行业选项"""
|
||||
if self.industry_hot:
|
||||
return await self.industry_hot.get_industry_options()
|
||||
return "模块未初始化"
|
||||
|
||||
async def get_personalized_options(self) -> str:
|
||||
"""获取个性化配置选项"""
|
||||
if self.personalized:
|
||||
return await self.personalized.get_config_options()
|
||||
return "模块未初始化"
|
||||
|
||||
def get_current_config(self) -> Optional[UserPreferences]:
|
||||
"""获取当前个性化配置"""
|
||||
if self.personalized:
|
||||
return self.personalized.get_current_config()
|
||||
return None
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_skill(api_client=None, formatter=None) -> DailyHotNewsSkill:
|
||||
"""创建技能实例"""
|
||||
skill = DailyHotNewsSkill(api_client=api_client, formatter=formatter)
|
||||
await skill.initialize()
|
||||
return skill
|
||||
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
async def example():
|
||||
# 创建技能实例(不传入api_client时的模拟示例)
|
||||
skill = await create_skill()
|
||||
|
||||
# 示例1:展示主菜单
|
||||
result = await skill.handle_request("帮助")
|
||||
print(result["message"])
|
||||
|
||||
print("\n" + "="*60 + "\n")
|
||||
|
||||
# 示例2:展示标签选择
|
||||
result = await skill.get_news_digest_tags()
|
||||
print(result)
|
||||
|
||||
print("\n" + "="*60 + "\n")
|
||||
|
||||
# 示例3:展示行业选择
|
||||
result = await skill.get_industry_options()
|
||||
print(result)
|
||||
|
||||
print("\n" + "="*60 + "\n")
|
||||
|
||||
# 示例4:展示个性化配置选项
|
||||
result = await skill.get_personalized_options()
|
||||
print(result)
|
||||
|
||||
asyncio.run(example())
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DailyHotApi Skill - 响应格式化
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
from api_client import HOT_SOURCES
|
||||
|
||||
|
||||
class ResponseFormatter:
|
||||
"""响应格式化器"""
|
||||
|
||||
# 分类名称映射(中文)
|
||||
CATEGORY_NAMES = {
|
||||
"video": "🎬 视频/直播",
|
||||
"social": "💬 社交媒体",
|
||||
"news": "📰 新闻资讯",
|
||||
"tech": "💻 科技/技术",
|
||||
"game": "🎮 游戏/ACG",
|
||||
"reading": "📚 阅读/文化",
|
||||
"tool": "🔧 工具/其他",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_hot_list(data: Dict[str, Any]) -> str:
|
||||
"""格式化热榜列表为文本"""
|
||||
lines = []
|
||||
platform = data.get("platform", "未知平台")
|
||||
update_time = data.get("update_time", "")
|
||||
|
||||
# 头部
|
||||
lines.append(f"🔥 **{platform}**")
|
||||
if update_time:
|
||||
lines.append(f"更新时间: {update_time}")
|
||||
lines.append("")
|
||||
|
||||
# 列表
|
||||
items = data.get("data", [])
|
||||
if not items:
|
||||
lines.append("暂无数据")
|
||||
return "\n".join(lines)
|
||||
|
||||
for item in items:
|
||||
rank = item.get("rank", 0)
|
||||
title = item.get("title", "")
|
||||
hot = item.get("hot", "")
|
||||
url = item.get("url", "")
|
||||
|
||||
# 热度处理
|
||||
hot_str = f" {hot}" if hot else ""
|
||||
|
||||
# 标题处理(过长截断)
|
||||
if len(title) > 40:
|
||||
title = title[:40] + "..."
|
||||
|
||||
lines.append(f"{rank:2d}. {title}{hot_str}")
|
||||
|
||||
# 底部
|
||||
lines.append("")
|
||||
lines.append(f"共 {len(items)} 条")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_hot_list_compact(data: Dict[str, Any], max_items: int = 10) -> str:
|
||||
"""格式化热榜列表为紧凑格式"""
|
||||
lines = []
|
||||
platform = data.get("platform", "未知平台")
|
||||
|
||||
lines.append(f"🔥 **{platform}**")
|
||||
lines.append("-" * 40)
|
||||
|
||||
items = data.get("data", [])[:max_items]
|
||||
for item in items:
|
||||
rank = item.get("rank", 0)
|
||||
title = item.get("title", "")
|
||||
hot = item.get("hot", "")
|
||||
|
||||
# 简化标题
|
||||
title = title.replace("\n", " ")
|
||||
if len(title) > 30:
|
||||
title = title[:30] + "..."
|
||||
|
||||
hot_str = f" 📈 {hot}" if hot else ""
|
||||
lines.append(f"{rank:2d}. {title}{hot_str}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_all_sources() -> str:
|
||||
"""格式化所有热榜源列表"""
|
||||
from api_client import api_client
|
||||
|
||||
sources_by_cat = api_client.get_sources_by_category()
|
||||
lines = []
|
||||
|
||||
lines.append("📊 **支持的热榜源(共 54 个)**")
|
||||
lines.append("")
|
||||
|
||||
for cat_key, cat_name in ResponseFormatter.CATEGORY_NAMES.items():
|
||||
if cat_key in sources_by_cat:
|
||||
sources = sources_by_cat[cat_key]
|
||||
lines.append(f"### {cat_name}")
|
||||
lines.append(f"共 {len(sources)} 个")
|
||||
|
||||
for source in sources:
|
||||
lines.append(f"• **{source['name']}** (`{source['id']}`)")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_sources_by_category() -> str:
|
||||
"""按类别格式化热榜源"""
|
||||
from api_client import api_client
|
||||
|
||||
sources_by_cat = api_client.get_sources_by_category()
|
||||
lines = []
|
||||
|
||||
for cat_key, cat_name in ResponseFormatter.CATEGORY_NAMES.items():
|
||||
if cat_key not in sources_by_cat:
|
||||
continue
|
||||
|
||||
lines.append(f"\n{cat_name}\n{'─' * 30}")
|
||||
sources = sources_by_cat[cat_key]
|
||||
for source in sources:
|
||||
lines.append(f"• {source['name']} (`{source['id']}`)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_search_results(results: List[Dict], query: str) -> str:
|
||||
"""格式化搜索结果"""
|
||||
lines = []
|
||||
|
||||
if not results:
|
||||
return f"❌ 没有找到与「{query}」相关的热榜源"
|
||||
|
||||
lines.append(f"🔍 搜索「{query}」结果 ({len(results)} 个)")
|
||||
lines.append("")
|
||||
|
||||
for result in results:
|
||||
lines.append(f"• **{result['name']}** (`{result['id']}`)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_error(message: str, suggestion: str = "") -> str:
|
||||
"""格式化错误信息"""
|
||||
lines = [f"❌ {message}"]
|
||||
|
||||
if suggestion:
|
||||
lines.append("")
|
||||
lines.append(f"💡 {suggestion}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_service_status(is_running: bool, url: str) -> str:
|
||||
"""格式化服务状态"""
|
||||
if is_running:
|
||||
return f"✅ 每日热榜服务运行中\n\n📡 API 地址: {url}"
|
||||
else:
|
||||
return f"❌ 每日热榜服务未运行\n\n📡 预期地址: {url}\n\n💡 请使用 `./deploy.sh status` 查看状态"
|
||||
|
||||
|
||||
# 全局格式化器实例
|
||||
formatter = ResponseFormatter()
|
||||
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
行业热榜垂直(增强版) - Industry Hot Module
|
||||
|
||||
功能:
|
||||
- 十大行业分类:科技互联网、游戏、汽车、金融财经、数码消费、娱乐影视、房产家居、医疗健康、旅游出行、餐饮消费
|
||||
- 用户自主选择行业
|
||||
- 行业描述和平台标注
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import asyncio
|
||||
|
||||
# 十大行业映射
|
||||
INDUSTRIES: Dict[str, Dict[str, Any]] = {
|
||||
"科技互联网": {
|
||||
"platforms": ["ithome", "36kr", "csdn", "juejin", "oschina", "infoq"],
|
||||
"description": "IT之家、36氪、CSDN、稀土掘金、开源中国、InfoQ"
|
||||
},
|
||||
"游戏行业": {
|
||||
"platforms": ["genshin", "miyoushe", "lol", "bilibili", "douyu", "huya", "netease-game"],
|
||||
"description": "原神、米游社、英雄联盟、B站、斗鱼、虎牙、网易游戏"
|
||||
},
|
||||
"汽车行业": {
|
||||
"platforms": ["autohome", "car", "懂车帝", "bitauto", "soufun-auto"],
|
||||
"description": "汽车之家、懂车帝、易车网、苏宁汽车"
|
||||
},
|
||||
"金融财经": {
|
||||
"platforms": ["sina-money", "eastmoney", "xueqiu", "jrj", "cnstock", "wallstreetcn", "money163"],
|
||||
"description": "新浪财经、东方财富、雪球、金融界、中国财经网、华尔街见闻、网易财经"
|
||||
},
|
||||
"数码消费": {
|
||||
"platforms": ["coolapk", "ithome", "sspai", "geekpark", "少数派", "smzdm"],
|
||||
"description": "酷安、IT之家、少数派、什么值得买"
|
||||
},
|
||||
"娱乐影视": {
|
||||
"platforms": ["weibo", "douban-group", "douban-movie", "mtime", "movie", "bilibili"],
|
||||
"description": "微博、豆瓣、豆瓣电影、时光网、B站"
|
||||
},
|
||||
"房产家居": {
|
||||
"platforms": ["lfang", "soufunianjia", "anjuke", "house", "lianjia"],
|
||||
"description": "链家、安居客、房天下、贝壳找房"
|
||||
},
|
||||
"医疗健康": {
|
||||
"platforms": ["zhihu", "知乎", "sina-health", "health", "baikemy", "丁香园"],
|
||||
"description": "知乎、新浪健康、丁香园、百度健康"
|
||||
},
|
||||
"旅游出行": {
|
||||
"platforms": ["mafengwo", "ctrip", "qunar", "飞猪", "马蜂窝", "携程"],
|
||||
"description": "马蜂窝、携程、去哪儿、飞猪"
|
||||
},
|
||||
"餐饮消费": {
|
||||
"platforms": ["dianping", "xiaohongshu", "大众点评", "ele.me", "meituan"],
|
||||
"description": "大众点评、美团、饿了么、小红书"
|
||||
}
|
||||
}
|
||||
|
||||
# 所有行业名称
|
||||
ALL_INDUSTRIES = list(INDUSTRIES.keys())
|
||||
|
||||
|
||||
class IndustryMode(Enum):
|
||||
"""行业模式"""
|
||||
SINGLE = "single" # 单行业
|
||||
MULTI = "multi" # 多行业
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndustryConfig:
|
||||
"""行业配置"""
|
||||
industries: List[str] # 选择的行业列表
|
||||
mode: IndustryMode = IndustryMode.SINGLE
|
||||
items_per_platform: int = 10 # 每个平台显示的条目数
|
||||
total_items: int = 50 # 总条目数限制
|
||||
include_description: bool = True # 是否包含行业描述
|
||||
|
||||
|
||||
class IndustryHot:
|
||||
"""行业热榜类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None):
|
||||
"""
|
||||
初始化行业热榜
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(可选)
|
||||
formatter: 格式化器实例(可选)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
self._all_platforms = self._get_all_platforms()
|
||||
|
||||
def _get_all_platforms(self) -> List[str]:
|
||||
"""获取所有行业相关的平台"""
|
||||
platforms = set()
|
||||
for industry_info in INDUSTRIES.values():
|
||||
platforms.update(industry_info.get("platforms", []))
|
||||
return list(platforms)
|
||||
|
||||
async def fetch_all_hot_data(self, limit_per_platform: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
一次性获取全部平台的热榜数据
|
||||
|
||||
Args:
|
||||
limit_per_platform: 每个平台获取的条目数
|
||||
|
||||
Returns:
|
||||
全部平台的热榜数据列表
|
||||
"""
|
||||
all_items = []
|
||||
|
||||
if self.api_client:
|
||||
for platform in self._all_platforms:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform, limit=limit_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform}: {e}")
|
||||
continue
|
||||
|
||||
return all_items
|
||||
|
||||
async def get_industry_options(self) -> str:
|
||||
"""
|
||||
获取行业选择选项(AI引导话术)
|
||||
|
||||
Returns:
|
||||
格式化的行业选择文本
|
||||
"""
|
||||
options_text = "🏭 **请选择您关注的行业**\n\n"
|
||||
|
||||
for i, (industry, info) in enumerate(INDUSTRIES.items(), 1):
|
||||
platforms_count = len(info["platforms"])
|
||||
options_text += f"{i}. **{industry}**\n"
|
||||
options_text += f" 📋 包含 {platforms_count} 个平台\n"
|
||||
options_text += f" 🔗 {info['description']}\n\n"
|
||||
|
||||
options_text += "-" * 50 + "\n"
|
||||
options_text += "💡 您可以输入行业名称或数字编号,支持多选(如:1,3或汽车+金融)"
|
||||
return options_text
|
||||
|
||||
def parse_industries_from_input(self, user_input: str) -> List[str]:
|
||||
"""
|
||||
解析用户输入的行业
|
||||
|
||||
Args:
|
||||
user_input: 用户输入的文本
|
||||
|
||||
Returns:
|
||||
匹配的行业列表
|
||||
"""
|
||||
user_input = user_input.strip().lower()
|
||||
matched_industries = []
|
||||
|
||||
# 处理数字选择
|
||||
import re
|
||||
number_pattern = re.findall(r'\d+', user_input)
|
||||
for num in number_pattern:
|
||||
idx = int(num) - 1
|
||||
if 0 <= idx < len(ALL_INDUSTRIES):
|
||||
matched_industries.append(ALL_INDUSTRIES[idx])
|
||||
|
||||
# 行业关键词映射(行业名称 -> 包含的关键词)
|
||||
industry_keywords = {
|
||||
"科技互联网": ["科技", "互联网", "IT", "技术"],
|
||||
"游戏行业": ["游戏", "手游", "网游"],
|
||||
"汽车行业": ["汽车", "车", "车企", "新能源车"],
|
||||
"金融财经": ["金融", "财经", "投资", "理财", "股票"],
|
||||
"数码消费": ["数码", "手机", "电脑", "电子"],
|
||||
"娱乐影视": ["娱乐", "影视", "电影", "综艺", "明星"],
|
||||
"房产家居": ["房产", "房", "家居", "装修", "买房"],
|
||||
"医疗健康": ["医疗", "健康", "医药", "养生"],
|
||||
"旅游出行": ["旅游", "出行", "旅行", "机票", "酒店"],
|
||||
"餐饮消费": ["餐饮", "美食", "外卖", "餐厅", "消费"]
|
||||
}
|
||||
|
||||
# 处理行业关键词
|
||||
for industry, keywords in industry_keywords.items():
|
||||
for keyword in keywords:
|
||||
if keyword in user_input:
|
||||
if industry not in matched_industries:
|
||||
matched_industries.append(industry)
|
||||
break
|
||||
|
||||
# 处理完整行业名称(向后兼容)
|
||||
for industry in ALL_INDUSTRIES:
|
||||
if industry.lower() in user_input or industry in user_input:
|
||||
if industry not in matched_industries:
|
||||
matched_industries.append(industry)
|
||||
|
||||
return matched_industries if matched_industries else []
|
||||
|
||||
async def get_industry_hot(self, industries: List[str], config: Optional[IndustryConfig] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
获取行业热榜(新版:先全部获取,再按行业筛选)
|
||||
|
||||
Args:
|
||||
industries: 行业列表
|
||||
config: 配置对象(可选)
|
||||
|
||||
Returns:
|
||||
行业热榜数据
|
||||
"""
|
||||
if config is None:
|
||||
config = IndustryConfig(industries=industries)
|
||||
|
||||
# 步骤1:全部获取
|
||||
all_items = await self.fetch_all_hot_data(limit_per_platform=config.items_per_platform)
|
||||
|
||||
# 步骤2:按行业筛选
|
||||
# 获取所有相关平台
|
||||
target_platforms = set()
|
||||
for industry in industries:
|
||||
if industry in INDUSTRIES:
|
||||
target_platforms.update(INDUSTRIES[industry]["platforms"])
|
||||
|
||||
filtered_items = []
|
||||
for item in all_items:
|
||||
platform = item.get("source_platform", "")
|
||||
if platform in target_platforms:
|
||||
item["source_industry"] = next((ind for ind in industries if ind in INDUSTRIES and platform in INDUSTRIES[ind]["platforms"]), industries[0])
|
||||
filtered_items.append(item)
|
||||
|
||||
# 去重和排序
|
||||
merged_items = self._merge_items(filtered_items)
|
||||
|
||||
# 限制总数
|
||||
merged_items = merged_items[:config.total_items]
|
||||
|
||||
return {
|
||||
"industries": industries,
|
||||
"industry_descriptions": {ind: INDUSTRIES[ind]["description"] for ind in industries},
|
||||
"total_items": len(merged_items),
|
||||
"items": merged_items
|
||||
}
|
||||
|
||||
def _merge_items(self, items: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
合并和去重条目
|
||||
|
||||
Args:
|
||||
items: 条目列表
|
||||
|
||||
Returns:
|
||||
合并后的条目列表
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
# 按标题去重
|
||||
seen_titles = set()
|
||||
unique_items = []
|
||||
|
||||
for item in items:
|
||||
title = item.get("title", "").strip().lower()
|
||||
if title and title not in seen_titles:
|
||||
seen_titles.add(title)
|
||||
unique_items.append(item)
|
||||
|
||||
# 按热度排序
|
||||
unique_items.sort(key=lambda x: x.get("hot", 0) or x.get("score", 0), reverse=True)
|
||||
|
||||
return unique_items
|
||||
|
||||
def format_industry_response(self, hot_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化行业热榜响应
|
||||
|
||||
Args:
|
||||
hot_data: 热榜数据
|
||||
|
||||
Returns:
|
||||
格式化的响应文本
|
||||
"""
|
||||
if not hot_data.get("items"):
|
||||
return "❌ 暂无行业热榜数据"
|
||||
|
||||
industries = hot_data["industries"]
|
||||
items = hot_data["items"]
|
||||
|
||||
response = f"🏭 **行业热榜 - {', '.join(industries)}**\n\n"
|
||||
|
||||
# 显示行业描述
|
||||
for industry in industries:
|
||||
desc = hot_data["industry_descriptions"].get(industry, "")
|
||||
response += f"📌 **{industry}**: {desc}\n"
|
||||
|
||||
response += "-" * 50 + "\n"
|
||||
response += f"共 {hot_data['total_items']} 条热榜\n\n"
|
||||
|
||||
# 按行业分组显示
|
||||
items_by_industry = {}
|
||||
for item in items:
|
||||
industry = item.get("source_industry", "其他")
|
||||
if industry not in items_by_industry:
|
||||
items_by_industry[industry] = []
|
||||
items_by_industry[industry].append(item)
|
||||
|
||||
for industry, ind_items in items_by_industry.items():
|
||||
response += f"\n📊 **{industry}**\n"
|
||||
for i, item in enumerate(ind_items[:5], 1): # 每个行业显示5条
|
||||
title = item.get("title", "无标题")
|
||||
hot = item.get("hot", item.get("score", ""))
|
||||
platform = item.get("source_platform", "")
|
||||
|
||||
response += f"{i}. {title}\n"
|
||||
if hot:
|
||||
response += f" 🔥 {hot}"
|
||||
if platform:
|
||||
response += f" | 📱 {platform}"
|
||||
response += "\n"
|
||||
|
||||
return response
|
||||
|
||||
async def process_user_request(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
# 解析行业
|
||||
industries = self.parse_industries_from_input(user_input)
|
||||
|
||||
if not industries:
|
||||
# 返回引导信息
|
||||
return {
|
||||
"action": "ask_industry",
|
||||
"message": await self.get_industry_options()
|
||||
}
|
||||
|
||||
# 获取热榜
|
||||
hot_data = await self.get_industry_hot(industries)
|
||||
|
||||
# 格式化响应
|
||||
response_text = self.format_industry_response(hot_data)
|
||||
|
||||
return {
|
||||
"action": "show_industry_hot",
|
||||
"data": hot_data,
|
||||
"message": response_text
|
||||
}
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_industry_hot(api_client=None, formatter=None) -> IndustryHot:
|
||||
"""创建行业热榜实例"""
|
||||
return IndustryHot(api_client=api_client, formatter=formatter)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
async def test():
|
||||
industry_hot = await create_industry_hot()
|
||||
print(await industry_hot.get_industry_options())
|
||||
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
result = await industry_hot.process_user_request("汽车和金融")
|
||||
print(result["message"])
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
热点新闻摘要(增强版) - News Digest Module
|
||||
|
||||
功能:
|
||||
- 15种标签分类:科技、互联网、游戏、娱乐、社会、财经、汽车、体育、教育、健康、国际、房产、数码、时尚、美食
|
||||
- AI主动引导用户选择标签
|
||||
- 按标签获取和合并热榜
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import asyncio
|
||||
|
||||
# 标签到平台的映射
|
||||
TAG_MAPPING: Dict[str, List[str]] = {
|
||||
"科技": ["ithome", "36kr", "sspai", "csdn", "juejin", "51cto", "oschina", "infoq"],
|
||||
"互联网": ["sina-news", "netease-news", "qq-news", "sohu-news", "ifeng"],
|
||||
"游戏": ["genshin", "miyoushe", "lol", "hupu", "bilibili", "douyu", "huya", "netease-game"],
|
||||
"娱乐": ["weibo", "douban-group", "douban-movie", "mtime", "movie"],
|
||||
"社会": ["sina-news", "netease-news", "qq-news", "sohu-news", "ifeng", "qq"],
|
||||
"财经": ["sina-money", "eastmoney", "xueqiu", "jrj", "cnstock", "wallstreetcn"],
|
||||
"汽车": ["autohome", "car", "懂车帝", "bitauto", "car1"],
|
||||
"体育": ["hupu", "sports", "sina-sports", "qq-sports", "zhibo8"],
|
||||
"教育": ["zhihu", "知乎", "bilibili", "jike", "dazhihui"],
|
||||
"健康": ["zhihu", "知乎", "sina-health", "health", "baikemy"],
|
||||
"国际": ["sina-news", "netease-news", "qq-news", "ifeng", "cnn", "bbc"],
|
||||
"房产": ["lfang", "soufunianjia", "anjuke", "house"],
|
||||
"数码": ["ithome", "coolapk", "sspai", "geekpark", "少数派"],
|
||||
"时尚": ["mogujie", "meilishuo", "xiaohongshu", "微博时尚", "yoho"],
|
||||
"美食": ["dianping", "xiaohongshu", "大众点评", "maoyan", "ele.me"]
|
||||
}
|
||||
|
||||
# 所有可用的标签
|
||||
ALL_TAGS = list(TAG_MAPPING.keys())
|
||||
|
||||
|
||||
class DigestMode(Enum):
|
||||
"""摘要模式"""
|
||||
SINGLE = "single" # 单标签
|
||||
MULTI = "multi" # 多标签
|
||||
|
||||
|
||||
@dataclass
|
||||
class DigestConfig:
|
||||
"""摘要配置"""
|
||||
tags: List[str] # 选择的标签列表
|
||||
mode: DigestMode = DigestMode.MULTI
|
||||
items_per_platform: int = 10 # 每个平台显示的条目数
|
||||
total_items: int = 50 # 总条目数限制
|
||||
merge_strategy: str = "score" # 合并策略:score(按热度), time(按时间), random(随机)
|
||||
|
||||
|
||||
class NewsDigest:
|
||||
"""热点新闻摘要类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None):
|
||||
"""
|
||||
初始化新闻摘要
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(可选,如果不提供则需要外部传入)
|
||||
formatter: 格式化器实例(可选)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
self.platforms = self._get_all_platforms()
|
||||
|
||||
def _get_all_platforms(self) -> List[str]:
|
||||
"""获取所有可用的平台"""
|
||||
platforms = set()
|
||||
for tag, plats in TAG_MAPPING.items():
|
||||
platforms.update(plats)
|
||||
return list(platforms)
|
||||
|
||||
async def fetch_all_hot_data(self, limit_per_platform: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
一次性获取全部54个平台的热榜数据
|
||||
|
||||
Args:
|
||||
limit_per_platform: 每个平台获取的条目数
|
||||
|
||||
Returns:
|
||||
全部平台的热榜数据列表
|
||||
"""
|
||||
all_items = []
|
||||
|
||||
if self.api_client:
|
||||
for platform in self.platforms:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform, limit=limit_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform}: {e}")
|
||||
continue
|
||||
|
||||
return all_items
|
||||
|
||||
async def get_tag_options(self) -> str:
|
||||
"""
|
||||
获取标签选择选项(AI引导话术)
|
||||
|
||||
Returns:
|
||||
格式化的标签选择文本
|
||||
"""
|
||||
options_text = "📊 **请选择您感兴趣的标签**\n\n"
|
||||
|
||||
# 分两列显示
|
||||
tags_left = ALL_TAGS[:8]
|
||||
tags_right = ALL_TAGS[8:]
|
||||
|
||||
for i, tag in enumerate(tags_left):
|
||||
right_tag = tags_right[i] if i < len(tags_right) else ""
|
||||
left_plats = ", ".join(TAG_MAPPING[tag][:3])
|
||||
right_plats = f"│ {i+8+1}. {right_tag}: {', '.join(TAG_MAPPING[right_tag][:3])}" if right_tag else ""
|
||||
options_text += f"{i+1}. {tag} ({left_plats}) {right_plats}\n"
|
||||
|
||||
options_text += "\n💡 您可以输入标签名称或数字编号,支持多选(如:1,3或科技+游戏)"
|
||||
return options_text
|
||||
|
||||
def parse_tags_from_input(self, user_input: str) -> List[str]:
|
||||
"""
|
||||
解析用户输入的标签
|
||||
|
||||
Args:
|
||||
user_input: 用户输入的文本
|
||||
|
||||
Returns:
|
||||
匹配的标签列表
|
||||
"""
|
||||
user_input = user_input.strip().lower()
|
||||
matched_tags = []
|
||||
|
||||
# 处理数字选择
|
||||
numbers = []
|
||||
import re
|
||||
number_pattern = re.findall(r'\d+', user_input)
|
||||
for num in number_pattern:
|
||||
idx = int(num) - 1
|
||||
if 0 <= idx < len(ALL_TAGS):
|
||||
numbers.append(ALL_TAGS[idx])
|
||||
|
||||
# 处理标签名称
|
||||
for tag in ALL_TAGS:
|
||||
if tag.lower() in user_input or tag in user_input:
|
||||
if tag not in matched_tags and tag not in numbers:
|
||||
matched_tags.append(tag)
|
||||
|
||||
# 合并数字选择的结果
|
||||
matched_tags.extend([t for t in numbers if t not in matched_tags])
|
||||
|
||||
return matched_tags if matched_tags else []
|
||||
|
||||
async def get_digest_by_tags(self, tags: List[str], config: Optional[DigestConfig] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
按标签获取新闻摘要(新版:先全部获取,再按标签筛选)
|
||||
|
||||
Args:
|
||||
tags: 标签列表
|
||||
config: 配置对象(可选)
|
||||
|
||||
Returns:
|
||||
合并后的热榜数据
|
||||
"""
|
||||
if config is None:
|
||||
config = DigestConfig(tags=tags)
|
||||
|
||||
# 步骤1:全部获取
|
||||
all_items = await self.fetch_all_hot_data(limit_per_platform=config.items_per_platform)
|
||||
|
||||
# 步骤2:按标签筛选
|
||||
# 获取所有相关平台
|
||||
target_platforms = set()
|
||||
for tag in tags:
|
||||
if tag in TAG_MAPPING:
|
||||
target_platforms.update(TAG_MAPPING[tag])
|
||||
|
||||
filtered_items = []
|
||||
for item in all_items:
|
||||
platform = item.get("source_platform", "")
|
||||
if platform in target_platforms:
|
||||
item["source_tag"] = next((t for t in tags if t in TAG_MAPPING and platform in TAG_MAPPING[t]), tags[0])
|
||||
filtered_items.append(item)
|
||||
|
||||
# 去重和合并
|
||||
merged_items = self._merge_items(filtered_items, config.merge_strategy)
|
||||
|
||||
# 限制总数
|
||||
merged_items = merged_items[:config.total_items]
|
||||
|
||||
return {
|
||||
"tags": tags,
|
||||
"platforms": list(target_platforms),
|
||||
"total_items": len(merged_items),
|
||||
"items": merged_items
|
||||
}
|
||||
|
||||
def _merge_items(self, items: List[Dict], strategy: str = "score") -> List[Dict]:
|
||||
"""
|
||||
合并和去重条目
|
||||
|
||||
Args:
|
||||
items: 条目列表
|
||||
strategy: 合并策略
|
||||
|
||||
Returns:
|
||||
合并后的条目列表
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
# 按标题去重
|
||||
seen_titles = set()
|
||||
unique_items = []
|
||||
|
||||
for item in items:
|
||||
title = item.get("title", "").strip().lower()
|
||||
if title and title not in seen_titles:
|
||||
seen_titles.add(title)
|
||||
unique_items.append(item)
|
||||
|
||||
# 根据策略排序
|
||||
if strategy == "score":
|
||||
# 按热度/分数排序
|
||||
unique_items.sort(key=lambda x: x.get("hot", 0) or x.get("score", 0), reverse=True)
|
||||
elif strategy == "time":
|
||||
# 按时间排序
|
||||
unique_items.sort(key=lambda x: x.get("time", "") or "", reverse=True)
|
||||
|
||||
return unique_items
|
||||
|
||||
def format_digest_response(self, digest_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化摘要响应
|
||||
|
||||
Args:
|
||||
digest_data: 摘要数据
|
||||
|
||||
Returns:
|
||||
格式化的响应文本
|
||||
"""
|
||||
if not digest_data.get("items"):
|
||||
return "❌ 暂无热点数据"
|
||||
|
||||
tags = digest_data["tags"]
|
||||
items = digest_data["items"]
|
||||
|
||||
response = f"📰 **热点摘要 - {', '.join(tags)}**\n"
|
||||
response += f"来源平台: {', '.join(digest_data['platforms'])}\n"
|
||||
response += f"共 {digest_data['total_items']} 条热点\n"
|
||||
response += "-" * 40 + "\n\n"
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "无标题")
|
||||
hot = item.get("hot", item.get("score", ""))
|
||||
platform = item.get("source_platform", "")
|
||||
|
||||
response += f"{i}. {title}\n"
|
||||
if hot:
|
||||
response += f" 🔥 热度: {hot}"
|
||||
if platform:
|
||||
response += f" | 📱 {platform}"
|
||||
response += "\n\n"
|
||||
|
||||
return response
|
||||
|
||||
async def process_user_request(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
# 解析标签
|
||||
tags = self.parse_tags_from_input(user_input)
|
||||
|
||||
if not tags:
|
||||
# 返回引导信息
|
||||
return {
|
||||
"action": "ask_tag",
|
||||
"message": await self.get_tag_options()
|
||||
}
|
||||
|
||||
# 获取摘要
|
||||
digest_data = await self.get_digest_by_tags(tags)
|
||||
|
||||
# 格式化响应
|
||||
response_text = self.format_digest_response(digest_data)
|
||||
|
||||
return {
|
||||
"action": "show_digest",
|
||||
"data": digest_data,
|
||||
"message": response_text
|
||||
}
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_digest(api_client=None, formatter=None) -> NewsDigest:
|
||||
"""创建新闻摘要实例"""
|
||||
return NewsDigest(api_client=api_client, formatter=formatter)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
async def test():
|
||||
digest = await create_digest()
|
||||
print(await digest.get_tag_options())
|
||||
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
result = await digest.process_user_request("科技和游戏")
|
||||
print(result["message"])
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,557 @@
|
||||
"""
|
||||
个性化订阅(增强版) - Personalized Subscription Module
|
||||
|
||||
功能:
|
||||
- 用户自主配置:关键词、平台、排除项
|
||||
- AI主动提供备选项
|
||||
- 关键词过滤和偏好排序
|
||||
- 用户配置存储
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any, Set
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
import json
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# 可选的关键词标签
|
||||
KEYWORD_OPTIONS = [
|
||||
"AI", "ChatGPT", "人工智能", "大模型", "机器学习",
|
||||
"游戏", "原神", "英雄联盟", "王者荣耀", "米哈游",
|
||||
"科技产品", "iPhone", "华为", "小米", "特斯拉",
|
||||
"新能源汽车", "比亚迪", "宁德时代", "蔚来", "小鹏",
|
||||
"互联网", "字节跳动", "腾讯", "阿里巴巴", "美团",
|
||||
"电商", "直播", "短视频", "网红", "明星八卦",
|
||||
"影视", "电影", "电视剧", "综艺", "动漫",
|
||||
"财经", "股票", "基金", "加密货币", "比特币",
|
||||
"房产", "房价", "房地产", "房贷", "租房",
|
||||
"美食", "餐厅", "外卖", "网红店", "探店",
|
||||
"旅游", "出行", "机票", "酒店", "景点",
|
||||
"时尚", "穿搭", "美妆", "护肤", "奢侈品",
|
||||
"体育", "足球", "篮球", "NBA", "奥运会",
|
||||
"教育", "高考", "考研", "留学", "职场"
|
||||
]
|
||||
|
||||
# 可选平台
|
||||
PLATFORM_OPTIONS = [
|
||||
"微博", "知乎", "B站", "抖音", "快手",
|
||||
"原神", "米游社", "IT之家", "36氪", "虎嗅",
|
||||
"豆瓣", "小红书", "今日头条", "澎湃新闻", "观察者网"
|
||||
]
|
||||
|
||||
# 排除关键词示例
|
||||
EXCLUDE_OPTIONS = [
|
||||
"广告", "推广", "营销号", "震惊", "必看",
|
||||
"流量明星", "网红脸", "擦边", "引战"
|
||||
]
|
||||
|
||||
|
||||
class SubscriptionMode(Enum):
|
||||
"""订阅模式"""
|
||||
INCLUDE = "include" # 包含模式
|
||||
EXCLUDE = "exclude" # 排除模式
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserPreferences:
|
||||
"""用户偏好配置"""
|
||||
keywords: List[str] = field(default_factory=list) # 关注的关键词
|
||||
platforms: List[str] = field(default_factory=list) # 关注的平台
|
||||
exclude_keywords: List[str] = field(default_factory=list) # 排除的关键词
|
||||
subscription_mode: SubscriptionMode = SubscriptionMode.INCLUDE
|
||||
items_per_platform: int = 10
|
||||
total_items: int = 30
|
||||
sort_by: str = "relevance" # relevance(相关性), hot(热度), time(时间)
|
||||
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
data = asdict(self)
|
||||
data["subscription_mode"] = self.subscription_mode.value
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> "UserPreferences":
|
||||
"""从字典创建"""
|
||||
if "subscription_mode" in data and isinstance(data["subscription_mode"], str):
|
||||
data["subscription_mode"] = SubscriptionMode(data["subscription_mode"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class PersonalizedSubscription:
|
||||
"""个性化订阅类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None, storage_path: str = None):
|
||||
"""
|
||||
初始化个性化订阅
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(可选)
|
||||
formatter: 格式化器实例(可选)
|
||||
storage_path: 配置存储路径(可选)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
self.storage_path = storage_path or os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"personalized_config.json"
|
||||
)
|
||||
self.current_config = None
|
||||
self._all_platforms = self._get_all_platforms()
|
||||
self._load_config()
|
||||
|
||||
def _get_all_platforms(self) -> List[str]:
|
||||
"""获取所有可用的平台"""
|
||||
return [self._platform_name_to_id(p) for p in PLATFORM_OPTIONS]
|
||||
|
||||
def _platform_name_to_id(self, platform_name: str) -> str:
|
||||
"""将平台名称转换为API ID"""
|
||||
mapping = {
|
||||
"微博": "weibo",
|
||||
"知乎": "zhihu",
|
||||
"B站": "bilibili",
|
||||
"抖音": "douyin",
|
||||
"快手": "kuaishou",
|
||||
"原神": "genshin",
|
||||
"米游社": "miyoushe",
|
||||
"IT之家": "ithome",
|
||||
"36氪": "36kr",
|
||||
"虎嗅": "huxiu",
|
||||
"豆瓣": "douban-group",
|
||||
"小红书": "xiaohongshu",
|
||||
"今日头条": "jinritoutiao",
|
||||
"澎湃新闻": "thepaper",
|
||||
"观察者网": "guanchazhe"
|
||||
}
|
||||
return mapping.get(platform_name, platform_name.lower())
|
||||
|
||||
def _load_config(self) -> Optional[UserPreferences]:
|
||||
"""加载用户配置"""
|
||||
if os.path.exists(self.storage_path):
|
||||
try:
|
||||
with open(self.storage_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.current_config = UserPreferences.from_dict(data)
|
||||
return self.current_config
|
||||
except Exception as e:
|
||||
print(f"Error loading config: {e}")
|
||||
return None
|
||||
|
||||
def _save_config(self, config: UserPreferences) -> bool:
|
||||
"""保存用户配置"""
|
||||
try:
|
||||
# 更新修改时间
|
||||
config.updated_at = datetime.now().isoformat()
|
||||
|
||||
with open(self.storage_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
self.current_config = config
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving config: {e}")
|
||||
return False
|
||||
|
||||
async def fetch_all_hot_data(self, limit_per_platform: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
一次性获取全部平台的热榜数据
|
||||
|
||||
Args:
|
||||
limit_per_platform: 每个平台获取的条目数
|
||||
|
||||
Returns:
|
||||
全部平台的热榜数据列表
|
||||
"""
|
||||
all_items = []
|
||||
|
||||
if self.api_client:
|
||||
for platform in self._all_platforms:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform, limit=limit_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform}: {e}")
|
||||
continue
|
||||
|
||||
return all_items
|
||||
|
||||
async def get_config_options(self) -> str:
|
||||
"""
|
||||
获取配置选项(AI引导话术)
|
||||
|
||||
Returns:
|
||||
格式化的配置引导文本
|
||||
"""
|
||||
options_text = "⚙️ **个性化热榜配置**\n\n"
|
||||
|
||||
# 关键词选项
|
||||
options_text += "**【关键词】**\n"
|
||||
options_text += "可选标签:\n"
|
||||
|
||||
# 分组显示关键词
|
||||
keyword_groups = [
|
||||
("科技类", ["AI", "人工智能", "大模型", "科技产品", "iPhone", "华为", "小米"]),
|
||||
("游戏类", ["游戏", "原神", "英雄联盟", "王者荣耀", "米哈游"]),
|
||||
("汽车类", ["特斯拉", "新能源汽车", "比亚迪", "蔚来", "小鹏"]),
|
||||
("财经类", ["财经", "股票", "基金", "加密货币", "比特币"]),
|
||||
("娱乐类", ["影视", "综艺", "明星八卦", "网红"]),
|
||||
]
|
||||
|
||||
for group_name, keywords in keyword_groups:
|
||||
options_text += f" • {group_name}: {', '.join(keywords)}\n"
|
||||
|
||||
options_text += "\n**【平台】**\n"
|
||||
options_text += f"可选:{', '.join(PLATFORM_OPTIONS)}\n"
|
||||
|
||||
options_text += "\n**【排除项】**\n"
|
||||
options_text += f"可选:{', '.join(EXCLUDE_OPTIONS)}\n"
|
||||
|
||||
options_text += "\n" + "-" * 50 + "\n"
|
||||
options_text += "💡 请告诉我您的偏好设置,我会帮您定制热榜!\n"
|
||||
options_text += "示例:关注AI和游戏,平台选微博、B站、IT之家,排除广告"
|
||||
|
||||
return options_text
|
||||
|
||||
def parse_config_from_input(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
从用户输入解析配置
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
解析的配置信息
|
||||
"""
|
||||
user_input = user_input.lower()
|
||||
|
||||
# 解析关键词
|
||||
keywords = []
|
||||
for kw in KEYWORD_OPTIONS:
|
||||
if kw.lower() in user_input:
|
||||
keywords.append(kw)
|
||||
|
||||
# 解析平台
|
||||
platforms = []
|
||||
platform_mapping = {
|
||||
"微博": ["微博", "weibo"],
|
||||
"知乎": ["知乎", "zhihu"],
|
||||
"B站": ["b站", "bilibili", "B站"],
|
||||
"抖音": ["抖音", "tiktok"],
|
||||
"快手": ["快手"],
|
||||
"原神": ["原神", "genshin"],
|
||||
"米游社": ["米游社", "miyoushe"],
|
||||
"IT之家": ["it之家", "ithome", "IT之家"],
|
||||
"36氪": ["36氪", "36kr"],
|
||||
"虎嗅": ["虎嗅", "huxiu"],
|
||||
"豆瓣": ["豆瓣", "douban"],
|
||||
"小红书": ["小红书", "xiaohongshu"],
|
||||
"今日头条": ["今日头条", "头条"],
|
||||
"澎湃新闻": ["澎湃", "澎湃新闻"],
|
||||
"观察者网": ["观察者网", "guanchazhe"],
|
||||
}
|
||||
|
||||
for platform, aliases in platform_mapping.items():
|
||||
for alias in aliases:
|
||||
if alias.lower() in user_input:
|
||||
if platform not in platforms:
|
||||
platforms.append(platform)
|
||||
break
|
||||
|
||||
# 解析排除关键词
|
||||
exclude_keywords = []
|
||||
for ex in EXCLUDE_OPTIONS:
|
||||
if ex.lower() in user_input:
|
||||
exclude_keywords.append(ex)
|
||||
|
||||
return {
|
||||
"keywords": keywords,
|
||||
"platforms": platforms,
|
||||
"exclude_keywords": exclude_keywords
|
||||
}
|
||||
|
||||
async def configure_subscription(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
配置个性化订阅
|
||||
|
||||
Args:
|
||||
user_input: 用户输入的配置信息
|
||||
|
||||
Returns:
|
||||
配置结果
|
||||
"""
|
||||
parsed = self.parse_config_from_input(user_input)
|
||||
|
||||
# 检查是否有配置信息
|
||||
if not parsed["keywords"] and not parsed["platforms"]:
|
||||
# 返回引导信息
|
||||
return {
|
||||
"action": "ask_config",
|
||||
"message": await self.get_config_options()
|
||||
}
|
||||
|
||||
# 创建配置
|
||||
config = UserPreferences(
|
||||
keywords=parsed["keywords"],
|
||||
platforms=parsed["platforms"],
|
||||
exclude_keywords=parsed["exclude_keywords"]
|
||||
)
|
||||
|
||||
# 保存配置
|
||||
if self._save_config(config):
|
||||
return {
|
||||
"action": "config_saved",
|
||||
"config": config,
|
||||
"message": self._format_config_confirmation(config)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"action": "error",
|
||||
"message": "❌ 配置保存失败,请重试"
|
||||
}
|
||||
|
||||
def _format_config_confirmation(self, config: UserPreferences) -> str:
|
||||
"""
|
||||
格式化配置确认信息
|
||||
|
||||
Args:
|
||||
config: 用户配置
|
||||
|
||||
Returns:
|
||||
确认信息文本
|
||||
"""
|
||||
response = "✅ **配置完成!**\n\n"
|
||||
|
||||
response += f"**关键词**: {', '.join(config.keywords) if config.keywords else '未设置'}\n"
|
||||
response += f"**平台**: {', '.join(config.platforms) if config.platforms else '未设置'}\n"
|
||||
response += f"**排除项**: {', '.join(config.exclude_keywords) if config.exclude_keywords else '无'}\n"
|
||||
|
||||
response += "\n" + "-" * 40 + "\n"
|
||||
response += "📊 您可以输入「查看热榜」或「刷新热榜」来获取个性化热榜内容"
|
||||
|
||||
return response
|
||||
|
||||
async def get_personalized_hot(self, config: Optional[UserPreferences] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
获取个性化热榜(新版:先全部获取,再按用户配置过滤)
|
||||
|
||||
Args:
|
||||
config: 配置对象(可选,默认使用当前配置)
|
||||
|
||||
Returns:
|
||||
个性化热榜数据
|
||||
"""
|
||||
if config is None:
|
||||
config = self.current_config
|
||||
|
||||
if not config:
|
||||
return {
|
||||
"error": "未配置个性化订阅",
|
||||
"action": "ask_config",
|
||||
"message": "请先配置您的个性化热榜偏好"
|
||||
}
|
||||
|
||||
# 步骤1:全部获取(如果用户配置了平台,则获取配置的平台;否则获取全部)
|
||||
if config.platforms:
|
||||
platform_ids = [self._platform_name_to_id(p) for p in config.platforms]
|
||||
else:
|
||||
platform_ids = self._all_platforms
|
||||
|
||||
all_items = []
|
||||
if self.api_client:
|
||||
for platform_id in platform_ids:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform_id, limit=config.items_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform_id
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform_id}: {e}")
|
||||
continue
|
||||
|
||||
# 步骤2:按用户配置过滤
|
||||
filtered_items = self._filter_items(all_items, config)
|
||||
|
||||
# 限制总数
|
||||
filtered_items = filtered_items[:config.total_items]
|
||||
|
||||
return {
|
||||
"config": config,
|
||||
"total_items": len(filtered_items),
|
||||
"items": filtered_items
|
||||
}
|
||||
|
||||
def _filter_items(self, items: List[Dict], config: UserPreferences) -> List[Dict]:
|
||||
"""
|
||||
过滤条目
|
||||
|
||||
Args:
|
||||
items: 条目列表
|
||||
config: 用户配置
|
||||
|
||||
Returns:
|
||||
过滤后的条目列表
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
filtered = []
|
||||
|
||||
for item in items:
|
||||
title = item.get("title", "").lower()
|
||||
desc = item.get("description", "").lower()
|
||||
|
||||
# 排除关键词过滤
|
||||
should_exclude = False
|
||||
for ex_kw in config.exclude_keywords:
|
||||
if ex_kw.lower() in title or ex_kw.lower() in desc:
|
||||
should_exclude = True
|
||||
break
|
||||
|
||||
if should_exclude:
|
||||
continue
|
||||
|
||||
# 关键词匹配(如果设置了关键词)
|
||||
if config.keywords:
|
||||
matches_keyword = False
|
||||
for kw in config.keywords:
|
||||
if kw.lower() in title or kw.lower() in desc:
|
||||
matches_keyword = True
|
||||
break
|
||||
|
||||
if not matches_keyword:
|
||||
continue
|
||||
|
||||
filtered.append(item)
|
||||
|
||||
# 排序
|
||||
if config.sort_by == "hot":
|
||||
filtered.sort(key=lambda x: x.get("hot", 0) or x.get("score", 0), reverse=True)
|
||||
elif config.sort_by == "time":
|
||||
filtered.sort(key=lambda x: x.get("time", "") or "", reverse=True)
|
||||
# relevance保持原顺序
|
||||
|
||||
return filtered
|
||||
|
||||
def format_personalized_response(self, hot_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化个性化热榜响应
|
||||
|
||||
Args:
|
||||
hot_data: 热榜数据
|
||||
|
||||
Returns:
|
||||
格式化的响应文本
|
||||
"""
|
||||
if "error" in hot_data:
|
||||
return hot_data.get("message", "❌ 获取失败")
|
||||
|
||||
config = hot_data.get("config")
|
||||
items = hot_data.get("items", [])
|
||||
|
||||
if not items:
|
||||
return "❌ 暂无符合条件的热榜内容"
|
||||
|
||||
response = "🎯 **个性化热榜**\n\n"
|
||||
|
||||
if config and config.keywords:
|
||||
response += f"关键词: {', '.join(config.keywords)}\n"
|
||||
if config and config.platforms:
|
||||
response += f"平台: {', '.join(config.platforms)}\n"
|
||||
|
||||
response += "-" * 40 + "\n"
|
||||
response += f"共 {hot_data['total_items']} 条\n\n"
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "无标题")
|
||||
hot = item.get("hot", item.get("score", ""))
|
||||
platform = item.get("source_platform", "")
|
||||
|
||||
response += f"{i}. {title}\n"
|
||||
if hot:
|
||||
response += f" 🔥 {hot}"
|
||||
if platform:
|
||||
response += f" | 📱 {platform}"
|
||||
response += "\n"
|
||||
|
||||
return response
|
||||
|
||||
async def process_user_request(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
user_input_lower = user_input.lower()
|
||||
|
||||
# 检查是否是查看热榜请求
|
||||
if "热榜" in user_input or "hot" in user_input_lower:
|
||||
if self.current_config:
|
||||
hot_data = await self.get_personalized_hot()
|
||||
response_text = self.format_personalized_response(hot_data)
|
||||
return {
|
||||
"action": "show_hot",
|
||||
"data": hot_data,
|
||||
"message": response_text
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"action": "ask_config",
|
||||
"message": "请先配置个性化热榜偏好"
|
||||
}
|
||||
|
||||
# 检查是否是配置请求
|
||||
if "配置" in user_input or "设置" in user_input or "偏好" in user_input:
|
||||
return await self.configure_subscription(user_input)
|
||||
|
||||
# 默认当作配置处理
|
||||
return await self.configure_subscription(user_input)
|
||||
|
||||
def get_current_config(self) -> Optional[UserPreferences]:
|
||||
"""获取当前配置"""
|
||||
return self.current_config
|
||||
|
||||
def clear_config(self) -> bool:
|
||||
"""清除配置"""
|
||||
if self.storage_path and os.path.exists(self.storage_path):
|
||||
try:
|
||||
os.remove(self.storage_path)
|
||||
self.current_config = None
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error clearing config: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_personalized(api_client=None, formatter=None, storage_path: str = None) -> PersonalizedSubscription:
|
||||
"""创建个性化订阅实例"""
|
||||
return PersonalizedSubscription(api_client=api_client, formatter=formatter, storage_path=storage_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
async def test():
|
||||
ps = await create_personalized()
|
||||
|
||||
print("配置选项:")
|
||||
print(await ps.get_config_options())
|
||||
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
# 测试配置
|
||||
result = await ps.process_user_request("关注AI和游戏,平台选微博、B站、IT之家")
|
||||
print(result["message"])
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,9 @@
|
||||
# DailyHotApi Skill - 依赖列表
|
||||
|
||||
# 核心依赖
|
||||
requests>=2.28.0
|
||||
aiohttp>=3.8.0
|
||||
|
||||
# 可选依赖(用于测试)
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.20.0
|
||||
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
舆情监控 - Sentiment Monitoring Module
|
||||
|
||||
功能:
|
||||
- 直接用全量数据做关键词过滤
|
||||
- 监控特定关键词的舆情
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
|
||||
class SentimentType(Enum):
|
||||
"""舆情类型"""
|
||||
ALL = "all" # 全部
|
||||
POSITIVE = "positive" # 正面
|
||||
NEGATIVE = "negative" # 负面
|
||||
NEUTRAL = "neutral" # 中性
|
||||
|
||||
|
||||
@dataclass
|
||||
class SentimentConfig:
|
||||
"""舆情监控配置"""
|
||||
keywords: List[str] = field(default_factory=list) # 监控关键词
|
||||
sentiment_type: SentimentType = SentimentType.ALL # 舆情类型
|
||||
items_per_platform: int = 10 # 每个平台获取的条目数
|
||||
total_items: int = 50 # 总条目数限制
|
||||
include_platforms: List[str] = None # 包含的平台
|
||||
exclude_platforms: List[str] = None # 排除的平台
|
||||
|
||||
|
||||
class SentimentMonitor:
|
||||
"""舆情监控类"""
|
||||
|
||||
def __init__(self, api_client=None, formatter=None):
|
||||
"""
|
||||
初始化舆情监控
|
||||
|
||||
Args:
|
||||
api_client: API客户端实例(可选)
|
||||
formatter: 格式化器实例(可选)
|
||||
"""
|
||||
self.api_client = api_client
|
||||
self.formatter = formatter
|
||||
self.all_platforms = self._get_default_platforms()
|
||||
|
||||
def _get_default_platforms(self) -> List[str]:
|
||||
"""获取默认的全部平台列表"""
|
||||
return [
|
||||
"weibo", "zhihu", "douban-group", "douban-movie",
|
||||
"ithome", "36kr", "sspai", "csdn", "juejin",
|
||||
"genshin", "miyoushe", "bilibili", "hupu",
|
||||
"sina-news", "netease-news", "qq-news",
|
||||
"sina-money", "eastmoney", "xueqiu",
|
||||
"autohome", "懂车帝", "mafengwo", "ctrip",
|
||||
"dianping", "xiaohongshu", "weibo"
|
||||
]
|
||||
|
||||
async def fetch_all_hot_data(self, limit_per_platform: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
一次性获取全部平台的热榜数据
|
||||
|
||||
Args:
|
||||
limit_per_platform: 每个平台获取的条目数
|
||||
|
||||
Returns:
|
||||
全部平台的热榜数据列表
|
||||
"""
|
||||
all_items = []
|
||||
|
||||
if self.api_client:
|
||||
for platform in self.all_platforms:
|
||||
try:
|
||||
items = await self.api_client.get_hot榜单(platform, limit=limit_per_platform)
|
||||
if items:
|
||||
for item in items:
|
||||
item["source_platform"] = platform
|
||||
all_items.extend(items)
|
||||
except Exception as e:
|
||||
print(f"Error fetching {platform}: {e}")
|
||||
continue
|
||||
|
||||
return all_items
|
||||
|
||||
async def monitor_keywords(self, keywords: List[str], config: Optional[SentimentConfig] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
监控关键词舆情(新版:先全部获取,再关键词过滤)
|
||||
|
||||
Args:
|
||||
keywords: 关键词列表
|
||||
config: 配置对象(可选)
|
||||
|
||||
Returns:
|
||||
舆情监控数据
|
||||
"""
|
||||
if config is None:
|
||||
config = SentimentConfig(keywords=keywords)
|
||||
|
||||
# 步骤1:全部获取
|
||||
all_items = await self.fetch_all_hot_data(limit_per_platform=config.items_per_platform)
|
||||
|
||||
# 步骤2:关键词过滤
|
||||
filtered_items = self._filter_by_keywords(all_items, config.keywords)
|
||||
|
||||
# 平台过滤
|
||||
if config.include_platforms:
|
||||
filtered_items = [item for item in filtered_items if item.get("source_platform") in config.include_platforms]
|
||||
if config.exclude_platforms:
|
||||
filtered_items = [item for item in filtered_items if item.get("source_platform") not in config.exclude_platforms]
|
||||
|
||||
# 限制总数
|
||||
filtered_items = filtered_items[:config.total_items]
|
||||
|
||||
return {
|
||||
"keywords": config.keywords,
|
||||
"total_items": len(filtered_items),
|
||||
"items": filtered_items
|
||||
}
|
||||
|
||||
def _filter_by_keywords(self, items: List[Dict], keywords: List[str]) -> List[Dict]:
|
||||
"""
|
||||
按关键词过滤
|
||||
|
||||
Args:
|
||||
items: 条目列表
|
||||
keywords: 关键词列表
|
||||
|
||||
Returns:
|
||||
过滤后的条目列表
|
||||
"""
|
||||
if not keywords:
|
||||
return items
|
||||
|
||||
filtered = []
|
||||
|
||||
for item in items:
|
||||
title = item.get("title", "").lower()
|
||||
desc = item.get("description", "").lower()
|
||||
|
||||
# 检查是否匹配任何关键词
|
||||
matches = False
|
||||
for kw in keywords:
|
||||
if kw.lower() in title or kw.lower() in desc:
|
||||
matches = True
|
||||
break
|
||||
|
||||
if matches:
|
||||
filtered.append(item)
|
||||
|
||||
# 按热度排序
|
||||
filtered.sort(key=lambda x: x.get("hot", 0) or x.get("score", 0), reverse=True)
|
||||
|
||||
return filtered
|
||||
|
||||
def format_monitoring_response(self, monitor_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化监控响应
|
||||
|
||||
Args:
|
||||
monitor_data: 监控数据
|
||||
|
||||
Returns:
|
||||
格式化的响应文本
|
||||
"""
|
||||
items = monitor_data.get("items", [])
|
||||
keywords = monitor_data.get("keywords", [])
|
||||
|
||||
if not items:
|
||||
return f"❌ 暂无关于「{', '.join(keywords)}」的舆情数据"
|
||||
|
||||
response = f"🔍 **舆情监控 - {', '.join(keywords)}**\n"
|
||||
response += f"共 {monitor_data['total_items']} 条相关内容\n"
|
||||
response += "-" * 40 + "\n\n"
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "无标题")
|
||||
hot = item.get("hot", item.get("score", ""))
|
||||
platform = item.get("source_platform", "")
|
||||
|
||||
response += f"{i}. {title}\n"
|
||||
if hot:
|
||||
response += f" 🔥 热度: {hot}"
|
||||
if platform:
|
||||
response += f" | 📱 {platform}"
|
||||
response += "\n\n"
|
||||
|
||||
return response
|
||||
|
||||
def parse_keywords_from_input(self, user_input: str) -> List[str]:
|
||||
"""
|
||||
从用户输入解析关键词
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
关键词列表
|
||||
"""
|
||||
user_input = user_input.strip()
|
||||
|
||||
# 常用监控关键词
|
||||
common_keywords = [
|
||||
"AI", "人工智能", "ChatGPT", "大模型",
|
||||
"特斯拉", "比亚迪", "新能源汽车",
|
||||
"华为", "iPhone", "小米",
|
||||
"直播", "电商", "网红",
|
||||
"房价", "股票", "基金",
|
||||
"高考", "考研", "留学"
|
||||
]
|
||||
|
||||
matched_keywords = []
|
||||
|
||||
for kw in common_keywords:
|
||||
if kw.lower() in user_input.lower():
|
||||
matched_keywords.append(kw)
|
||||
|
||||
# 如果没有匹配到常用关键词,尝试提取用户输入的词
|
||||
if not matched_keywords:
|
||||
# 按逗号、空格等分隔
|
||||
import re
|
||||
words = re.split(r'[,,\s]+', user_input)
|
||||
matched_keywords = [w for w in words if w.strip()]
|
||||
|
||||
return matched_keywords
|
||||
|
||||
async def process_user_request(self, user_input: str) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户请求
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
# 解析关键词
|
||||
keywords = self.parse_keywords_from_input(user_input)
|
||||
|
||||
if not keywords:
|
||||
# 返回引导信息
|
||||
return {
|
||||
"action": "ask_keywords",
|
||||
"message": "🔍 **舆情监控**\n\n"
|
||||
"请输入您想监控的关键词,例如:\n"
|
||||
"• AI、人工智能、ChatGPT\n"
|
||||
"• 特斯拉、比亚迪、新能源汽车\n"
|
||||
"• 华为、iPhone、小米\n"
|
||||
"• 直播、电商、网红\n"
|
||||
"• 房价、股票、基金\n\n"
|
||||
"💡 您可以直接输入任意关键词进行监控"
|
||||
}
|
||||
|
||||
# 获取监控数据
|
||||
monitor_data = await self.monitor_keywords(keywords)
|
||||
|
||||
# 格式化响应
|
||||
response_text = self.format_monitoring_response(monitor_data)
|
||||
|
||||
return {
|
||||
"action": "show_monitoring",
|
||||
"data": monitor_data,
|
||||
"message": response_text
|
||||
}
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def create_sentiment_monitor(api_client=None, formatter=None) -> SentimentMonitor:
|
||||
"""创建舆情监控实例"""
|
||||
return SentimentMonitor(api_client=api_client, formatter=formatter)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
async def test():
|
||||
monitor = await create_sentiment_monitor()
|
||||
|
||||
print("测试舆情监控...")
|
||||
result = await monitor.process_user_request("监控AI和特斯拉")
|
||||
print(result["message"])
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,345 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
每日热榜 Skill - 数据存储模块
|
||||
|
||||
功能:
|
||||
- 自动保存每日热榜数据
|
||||
- 查询历史热榜记录
|
||||
- 管理数据文件
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from config import config
|
||||
|
||||
|
||||
class DataStorage:
|
||||
"""数据存储类"""
|
||||
|
||||
def __init__(self):
|
||||
self.data_dir = config.data_dir
|
||||
self.auto_save = config.auto_save
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
|
||||
def save_hot_list(self, source_id: str, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
保存热榜数据到文件
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID(如 weibo, zhihu)
|
||||
data: 热榜数据字典
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
if not self.auto_save:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 获取今日日期的文件路径
|
||||
file_path = config.get_data_path(source_id)
|
||||
|
||||
# 构建存储结构
|
||||
storage_data = {
|
||||
"source_id": source_id,
|
||||
"save_time": datetime.now().isoformat(),
|
||||
"update_time": data.get("update_time", ""),
|
||||
"total": data.get("total", 0),
|
||||
"data": data.get("data", [])
|
||||
}
|
||||
|
||||
# 读取现有数据(如果有)
|
||||
existing_data = []
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
existing_data = json.load(f)
|
||||
if not isinstance(existing_data, list):
|
||||
existing_data = []
|
||||
except:
|
||||
existing_data = []
|
||||
|
||||
# 添加新数据到列表
|
||||
existing_data.append(storage_data)
|
||||
|
||||
# 保存到文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(existing_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DataStorage] 保存失败: {e}")
|
||||
return False
|
||||
|
||||
def load_history(self, source_id: str = "", days: int = 7) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
加载历史热榜数据
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID(空则加载所有源)
|
||||
days: 加载最近几天的数据
|
||||
|
||||
Returns:
|
||||
按日期组织的热榜数据
|
||||
"""
|
||||
result = {}
|
||||
|
||||
if source_id:
|
||||
# 加载指定源的历史数据
|
||||
source_dir = os.path.join(self.data_dir, source_id)
|
||||
if not os.path.exists(source_dir):
|
||||
return {}
|
||||
|
||||
for i in range(days):
|
||||
date_str = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
file_path = os.path.join(source_dir, f"{date_str}.json")
|
||||
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
result[date_str] = data
|
||||
except:
|
||||
continue
|
||||
|
||||
else:
|
||||
# 加载所有源的历史数据
|
||||
if not os.path.exists(self.data_dir):
|
||||
return {}
|
||||
|
||||
for source in os.listdir(self.data_dir):
|
||||
source_dir = os.path.join(self.data_dir, source)
|
||||
if not os.path.isdir(source_dir):
|
||||
continue
|
||||
|
||||
result[source] = {}
|
||||
for i in range(days):
|
||||
date_str = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
file_path = os.path.join(source_dir, f"{date_str}.json")
|
||||
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
result[source][date_str] = data
|
||||
except:
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def get_saved_dates(self, source_id: str) -> List[str]:
|
||||
"""
|
||||
获取指定热榜源已保存的日期列表
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID
|
||||
|
||||
Returns:
|
||||
已保存的日期列表(降序)
|
||||
"""
|
||||
dates = []
|
||||
source_dir = os.path.join(self.data_dir, source_id)
|
||||
|
||||
if not os.path.exists(source_dir):
|
||||
return []
|
||||
|
||||
for filename in os.listdir(source_dir):
|
||||
if filename.endswith('.json'):
|
||||
date_str = filename.replace('.json', '')
|
||||
dates.append(date_str)
|
||||
|
||||
return sorted(dates, reverse=True)
|
||||
|
||||
def get_old_data_files(self, days: int = 7) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取指定天数之前的旧数据文件列表
|
||||
|
||||
Args:
|
||||
days: 天数阈值(默认7天)
|
||||
|
||||
Returns:
|
||||
旧文件列表 [{source_id, date_str, file_path}]
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
old_files = []
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
if not os.path.exists(self.data_dir):
|
||||
return []
|
||||
|
||||
for source in os.listdir(self.data_dir):
|
||||
source_dir = os.path.join(self.data_dir, source)
|
||||
if not os.path.isdir(source_dir):
|
||||
continue
|
||||
|
||||
for filename in os.listdir(source_dir):
|
||||
if not filename.endswith('.json'):
|
||||
continue
|
||||
|
||||
try:
|
||||
file_date = datetime.strptime(filename.replace('.json', ''), "%Y-%m-%d")
|
||||
if file_date < cutoff_date:
|
||||
old_files.append({
|
||||
"source_id": source,
|
||||
"date_str": filename.replace('.json', ''),
|
||||
"file_path": os.path.join(source_dir, filename)
|
||||
})
|
||||
except:
|
||||
continue
|
||||
|
||||
return old_files
|
||||
|
||||
def cleanup_old_files(self, files: List[Dict[str, str]]) -> int:
|
||||
"""
|
||||
删除指定的旧文件
|
||||
|
||||
Args:
|
||||
files: 旧文件列表
|
||||
|
||||
Returns:
|
||||
删除的文件数量
|
||||
"""
|
||||
deleted_count = 0
|
||||
for f in files:
|
||||
try:
|
||||
if os.path.exists(f["file_path"]):
|
||||
os.remove(f["file_path"])
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
print(f"[DataStorage] 删除失败 {f['file_path']}: {e}")
|
||||
|
||||
return deleted_count
|
||||
|
||||
def load_hot_list(self, source_id: str, date_str: str = "") -> Optional[List[Dict]]:
|
||||
"""
|
||||
加载指定日期的热榜数据
|
||||
|
||||
Args:
|
||||
source_id: 热榜源 ID
|
||||
date_str: 日期(默认今天)
|
||||
|
||||
Returns:
|
||||
热榜数据列表
|
||||
"""
|
||||
if not date_str:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
file_path = config.get_data_path(source_id, date_str)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
# 返回当天的最后一条记录
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
return data[-1].get("data", [])
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def list_all_data(self) -> Dict[str, int]:
|
||||
"""
|
||||
列出所有已保存的热榜数据统计
|
||||
|
||||
Returns:
|
||||
源ID -> 保存记录数
|
||||
"""
|
||||
stats = {}
|
||||
|
||||
if not os.path.exists(self.data_dir):
|
||||
return {}
|
||||
|
||||
for source in os.listdir(self.data_dir):
|
||||
source_dir = os.path.join(self.data_dir, source)
|
||||
if not os.path.isdir(source_dir):
|
||||
continue
|
||||
|
||||
count = 0
|
||||
for filename in os.listdir(source_dir):
|
||||
if filename.endswith('.json'):
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
stats[source] = count
|
||||
|
||||
return stats
|
||||
|
||||
def clear_old_data(self, keep_days: int = 30) -> int:
|
||||
"""
|
||||
清理旧数据
|
||||
|
||||
Args:
|
||||
keep_days: 保留最近几天的数据
|
||||
|
||||
Returns:
|
||||
删除的文件数量
|
||||
"""
|
||||
deleted_count = 0
|
||||
cutoff_date = datetime.now() - timedelta(days=keep_days)
|
||||
|
||||
if not os.path.exists(self.data_dir):
|
||||
return 0
|
||||
|
||||
for source in os.listdir(self.data_dir):
|
||||
source_dir = os.path.join(self.data_dir, source)
|
||||
if not os.path.isdir(source_dir):
|
||||
continue
|
||||
|
||||
for filename in os.listdir(source_dir):
|
||||
if not filename.endswith('.json'):
|
||||
continue
|
||||
|
||||
try:
|
||||
file_date = datetime.strptime(filename.replace('.json', ''), "%Y-%m-%d")
|
||||
if file_date < cutoff_date:
|
||||
file_path = os.path.join(source_dir, filename)
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
return deleted_count
|
||||
|
||||
|
||||
# 全局存储实例
|
||||
storage = DataStorage()
|
||||
|
||||
|
||||
def auto_save_hot_list(source_id: str, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
自动保存热榜数据的便捷函数
|
||||
|
||||
在获取热榜数据后调用此函数保存数据
|
||||
"""
|
||||
return storage.save_hot_list(source_id, data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试存储功能
|
||||
print("📊 每日热榜数据存储管理")
|
||||
print("=" * 50)
|
||||
|
||||
# 列出所有数据
|
||||
stats = storage.list_all_data()
|
||||
if stats:
|
||||
print("\n已保存的热榜数据:")
|
||||
for source, count in stats.items():
|
||||
print(f" • {source}: {count} 条记录")
|
||||
else:
|
||||
print("\n暂无保存的数据")
|
||||
|
||||
# 列出已保存的日期
|
||||
print("\n已保存的日期(微博):")
|
||||
dates = storage.get_saved_dates("weibo")
|
||||
if dates:
|
||||
for date in dates[:7]:
|
||||
print(f" • {date}")
|
||||
else:
|
||||
print(" 暂无数据")
|
||||
Reference in New Issue
Block a user