dispatcher.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. # coding=utf-8
  2. """
  3. 通知调度器模块
  4. 提供统一的通知分发接口。
  5. 支持所有通知渠道的多账号配置,使用 `;` 分隔多个账号。
  6. 使用示例:
  7. dispatcher = NotificationDispatcher(config, get_time_func, split_content_func)
  8. results = dispatcher.dispatch_all(report_data, report_type, ...)
  9. """
  10. from __future__ import annotations
  11. from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
  12. from trendradar.core.config import (
  13. get_account_at_index,
  14. limit_accounts,
  15. parse_multi_account_config,
  16. validate_paired_configs,
  17. )
  18. from .senders import (
  19. send_to_bark,
  20. send_to_dingtalk,
  21. send_to_email,
  22. send_to_feishu,
  23. send_to_ntfy,
  24. send_to_slack,
  25. send_to_telegram,
  26. send_to_wework,
  27. send_to_generic_webhook,
  28. )
  29. from .renderer import (
  30. render_rss_feishu_content,
  31. render_rss_dingtalk_content,
  32. render_rss_markdown_content,
  33. )
  34. # 类型检查时导入,运行时不导入(避免循环导入)
  35. if TYPE_CHECKING:
  36. from trendradar.ai import AIAnalysisResult, AITranslator
  37. class NotificationDispatcher:
  38. """
  39. 统一的多账号通知调度器
  40. 将多账号发送逻辑封装,提供简洁的 dispatch_all 接口。
  41. 内部处理账号解析、数量限制、配对验证等逻辑。
  42. """
  43. def __init__(
  44. self,
  45. config: Dict[str, Any],
  46. get_time_func: Callable,
  47. split_content_func: Callable,
  48. translator: Optional["AITranslator"] = None,
  49. ):
  50. """
  51. 初始化通知调度器
  52. Args:
  53. config: 完整的配置字典,包含所有通知渠道的配置
  54. get_time_func: 获取当前时间的函数
  55. split_content_func: 内容分批函数
  56. translator: AI 翻译器实例(可选)
  57. """
  58. self.config = config
  59. self.get_time_func = get_time_func
  60. self.split_content_func = split_content_func
  61. self.max_accounts = config.get("MAX_ACCOUNTS_PER_CHANNEL", 3)
  62. self.translator = translator
  63. def _translate_content(
  64. self,
  65. report_data: Dict,
  66. rss_items: Optional[List[Dict]] = None,
  67. rss_new_items: Optional[List[Dict]] = None,
  68. standalone_data: Optional[Dict] = None,
  69. display_regions: Optional[Dict] = None,
  70. ) -> tuple:
  71. """
  72. 翻译推送内容
  73. Args:
  74. report_data: 报告数据
  75. rss_items: RSS 统计条目
  76. rss_new_items: RSS 新增条目
  77. standalone_data: 独立展示区数据
  78. display_regions: 区域显示配置(不展示的区域跳过翻译)
  79. Returns:
  80. tuple: (翻译后的 report_data, rss_items, rss_new_items, standalone_data)
  81. """
  82. if not self.translator or not self.translator.enabled:
  83. return report_data, rss_items, rss_new_items, standalone_data
  84. import copy
  85. print(f"[翻译] 开始翻译内容到 {self.translator.target_language}...")
  86. scope = self.translator.scope
  87. display_regions = display_regions or {}
  88. # 深拷贝避免修改原始数据
  89. report_data = copy.deepcopy(report_data)
  90. rss_items = copy.deepcopy(rss_items) if rss_items else None
  91. rss_new_items = copy.deepcopy(rss_new_items) if rss_new_items else None
  92. standalone_data = copy.deepcopy(standalone_data) if standalone_data else None
  93. # 收集所有需要翻译的标题
  94. titles_to_translate = []
  95. title_locations = [] # 记录标题位置,用于回填
  96. # 1. 热榜标题(scope 开启 且 区域展示)
  97. if scope.get("HOTLIST", True) and display_regions.get("HOTLIST", True):
  98. for stat_idx, stat in enumerate(report_data.get("stats", [])):
  99. for title_idx, title_data in enumerate(stat.get("titles", [])):
  100. titles_to_translate.append(title_data.get("title", ""))
  101. title_locations.append(("stats", stat_idx, title_idx))
  102. # 2. 新增热点标题
  103. for source_idx, source in enumerate(report_data.get("new_titles", [])):
  104. for title_idx, title_data in enumerate(source.get("titles", [])):
  105. titles_to_translate.append(title_data.get("title", ""))
  106. title_locations.append(("new_titles", source_idx, title_idx))
  107. # 3. RSS 统计标题(结构与 stats 一致:[{word, count, titles: [{title, ...}]}])
  108. if rss_items and scope.get("RSS", True) and display_regions.get("RSS", True):
  109. for stat_idx, stat in enumerate(rss_items):
  110. for title_idx, title_data in enumerate(stat.get("titles", [])):
  111. titles_to_translate.append(title_data.get("title", ""))
  112. title_locations.append(("rss_items", stat_idx, title_idx))
  113. # 4. RSS 新增标题(结构与 stats 一致)
  114. if rss_new_items and scope.get("RSS", True) and display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True):
  115. for stat_idx, stat in enumerate(rss_new_items):
  116. for title_idx, title_data in enumerate(stat.get("titles", [])):
  117. titles_to_translate.append(title_data.get("title", ""))
  118. title_locations.append(("rss_new_items", stat_idx, title_idx))
  119. # 5. 独立展示区 - 热榜平台
  120. if standalone_data and scope.get("STANDALONE", True) and display_regions.get("STANDALONE", False):
  121. for plat_idx, platform in enumerate(standalone_data.get("platforms", [])):
  122. for item_idx, item in enumerate(platform.get("items", [])):
  123. titles_to_translate.append(item.get("title", ""))
  124. title_locations.append(("standalone_platforms", plat_idx, item_idx))
  125. # 6. 独立展示区 - RSS 源
  126. for feed_idx, feed in enumerate(standalone_data.get("rss_feeds", [])):
  127. for item_idx, item in enumerate(feed.get("items", [])):
  128. titles_to_translate.append(item.get("title", ""))
  129. title_locations.append(("standalone_rss", feed_idx, item_idx))
  130. if not titles_to_translate:
  131. print("[翻译] 没有需要翻译的内容")
  132. return report_data, rss_items, rss_new_items, standalone_data
  133. print(f"[翻译] 共 {len(titles_to_translate)} 条标题待翻译")
  134. # 批量翻译
  135. result = self.translator.translate_batch(titles_to_translate)
  136. if result.success_count == 0:
  137. print(f"[翻译] 翻译失败: {result.results[0].error if result.results else '未知错误'}")
  138. return report_data, rss_items, rss_new_items, standalone_data
  139. print(f"[翻译] 翻译完成: {result.success_count}/{result.total_count} 成功")
  140. # debug 模式:输出完整 prompt、AI 原始响应、逐条对照
  141. if self.config.get("DEBUG", False):
  142. if result.prompt:
  143. print(f"[翻译][DEBUG] === 发送给 AI 的 Prompt ===")
  144. print(result.prompt)
  145. print(f"[翻译][DEBUG] === Prompt 结束 ===")
  146. if result.raw_response:
  147. print(f"[翻译][DEBUG] === AI 原始响应 ===")
  148. print(result.raw_response)
  149. print(f"[翻译][DEBUG] === 响应结束 ===")
  150. # 行数不匹配警告
  151. expected = len(titles_to_translate)
  152. if result.parsed_count != expected:
  153. print(f"[翻译][DEBUG] ⚠️ 行数不匹配:期望 {expected} 条,AI 返回 {result.parsed_count} 条")
  154. # 逐条对照
  155. unchanged_count = 0
  156. for i, res in enumerate(result.results):
  157. if not res.success and res.error:
  158. print(f"[翻译][DEBUG] [{i+1}] !! 失败: {res.error}")
  159. elif res.original_text == res.translated_text:
  160. unchanged_count += 1
  161. else:
  162. print(f"[翻译][DEBUG] [{i+1}] {res.original_text} => {res.translated_text}")
  163. if unchanged_count > 0:
  164. print(f"[翻译][DEBUG] (另有 {unchanged_count} 条未变化,已省略)")
  165. # 回填翻译结果
  166. for i, (loc_type, idx1, idx2) in enumerate(title_locations):
  167. if i < len(result.results) and result.results[i].success:
  168. translated = result.results[i].translated_text
  169. if loc_type == "stats":
  170. report_data["stats"][idx1]["titles"][idx2]["title"] = translated
  171. elif loc_type == "new_titles":
  172. report_data["new_titles"][idx1]["titles"][idx2]["title"] = translated
  173. elif loc_type == "rss_items" and rss_items:
  174. rss_items[idx1]["titles"][idx2]["title"] = translated
  175. elif loc_type == "rss_new_items" and rss_new_items:
  176. rss_new_items[idx1]["titles"][idx2]["title"] = translated
  177. elif loc_type == "standalone_platforms" and standalone_data:
  178. standalone_data["platforms"][idx1]["items"][idx2]["title"] = translated
  179. elif loc_type == "standalone_rss" and standalone_data:
  180. standalone_data["rss_feeds"][idx1]["items"][idx2]["title"] = translated
  181. return report_data, rss_items, rss_new_items, standalone_data
  182. def dispatch_all(
  183. self,
  184. report_data: Dict,
  185. report_type: str,
  186. update_info: Optional[Dict] = None,
  187. proxy_url: Optional[str] = None,
  188. mode: str = "daily",
  189. html_file_path: Optional[str] = None,
  190. rss_items: Optional[List[Dict]] = None,
  191. rss_new_items: Optional[List[Dict]] = None,
  192. ai_analysis: Optional[AIAnalysisResult] = None,
  193. standalone_data: Optional[Dict] = None,
  194. ) -> Dict[str, bool]:
  195. """
  196. 分发通知到所有已配置的渠道(支持热榜+RSS合并推送+AI分析+独立展示区)
  197. Args:
  198. report_data: 报告数据(由 prepare_report_data 生成)
  199. report_type: 报告类型(如 "全天汇总"、"当前榜单"、"增量分析")
  200. update_info: 版本更新信息(可选)
  201. proxy_url: 代理 URL(可选)
  202. mode: 报告模式 (daily/current/incremental)
  203. html_file_path: HTML 报告文件路径(邮件使用)
  204. rss_items: RSS 统计条目列表(用于 RSS 统计区块)
  205. rss_new_items: RSS 新增条目列表(用于 RSS 新增区块)
  206. ai_analysis: AI 分析结果(可选)
  207. standalone_data: 独立展示区数据(可选)
  208. Returns:
  209. Dict[str, bool]: 每个渠道的发送结果,key 为渠道名,value 为是否成功
  210. """
  211. results = {}
  212. # 获取区域显示配置
  213. display_regions = self.config.get("DISPLAY", {}).get("REGIONS", {})
  214. # 执行翻译(如果启用,根据 display_regions 跳过不展示的区域)
  215. report_data, rss_items, rss_new_items, standalone_data = self._translate_content(
  216. report_data, rss_items, rss_new_items, standalone_data, display_regions
  217. )
  218. # 飞书
  219. if self.config.get("FEISHU_WEBHOOK_URL"):
  220. results["feishu"] = self._send_feishu(
  221. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  222. ai_analysis, display_regions, standalone_data
  223. )
  224. # 钉钉
  225. if self.config.get("DINGTALK_WEBHOOK_URL"):
  226. results["dingtalk"] = self._send_dingtalk(
  227. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  228. ai_analysis, display_regions, standalone_data
  229. )
  230. # 企业微信
  231. if self.config.get("WEWORK_WEBHOOK_URL"):
  232. results["wework"] = self._send_wework(
  233. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  234. ai_analysis, display_regions, standalone_data
  235. )
  236. # Telegram(需要配对验证)
  237. if self.config.get("TELEGRAM_BOT_TOKEN") and self.config.get("TELEGRAM_CHAT_ID"):
  238. results["telegram"] = self._send_telegram(
  239. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  240. ai_analysis, display_regions, standalone_data
  241. )
  242. # ntfy(需要配对验证)
  243. if self.config.get("NTFY_SERVER_URL") and self.config.get("NTFY_TOPIC"):
  244. results["ntfy"] = self._send_ntfy(
  245. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  246. ai_analysis, display_regions, standalone_data
  247. )
  248. # Bark
  249. if self.config.get("BARK_URL"):
  250. results["bark"] = self._send_bark(
  251. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  252. ai_analysis, display_regions, standalone_data
  253. )
  254. # Slack
  255. if self.config.get("SLACK_WEBHOOK_URL"):
  256. results["slack"] = self._send_slack(
  257. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  258. ai_analysis, display_regions, standalone_data
  259. )
  260. # 通用 Webhook
  261. if self.config.get("GENERIC_WEBHOOK_URL"):
  262. results["generic_webhook"] = self._send_generic_webhook(
  263. report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
  264. ai_analysis, display_regions, standalone_data
  265. )
  266. # 邮件(保持原有逻辑,已支持多收件人,AI 分析已嵌入 HTML)
  267. if (
  268. self.config.get("EMAIL_FROM")
  269. and self.config.get("EMAIL_PASSWORD")
  270. and self.config.get("EMAIL_TO")
  271. ):
  272. results["email"] = self._send_email(report_type, html_file_path)
  273. return results
  274. def _send_to_multi_accounts(
  275. self,
  276. channel_name: str,
  277. config_value: str,
  278. send_func: Callable[..., bool],
  279. **kwargs,
  280. ) -> bool:
  281. """
  282. 通用多账号发送逻辑
  283. Args:
  284. channel_name: 渠道名称(用于日志和账号数量限制提示)
  285. config_value: 配置值(可能包含多个账号,用 ; 分隔)
  286. send_func: 发送函数,签名为 (account, account_label=..., **kwargs) -> bool
  287. **kwargs: 传递给发送函数的其他参数
  288. Returns:
  289. bool: 任一账号发送成功则返回 True
  290. """
  291. accounts = parse_multi_account_config(config_value)
  292. if not accounts:
  293. return False
  294. accounts = limit_accounts(accounts, self.max_accounts, channel_name)
  295. results = []
  296. for i, account in enumerate(accounts):
  297. if account:
  298. account_label = f"账号{i+1}" if len(accounts) > 1 else ""
  299. result = send_func(account, account_label=account_label, **kwargs)
  300. results.append(result)
  301. return any(results) if results else False
  302. def _send_feishu(
  303. self,
  304. report_data: Dict,
  305. report_type: str,
  306. update_info: Optional[Dict],
  307. proxy_url: Optional[str],
  308. mode: str,
  309. rss_items: Optional[List[Dict]] = None,
  310. rss_new_items: Optional[List[Dict]] = None,
  311. ai_analysis: Optional[AIAnalysisResult] = None,
  312. display_regions: Optional[Dict] = None,
  313. standalone_data: Optional[Dict] = None,
  314. ) -> bool:
  315. """发送到飞书(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  316. display_regions = display_regions or {}
  317. if not display_regions.get("HOTLIST", True):
  318. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  319. return self._send_to_multi_accounts(
  320. channel_name="飞书",
  321. config_value=self.config["FEISHU_WEBHOOK_URL"],
  322. send_func=lambda url, account_label: send_to_feishu(
  323. webhook_url=url,
  324. report_data=report_data,
  325. report_type=report_type,
  326. update_info=update_info,
  327. proxy_url=proxy_url,
  328. mode=mode,
  329. account_label=account_label,
  330. batch_size=self.config.get("FEISHU_BATCH_SIZE", 29000),
  331. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  332. split_content_func=self.split_content_func,
  333. get_time_func=self.get_time_func,
  334. rss_items=rss_items if display_regions.get("RSS", True) else None,
  335. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  336. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  337. display_regions=display_regions,
  338. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  339. ),
  340. )
  341. def _send_dingtalk(
  342. self,
  343. report_data: Dict,
  344. report_type: str,
  345. update_info: Optional[Dict],
  346. proxy_url: Optional[str],
  347. mode: str,
  348. rss_items: Optional[List[Dict]] = None,
  349. rss_new_items: Optional[List[Dict]] = None,
  350. ai_analysis: Optional[AIAnalysisResult] = None,
  351. display_regions: Optional[Dict] = None,
  352. standalone_data: Optional[Dict] = None,
  353. ) -> bool:
  354. """发送到钉钉(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  355. display_regions = display_regions or {}
  356. if not display_regions.get("HOTLIST", True):
  357. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  358. return self._send_to_multi_accounts(
  359. channel_name="钉钉",
  360. config_value=self.config["DINGTALK_WEBHOOK_URL"],
  361. send_func=lambda url, account_label: send_to_dingtalk(
  362. webhook_url=url,
  363. report_data=report_data,
  364. report_type=report_type,
  365. update_info=update_info,
  366. proxy_url=proxy_url,
  367. mode=mode,
  368. account_label=account_label,
  369. batch_size=self.config.get("DINGTALK_BATCH_SIZE", 20000),
  370. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  371. split_content_func=self.split_content_func,
  372. rss_items=rss_items if display_regions.get("RSS", True) else None,
  373. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  374. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  375. display_regions=display_regions,
  376. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  377. ),
  378. )
  379. def _send_wework(
  380. self,
  381. report_data: Dict,
  382. report_type: str,
  383. update_info: Optional[Dict],
  384. proxy_url: Optional[str],
  385. mode: str,
  386. rss_items: Optional[List[Dict]] = None,
  387. rss_new_items: Optional[List[Dict]] = None,
  388. ai_analysis: Optional[AIAnalysisResult] = None,
  389. display_regions: Optional[Dict] = None,
  390. standalone_data: Optional[Dict] = None,
  391. ) -> bool:
  392. """发送到企业微信(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  393. display_regions = display_regions or {}
  394. if not display_regions.get("HOTLIST", True):
  395. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  396. return self._send_to_multi_accounts(
  397. channel_name="企业微信",
  398. config_value=self.config["WEWORK_WEBHOOK_URL"],
  399. send_func=lambda url, account_label: send_to_wework(
  400. webhook_url=url,
  401. report_data=report_data,
  402. report_type=report_type,
  403. update_info=update_info,
  404. proxy_url=proxy_url,
  405. mode=mode,
  406. account_label=account_label,
  407. batch_size=self.config.get("MESSAGE_BATCH_SIZE", 4000),
  408. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  409. msg_type=self.config.get("WEWORK_MSG_TYPE", "markdown"),
  410. split_content_func=self.split_content_func,
  411. rss_items=rss_items if display_regions.get("RSS", True) else None,
  412. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  413. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  414. display_regions=display_regions,
  415. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  416. ),
  417. )
  418. def _send_telegram(
  419. self,
  420. report_data: Dict,
  421. report_type: str,
  422. update_info: Optional[Dict],
  423. proxy_url: Optional[str],
  424. mode: str,
  425. rss_items: Optional[List[Dict]] = None,
  426. rss_new_items: Optional[List[Dict]] = None,
  427. ai_analysis: Optional[AIAnalysisResult] = None,
  428. display_regions: Optional[Dict] = None,
  429. standalone_data: Optional[Dict] = None,
  430. ) -> bool:
  431. """发送到 Telegram(多账号,需验证 token 和 chat_id 配对,支持热榜+RSS合并+AI分析+独立展示区)"""
  432. display_regions = display_regions or {}
  433. if not display_regions.get("HOTLIST", True):
  434. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  435. telegram_tokens = parse_multi_account_config(self.config["TELEGRAM_BOT_TOKEN"])
  436. telegram_chat_ids = parse_multi_account_config(self.config["TELEGRAM_CHAT_ID"])
  437. if not telegram_tokens or not telegram_chat_ids:
  438. return False
  439. valid, count = validate_paired_configs(
  440. {"bot_token": telegram_tokens, "chat_id": telegram_chat_ids},
  441. "Telegram",
  442. required_keys=["bot_token", "chat_id"],
  443. )
  444. if not valid or count == 0:
  445. return False
  446. telegram_tokens = limit_accounts(telegram_tokens, self.max_accounts, "Telegram")
  447. telegram_chat_ids = telegram_chat_ids[: len(telegram_tokens)]
  448. results = []
  449. for i in range(len(telegram_tokens)):
  450. token = telegram_tokens[i]
  451. chat_id = telegram_chat_ids[i]
  452. if token and chat_id:
  453. account_label = f"账号{i+1}" if len(telegram_tokens) > 1 else ""
  454. result = send_to_telegram(
  455. bot_token=token,
  456. chat_id=chat_id,
  457. report_data=report_data,
  458. report_type=report_type,
  459. update_info=update_info,
  460. proxy_url=proxy_url,
  461. mode=mode,
  462. account_label=account_label,
  463. batch_size=self.config.get("MESSAGE_BATCH_SIZE", 4000),
  464. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  465. split_content_func=self.split_content_func,
  466. rss_items=rss_items if display_regions.get("RSS", True) else None,
  467. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  468. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  469. display_regions=display_regions,
  470. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  471. )
  472. results.append(result)
  473. return any(results) if results else False
  474. def _send_ntfy(
  475. self,
  476. report_data: Dict,
  477. report_type: str,
  478. update_info: Optional[Dict],
  479. proxy_url: Optional[str],
  480. mode: str,
  481. rss_items: Optional[List[Dict]] = None,
  482. rss_new_items: Optional[List[Dict]] = None,
  483. ai_analysis: Optional[AIAnalysisResult] = None,
  484. display_regions: Optional[Dict] = None,
  485. standalone_data: Optional[Dict] = None,
  486. ) -> bool:
  487. """发送到 ntfy(多账号,需验证 topic 和 token 配对,支持热榜+RSS合并+AI分析+独立展示区)"""
  488. display_regions = display_regions or {}
  489. if not display_regions.get("HOTLIST", True):
  490. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  491. ntfy_server_url = self.config["NTFY_SERVER_URL"]
  492. ntfy_topics = parse_multi_account_config(self.config["NTFY_TOPIC"])
  493. ntfy_tokens = parse_multi_account_config(self.config.get("NTFY_TOKEN", ""))
  494. if not ntfy_server_url or not ntfy_topics:
  495. return False
  496. if ntfy_tokens and len(ntfy_tokens) != len(ntfy_topics):
  497. print(
  498. f"❌ ntfy 配置错误:topic 数量({len(ntfy_topics)})与 token 数量({len(ntfy_tokens)})不一致,跳过 ntfy 推送"
  499. )
  500. return False
  501. ntfy_topics = limit_accounts(ntfy_topics, self.max_accounts, "ntfy")
  502. if ntfy_tokens:
  503. ntfy_tokens = ntfy_tokens[: len(ntfy_topics)]
  504. results = []
  505. for i, topic in enumerate(ntfy_topics):
  506. if topic:
  507. token = get_account_at_index(ntfy_tokens, i, "") if ntfy_tokens else ""
  508. account_label = f"账号{i+1}" if len(ntfy_topics) > 1 else ""
  509. result = send_to_ntfy(
  510. server_url=ntfy_server_url,
  511. topic=topic,
  512. token=token,
  513. report_data=report_data,
  514. report_type=report_type,
  515. update_info=update_info,
  516. proxy_url=proxy_url,
  517. mode=mode,
  518. account_label=account_label,
  519. batch_size=3800,
  520. split_content_func=self.split_content_func,
  521. rss_items=rss_items if display_regions.get("RSS", True) else None,
  522. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  523. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  524. display_regions=display_regions,
  525. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  526. )
  527. results.append(result)
  528. return any(results) if results else False
  529. def _send_bark(
  530. self,
  531. report_data: Dict,
  532. report_type: str,
  533. update_info: Optional[Dict],
  534. proxy_url: Optional[str],
  535. mode: str,
  536. rss_items: Optional[List[Dict]] = None,
  537. rss_new_items: Optional[List[Dict]] = None,
  538. ai_analysis: Optional[AIAnalysisResult] = None,
  539. display_regions: Optional[Dict] = None,
  540. standalone_data: Optional[Dict] = None,
  541. ) -> bool:
  542. """发送到 Bark(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  543. display_regions = display_regions or {}
  544. if not display_regions.get("HOTLIST", True):
  545. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  546. return self._send_to_multi_accounts(
  547. channel_name="Bark",
  548. config_value=self.config["BARK_URL"],
  549. send_func=lambda url, account_label: send_to_bark(
  550. bark_url=url,
  551. report_data=report_data,
  552. report_type=report_type,
  553. update_info=update_info,
  554. proxy_url=proxy_url,
  555. mode=mode,
  556. account_label=account_label,
  557. batch_size=self.config.get("BARK_BATCH_SIZE", 3600),
  558. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  559. split_content_func=self.split_content_func,
  560. rss_items=rss_items if display_regions.get("RSS", True) else None,
  561. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  562. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  563. display_regions=display_regions,
  564. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  565. ),
  566. )
  567. def _send_slack(
  568. self,
  569. report_data: Dict,
  570. report_type: str,
  571. update_info: Optional[Dict],
  572. proxy_url: Optional[str],
  573. mode: str,
  574. rss_items: Optional[List[Dict]] = None,
  575. rss_new_items: Optional[List[Dict]] = None,
  576. ai_analysis: Optional[AIAnalysisResult] = None,
  577. display_regions: Optional[Dict] = None,
  578. standalone_data: Optional[Dict] = None,
  579. ) -> bool:
  580. """发送到 Slack(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  581. display_regions = display_regions or {}
  582. if not display_regions.get("HOTLIST", True):
  583. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  584. return self._send_to_multi_accounts(
  585. channel_name="Slack",
  586. config_value=self.config["SLACK_WEBHOOK_URL"],
  587. send_func=lambda url, account_label: send_to_slack(
  588. webhook_url=url,
  589. report_data=report_data,
  590. report_type=report_type,
  591. update_info=update_info,
  592. proxy_url=proxy_url,
  593. mode=mode,
  594. account_label=account_label,
  595. batch_size=self.config.get("SLACK_BATCH_SIZE", 4000),
  596. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  597. split_content_func=self.split_content_func,
  598. rss_items=rss_items if display_regions.get("RSS", True) else None,
  599. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  600. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  601. display_regions=display_regions,
  602. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  603. ),
  604. )
  605. def _send_generic_webhook(
  606. self,
  607. report_data: Dict,
  608. report_type: str,
  609. update_info: Optional[Dict],
  610. proxy_url: Optional[str],
  611. mode: str,
  612. rss_items: Optional[List[Dict]] = None,
  613. rss_new_items: Optional[List[Dict]] = None,
  614. ai_analysis: Optional[AIAnalysisResult] = None,
  615. display_regions: Optional[Dict] = None,
  616. standalone_data: Optional[Dict] = None,
  617. ) -> bool:
  618. """发送到通用 Webhook(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
  619. display_regions = display_regions or {}
  620. if not display_regions.get("HOTLIST", True):
  621. report_data = {"stats": [], "failed_ids": [], "new_titles": [], "id_to_name": {}}
  622. urls = parse_multi_account_config(self.config.get("GENERIC_WEBHOOK_URL", ""))
  623. templates = parse_multi_account_config(self.config.get("GENERIC_WEBHOOK_TEMPLATE", ""))
  624. if not urls:
  625. return False
  626. urls = limit_accounts(urls, self.max_accounts, "通用Webhook")
  627. results = []
  628. for i, url in enumerate(urls):
  629. if not url:
  630. continue
  631. template = ""
  632. if templates:
  633. if i < len(templates):
  634. template = templates[i]
  635. elif len(templates) == 1:
  636. template = templates[0]
  637. account_label = f"账号{i+1}" if len(urls) > 1 else ""
  638. result = send_to_generic_webhook(
  639. webhook_url=url,
  640. payload_template=template,
  641. report_data=report_data,
  642. report_type=report_type,
  643. update_info=update_info,
  644. proxy_url=proxy_url,
  645. mode=mode,
  646. account_label=account_label,
  647. batch_size=self.config.get("MESSAGE_BATCH_SIZE", 4000),
  648. batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
  649. split_content_func=self.split_content_func,
  650. rss_items=rss_items if display_regions.get("RSS", True) else None,
  651. rss_new_items=rss_new_items if (display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True)) else None,
  652. ai_analysis=ai_analysis if display_regions.get("AI_ANALYSIS", True) else None,
  653. display_regions=display_regions,
  654. standalone_data=standalone_data if display_regions.get("STANDALONE", False) else None,
  655. )
  656. results.append(result)
  657. return any(results) if results else False
  658. def _send_email(
  659. self,
  660. report_type: str,
  661. html_file_path: Optional[str],
  662. ) -> bool:
  663. """发送邮件(保持原有逻辑,已支持多收件人)
  664. Note:
  665. AI 分析内容已在 HTML 生成时嵌入,无需在此传递
  666. """
  667. return send_to_email(
  668. from_email=self.config["EMAIL_FROM"],
  669. password=self.config["EMAIL_PASSWORD"],
  670. to_email=self.config["EMAIL_TO"],
  671. report_type=report_type,
  672. html_file_path=html_file_path,
  673. custom_smtp_server=self.config.get("EMAIL_SMTP_SERVER", ""),
  674. custom_smtp_port=self.config.get("EMAIL_SMTP_PORT", ""),
  675. get_time_func=self.get_time_func,
  676. )
  677. # === RSS 通知方法 ===
  678. def dispatch_rss(
  679. self,
  680. rss_items: List[Dict],
  681. feeds_info: Optional[Dict[str, str]] = None,
  682. proxy_url: Optional[str] = None,
  683. html_file_path: Optional[str] = None,
  684. ) -> Dict[str, bool]:
  685. """
  686. 分发 RSS 通知到所有已配置的渠道
  687. Args:
  688. rss_items: RSS 条目列表,每个条目包含:
  689. - title: 标题
  690. - feed_id: RSS 源 ID
  691. - feed_name: RSS 源名称
  692. - url: 链接
  693. - published_at: 发布时间
  694. - summary: 摘要(可选)
  695. - author: 作者(可选)
  696. feeds_info: RSS 源 ID 到名称的映射
  697. proxy_url: 代理 URL(可选)
  698. html_file_path: HTML 报告文件路径(邮件使用)
  699. Returns:
  700. Dict[str, bool]: 每个渠道的发送结果
  701. """
  702. if not rss_items:
  703. print("[RSS通知] 没有 RSS 内容,跳过通知")
  704. return {}
  705. results = {}
  706. report_type = "RSS 订阅更新"
  707. # 飞书
  708. if self.config.get("FEISHU_WEBHOOK_URL"):
  709. results["feishu"] = self._send_rss_feishu(
  710. rss_items, feeds_info, proxy_url
  711. )
  712. # 钉钉
  713. if self.config.get("DINGTALK_WEBHOOK_URL"):
  714. results["dingtalk"] = self._send_rss_dingtalk(
  715. rss_items, feeds_info, proxy_url
  716. )
  717. # 企业微信
  718. if self.config.get("WEWORK_WEBHOOK_URL"):
  719. results["wework"] = self._send_rss_markdown(
  720. rss_items, feeds_info, proxy_url, "wework"
  721. )
  722. # Telegram
  723. if self.config.get("TELEGRAM_BOT_TOKEN") and self.config.get("TELEGRAM_CHAT_ID"):
  724. results["telegram"] = self._send_rss_markdown(
  725. rss_items, feeds_info, proxy_url, "telegram"
  726. )
  727. # ntfy
  728. if self.config.get("NTFY_SERVER_URL") and self.config.get("NTFY_TOPIC"):
  729. results["ntfy"] = self._send_rss_markdown(
  730. rss_items, feeds_info, proxy_url, "ntfy"
  731. )
  732. # Bark
  733. if self.config.get("BARK_URL"):
  734. results["bark"] = self._send_rss_markdown(
  735. rss_items, feeds_info, proxy_url, "bark"
  736. )
  737. # Slack
  738. if self.config.get("SLACK_WEBHOOK_URL"):
  739. results["slack"] = self._send_rss_markdown(
  740. rss_items, feeds_info, proxy_url, "slack"
  741. )
  742. # 邮件
  743. if (
  744. self.config.get("EMAIL_FROM")
  745. and self.config.get("EMAIL_PASSWORD")
  746. and self.config.get("EMAIL_TO")
  747. ):
  748. results["email"] = self._send_email(report_type, html_file_path)
  749. return results
  750. def _send_rss_feishu(
  751. self,
  752. rss_items: List[Dict],
  753. feeds_info: Optional[Dict[str, str]],
  754. proxy_url: Optional[str],
  755. ) -> bool:
  756. """发送 RSS 到飞书"""
  757. import requests
  758. content = render_rss_feishu_content(
  759. rss_items=rss_items,
  760. feeds_info=feeds_info,
  761. get_time_func=self.get_time_func,
  762. )
  763. webhooks = parse_multi_account_config(self.config["FEISHU_WEBHOOK_URL"])
  764. webhooks = limit_accounts(webhooks, self.max_accounts, "飞书")
  765. results = []
  766. for i, webhook_url in enumerate(webhooks):
  767. if not webhook_url:
  768. continue
  769. account_label = f"账号{i+1}" if len(webhooks) > 1 else ""
  770. try:
  771. # 分批发送
  772. batches = self.split_content_func(
  773. content, self.config.get("FEISHU_BATCH_SIZE", 29000)
  774. )
  775. for batch_idx, batch_content in enumerate(batches):
  776. payload = {
  777. "msg_type": "interactive",
  778. "card": {
  779. "header": {
  780. "title": {
  781. "tag": "plain_text",
  782. "content": f"📰 RSS 订阅更新 {f'({batch_idx + 1}/{len(batches)})' if len(batches) > 1 else ''}",
  783. },
  784. "template": "green",
  785. },
  786. "elements": [
  787. {"tag": "markdown", "content": batch_content}
  788. ],
  789. },
  790. }
  791. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  792. resp = requests.post(webhook_url, json=payload, proxies=proxies, timeout=30)
  793. resp.raise_for_status()
  794. print(f"✅ 飞书{account_label} RSS 通知发送成功")
  795. results.append(True)
  796. except Exception as e:
  797. print(f"❌ 飞书{account_label} RSS 通知发送失败: {e}")
  798. results.append(False)
  799. return any(results) if results else False
  800. def _send_rss_dingtalk(
  801. self,
  802. rss_items: List[Dict],
  803. feeds_info: Optional[Dict[str, str]],
  804. proxy_url: Optional[str],
  805. ) -> bool:
  806. """发送 RSS 到钉钉"""
  807. import requests
  808. content = render_rss_dingtalk_content(
  809. rss_items=rss_items,
  810. feeds_info=feeds_info,
  811. get_time_func=self.get_time_func,
  812. )
  813. webhooks = parse_multi_account_config(self.config["DINGTALK_WEBHOOK_URL"])
  814. webhooks = limit_accounts(webhooks, self.max_accounts, "钉钉")
  815. results = []
  816. for i, webhook_url in enumerate(webhooks):
  817. if not webhook_url:
  818. continue
  819. account_label = f"账号{i+1}" if len(webhooks) > 1 else ""
  820. try:
  821. batches = self.split_content_func(
  822. content, self.config.get("DINGTALK_BATCH_SIZE", 20000)
  823. )
  824. for batch_idx, batch_content in enumerate(batches):
  825. title = f"📰 RSS 订阅更新 {f'({batch_idx + 1}/{len(batches)})' if len(batches) > 1 else ''}"
  826. payload = {
  827. "msgtype": "markdown",
  828. "markdown": {
  829. "title": title,
  830. "text": batch_content,
  831. },
  832. }
  833. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  834. resp = requests.post(webhook_url, json=payload, proxies=proxies, timeout=30)
  835. resp.raise_for_status()
  836. print(f"✅ 钉钉{account_label} RSS 通知发送成功")
  837. results.append(True)
  838. except Exception as e:
  839. print(f"❌ 钉钉{account_label} RSS 通知发送失败: {e}")
  840. results.append(False)
  841. return any(results) if results else False
  842. def _send_rss_markdown(
  843. self,
  844. rss_items: List[Dict],
  845. feeds_info: Optional[Dict[str, str]],
  846. proxy_url: Optional[str],
  847. channel: str,
  848. ) -> bool:
  849. """发送 RSS 到 Markdown 兼容渠道(企业微信、Telegram、ntfy、Bark、Slack)"""
  850. content = render_rss_markdown_content(
  851. rss_items=rss_items,
  852. feeds_info=feeds_info,
  853. get_time_func=self.get_time_func,
  854. )
  855. try:
  856. if channel == "wework":
  857. return self._send_rss_wework(content, proxy_url)
  858. elif channel == "telegram":
  859. return self._send_rss_telegram(content, proxy_url)
  860. elif channel == "ntfy":
  861. return self._send_rss_ntfy(content, proxy_url)
  862. elif channel == "bark":
  863. return self._send_rss_bark(content, proxy_url)
  864. elif channel == "slack":
  865. return self._send_rss_slack(content, proxy_url)
  866. except Exception as e:
  867. print(f"❌ {channel} RSS 通知发送失败: {e}")
  868. return False
  869. return False
  870. def _send_rss_wework(self, content: str, proxy_url: Optional[str]) -> bool:
  871. """发送 RSS 到企业微信"""
  872. import requests
  873. webhooks = parse_multi_account_config(self.config["WEWORK_WEBHOOK_URL"])
  874. webhooks = limit_accounts(webhooks, self.max_accounts, "企业微信")
  875. results = []
  876. for i, webhook_url in enumerate(webhooks):
  877. if not webhook_url:
  878. continue
  879. account_label = f"账号{i+1}" if len(webhooks) > 1 else ""
  880. try:
  881. batches = self.split_content_func(
  882. content, self.config.get("MESSAGE_BATCH_SIZE", 4000)
  883. )
  884. for batch_content in batches:
  885. payload = {
  886. "msgtype": "markdown",
  887. "markdown": {"content": batch_content},
  888. }
  889. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  890. resp = requests.post(webhook_url, json=payload, proxies=proxies, timeout=30)
  891. resp.raise_for_status()
  892. print(f"✅ 企业微信{account_label} RSS 通知发送成功")
  893. results.append(True)
  894. except Exception as e:
  895. print(f"❌ 企业微信{account_label} RSS 通知发送失败: {e}")
  896. results.append(False)
  897. return any(results) if results else False
  898. def _send_rss_telegram(self, content: str, proxy_url: Optional[str]) -> bool:
  899. """发送 RSS 到 Telegram"""
  900. import requests
  901. tokens = parse_multi_account_config(self.config["TELEGRAM_BOT_TOKEN"])
  902. chat_ids = parse_multi_account_config(self.config["TELEGRAM_CHAT_ID"])
  903. if not tokens or not chat_ids:
  904. return False
  905. results = []
  906. for i in range(min(len(tokens), len(chat_ids), self.max_accounts)):
  907. token = tokens[i]
  908. chat_id = chat_ids[i]
  909. if not token or not chat_id:
  910. continue
  911. account_label = f"账号{i+1}" if len(tokens) > 1 else ""
  912. try:
  913. batches = self.split_content_func(
  914. content, self.config.get("MESSAGE_BATCH_SIZE", 4000)
  915. )
  916. for batch_content in batches:
  917. url = f"https://api.telegram.org/bot{token}/sendMessage"
  918. payload = {
  919. "chat_id": chat_id,
  920. "text": batch_content,
  921. "parse_mode": "Markdown",
  922. }
  923. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  924. resp = requests.post(url, json=payload, proxies=proxies, timeout=30)
  925. resp.raise_for_status()
  926. print(f"✅ Telegram{account_label} RSS 通知发送成功")
  927. results.append(True)
  928. except Exception as e:
  929. print(f"❌ Telegram{account_label} RSS 通知发送失败: {e}")
  930. results.append(False)
  931. return any(results) if results else False
  932. def _send_rss_ntfy(self, content: str, proxy_url: Optional[str]) -> bool:
  933. """发送 RSS 到 ntfy"""
  934. import requests
  935. server_url = self.config["NTFY_SERVER_URL"]
  936. topics = parse_multi_account_config(self.config["NTFY_TOPIC"])
  937. tokens = parse_multi_account_config(self.config.get("NTFY_TOKEN", ""))
  938. if not server_url or not topics:
  939. return False
  940. topics = limit_accounts(topics, self.max_accounts, "ntfy")
  941. results = []
  942. for i, topic in enumerate(topics):
  943. if not topic:
  944. continue
  945. token = tokens[i] if tokens and i < len(tokens) else ""
  946. account_label = f"账号{i+1}" if len(topics) > 1 else ""
  947. try:
  948. batches = self.split_content_func(content, 3800)
  949. for batch_content in batches:
  950. url = f"{server_url.rstrip('/')}/{topic}"
  951. headers = {"Title": "RSS 订阅更新", "Markdown": "yes"}
  952. if token:
  953. headers["Authorization"] = f"Bearer {token}"
  954. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  955. resp = requests.post(
  956. url, data=batch_content.encode("utf-8"),
  957. headers=headers, proxies=proxies, timeout=30
  958. )
  959. resp.raise_for_status()
  960. print(f"✅ ntfy{account_label} RSS 通知发送成功")
  961. results.append(True)
  962. except Exception as e:
  963. print(f"❌ ntfy{account_label} RSS 通知发送失败: {e}")
  964. results.append(False)
  965. return any(results) if results else False
  966. def _send_rss_bark(self, content: str, proxy_url: Optional[str]) -> bool:
  967. """发送 RSS 到 Bark"""
  968. import requests
  969. import urllib.parse
  970. urls = parse_multi_account_config(self.config["BARK_URL"])
  971. urls = limit_accounts(urls, self.max_accounts, "Bark")
  972. results = []
  973. for i, bark_url in enumerate(urls):
  974. if not bark_url:
  975. continue
  976. account_label = f"账号{i+1}" if len(urls) > 1 else ""
  977. try:
  978. batches = self.split_content_func(
  979. content, self.config.get("BARK_BATCH_SIZE", 3600)
  980. )
  981. for batch_content in batches:
  982. title = urllib.parse.quote("📰 RSS 订阅更新")
  983. body = urllib.parse.quote(batch_content)
  984. url = f"{bark_url.rstrip('/')}/{title}/{body}"
  985. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  986. resp = requests.get(url, proxies=proxies, timeout=30)
  987. resp.raise_for_status()
  988. print(f"✅ Bark{account_label} RSS 通知发送成功")
  989. results.append(True)
  990. except Exception as e:
  991. print(f"❌ Bark{account_label} RSS 通知发送失败: {e}")
  992. results.append(False)
  993. return any(results) if results else False
  994. def _send_rss_slack(self, content: str, proxy_url: Optional[str]) -> bool:
  995. """发送 RSS 到 Slack"""
  996. import requests
  997. webhooks = parse_multi_account_config(self.config["SLACK_WEBHOOK_URL"])
  998. webhooks = limit_accounts(webhooks, self.max_accounts, "Slack")
  999. results = []
  1000. for i, webhook_url in enumerate(webhooks):
  1001. if not webhook_url:
  1002. continue
  1003. account_label = f"账号{i+1}" if len(webhooks) > 1 else ""
  1004. try:
  1005. batches = self.split_content_func(
  1006. content, self.config.get("SLACK_BATCH_SIZE", 4000)
  1007. )
  1008. for batch_content in batches:
  1009. payload = {
  1010. "blocks": [
  1011. {
  1012. "type": "section",
  1013. "text": {
  1014. "type": "mrkdwn",
  1015. "text": batch_content,
  1016. },
  1017. }
  1018. ]
  1019. }
  1020. proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
  1021. resp = requests.post(webhook_url, json=payload, proxies=proxies, timeout=30)
  1022. resp.raise_for_status()
  1023. print(f"✅ Slack{account_label} RSS 通知发送成功")
  1024. results.append(True)
  1025. except Exception as e:
  1026. print(f"❌ Slack{account_label} RSS 通知发送失败: {e}")
  1027. results.append(False)
  1028. return any(results) if results else False