215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
"""
|
||
60秒读懂世界插件
|
||
|
||
每日新闻推送和指令查询
|
||
"""
|
||
|
||
import httpx
|
||
import uuid
|
||
from pathlib import Path
|
||
from loguru import logger
|
||
from utils.plugin_base import PluginBase
|
||
from utils.decorators import on_text_message, schedule
|
||
from WechatHook import WechatHookClient
|
||
import tomllib
|
||
|
||
|
||
class News60s(PluginBase):
|
||
"""60秒读懂世界插件"""
|
||
|
||
description = "60秒读懂世界 - 每日新闻推送"
|
||
author = "ShiHao"
|
||
version = "1.0.0"
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.config = None
|
||
self.images_dir = None
|
||
|
||
async def async_init(self):
|
||
"""异步初始化"""
|
||
config_path = Path(__file__).parent / "config.toml"
|
||
with open(config_path, "rb") as f:
|
||
self.config = tomllib.load(f)
|
||
|
||
self.images_dir = Path(__file__).parent / "images"
|
||
self.images_dir.mkdir(exist_ok=True)
|
||
|
||
logger.success("60秒读懂世界插件初始化完成")
|
||
|
||
async def fetch_news_image(self) -> str:
|
||
"""获取60秒新闻图片"""
|
||
try:
|
||
timeout = httpx.Timeout(connect=10.0, read=120.0, write=10.0, pool=10.0)
|
||
|
||
# 配置代理
|
||
proxy = None
|
||
proxy_config = self.config.get("proxy", {})
|
||
if proxy_config.get("enabled", False):
|
||
proxy_type = proxy_config.get("type", "socks5")
|
||
proxy_host = proxy_config.get("host", "127.0.0.1")
|
||
proxy_port = proxy_config.get("port", 7890)
|
||
proxy = f"{proxy_type}://{proxy_host}:{proxy_port}"
|
||
|
||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy, follow_redirects=True) as client:
|
||
response = await client.get("https://60s.viki.moe/v2/60s?encoding=image")
|
||
response.raise_for_status()
|
||
|
||
# 保存图片
|
||
uid = uuid.uuid4().hex[:8]
|
||
file_path = self.images_dir / f"news_{uid}.jpg"
|
||
with open(file_path, "wb") as f:
|
||
f.write(response.content)
|
||
|
||
logger.info(f"60秒新闻图片下载成功: {file_path}")
|
||
return str(file_path)
|
||
except Exception as e:
|
||
logger.error(f"获取60秒新闻失败: {e}")
|
||
return None
|
||
|
||
def is_target_group(self, from_wxid: str) -> bool:
|
||
"""检查是否是目标群组"""
|
||
whitelist = self.config["behavior"].get("group_whitelist", [])
|
||
# 空白名单表示所有群组
|
||
if not whitelist:
|
||
return True
|
||
return from_wxid in whitelist
|
||
|
||
@on_text_message(priority=70)
|
||
async def handle_message(self, bot: WechatHookClient, message: dict):
|
||
"""处理文本消息"""
|
||
if not self.config["behavior"]["enable_command"]:
|
||
return True
|
||
|
||
content = message.get("Content", "").strip()
|
||
from_wxid = message.get("FromWxid", "")
|
||
is_group = message.get("IsGroup", False)
|
||
|
||
# 只处理群聊
|
||
if not is_group:
|
||
return True
|
||
|
||
# 检查是否是触发指令(支持 "@机器人 /60s" 或直接 "/60s")
|
||
keywords = self.config["behavior"]["command_keywords"]
|
||
matched = False
|
||
for keyword in keywords:
|
||
if content == keyword or content.endswith(f" {keyword}"):
|
||
matched = True
|
||
break
|
||
|
||
if not matched:
|
||
return True
|
||
|
||
logger.info(f"收到60秒新闻请求: {from_wxid}")
|
||
|
||
try:
|
||
image_path = await self.fetch_news_image()
|
||
if image_path:
|
||
await bot.send_image(from_wxid, image_path)
|
||
logger.success(f"已发送60秒新闻到: {from_wxid}")
|
||
else:
|
||
await bot.send_text(from_wxid, "❌ 获取新闻失败,请稍后重试")
|
||
except Exception as e:
|
||
logger.error(f"发送60秒新闻失败: {e}")
|
||
await bot.send_text(from_wxid, f"❌ 发送失败: {str(e)}")
|
||
|
||
return False
|
||
|
||
@schedule('cron', hour=8, minute=0)
|
||
async def scheduled_news(self, bot=None):
|
||
"""定时推送新闻"""
|
||
if not self.config["schedule"]["enabled"]:
|
||
return
|
||
|
||
logger.info("开始定时推送60秒新闻")
|
||
|
||
try:
|
||
image_path = await self.fetch_news_image()
|
||
if not image_path:
|
||
logger.error("定时任务:获取新闻图片失败")
|
||
return
|
||
|
||
# 获取bot实例
|
||
if not bot:
|
||
from utils.plugin_manager import PluginManager
|
||
bot = PluginManager().bot
|
||
|
||
if not bot:
|
||
logger.error("定时任务:无法获取bot实例")
|
||
return
|
||
|
||
# 获取所有群聊
|
||
import tomllib
|
||
with open("main_config.toml", "rb") as f:
|
||
main_config = tomllib.load(f)
|
||
|
||
# 推送到目标群组
|
||
whitelist = self.config["behavior"].get("group_whitelist", [])
|
||
|
||
if whitelist:
|
||
# 有白名单,只推送到白名单群组
|
||
target_groups = whitelist
|
||
else:
|
||
# 无白名单,推送到所有群聊(需要从数据库或其他地方获取)
|
||
# 这里暂时只推送到白名单,避免骚扰
|
||
logger.warning("未配置群组白名单,跳过定时推送")
|
||
return
|
||
|
||
success_count = 0
|
||
for group_id in target_groups:
|
||
try:
|
||
await bot.send_image(group_id, image_path)
|
||
success_count += 1
|
||
logger.info(f"已推送60秒新闻到: {group_id}")
|
||
except Exception as e:
|
||
logger.error(f"推送到 {group_id} 失败: {e}")
|
||
|
||
logger.success(f"定时推送完成,成功: {success_count}/{len(target_groups)}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"定时推送60秒新闻失败: {e}")
|
||
|
||
def get_llm_tools(self):
|
||
"""返回LLM工具定义"""
|
||
return [{
|
||
"type": "function",
|
||
"function": {
|
||
"name": "get_daily_news",
|
||
"description": "获取每日60秒读懂世界新闻图片。当用户询问今日新闻、每日新闻、60秒新闻、早报等内容时调用此工具。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {},
|
||
"required": []
|
||
}
|
||
}
|
||
}]
|
||
|
||
async def execute_llm_tool(self, tool_name: str, arguments: dict, bot: WechatHookClient, from_wxid: str) -> dict:
|
||
"""执行LLM工具调用"""
|
||
if tool_name != "get_daily_news":
|
||
return None
|
||
|
||
try:
|
||
logger.info(f"LLM工具调用60秒新闻: {from_wxid}")
|
||
|
||
image_path = await self.fetch_news_image()
|
||
if image_path:
|
||
await bot.send_image(from_wxid, image_path)
|
||
return {
|
||
"success": True,
|
||
"message": "已获取并发送今日新闻图片",
|
||
"no_reply": True # 已发送图片,不需要AI再回复
|
||
}
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"message": "获取新闻图片失败,请稍后重试"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"LLM工具执行失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"message": f"执行失败: {str(e)}"
|
||
}
|