formatter.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # coding=utf-8
  2. """
  3. AI 分析结果格式化模块
  4. 将 AI 分析结果格式化为各推送渠道的样式
  5. """
  6. import html as html_lib
  7. from .analyzer import AIAnalysisResult
  8. def _escape_html(text: str) -> str:
  9. """转义 HTML 特殊字符,防止 XSS 攻击"""
  10. return html_lib.escape(text) if text else ""
  11. def render_ai_analysis_markdown(result: AIAnalysisResult) -> str:
  12. """渲染为通用 Markdown 格式(Telegram、企业微信、ntfy、Bark、Slack)"""
  13. if not result.success:
  14. return f"⚠️ AI 分析失败: {result.error}"
  15. lines = ["**✨ AI 热点分析**", ""]
  16. if result.summary:
  17. lines.extend(["**趋势概述**", result.summary, ""])
  18. if result.keyword_analysis:
  19. lines.extend(["**热度走势**", result.keyword_analysis, ""])
  20. if result.sentiment:
  21. lines.extend(["**情感倾向**", result.sentiment, ""])
  22. if result.cross_platform:
  23. lines.extend(["**跨平台关联**", result.cross_platform, ""])
  24. if result.impact:
  25. lines.extend(["**潜在影响**", result.impact, ""])
  26. if result.signals:
  27. lines.extend(["**值得关注**", result.signals, ""])
  28. if result.conclusion:
  29. lines.extend(["**总结建议**", result.conclusion])
  30. return "\n".join(lines)
  31. def render_ai_analysis_feishu(result: AIAnalysisResult) -> str:
  32. """渲染为飞书卡片 Markdown 格式"""
  33. if not result.success:
  34. return f"⚠️ AI 分析失败: {result.error}"
  35. lines = ["**✨ AI 热点分析**", ""]
  36. if result.summary:
  37. lines.extend(["**趋势概述**", result.summary, ""])
  38. if result.keyword_analysis:
  39. lines.extend(["**热度走势**", result.keyword_analysis, ""])
  40. if result.sentiment:
  41. lines.extend(["**情感倾向**", result.sentiment, ""])
  42. if result.cross_platform:
  43. lines.extend(["**跨平台关联**", result.cross_platform, ""])
  44. if result.impact:
  45. lines.extend(["**潜在影响**", result.impact, ""])
  46. if result.signals:
  47. lines.extend(["**值得关注**", result.signals, ""])
  48. if result.conclusion:
  49. lines.extend(["**总结建议**", result.conclusion])
  50. return "\n".join(lines)
  51. def render_ai_analysis_dingtalk(result: AIAnalysisResult) -> str:
  52. """渲染为钉钉 Markdown 格式"""
  53. if not result.success:
  54. return f"⚠️ AI 分析失败: {result.error}"
  55. lines = ["### ✨ AI 热点分析", ""]
  56. if result.summary:
  57. lines.extend(["#### 趋势概述", result.summary, ""])
  58. if result.keyword_analysis:
  59. lines.extend(["#### 热度走势", result.keyword_analysis, ""])
  60. if result.sentiment:
  61. lines.extend(["#### 情感倾向", result.sentiment, ""])
  62. if result.cross_platform:
  63. lines.extend(["#### 跨平台关联", result.cross_platform, ""])
  64. if result.impact:
  65. lines.extend(["#### 潜在影响", result.impact, ""])
  66. if result.signals:
  67. lines.extend(["#### 值得关注", result.signals, ""])
  68. if result.conclusion:
  69. lines.extend(["#### 总结建议", result.conclusion])
  70. return "\n".join(lines)
  71. def render_ai_analysis_html(result: AIAnalysisResult) -> str:
  72. """渲染为 HTML 格式(邮件)"""
  73. if not result.success:
  74. return f'<div class="ai-error">⚠️ AI 分析失败: {_escape_html(result.error)}</div>'
  75. html_parts = ['<div class="ai-analysis">', '<h3>✨ AI 热点分析</h3>']
  76. if result.summary:
  77. html_parts.extend([
  78. '<div class="ai-section">',
  79. '<h4>趋势概述</h4>',
  80. f'<p>{_escape_html(result.summary)}</p>',
  81. '</div>'
  82. ])
  83. if result.keyword_analysis:
  84. html_parts.extend([
  85. '<div class="ai-section">',
  86. '<h4>热度走势</h4>',
  87. f'<p>{_escape_html(result.keyword_analysis)}</p>',
  88. '</div>'
  89. ])
  90. if result.sentiment:
  91. html_parts.extend([
  92. '<div class="ai-section">',
  93. '<h4>情感倾向</h4>',
  94. f'<p>{_escape_html(result.sentiment)}</p>',
  95. '</div>'
  96. ])
  97. if result.cross_platform:
  98. html_parts.extend([
  99. '<div class="ai-section">',
  100. '<h4>跨平台关联</h4>',
  101. f'<p>{_escape_html(result.cross_platform)}</p>',
  102. '</div>'
  103. ])
  104. if result.impact:
  105. html_parts.extend([
  106. '<div class="ai-section">',
  107. '<h4>潜在影响</h4>',
  108. f'<p>{_escape_html(result.impact)}</p>',
  109. '</div>'
  110. ])
  111. if result.signals:
  112. html_parts.extend([
  113. '<div class="ai-section">',
  114. '<h4>值得关注</h4>',
  115. f'<p>{_escape_html(result.signals)}</p>',
  116. '</div>'
  117. ])
  118. if result.conclusion:
  119. html_parts.extend([
  120. '<div class="ai-section ai-conclusion">',
  121. '<h4>总结建议</h4>',
  122. f'<p>{_escape_html(result.conclusion)}</p>',
  123. '</div>'
  124. ])
  125. html_parts.append('</div>')
  126. return "\n".join(html_parts)
  127. def render_ai_analysis_plain(result: AIAnalysisResult) -> str:
  128. """渲染为纯文本格式"""
  129. if not result.success:
  130. return f"AI 分析失败: {result.error}"
  131. lines = ["【AI 热点分析】", ""]
  132. if result.summary:
  133. lines.extend(["[趋势概述]", result.summary, ""])
  134. if result.keyword_analysis:
  135. lines.extend(["[热度走势]", result.keyword_analysis, ""])
  136. if result.sentiment:
  137. lines.extend(["[情感倾向]", result.sentiment, ""])
  138. if result.cross_platform:
  139. lines.extend(["[跨平台关联]", result.cross_platform, ""])
  140. if result.impact:
  141. lines.extend(["[潜在影响]", result.impact, ""])
  142. if result.signals:
  143. lines.extend(["[值得关注]", result.signals, ""])
  144. if result.conclusion:
  145. lines.extend(["[总结建议]", result.conclusion])
  146. return "\n".join(lines)
  147. def get_ai_analysis_renderer(channel: str):
  148. """根据渠道获取对应的渲染函数"""
  149. renderers = {
  150. "feishu": render_ai_analysis_feishu,
  151. "dingtalk": render_ai_analysis_dingtalk,
  152. "wework": render_ai_analysis_markdown,
  153. "telegram": render_ai_analysis_markdown,
  154. "email": render_ai_analysis_html,
  155. "ntfy": render_ai_analysis_markdown,
  156. "bark": render_ai_analysis_plain,
  157. "slack": render_ai_analysis_markdown,
  158. }
  159. return renderers.get(channel, render_ai_analysis_markdown)