splitter.py 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652
  1. # coding=utf-8
  2. """
  3. 消息分批处理模块
  4. 提供消息内容分批拆分功能,确保消息大小不超过各平台限制
  5. """
  6. from datetime import datetime
  7. from typing import Dict, List, Optional, Callable
  8. from trendradar.report.formatter import format_title_for_platform
  9. from trendradar.report.helpers import format_rank_display
  10. from trendradar.utils.time import format_iso_time_friendly, convert_time_for_display
  11. # 默认批次大小配置
  12. DEFAULT_BATCH_SIZES = {
  13. "dingtalk": 20000,
  14. "feishu": 29000,
  15. "ntfy": 3800,
  16. "default": 4000,
  17. }
  18. # 默认区域顺序
  19. DEFAULT_REGION_ORDER = ["hotlist", "rss", "new_items", "standalone", "ai_analysis"]
  20. def split_content_into_batches(
  21. report_data: Dict,
  22. format_type: str,
  23. update_info: Optional[Dict] = None,
  24. max_bytes: Optional[int] = None,
  25. mode: str = "daily",
  26. batch_sizes: Optional[Dict[str, int]] = None,
  27. feishu_separator: str = "---",
  28. region_order: Optional[List[str]] = None,
  29. get_time_func: Optional[Callable[[], datetime]] = None,
  30. rss_items: Optional[list] = None,
  31. rss_new_items: Optional[list] = None,
  32. timezone: str = "Asia/Shanghai",
  33. display_mode: str = "keyword",
  34. ai_content: Optional[str] = None,
  35. standalone_data: Optional[Dict] = None,
  36. rank_threshold: int = 10,
  37. ai_stats: Optional[Dict] = None,
  38. report_type: str = "热点分析报告",
  39. show_new_section: bool = True,
  40. ) -> List[str]:
  41. """分批处理消息内容,确保词组标题+至少第一条新闻的完整性(支持热榜+RSS合并+AI分析+独立展示区)
  42. 热榜统计与RSS统计并列显示,热榜新增与RSS新增并列显示。
  43. region_order 控制各区域的显示顺序。
  44. AI分析内容根据 region_order 中的位置显示。
  45. 独立展示区根据 region_order 中的位置显示。
  46. Args:
  47. report_data: 报告数据字典,包含 stats, new_titles, failed_ids, total_new_count
  48. format_type: 格式类型 (feishu, dingtalk, wework, telegram, ntfy, bark, slack)
  49. update_info: 版本更新信息(可选)
  50. max_bytes: 最大字节数(可选,如果不指定则使用默认配置)
  51. mode: 报告模式 (daily, incremental, current)
  52. batch_sizes: 批次大小配置字典(可选)
  53. feishu_separator: 飞书消息分隔符
  54. region_order: 区域显示顺序列表
  55. get_time_func: 获取当前时间的函数(可选)
  56. rss_items: RSS 统计条目列表(按源分组,用于合并推送)
  57. rss_new_items: RSS 新增条目列表(可选,用于新增区块)
  58. timezone: 时区名称(用于 RSS 时间格式化)
  59. display_mode: 显示模式 (keyword=按关键词分组, platform=按平台分组)
  60. ai_content: AI 分析内容(已渲染的字符串,可选)
  61. standalone_data: 独立展示区数据(可选),包含 platforms 和 rss_feeds 列表
  62. ai_stats: AI 分析统计数据(可选),包含 total_news, analyzed_news, max_news_limit 等
  63. Returns:
  64. 分批后的消息内容列表
  65. """
  66. if region_order is None:
  67. region_order = DEFAULT_REGION_ORDER
  68. # 合并批次大小配置
  69. sizes = {**DEFAULT_BATCH_SIZES, **(batch_sizes or {})}
  70. if max_bytes is None:
  71. if format_type == "dingtalk":
  72. max_bytes = sizes.get("dingtalk", 20000)
  73. elif format_type == "feishu":
  74. max_bytes = sizes.get("feishu", 29000)
  75. elif format_type == "ntfy":
  76. max_bytes = sizes.get("ntfy", 3800)
  77. else:
  78. max_bytes = sizes.get("default", 4000)
  79. batches = []
  80. total_hotlist_count = sum(
  81. len(stat["titles"]) for stat in report_data["stats"] if stat["count"] > 0
  82. )
  83. total_titles = total_hotlist_count
  84. # 累加 RSS 条目数
  85. if rss_items:
  86. total_titles += sum(stat.get("count", 0) for stat in rss_items)
  87. now = get_time_func() if get_time_func else datetime.now()
  88. # 构建头部信息
  89. base_header = ""
  90. # 准备 AI 分析统计行(如果存在)
  91. ai_stats_line = ""
  92. if ai_stats and ai_stats.get("analyzed_news", 0) > 0:
  93. analyzed_news = ai_stats.get("analyzed_news", 0)
  94. if format_type in ("wework", "bark", "ntfy", "feishu", "dingtalk"):
  95. ai_stats_line = f"**AI 分析数:** {analyzed_news}\n"
  96. elif format_type == "slack":
  97. ai_stats_line = f"*AI 分析数:* {analyzed_news}\n"
  98. elif format_type == "telegram":
  99. ai_stats_line = f"AI 分析数: {analyzed_news}\n"
  100. # 构建统一的头部(总是显示总新闻数、时间和类型)
  101. if format_type in ("wework", "bark"):
  102. base_header = f"**总新闻数:** {total_titles}\n"
  103. base_header += ai_stats_line
  104. base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  105. base_header += f"**类型:** {report_type}\n\n"
  106. elif format_type == "telegram":
  107. base_header = f"总新闻数: {total_titles}\n"
  108. base_header += ai_stats_line
  109. base_header += f"时间: {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  110. base_header += f"类型: {report_type}\n\n"
  111. elif format_type == "ntfy":
  112. base_header = f"**总新闻数:** {total_titles}\n"
  113. base_header += ai_stats_line
  114. base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  115. base_header += f"**类型:** {report_type}\n\n"
  116. elif format_type == "feishu":
  117. base_header = f"**总新闻数:** {total_titles}\n"
  118. base_header += ai_stats_line
  119. base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  120. base_header += f"**类型:** {report_type}\n\n"
  121. base_header += "---\n\n"
  122. elif format_type == "dingtalk":
  123. base_header = f"**总新闻数:** {total_titles}\n"
  124. base_header += ai_stats_line
  125. base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  126. base_header += f"**类型:** {report_type}\n\n"
  127. base_header += "---\n\n"
  128. elif format_type == "slack":
  129. base_header = f"*总新闻数:* {total_titles}\n"
  130. base_header += ai_stats_line
  131. base_header += f"*时间:* {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
  132. base_header += f"*类型:* {report_type}\n\n"
  133. base_footer = ""
  134. if format_type in ("wework", "bark"):
  135. base_footer = f"\n\n\n> 更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
  136. if update_info:
  137. base_footer += f"\n> TrendRadar 发现新版本 **{update_info['remote_version']}**,当前 **{update_info['current_version']}**"
  138. elif format_type == "telegram":
  139. base_footer = f"\n\n更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
  140. if update_info:
  141. base_footer += f"\nTrendRadar 发现新版本 {update_info['remote_version']},当前 {update_info['current_version']}"
  142. elif format_type == "ntfy":
  143. base_footer = f"\n\n> 更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
  144. if update_info:
  145. base_footer += f"\n> TrendRadar 发现新版本 **{update_info['remote_version']}**,当前 **{update_info['current_version']}**"
  146. elif format_type == "feishu":
  147. base_footer = f"\n\n<font color='grey'>更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}</font>"
  148. if update_info:
  149. base_footer += f"\n<font color='grey'>TrendRadar 发现新版本 {update_info['remote_version']},当前 {update_info['current_version']}</font>"
  150. elif format_type == "dingtalk":
  151. base_footer = f"\n\n> 更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
  152. if update_info:
  153. base_footer += f"\n> TrendRadar 发现新版本 **{update_info['remote_version']}**,当前 **{update_info['current_version']}**"
  154. elif format_type == "slack":
  155. base_footer = f"\n\n_更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}_"
  156. if update_info:
  157. base_footer += f"\n_TrendRadar 发现新版本 *{update_info['remote_version']}*,当前 *{update_info['current_version']}_"
  158. # 根据 display_mode 选择统计标题
  159. stats_title = "热点词汇统计" if display_mode == "keyword" else "热点新闻统计"
  160. stats_header = ""
  161. if report_data["stats"]:
  162. if format_type in ("wework", "bark"):
  163. stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
  164. elif format_type == "telegram":
  165. stats_header = f"📊 {stats_title} (共 {total_hotlist_count} 条)\n\n"
  166. elif format_type == "ntfy":
  167. stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
  168. elif format_type == "feishu":
  169. stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
  170. elif format_type == "dingtalk":
  171. stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
  172. elif format_type == "slack":
  173. stats_header = f"📊 *{stats_title}* (共 {total_hotlist_count} 条)\n\n"
  174. current_batch = base_header
  175. current_batch_has_content = False
  176. # 当没有热榜数据时的处理
  177. # 注意:如果有 ai_content,不应该返回"暂无匹配"消息,而应该继续处理 AI 内容
  178. if (
  179. not report_data["stats"]
  180. and not report_data["new_titles"]
  181. and not report_data["failed_ids"]
  182. and not ai_content # 有 AI 内容时不返回"暂无匹配"
  183. and not rss_items # 有 RSS 内容时也不返回
  184. and not standalone_data # 有独立展示区数据时也不返回
  185. ):
  186. if mode == "incremental":
  187. mode_text = "增量模式下暂无新增匹配的热点词汇"
  188. elif mode == "current":
  189. mode_text = "当前榜单模式下暂无匹配的热点词汇"
  190. else:
  191. mode_text = "暂无匹配的热点词汇"
  192. simple_content = f"📭 {mode_text}\n\n"
  193. final_content = base_header + simple_content + base_footer
  194. batches.append(final_content)
  195. return batches
  196. # 定义处理热点词汇统计的函数
  197. def process_stats_section(current_batch, current_batch_has_content, batches, add_separator=True):
  198. """处理热点词汇统计"""
  199. if not report_data["stats"]:
  200. return current_batch, current_batch_has_content, batches
  201. total_count = len(report_data["stats"])
  202. # 根据 add_separator 决定是否添加前置分割线
  203. actual_stats_header = ""
  204. if add_separator and current_batch_has_content:
  205. # 需要添加分割线
  206. if format_type == "feishu":
  207. actual_stats_header = f"\n{feishu_separator}\n\n{stats_header}"
  208. elif format_type == "dingtalk":
  209. actual_stats_header = f"\n---\n\n{stats_header}"
  210. elif format_type in ("wework", "bark"):
  211. actual_stats_header = f"\n\n\n\n{stats_header}"
  212. else:
  213. actual_stats_header = f"\n\n{stats_header}"
  214. else:
  215. # 不需要分割线(第一个区域)
  216. actual_stats_header = stats_header
  217. # 添加统计标题
  218. test_content = current_batch + actual_stats_header
  219. if (
  220. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  221. < max_bytes
  222. ):
  223. current_batch = test_content
  224. current_batch_has_content = True
  225. else:
  226. if current_batch_has_content:
  227. batches.append(current_batch + base_footer)
  228. # 新批次开头不需要分割线,使用原始 stats_header
  229. current_batch = base_header + stats_header
  230. current_batch_has_content = True
  231. # 逐个处理词组(确保词组标题+第一条新闻的原子性)
  232. for i, stat in enumerate(report_data["stats"]):
  233. word = stat["word"]
  234. count = stat["count"]
  235. sequence_display = f"[{i + 1}/{total_count}]"
  236. # 构建词组标题
  237. word_header = ""
  238. if format_type in ("wework", "bark"):
  239. if count >= 10:
  240. word_header = (
  241. f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  242. )
  243. elif count >= 5:
  244. word_header = (
  245. f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  246. )
  247. else:
  248. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  249. elif format_type == "telegram":
  250. if count >= 10:
  251. word_header = f"🔥 {sequence_display} {word} : {count} 条\n\n"
  252. elif count >= 5:
  253. word_header = f"📈 {sequence_display} {word} : {count} 条\n\n"
  254. else:
  255. word_header = f"📌 {sequence_display} {word} : {count} 条\n\n"
  256. elif format_type == "ntfy":
  257. if count >= 10:
  258. word_header = (
  259. f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  260. )
  261. elif count >= 5:
  262. word_header = (
  263. f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  264. )
  265. else:
  266. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  267. elif format_type == "feishu":
  268. if count >= 10:
  269. word_header = f"🔥 <font color='grey'>{sequence_display}</font> **{word}** : <font color='red'>{count}</font> 条\n\n"
  270. elif count >= 5:
  271. word_header = f"📈 <font color='grey'>{sequence_display}</font> **{word}** : <font color='orange'>{count}</font> 条\n\n"
  272. else:
  273. word_header = f"📌 <font color='grey'>{sequence_display}</font> **{word}** : {count} 条\n\n"
  274. elif format_type == "dingtalk":
  275. if count >= 10:
  276. word_header = (
  277. f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  278. )
  279. elif count >= 5:
  280. word_header = (
  281. f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  282. )
  283. else:
  284. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  285. elif format_type == "slack":
  286. if count >= 10:
  287. word_header = (
  288. f"🔥 {sequence_display} *{word}* : *{count}* 条\n\n"
  289. )
  290. elif count >= 5:
  291. word_header = (
  292. f"📈 {sequence_display} *{word}* : *{count}* 条\n\n"
  293. )
  294. else:
  295. word_header = f"📌 {sequence_display} *{word}* : {count} 条\n\n"
  296. # 构建第一条新闻
  297. # display_mode: keyword=显示来源, platform=显示关键词
  298. show_source = display_mode == "keyword"
  299. show_keyword = display_mode == "platform"
  300. first_news_line = ""
  301. if stat["titles"]:
  302. first_title_data = stat["titles"][0]
  303. if format_type in ("wework", "bark"):
  304. formatted_title = format_title_for_platform(
  305. "wework", first_title_data, show_source=show_source, show_keyword=show_keyword
  306. )
  307. elif format_type == "telegram":
  308. formatted_title = format_title_for_platform(
  309. "telegram", first_title_data, show_source=show_source, show_keyword=show_keyword
  310. )
  311. elif format_type == "ntfy":
  312. formatted_title = format_title_for_platform(
  313. "ntfy", first_title_data, show_source=show_source, show_keyword=show_keyword
  314. )
  315. elif format_type == "feishu":
  316. formatted_title = format_title_for_platform(
  317. "feishu", first_title_data, show_source=show_source, show_keyword=show_keyword
  318. )
  319. elif format_type == "dingtalk":
  320. formatted_title = format_title_for_platform(
  321. "dingtalk", first_title_data, show_source=show_source, show_keyword=show_keyword
  322. )
  323. elif format_type == "slack":
  324. formatted_title = format_title_for_platform(
  325. "slack", first_title_data, show_source=show_source, show_keyword=show_keyword
  326. )
  327. else:
  328. formatted_title = f"{first_title_data['title']}"
  329. first_news_line = f" 1. {formatted_title}\n"
  330. if len(stat["titles"]) > 1:
  331. first_news_line += "\n"
  332. # 原子性检查:词组标题+第一条新闻必须一起处理
  333. word_with_first_news = word_header + first_news_line
  334. test_content = current_batch + word_with_first_news
  335. if (
  336. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  337. >= max_bytes
  338. ):
  339. # 当前批次容纳不下,开启新批次
  340. if current_batch_has_content:
  341. batches.append(current_batch + base_footer)
  342. current_batch = base_header + stats_header + word_with_first_news
  343. current_batch_has_content = True
  344. start_index = 1
  345. else:
  346. current_batch = test_content
  347. current_batch_has_content = True
  348. start_index = 1
  349. # 处理剩余新闻条目
  350. for j in range(start_index, len(stat["titles"])):
  351. title_data = stat["titles"][j]
  352. if format_type in ("wework", "bark"):
  353. formatted_title = format_title_for_platform(
  354. "wework", title_data, show_source=show_source, show_keyword=show_keyword
  355. )
  356. elif format_type == "telegram":
  357. formatted_title = format_title_for_platform(
  358. "telegram", title_data, show_source=show_source, show_keyword=show_keyword
  359. )
  360. elif format_type == "ntfy":
  361. formatted_title = format_title_for_platform(
  362. "ntfy", title_data, show_source=show_source, show_keyword=show_keyword
  363. )
  364. elif format_type == "feishu":
  365. formatted_title = format_title_for_platform(
  366. "feishu", title_data, show_source=show_source, show_keyword=show_keyword
  367. )
  368. elif format_type == "dingtalk":
  369. formatted_title = format_title_for_platform(
  370. "dingtalk", title_data, show_source=show_source, show_keyword=show_keyword
  371. )
  372. elif format_type == "slack":
  373. formatted_title = format_title_for_platform(
  374. "slack", title_data, show_source=show_source, show_keyword=show_keyword
  375. )
  376. else:
  377. formatted_title = f"{title_data['title']}"
  378. news_line = f" {j + 1}. {formatted_title}\n"
  379. if j < len(stat["titles"]) - 1:
  380. news_line += "\n"
  381. test_content = current_batch + news_line
  382. if (
  383. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  384. >= max_bytes
  385. ):
  386. if current_batch_has_content:
  387. batches.append(current_batch + base_footer)
  388. current_batch = base_header + stats_header + word_header + news_line
  389. current_batch_has_content = True
  390. else:
  391. current_batch = test_content
  392. current_batch_has_content = True
  393. # 词组间分隔符
  394. if i < len(report_data["stats"]) - 1:
  395. separator = ""
  396. if format_type in ("wework", "bark"):
  397. separator = f"\n\n\n\n"
  398. elif format_type == "telegram":
  399. separator = f"\n\n"
  400. elif format_type == "ntfy":
  401. separator = f"\n\n"
  402. elif format_type == "feishu":
  403. separator = f"\n{feishu_separator}\n\n"
  404. elif format_type == "dingtalk":
  405. separator = f"\n---\n\n"
  406. elif format_type == "slack":
  407. separator = f"\n\n"
  408. test_content = current_batch + separator
  409. if (
  410. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  411. < max_bytes
  412. ):
  413. current_batch = test_content
  414. return current_batch, current_batch_has_content, batches
  415. # 定义处理新增新闻的函数
  416. def process_new_titles_section(current_batch, current_batch_has_content, batches, add_separator=True):
  417. """处理新增新闻"""
  418. if not show_new_section or not report_data["new_titles"]:
  419. return current_batch, current_batch_has_content, batches
  420. # 根据 add_separator 决定是否添加前置分割线
  421. new_header = ""
  422. if add_separator and current_batch_has_content:
  423. # 需要添加分割线
  424. if format_type in ("wework", "bark"):
  425. new_header = f"\n\n\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  426. elif format_type == "telegram":
  427. new_header = (
  428. f"\n\n🆕 本次新增热点新闻 (共 {report_data['total_new_count']} 条)\n\n"
  429. )
  430. elif format_type == "ntfy":
  431. new_header = f"\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  432. elif format_type == "feishu":
  433. new_header = f"\n{feishu_separator}\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  434. elif format_type == "dingtalk":
  435. new_header = f"\n---\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  436. elif format_type == "slack":
  437. new_header = f"\n\n🆕 *本次新增热点新闻* (共 {report_data['total_new_count']} 条)\n\n"
  438. else:
  439. # 不需要分割线(第一个区域)
  440. if format_type in ("wework", "bark"):
  441. new_header = f"🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  442. elif format_type == "telegram":
  443. new_header = f"🆕 本次新增热点新闻 (共 {report_data['total_new_count']} 条)\n\n"
  444. elif format_type == "ntfy":
  445. new_header = f"🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  446. elif format_type == "feishu":
  447. new_header = f"🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  448. elif format_type == "dingtalk":
  449. new_header = f"🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
  450. elif format_type == "slack":
  451. new_header = f"🆕 *本次新增热点新闻* (共 {report_data['total_new_count']} 条)\n\n"
  452. test_content = current_batch + new_header
  453. if (
  454. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  455. >= max_bytes
  456. ):
  457. if current_batch_has_content:
  458. batches.append(current_batch + base_footer)
  459. current_batch = base_header + new_header
  460. current_batch_has_content = True
  461. else:
  462. current_batch = test_content
  463. current_batch_has_content = True
  464. # 逐个处理新增新闻来源
  465. for source_data in report_data["new_titles"]:
  466. source_header = ""
  467. if format_type in ("wework", "bark"):
  468. source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
  469. elif format_type == "telegram":
  470. source_header = f"{source_data['source_name']} ({len(source_data['titles'])} 条):\n\n"
  471. elif format_type == "ntfy":
  472. source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
  473. elif format_type == "feishu":
  474. source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
  475. elif format_type == "dingtalk":
  476. source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
  477. elif format_type == "slack":
  478. source_header = f"*{source_data['source_name']}* ({len(source_data['titles'])} 条):\n\n"
  479. # 构建第一条新增新闻
  480. first_news_line = ""
  481. if source_data["titles"]:
  482. first_title_data = source_data["titles"][0]
  483. title_data_copy = first_title_data.copy()
  484. title_data_copy["is_new"] = False
  485. if format_type in ("wework", "bark"):
  486. formatted_title = format_title_for_platform(
  487. "wework", title_data_copy, show_source=False
  488. )
  489. elif format_type == "telegram":
  490. formatted_title = format_title_for_platform(
  491. "telegram", title_data_copy, show_source=False
  492. )
  493. elif format_type == "feishu":
  494. formatted_title = format_title_for_platform(
  495. "feishu", title_data_copy, show_source=False
  496. )
  497. elif format_type == "dingtalk":
  498. formatted_title = format_title_for_platform(
  499. "dingtalk", title_data_copy, show_source=False
  500. )
  501. elif format_type == "slack":
  502. formatted_title = format_title_for_platform(
  503. "slack", title_data_copy, show_source=False
  504. )
  505. else:
  506. formatted_title = f"{title_data_copy['title']}"
  507. first_news_line = f" 1. {formatted_title}\n"
  508. # 原子性检查:来源标题+第一条新闻
  509. source_with_first_news = source_header + first_news_line
  510. test_content = current_batch + source_with_first_news
  511. if (
  512. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  513. >= max_bytes
  514. ):
  515. if current_batch_has_content:
  516. batches.append(current_batch + base_footer)
  517. current_batch = base_header + new_header + source_with_first_news
  518. current_batch_has_content = True
  519. start_index = 1
  520. else:
  521. current_batch = test_content
  522. current_batch_has_content = True
  523. start_index = 1
  524. # 处理剩余新增新闻
  525. for j in range(start_index, len(source_data["titles"])):
  526. title_data = source_data["titles"][j]
  527. title_data_copy = title_data.copy()
  528. title_data_copy["is_new"] = False
  529. if format_type == "wework":
  530. formatted_title = format_title_for_platform(
  531. "wework", title_data_copy, show_source=False
  532. )
  533. elif format_type == "telegram":
  534. formatted_title = format_title_for_platform(
  535. "telegram", title_data_copy, show_source=False
  536. )
  537. elif format_type == "feishu":
  538. formatted_title = format_title_for_platform(
  539. "feishu", title_data_copy, show_source=False
  540. )
  541. elif format_type == "dingtalk":
  542. formatted_title = format_title_for_platform(
  543. "dingtalk", title_data_copy, show_source=False
  544. )
  545. elif format_type == "slack":
  546. formatted_title = format_title_for_platform(
  547. "slack", title_data_copy, show_source=False
  548. )
  549. else:
  550. formatted_title = f"{title_data_copy['title']}"
  551. news_line = f" {j + 1}. {formatted_title}\n"
  552. test_content = current_batch + news_line
  553. if (
  554. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  555. >= max_bytes
  556. ):
  557. if current_batch_has_content:
  558. batches.append(current_batch + base_footer)
  559. current_batch = base_header + new_header + source_header + news_line
  560. current_batch_has_content = True
  561. else:
  562. current_batch = test_content
  563. current_batch_has_content = True
  564. current_batch += "\n"
  565. return current_batch, current_batch_has_content, batches
  566. # 定义处理 AI 分析的函数
  567. def process_ai_section(current_batch, current_batch_has_content, batches, add_separator=True):
  568. """处理 AI 分析内容"""
  569. nonlocal ai_content
  570. if not ai_content:
  571. return current_batch, current_batch_has_content, batches
  572. # 根据 add_separator 决定是否添加前置分割线
  573. ai_separator = ""
  574. if add_separator and current_batch_has_content:
  575. # 需要添加分割线
  576. if format_type == "feishu":
  577. ai_separator = f"\n{feishu_separator}\n\n"
  578. elif format_type == "dingtalk":
  579. ai_separator = "\n---\n\n"
  580. elif format_type in ("wework", "bark"):
  581. ai_separator = "\n\n\n\n"
  582. elif format_type in ("telegram", "ntfy", "slack"):
  583. ai_separator = "\n\n"
  584. # 如果不需要分割线,ai_separator 保持为空字符串
  585. # 尝试将 AI 内容添加到当前批次
  586. test_content = current_batch + ai_separator + ai_content
  587. if (
  588. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  589. < max_bytes
  590. ):
  591. current_batch = test_content
  592. current_batch_has_content = True
  593. else:
  594. # 当前批次容纳不下,开启新批次
  595. if current_batch_has_content:
  596. batches.append(current_batch + base_footer)
  597. # AI 内容可能很长,需要考虑是否需要进一步分割
  598. ai_with_header = base_header + ai_content
  599. current_batch = ai_with_header
  600. current_batch_has_content = True
  601. return current_batch, current_batch_has_content, batches
  602. # 定义处理独立展示区的函数
  603. def process_standalone_section_wrapper(current_batch, current_batch_has_content, batches, add_separator=True):
  604. """处理独立展示区"""
  605. if not standalone_data:
  606. return current_batch, current_batch_has_content, batches
  607. return _process_standalone_section(
  608. standalone_data, format_type, feishu_separator, base_header, base_footer,
  609. max_bytes, current_batch, current_batch_has_content, batches, timezone,
  610. rank_threshold, add_separator
  611. )
  612. # 定义处理 RSS 统计的函数
  613. def process_rss_stats_wrapper(current_batch, current_batch_has_content, batches, add_separator=True):
  614. """处理 RSS 统计"""
  615. if not rss_items:
  616. return current_batch, current_batch_has_content, batches
  617. return _process_rss_stats_section(
  618. rss_items, format_type, feishu_separator, base_header, base_footer,
  619. max_bytes, current_batch, current_batch_has_content, batches, timezone,
  620. add_separator
  621. )
  622. # 定义处理 RSS 新增的函数
  623. def process_rss_new_wrapper(current_batch, current_batch_has_content, batches, add_separator=True):
  624. """处理 RSS 新增"""
  625. if not rss_new_items:
  626. return current_batch, current_batch_has_content, batches
  627. return _process_rss_new_titles_section(
  628. rss_new_items, format_type, feishu_separator, base_header, base_footer,
  629. max_bytes, current_batch, current_batch_has_content, batches, timezone,
  630. add_separator
  631. )
  632. # 按 region_order 顺序处理各区域
  633. # 记录是否已有区域内容(用于决定是否添加分割线)
  634. has_region_content = False
  635. for region in region_order:
  636. # 记录处理前的状态,用于判断该区域是否产生了内容
  637. batch_before = current_batch
  638. has_content_before = current_batch_has_content
  639. batches_len_before = len(batches)
  640. # 决定是否需要添加分割线(第一个有内容的区域不需要)
  641. add_separator = has_region_content
  642. if region == "hotlist":
  643. # 处理热榜统计
  644. current_batch, current_batch_has_content, batches = process_stats_section(
  645. current_batch, current_batch_has_content, batches, add_separator
  646. )
  647. elif region == "rss":
  648. # 处理 RSS 统计
  649. current_batch, current_batch_has_content, batches = process_rss_stats_wrapper(
  650. current_batch, current_batch_has_content, batches, add_separator
  651. )
  652. elif region == "new_items":
  653. # 处理热榜新增
  654. current_batch, current_batch_has_content, batches = process_new_titles_section(
  655. current_batch, current_batch_has_content, batches, add_separator
  656. )
  657. # 处理 RSS 新增(跟随 new_items,继承 add_separator 逻辑)
  658. # 如果热榜新增产生了内容,RSS 新增需要分割线
  659. new_batch_changed = (
  660. current_batch != batch_before or
  661. current_batch_has_content != has_content_before or
  662. len(batches) != batches_len_before
  663. )
  664. rss_new_separator = new_batch_changed or has_region_content
  665. current_batch, current_batch_has_content, batches = process_rss_new_wrapper(
  666. current_batch, current_batch_has_content, batches, rss_new_separator
  667. )
  668. elif region == "standalone":
  669. # 处理独立展示区
  670. current_batch, current_batch_has_content, batches = process_standalone_section_wrapper(
  671. current_batch, current_batch_has_content, batches, add_separator
  672. )
  673. elif region == "ai_analysis":
  674. # 处理 AI 分析
  675. current_batch, current_batch_has_content, batches = process_ai_section(
  676. current_batch, current_batch_has_content, batches, add_separator
  677. )
  678. # 检查该区域是否产生了内容
  679. region_produced_content = (
  680. current_batch != batch_before or
  681. current_batch_has_content != has_content_before or
  682. len(batches) != batches_len_before
  683. )
  684. if region_produced_content:
  685. has_region_content = True
  686. if report_data["failed_ids"]:
  687. failed_header = ""
  688. if format_type == "wework":
  689. failed_header = f"\n\n\n\n⚠️ **数据获取失败的平台:**\n\n"
  690. elif format_type == "telegram":
  691. failed_header = f"\n\n⚠️ 数据获取失败的平台:\n\n"
  692. elif format_type == "ntfy":
  693. failed_header = f"\n\n⚠️ **数据获取失败的平台:**\n\n"
  694. elif format_type == "feishu":
  695. failed_header = f"\n{feishu_separator}\n\n⚠️ **数据获取失败的平台:**\n\n"
  696. elif format_type == "dingtalk":
  697. failed_header = f"\n---\n\n⚠️ **数据获取失败的平台:**\n\n"
  698. test_content = current_batch + failed_header
  699. if (
  700. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  701. >= max_bytes
  702. ):
  703. if current_batch_has_content:
  704. batches.append(current_batch + base_footer)
  705. current_batch = base_header + failed_header
  706. current_batch_has_content = True
  707. else:
  708. current_batch = test_content
  709. current_batch_has_content = True
  710. for i, id_value in enumerate(report_data["failed_ids"], 1):
  711. if format_type == "feishu":
  712. failed_line = f" • <font color='red'>{id_value}</font>\n"
  713. elif format_type == "dingtalk":
  714. failed_line = f" • **{id_value}**\n"
  715. else:
  716. failed_line = f" • {id_value}\n"
  717. test_content = current_batch + failed_line
  718. if (
  719. len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
  720. >= max_bytes
  721. ):
  722. if current_batch_has_content:
  723. batches.append(current_batch + base_footer)
  724. current_batch = base_header + failed_header + failed_line
  725. current_batch_has_content = True
  726. else:
  727. current_batch = test_content
  728. current_batch_has_content = True
  729. # 完成最后批次
  730. if current_batch_has_content:
  731. batches.append(current_batch + base_footer)
  732. return batches
  733. def _process_rss_stats_section(
  734. rss_stats: list,
  735. format_type: str,
  736. feishu_separator: str,
  737. base_header: str,
  738. base_footer: str,
  739. max_bytes: int,
  740. current_batch: str,
  741. current_batch_has_content: bool,
  742. batches: List[str],
  743. timezone: str = "Asia/Shanghai",
  744. add_separator: bool = True,
  745. ) -> tuple:
  746. """处理 RSS 统计区块(按关键词分组,与热榜统计格式一致)
  747. Args:
  748. rss_stats: RSS 关键词统计列表,格式与热榜 stats 一致:
  749. [{"word": "AI", "count": 5, "titles": [...]}]
  750. format_type: 格式类型
  751. feishu_separator: 飞书分隔符
  752. base_header: 基础头部
  753. base_footer: 基础尾部
  754. max_bytes: 最大字节数
  755. current_batch: 当前批次内容
  756. current_batch_has_content: 当前批次是否有内容
  757. batches: 已完成的批次列表
  758. timezone: 时区名称
  759. add_separator: 是否在区块前添加分割线(第一个区域时为 False)
  760. Returns:
  761. (current_batch, current_batch_has_content, batches) 元组
  762. """
  763. if not rss_stats:
  764. return current_batch, current_batch_has_content, batches
  765. # 计算总条目数
  766. total_items = sum(stat["count"] for stat in rss_stats)
  767. total_keywords = len(rss_stats)
  768. # RSS 统计区块标题(根据 add_separator 决定是否添加前置分割线)
  769. rss_header = ""
  770. if add_separator and current_batch_has_content:
  771. # 需要添加分割线
  772. if format_type == "feishu":
  773. rss_header = f"\n{feishu_separator}\n\n📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  774. elif format_type == "dingtalk":
  775. rss_header = f"\n---\n\n📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  776. elif format_type in ("wework", "bark"):
  777. rss_header = f"\n\n\n\n📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  778. elif format_type == "telegram":
  779. rss_header = f"\n\n📰 RSS 订阅统计 (共 {total_items} 条)\n\n"
  780. elif format_type == "slack":
  781. rss_header = f"\n\n📰 *RSS 订阅统计* (共 {total_items} 条)\n\n"
  782. else:
  783. rss_header = f"\n\n📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  784. else:
  785. # 不需要分割线(第一个区域)
  786. if format_type == "feishu":
  787. rss_header = f"📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  788. elif format_type == "dingtalk":
  789. rss_header = f"📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  790. elif format_type == "telegram":
  791. rss_header = f"📰 RSS 订阅统计 (共 {total_items} 条)\n\n"
  792. elif format_type == "slack":
  793. rss_header = f"📰 *RSS 订阅统计* (共 {total_items} 条)\n\n"
  794. else:
  795. rss_header = f"📰 **RSS 订阅统计** (共 {total_items} 条)\n\n"
  796. # 添加 RSS 标题
  797. test_content = current_batch + rss_header
  798. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) < max_bytes:
  799. current_batch = test_content
  800. current_batch_has_content = True
  801. else:
  802. if current_batch_has_content:
  803. batches.append(current_batch + base_footer)
  804. current_batch = base_header + rss_header
  805. current_batch_has_content = True
  806. # 逐个处理关键词组(与热榜一致)
  807. for i, stat in enumerate(rss_stats):
  808. word = stat["word"]
  809. count = stat["count"]
  810. sequence_display = f"[{i + 1}/{total_keywords}]"
  811. # 构建关键词标题(与热榜格式一致)
  812. word_header = ""
  813. if format_type in ("wework", "bark"):
  814. if count >= 10:
  815. word_header = f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  816. elif count >= 5:
  817. word_header = f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  818. else:
  819. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  820. elif format_type == "telegram":
  821. if count >= 10:
  822. word_header = f"🔥 {sequence_display} {word} : {count} 条\n\n"
  823. elif count >= 5:
  824. word_header = f"📈 {sequence_display} {word} : {count} 条\n\n"
  825. else:
  826. word_header = f"📌 {sequence_display} {word} : {count} 条\n\n"
  827. elif format_type == "ntfy":
  828. if count >= 10:
  829. word_header = f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  830. elif count >= 5:
  831. word_header = f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  832. else:
  833. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  834. elif format_type == "feishu":
  835. if count >= 10:
  836. word_header = f"🔥 <font color='grey'>{sequence_display}</font> **{word}** : <font color='red'>{count}</font> 条\n\n"
  837. elif count >= 5:
  838. word_header = f"📈 <font color='grey'>{sequence_display}</font> **{word}** : <font color='orange'>{count}</font> 条\n\n"
  839. else:
  840. word_header = f"📌 <font color='grey'>{sequence_display}</font> **{word}** : {count} 条\n\n"
  841. elif format_type == "dingtalk":
  842. if count >= 10:
  843. word_header = f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
  844. elif count >= 5:
  845. word_header = f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
  846. else:
  847. word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
  848. elif format_type == "slack":
  849. if count >= 10:
  850. word_header = f"🔥 {sequence_display} *{word}* : *{count}* 条\n\n"
  851. elif count >= 5:
  852. word_header = f"📈 {sequence_display} *{word}* : *{count}* 条\n\n"
  853. else:
  854. word_header = f"📌 {sequence_display} *{word}* : {count} 条\n\n"
  855. # 构建第一条新闻(使用 format_title_for_platform)
  856. first_news_line = ""
  857. if stat["titles"]:
  858. first_title_data = stat["titles"][0]
  859. if format_type in ("wework", "bark"):
  860. formatted_title = format_title_for_platform("wework", first_title_data, show_source=True)
  861. elif format_type == "telegram":
  862. formatted_title = format_title_for_platform("telegram", first_title_data, show_source=True)
  863. elif format_type == "ntfy":
  864. formatted_title = format_title_for_platform("ntfy", first_title_data, show_source=True)
  865. elif format_type == "feishu":
  866. formatted_title = format_title_for_platform("feishu", first_title_data, show_source=True)
  867. elif format_type == "dingtalk":
  868. formatted_title = format_title_for_platform("dingtalk", first_title_data, show_source=True)
  869. elif format_type == "slack":
  870. formatted_title = format_title_for_platform("slack", first_title_data, show_source=True)
  871. else:
  872. formatted_title = f"{first_title_data['title']}"
  873. first_news_line = f" 1. {formatted_title}\n"
  874. if len(stat["titles"]) > 1:
  875. first_news_line += "\n"
  876. # 原子性检查:关键词标题 + 第一条新闻必须一起处理
  877. word_with_first_news = word_header + first_news_line
  878. test_content = current_batch + word_with_first_news
  879. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  880. if current_batch_has_content:
  881. batches.append(current_batch + base_footer)
  882. current_batch = base_header + rss_header + word_with_first_news
  883. current_batch_has_content = True
  884. start_index = 1
  885. else:
  886. current_batch = test_content
  887. current_batch_has_content = True
  888. start_index = 1
  889. # 处理剩余新闻条目
  890. for j in range(start_index, len(stat["titles"])):
  891. title_data = stat["titles"][j]
  892. if format_type in ("wework", "bark"):
  893. formatted_title = format_title_for_platform("wework", title_data, show_source=True)
  894. elif format_type == "telegram":
  895. formatted_title = format_title_for_platform("telegram", title_data, show_source=True)
  896. elif format_type == "ntfy":
  897. formatted_title = format_title_for_platform("ntfy", title_data, show_source=True)
  898. elif format_type == "feishu":
  899. formatted_title = format_title_for_platform("feishu", title_data, show_source=True)
  900. elif format_type == "dingtalk":
  901. formatted_title = format_title_for_platform("dingtalk", title_data, show_source=True)
  902. elif format_type == "slack":
  903. formatted_title = format_title_for_platform("slack", title_data, show_source=True)
  904. else:
  905. formatted_title = f"{title_data['title']}"
  906. news_line = f" {j + 1}. {formatted_title}\n"
  907. if j < len(stat["titles"]) - 1:
  908. news_line += "\n"
  909. test_content = current_batch + news_line
  910. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  911. if current_batch_has_content:
  912. batches.append(current_batch + base_footer)
  913. current_batch = base_header + rss_header + word_header + news_line
  914. current_batch_has_content = True
  915. else:
  916. current_batch = test_content
  917. current_batch_has_content = True
  918. # 关键词间分隔符
  919. if i < len(rss_stats) - 1:
  920. separator = ""
  921. if format_type in ("wework", "bark"):
  922. separator = "\n\n\n\n"
  923. elif format_type == "telegram":
  924. separator = "\n\n"
  925. elif format_type == "ntfy":
  926. separator = "\n\n"
  927. elif format_type == "feishu":
  928. separator = f"\n{feishu_separator}\n\n"
  929. elif format_type == "dingtalk":
  930. separator = "\n---\n\n"
  931. elif format_type == "slack":
  932. separator = "\n\n"
  933. test_content = current_batch + separator
  934. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) < max_bytes:
  935. current_batch = test_content
  936. return current_batch, current_batch_has_content, batches
  937. def _process_rss_new_titles_section(
  938. rss_new_stats: list,
  939. format_type: str,
  940. feishu_separator: str,
  941. base_header: str,
  942. base_footer: str,
  943. max_bytes: int,
  944. current_batch: str,
  945. current_batch_has_content: bool,
  946. batches: List[str],
  947. timezone: str = "Asia/Shanghai",
  948. add_separator: bool = True,
  949. ) -> tuple:
  950. """处理 RSS 新增区块(按来源分组,与热榜新增格式一致)
  951. Args:
  952. rss_new_stats: RSS 新增关键词统计列表,格式与热榜 stats 一致:
  953. [{"word": "AI", "count": 5, "titles": [...]}]
  954. format_type: 格式类型
  955. feishu_separator: 飞书分隔符
  956. base_header: 基础头部
  957. base_footer: 基础尾部
  958. max_bytes: 最大字节数
  959. current_batch: 当前批次内容
  960. current_batch_has_content: 当前批次是否有内容
  961. batches: 已完成的批次列表
  962. timezone: 时区名称
  963. add_separator: 是否在区块前添加分割线(第一个区域时为 False)
  964. Returns:
  965. (current_batch, current_batch_has_content, batches) 元组
  966. """
  967. if not rss_new_stats:
  968. return current_batch, current_batch_has_content, batches
  969. # 从关键词分组中提取所有条目,重新按来源分组
  970. source_map = {}
  971. for stat in rss_new_stats:
  972. for title_data in stat.get("titles", []):
  973. source_name = title_data.get("source_name", "未知来源")
  974. if source_name not in source_map:
  975. source_map[source_name] = []
  976. source_map[source_name].append(title_data)
  977. if not source_map:
  978. return current_batch, current_batch_has_content, batches
  979. # 计算总条目数
  980. total_items = sum(len(titles) for titles in source_map.values())
  981. # RSS 新增区块标题(根据 add_separator 决定是否添加前置分割线)
  982. new_header = ""
  983. if add_separator and current_batch_has_content:
  984. # 需要添加分割线
  985. if format_type in ("wework", "bark"):
  986. new_header = f"\n\n\n\n🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  987. elif format_type == "telegram":
  988. new_header = f"\n\n🆕 RSS 本次新增 (共 {total_items} 条)\n\n"
  989. elif format_type == "ntfy":
  990. new_header = f"\n\n🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  991. elif format_type == "feishu":
  992. new_header = f"\n{feishu_separator}\n\n🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  993. elif format_type == "dingtalk":
  994. new_header = f"\n---\n\n🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  995. elif format_type == "slack":
  996. new_header = f"\n\n🆕 *RSS 本次新增* (共 {total_items} 条)\n\n"
  997. else:
  998. # 不需要分割线(第一个区域)
  999. if format_type in ("wework", "bark"):
  1000. new_header = f"🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  1001. elif format_type == "telegram":
  1002. new_header = f"🆕 RSS 本次新增 (共 {total_items} 条)\n\n"
  1003. elif format_type == "ntfy":
  1004. new_header = f"🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  1005. elif format_type == "feishu":
  1006. new_header = f"🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  1007. elif format_type == "dingtalk":
  1008. new_header = f"🆕 **RSS 本次新增** (共 {total_items} 条)\n\n"
  1009. elif format_type == "slack":
  1010. new_header = f"🆕 *RSS 本次新增* (共 {total_items} 条)\n\n"
  1011. # 添加 RSS 新增标题
  1012. test_content = current_batch + new_header
  1013. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1014. if current_batch_has_content:
  1015. batches.append(current_batch + base_footer)
  1016. current_batch = base_header + new_header
  1017. current_batch_has_content = True
  1018. else:
  1019. current_batch = test_content
  1020. current_batch_has_content = True
  1021. # 按来源分组显示(与热榜新增格式一致)
  1022. source_list = list(source_map.items())
  1023. for i, (source_name, titles) in enumerate(source_list):
  1024. count = len(titles)
  1025. # 构建来源标题(与热榜新增格式一致)
  1026. source_header = ""
  1027. if format_type in ("wework", "bark"):
  1028. source_header = f"**{source_name}** ({count} 条):\n\n"
  1029. elif format_type == "telegram":
  1030. source_header = f"{source_name} ({count} 条):\n\n"
  1031. elif format_type == "ntfy":
  1032. source_header = f"**{source_name}** ({count} 条):\n\n"
  1033. elif format_type == "feishu":
  1034. source_header = f"**{source_name}** ({count} 条):\n\n"
  1035. elif format_type == "dingtalk":
  1036. source_header = f"**{source_name}** ({count} 条):\n\n"
  1037. elif format_type == "slack":
  1038. source_header = f"*{source_name}* ({count} 条):\n\n"
  1039. # 构建第一条新闻(不显示来源,禁用 new emoji)
  1040. first_news_line = ""
  1041. if titles:
  1042. first_title_data = titles[0].copy()
  1043. first_title_data["is_new"] = False
  1044. if format_type in ("wework", "bark"):
  1045. formatted_title = format_title_for_platform("wework", first_title_data, show_source=False)
  1046. elif format_type == "telegram":
  1047. formatted_title = format_title_for_platform("telegram", first_title_data, show_source=False)
  1048. elif format_type == "ntfy":
  1049. formatted_title = format_title_for_platform("ntfy", first_title_data, show_source=False)
  1050. elif format_type == "feishu":
  1051. formatted_title = format_title_for_platform("feishu", first_title_data, show_source=False)
  1052. elif format_type == "dingtalk":
  1053. formatted_title = format_title_for_platform("dingtalk", first_title_data, show_source=False)
  1054. elif format_type == "slack":
  1055. formatted_title = format_title_for_platform("slack", first_title_data, show_source=False)
  1056. else:
  1057. formatted_title = f"{first_title_data['title']}"
  1058. first_news_line = f" 1. {formatted_title}\n"
  1059. # 原子性检查:来源标题 + 第一条新闻必须一起处理
  1060. source_with_first_news = source_header + first_news_line
  1061. test_content = current_batch + source_with_first_news
  1062. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1063. if current_batch_has_content:
  1064. batches.append(current_batch + base_footer)
  1065. current_batch = base_header + new_header + source_with_first_news
  1066. current_batch_has_content = True
  1067. start_index = 1
  1068. else:
  1069. current_batch = test_content
  1070. current_batch_has_content = True
  1071. start_index = 1
  1072. # 处理剩余新闻条目(禁用 new emoji)
  1073. for j in range(start_index, len(titles)):
  1074. title_data = titles[j].copy()
  1075. title_data["is_new"] = False
  1076. if format_type in ("wework", "bark"):
  1077. formatted_title = format_title_for_platform("wework", title_data, show_source=False)
  1078. elif format_type == "telegram":
  1079. formatted_title = format_title_for_platform("telegram", title_data, show_source=False)
  1080. elif format_type == "ntfy":
  1081. formatted_title = format_title_for_platform("ntfy", title_data, show_source=False)
  1082. elif format_type == "feishu":
  1083. formatted_title = format_title_for_platform("feishu", title_data, show_source=False)
  1084. elif format_type == "dingtalk":
  1085. formatted_title = format_title_for_platform("dingtalk", title_data, show_source=False)
  1086. elif format_type == "slack":
  1087. formatted_title = format_title_for_platform("slack", title_data, show_source=False)
  1088. else:
  1089. formatted_title = f"{title_data['title']}"
  1090. news_line = f" {j + 1}. {formatted_title}\n"
  1091. test_content = current_batch + news_line
  1092. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1093. if current_batch_has_content:
  1094. batches.append(current_batch + base_footer)
  1095. current_batch = base_header + new_header + source_header + news_line
  1096. current_batch_has_content = True
  1097. else:
  1098. current_batch = test_content
  1099. current_batch_has_content = True
  1100. # 来源间添加空行(与热榜新增格式一致)
  1101. current_batch += "\n"
  1102. return current_batch, current_batch_has_content, batches
  1103. def _format_rss_item_line(
  1104. item: Dict,
  1105. index: int,
  1106. format_type: str,
  1107. timezone: str = "Asia/Shanghai",
  1108. ) -> str:
  1109. """格式化单条 RSS 条目
  1110. Args:
  1111. item: RSS 条目字典
  1112. index: 序号
  1113. format_type: 格式类型
  1114. timezone: 时区名称
  1115. Returns:
  1116. 格式化后的条目行字符串
  1117. """
  1118. title = item.get("title", "")
  1119. url = item.get("url", "")
  1120. published_at = item.get("published_at", "")
  1121. # 使用友好时间格式
  1122. if published_at:
  1123. friendly_time = format_iso_time_friendly(published_at, timezone, include_date=True)
  1124. else:
  1125. friendly_time = ""
  1126. # 构建条目行
  1127. if format_type == "feishu":
  1128. if url:
  1129. item_line = f" {index}. [{title}]({url})"
  1130. else:
  1131. item_line = f" {index}. {title}"
  1132. if friendly_time:
  1133. item_line += f" <font color='grey'>- {friendly_time}</font>"
  1134. elif format_type == "telegram":
  1135. if url:
  1136. item_line = f" {index}. {title} ({url})"
  1137. else:
  1138. item_line = f" {index}. {title}"
  1139. if friendly_time:
  1140. item_line += f" - {friendly_time}"
  1141. else:
  1142. if url:
  1143. item_line = f" {index}. [{title}]({url})"
  1144. else:
  1145. item_line = f" {index}. {title}"
  1146. if friendly_time:
  1147. item_line += f" `{friendly_time}`"
  1148. item_line += "\n"
  1149. return item_line
  1150. def _process_standalone_section(
  1151. standalone_data: Dict,
  1152. format_type: str,
  1153. feishu_separator: str,
  1154. base_header: str,
  1155. base_footer: str,
  1156. max_bytes: int,
  1157. current_batch: str,
  1158. current_batch_has_content: bool,
  1159. batches: List[str],
  1160. timezone: str = "Asia/Shanghai",
  1161. rank_threshold: int = 10,
  1162. add_separator: bool = True,
  1163. ) -> tuple:
  1164. """处理独立展示区区块
  1165. 独立展示区显示指定平台的完整热榜或 RSS 源内容,不受关键词过滤影响。
  1166. 热榜按原始排名排序,RSS 按发布时间排序。
  1167. Args:
  1168. standalone_data: 独立展示数据,格式:
  1169. {
  1170. "platforms": [{"id": "zhihu", "name": "知乎热榜", "items": [...]}],
  1171. "rss_feeds": [{"id": "hacker-news", "name": "Hacker News", "items": [...]}]
  1172. }
  1173. format_type: 格式类型
  1174. feishu_separator: 飞书分隔符
  1175. base_header: 基础头部
  1176. base_footer: 基础尾部
  1177. max_bytes: 最大字节数
  1178. current_batch: 当前批次内容
  1179. current_batch_has_content: 当前批次是否有内容
  1180. batches: 已完成的批次列表
  1181. timezone: 时区名称
  1182. rank_threshold: 排名高亮阈值
  1183. add_separator: 是否在区块前添加分割线(第一个区域时为 False)
  1184. Returns:
  1185. (current_batch, current_batch_has_content, batches) 元组
  1186. """
  1187. if not standalone_data:
  1188. return current_batch, current_batch_has_content, batches
  1189. platforms = standalone_data.get("platforms", [])
  1190. rss_feeds = standalone_data.get("rss_feeds", [])
  1191. if not platforms and not rss_feeds:
  1192. return current_batch, current_batch_has_content, batches
  1193. # 计算总条目数
  1194. total_platform_items = sum(len(p.get("items", [])) for p in platforms)
  1195. total_rss_items = sum(len(f.get("items", [])) for f in rss_feeds)
  1196. total_items = total_platform_items + total_rss_items
  1197. # 独立展示区标题(根据 add_separator 决定是否添加前置分割线)
  1198. section_header = ""
  1199. if add_separator and current_batch_has_content:
  1200. # 需要添加分割线
  1201. if format_type == "feishu":
  1202. section_header = f"\n{feishu_separator}\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
  1203. elif format_type == "dingtalk":
  1204. section_header = f"\n---\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
  1205. elif format_type in ("wework", "bark"):
  1206. section_header = f"\n\n\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
  1207. elif format_type == "telegram":
  1208. section_header = f"\n\n📋 独立展示区 (共 {total_items} 条)\n\n"
  1209. elif format_type == "slack":
  1210. section_header = f"\n\n📋 *独立展示区* (共 {total_items} 条)\n\n"
  1211. else:
  1212. section_header = f"\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
  1213. else:
  1214. # 不需要分割线(第一个区域)
  1215. if format_type == "feishu":
  1216. section_header = f"📋 **独立展示区** (共 {total_items} 条)\n\n"
  1217. elif format_type == "dingtalk":
  1218. section_header = f"📋 **独立展示区** (共 {total_items} 条)\n\n"
  1219. elif format_type == "telegram":
  1220. section_header = f"📋 独立展示区 (共 {total_items} 条)\n\n"
  1221. elif format_type == "slack":
  1222. section_header = f"📋 *独立展示区* (共 {total_items} 条)\n\n"
  1223. else:
  1224. section_header = f"📋 **独立展示区** (共 {total_items} 条)\n\n"
  1225. # 添加区块标题
  1226. test_content = current_batch + section_header
  1227. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) < max_bytes:
  1228. current_batch = test_content
  1229. current_batch_has_content = True
  1230. else:
  1231. if current_batch_has_content:
  1232. batches.append(current_batch + base_footer)
  1233. current_batch = base_header + section_header
  1234. current_batch_has_content = True
  1235. # 处理热榜平台
  1236. for platform in platforms:
  1237. platform_name = platform.get("name", platform.get("id", ""))
  1238. items = platform.get("items", [])
  1239. if not items:
  1240. continue
  1241. # 平台标题
  1242. platform_header = ""
  1243. if format_type in ("wework", "bark"):
  1244. platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
  1245. elif format_type == "telegram":
  1246. platform_header = f"{platform_name} ({len(items)} 条):\n\n"
  1247. elif format_type == "ntfy":
  1248. platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
  1249. elif format_type == "feishu":
  1250. platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
  1251. elif format_type == "dingtalk":
  1252. platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
  1253. elif format_type == "slack":
  1254. platform_header = f"*{platform_name}* ({len(items)} 条):\n\n"
  1255. # 构建第一条新闻
  1256. first_item_line = ""
  1257. if items:
  1258. first_item_line = _format_standalone_platform_item(items[0], 1, format_type, rank_threshold)
  1259. # 原子性检查
  1260. platform_with_first = platform_header + first_item_line
  1261. test_content = current_batch + platform_with_first
  1262. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1263. if current_batch_has_content:
  1264. batches.append(current_batch + base_footer)
  1265. current_batch = base_header + section_header + platform_with_first
  1266. current_batch_has_content = True
  1267. start_index = 1
  1268. else:
  1269. current_batch = test_content
  1270. current_batch_has_content = True
  1271. start_index = 1
  1272. # 处理剩余条目
  1273. for j in range(start_index, len(items)):
  1274. item_line = _format_standalone_platform_item(items[j], j + 1, format_type, rank_threshold)
  1275. test_content = current_batch + item_line
  1276. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1277. if current_batch_has_content:
  1278. batches.append(current_batch + base_footer)
  1279. current_batch = base_header + section_header + platform_header + item_line
  1280. current_batch_has_content = True
  1281. else:
  1282. current_batch = test_content
  1283. current_batch_has_content = True
  1284. current_batch += "\n"
  1285. # 处理 RSS 源
  1286. for feed in rss_feeds:
  1287. feed_name = feed.get("name", feed.get("id", ""))
  1288. items = feed.get("items", [])
  1289. if not items:
  1290. continue
  1291. # RSS 源标题
  1292. feed_header = ""
  1293. if format_type in ("wework", "bark"):
  1294. feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
  1295. elif format_type == "telegram":
  1296. feed_header = f"{feed_name} ({len(items)} 条):\n\n"
  1297. elif format_type == "ntfy":
  1298. feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
  1299. elif format_type == "feishu":
  1300. feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
  1301. elif format_type == "dingtalk":
  1302. feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
  1303. elif format_type == "slack":
  1304. feed_header = f"*{feed_name}* ({len(items)} 条):\n\n"
  1305. # 构建第一条 RSS
  1306. first_item_line = ""
  1307. if items:
  1308. first_item_line = _format_standalone_rss_item(items[0], 1, format_type, timezone)
  1309. # 原子性检查
  1310. feed_with_first = feed_header + first_item_line
  1311. test_content = current_batch + feed_with_first
  1312. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1313. if current_batch_has_content:
  1314. batches.append(current_batch + base_footer)
  1315. current_batch = base_header + section_header + feed_with_first
  1316. current_batch_has_content = True
  1317. start_index = 1
  1318. else:
  1319. current_batch = test_content
  1320. current_batch_has_content = True
  1321. start_index = 1
  1322. # 处理剩余条目
  1323. for j in range(start_index, len(items)):
  1324. item_line = _format_standalone_rss_item(items[j], j + 1, format_type, timezone)
  1325. test_content = current_batch + item_line
  1326. if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
  1327. if current_batch_has_content:
  1328. batches.append(current_batch + base_footer)
  1329. current_batch = base_header + section_header + feed_header + item_line
  1330. current_batch_has_content = True
  1331. else:
  1332. current_batch = test_content
  1333. current_batch_has_content = True
  1334. current_batch += "\n"
  1335. return current_batch, current_batch_has_content, batches
  1336. def _format_standalone_platform_item(item: Dict, index: int, format_type: str, rank_threshold: int = 10) -> str:
  1337. """格式化独立展示区的热榜条目(复用热点词汇统计区样式)
  1338. Args:
  1339. item: 热榜条目,包含 title, url, rank, ranks, first_time, last_time, count
  1340. index: 序号
  1341. format_type: 格式类型
  1342. rank_threshold: 排名高亮阈值
  1343. Returns:
  1344. 格式化后的条目行字符串
  1345. """
  1346. title = item.get("title", "")
  1347. url = item.get("url", "") or item.get("mobileUrl", "")
  1348. ranks = item.get("ranks", [])
  1349. rank = item.get("rank", 0)
  1350. first_time = item.get("first_time", "")
  1351. last_time = item.get("last_time", "")
  1352. count = item.get("count", 1)
  1353. # 使用 format_rank_display 格式化排名(复用热点词汇统计区逻辑)
  1354. # 如果没有 ranks 列表,用单个 rank 构造
  1355. if not ranks and rank > 0:
  1356. ranks = [rank]
  1357. rank_display = format_rank_display(ranks, rank_threshold, format_type) if ranks else ""
  1358. # 构建时间显示(用 ~ 连接范围,与热点词汇统计区一致)
  1359. # 将 HH-MM 格式转换为 HH:MM 格式
  1360. time_display = ""
  1361. if first_time and last_time and first_time != last_time:
  1362. first_time_display = convert_time_for_display(first_time)
  1363. last_time_display = convert_time_for_display(last_time)
  1364. time_display = f"{first_time_display}~{last_time_display}"
  1365. elif first_time:
  1366. time_display = convert_time_for_display(first_time)
  1367. # 构建次数显示(格式为 (N次),与热点词汇统计区一致)
  1368. count_display = f"({count}次)" if count > 1 else ""
  1369. # 根据格式类型构建条目行(复用热点词汇统计区样式)
  1370. if format_type == "feishu":
  1371. if url:
  1372. item_line = f" {index}. [{title}]({url})"
  1373. else:
  1374. item_line = f" {index}. {title}"
  1375. if rank_display:
  1376. item_line += f" {rank_display}"
  1377. if time_display:
  1378. item_line += f" <font color='grey'>- {time_display}</font>"
  1379. if count_display:
  1380. item_line += f" <font color='green'>{count_display}</font>"
  1381. elif format_type == "dingtalk":
  1382. if url:
  1383. item_line = f" {index}. [{title}]({url})"
  1384. else:
  1385. item_line = f" {index}. {title}"
  1386. if rank_display:
  1387. item_line += f" {rank_display}"
  1388. if time_display:
  1389. item_line += f" - {time_display}"
  1390. if count_display:
  1391. item_line += f" {count_display}"
  1392. elif format_type == "telegram":
  1393. if url:
  1394. item_line = f" {index}. {title} ({url})"
  1395. else:
  1396. item_line = f" {index}. {title}"
  1397. if rank_display:
  1398. item_line += f" {rank_display}"
  1399. if time_display:
  1400. item_line += f" - {time_display}"
  1401. if count_display:
  1402. item_line += f" {count_display}"
  1403. elif format_type == "slack":
  1404. if url:
  1405. item_line = f" {index}. <{url}|{title}>"
  1406. else:
  1407. item_line = f" {index}. {title}"
  1408. if rank_display:
  1409. item_line += f" {rank_display}"
  1410. if time_display:
  1411. item_line += f" _{time_display}_"
  1412. if count_display:
  1413. item_line += f" {count_display}"
  1414. else:
  1415. # wework, bark, ntfy
  1416. if url:
  1417. item_line = f" {index}. [{title}]({url})"
  1418. else:
  1419. item_line = f" {index}. {title}"
  1420. if rank_display:
  1421. item_line += f" {rank_display}"
  1422. if time_display:
  1423. item_line += f" - {time_display}"
  1424. if count_display:
  1425. item_line += f" {count_display}"
  1426. item_line += "\n"
  1427. return item_line
  1428. def _format_standalone_rss_item(
  1429. item: Dict, index: int, format_type: str, timezone: str = "Asia/Shanghai"
  1430. ) -> str:
  1431. """格式化独立展示区的 RSS 条目
  1432. Args:
  1433. item: RSS 条目,包含 title, url, published_at, author
  1434. index: 序号
  1435. format_type: 格式类型
  1436. timezone: 时区名称
  1437. Returns:
  1438. 格式化后的条目行字符串
  1439. """
  1440. title = item.get("title", "")
  1441. url = item.get("url", "")
  1442. published_at = item.get("published_at", "")
  1443. author = item.get("author", "")
  1444. # 使用友好时间格式
  1445. friendly_time = ""
  1446. if published_at:
  1447. friendly_time = format_iso_time_friendly(published_at, timezone, include_date=True)
  1448. # 构建元信息
  1449. meta_parts = []
  1450. if friendly_time:
  1451. meta_parts.append(friendly_time)
  1452. if author:
  1453. meta_parts.append(author)
  1454. meta_str = ", ".join(meta_parts)
  1455. # 根据格式类型构建条目行
  1456. if format_type == "feishu":
  1457. if url:
  1458. item_line = f" {index}. [{title}]({url})"
  1459. else:
  1460. item_line = f" {index}. {title}"
  1461. if meta_str:
  1462. item_line += f" <font color='grey'>- {meta_str}</font>"
  1463. elif format_type == "telegram":
  1464. if url:
  1465. item_line = f" {index}. {title} ({url})"
  1466. else:
  1467. item_line = f" {index}. {title}"
  1468. if meta_str:
  1469. item_line += f" - {meta_str}"
  1470. elif format_type == "slack":
  1471. if url:
  1472. item_line = f" {index}. <{url}|{title}>"
  1473. else:
  1474. item_line = f" {index}. {title}"
  1475. if meta_str:
  1476. item_line += f" _{meta_str}_"
  1477. else:
  1478. # wework, bark, ntfy, dingtalk
  1479. if url:
  1480. item_line = f" {index}. [{title}]({url})"
  1481. else:
  1482. item_line = f" {index}. {title}"
  1483. if meta_str:
  1484. item_line += f" `{meta_str}`"
  1485. item_line += "\n"
  1486. return item_line