433 lines
16 KiB
Python
433 lines
16 KiB
Python
# -*- 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)
|