# -*- 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")