- daily_news 插件内置百度新闻与60s图片获取逻辑,移除对 base.func_news 的业务依赖\n- epic_free 插件内置周五判断与免费游戏抓取逻辑,移除对 base.func_epic 的业务依赖\n- daily_ranking 插件内置排行生成与积分奖励逻辑,不再依赖 MessageStorage 业务封装\n- sehuatang_push 改为引用插件目录内的抓取与PDF生成实现,将核心业务代码迁入插件目录\n- 确保新插件可独立承载自身业务逻辑,平台层仅提供调度与基础设施能力
192 lines
7.0 KiB
Python
192 lines
7.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
import asyncio
|
||
import base64
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
import requests
|
||
|
||
from base.plugin_common.message_plugin_interface import MessagePluginInterface
|
||
from base.plugin_common.plugin_interface import PluginStatus
|
||
from utils.robot_cmd.robot_command import GroupBotManager
|
||
from wechat_ipad.models.appmsg_xml import LINK_XML_NEWS
|
||
|
||
|
||
class DailyNewsPlugin(MessagePluginInterface):
|
||
"""每日新闻定时插件。"""
|
||
|
||
FEATURE_KEY = "DAILY_NEWS"
|
||
FEATURE_DESCRIPTION = "📰 每日新闻自动播报 [每日8:30定时发送]"
|
||
|
||
@property
|
||
def name(self) -> str:
|
||
return "每日新闻"
|
||
|
||
@property
|
||
def version(self) -> str:
|
||
return "1.0.0"
|
||
|
||
@property
|
||
def description(self) -> str:
|
||
return "将百度新闻日报能力下沉为插件定时任务。"
|
||
|
||
@property
|
||
def author(self) -> str:
|
||
return "ABOT Team"
|
||
|
||
@property
|
||
def commands(self) -> List[str]:
|
||
# 该插件只负责后台调度,不处理前台命令。
|
||
return []
|
||
|
||
@property
|
||
def feature_key(self) -> Optional[str]:
|
||
return self.FEATURE_KEY
|
||
|
||
@property
|
||
def feature_description(self) -> Optional[str]:
|
||
return self.FEATURE_DESCRIPTION
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.feature = self.register_feature()
|
||
|
||
def initialize(self, context: Dict[str, Any]) -> bool:
|
||
self.LOG.debug(f"正在初始化 {self.name} 插件...")
|
||
return True
|
||
|
||
def start(self) -> bool:
|
||
self.status = PluginStatus.RUNNING
|
||
return True
|
||
|
||
def stop(self) -> bool:
|
||
self.status = PluginStatus.STOPPED
|
||
return True
|
||
|
||
def can_process(self, message: Dict[str, Any]) -> bool:
|
||
return False
|
||
|
||
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
||
return False, None
|
||
|
||
def get_schedule_actions(self) -> List[Dict[str, Any]]:
|
||
"""声明插件可调度动作。"""
|
||
return [
|
||
{
|
||
"action_key": "baidu_news_daily_push",
|
||
"name": "百度新闻日报推送",
|
||
"description": "每天推送百度新闻文本、60秒新闻图和资讯卡片",
|
||
"trigger_type": "at_times",
|
||
"trigger_config": {"time_list": ["08:30"]},
|
||
"target_scope": "all_enabled_groups",
|
||
"target_config": {},
|
||
"payload": {},
|
||
"default_enabled": True,
|
||
}
|
||
]
|
||
|
||
async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
||
if action_key != "baidu_news_daily_push":
|
||
return {
|
||
"success": False,
|
||
"summary": f"不支持的动作: {action_key}",
|
||
"detail": {"action_key": action_key},
|
||
}
|
||
|
||
if not self.bot:
|
||
return {"success": False, "summary": "bot 未注入", "detail": {}}
|
||
|
||
target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()]
|
||
if not target_groups:
|
||
# 兜底:当后台未指定范围时,直接按群功能开关收集目标群。
|
||
target_groups = [
|
||
gid for gid in GroupBotManager.get_group_list()
|
||
if GroupBotManager.get_group_permission(gid, self.feature).value == "enabled"
|
||
]
|
||
|
||
if not target_groups:
|
||
return {"success": False, "summary": "没有可推送目标群", "detail": {"target_count": 0}}
|
||
|
||
try:
|
||
# 新闻抓取逻辑内聚在插件内,避免依赖外部业务模块。
|
||
text_news = await asyncio.to_thread(self._get_baidu_news)
|
||
image_url = await asyncio.to_thread(self._get_news_60s_image)
|
||
except Exception as e:
|
||
return {"success": False, "summary": f"新闻抓取失败: {e}", "detail": {"error": str(e)}}
|
||
|
||
# 图片接口返回 URL,统一下载为 base64 再发送,兼容 wechat_ipad 图片发送接口。
|
||
image_base64 = ""
|
||
if image_url:
|
||
try:
|
||
image_base64 = await asyncio.to_thread(self._download_image_as_base64, image_url)
|
||
except Exception as e:
|
||
self.LOG.warning(f"每日新闻图片下载失败,将仅发送文本和卡片: {e}")
|
||
|
||
success_groups = []
|
||
failed_groups = {}
|
||
for gid in target_groups:
|
||
try:
|
||
if text_news:
|
||
await self.bot.send_text_message(gid, text_news)
|
||
if image_base64:
|
||
await self.bot.send_image_message(gid, image_base64)
|
||
await self.bot.send_link_xml_message(LINK_XML_NEWS, gid)
|
||
success_groups.append(gid)
|
||
except Exception as e:
|
||
failed_groups[gid] = str(e)
|
||
|
||
return {
|
||
"success": len(failed_groups) == 0,
|
||
"summary": f"每日新闻推送完成: 成功{len(success_groups)}群, 失败{len(failed_groups)}群",
|
||
"detail": {
|
||
"target_count": len(target_groups),
|
||
"success_groups": success_groups,
|
||
"failed_groups": failed_groups,
|
||
},
|
||
}
|
||
|
||
@staticmethod
|
||
def _download_image_as_base64(url: str) -> str:
|
||
"""下载图片并转为 base64,便于统一发送。"""
|
||
resp = requests.get(url, timeout=15)
|
||
resp.raise_for_status()
|
||
return base64.b64encode(resp.content).decode("utf-8")
|
||
|
||
@staticmethod
|
||
def _get_baidu_news() -> str:
|
||
"""获取百度热榜文本(插件内实现)。"""
|
||
headers = {
|
||
"User-Agent": (
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) "
|
||
"Gecko/20100101 Firefox/110.0"
|
||
)
|
||
}
|
||
url = "https://top.baidu.com/api/board?platform=wise&tab=realtime"
|
||
now = datetime.now()
|
||
current_date = now.strftime("%Y年%m月%d日")
|
||
weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
||
output = f"当前日期:{current_date} {weekdays[now.weekday()]}\n\n"
|
||
|
||
resp = requests.get(url, headers=headers, timeout=15)
|
||
resp.raise_for_status()
|
||
post = resp.json()
|
||
cards = post.get("data", {}).get("cards", [])
|
||
index = 1
|
||
for card in cards:
|
||
for block in card.get("content", []):
|
||
for article in block.get("content", []):
|
||
if isinstance(article, dict) and "word" in article:
|
||
title = str(article.get("word", "")).strip().replace(" ", "_")
|
||
output += f"{index} :#{title}\n"
|
||
index += 1
|
||
return output
|
||
|
||
@staticmethod
|
||
def _get_news_60s_image() -> Optional[str]:
|
||
"""获取 60s 新闻图片地址(插件内实现)。"""
|
||
api_url = "http://192.168.2.32:4399/v2/60s"
|
||
resp = requests.get(api_url, timeout=15)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
return (data or {}).get("data", {}).get("image")
|