Ver Fonte

v5.0.0: 集成AI模型,生成简报直推手机

sansan há 4 meses atrás
pai
commit
4c38fb6bb9
43 ficheiros alterados com 4750 adições e 1610 exclusões
  1. 26 172
      .github/ISSUE_TEMPLATE/01-bug-report.yml
  2. 18 75
      .github/ISSUE_TEMPLATE/02-feature-request.yml
  3. 45 0
      .github/ISSUE_TEMPLATE/03-ai-and-config.yml
  4. 0 195
      .github/ISSUE_TEMPLATE/03-config-help.yml
  5. 10 3
      .github/workflows/crawler.yml
  6. 356 133
      README-EN.md
  7. 140 268
      README-MCP-FAQ-EN.md
  8. 140 267
      README-MCP-FAQ.md
  9. 384 152
      README.md
  10. BIN
      _image/feishu.png
  11. 98 0
      config/ai_analysis_prompt.txt
  12. 74 3
      config/config.yaml
  13. 204 26
      config/frequency_words.txt
  14. 22 0
      docker/.env
  15. 9 0
      docker/docker-compose-build.yml
  16. 9 0
      docker/docker-compose.yml
  17. 6 0
      docker/manage.py
  18. 1 1
      mcp_server/__init__.py
  19. 212 30
      mcp_server/server.py
  20. 48 0
      mcp_server/services/cache_service.py
  21. 80 42
      mcp_server/services/data_service.py
  22. 5 1
      mcp_server/services/parser_service.py
  23. 88 56
      mcp_server/tools/analytics.py
  24. 41 21
      mcp_server/tools/data_query.py
  25. 11 19
      mcp_server/tools/search_tools.py
  26. 119 39
      mcp_server/tools/storage_sync.py
  27. 196 10
      mcp_server/tools/system.py
  28. 1 1
      pyproject.toml
  29. 1 1
      trendradar/__init__.py
  30. 242 8
      trendradar/__main__.py
  31. 27 0
      trendradar/ai/__init__.py
  32. 503 0
      trendradar/ai/analyzer.py
  33. 220 0
      trendradar/ai/formatter.py
  34. 14 1
      trendradar/context.py
  35. 28 8
      trendradar/core/frequency.py
  36. 62 0
      trendradar/core/loader.py
  37. 190 32
      trendradar/notification/dispatcher.py
  38. 330 20
      trendradar/notification/senders.py
  39. 479 17
      trendradar/notification/splitter.py
  40. 20 4
      trendradar/report/helpers.py
  41. 289 4
      trendradar/report/html.py
  42. 1 1
      version
  43. 1 0
      version_mcp

+ 26 - 172
.github/ISSUE_TEMPLATE/01-bug-report.yml

@@ -1,206 +1,60 @@
 # yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
 
 name: 🐛 遇到问题了
-description: 程序运行不正常或出现错误
+description: 程序运行不正常、报错或功能失效(含 AI 分析问题)
 title: "[问题] "
 labels: ["bug"]
 body:
   - type: markdown
     attributes:
       value: |
-        **详细清楚的问题描述能帮助项目作者更快理解和解决你遇到的困扰**。强烈建议上传截图,图文并茂会让问题更容易被理解和定位。
-
-        ---
-
-        ### 📋 提交问题前,请先检查以下事项
-
-        #### 1️⃣ **建议先查看文档** 📖
-        大部分常见问题在文档中都有详细说明,建议先查看相关章节:
-        - [📝 配置教程](https://github.com/sansan0/TrendRadar#-快速开始)
-        - [❓ 常见问题](https://github.com/sansan0/TrendRadar#问题答疑与1元点赞)
-        - [🐳 Docker部署](https://github.com/sansan0/TrendRadar#-docker-部署)
-
-        #### 2️⃣ **测试推送功能的注意事项** ⚠️
-        测试消息推送时,请确保以下配置正确:
-
-        **必须检查的配置项:**
-        - ✅ `report.mode` 设置为 `daily` 或 `current`(请勿使用 `incremental`,该模式仅在有新内容时才推送)
-        - ✅ `notification.push_window.enabled` 设置为 `false`(关闭推送时间窗口控制)
-        - ✅ `notification.enable_notification` 设置为 `true`(确保通知功能已启用)
-
-        **说明:**
-        - 推送时间窗口控制(`push_window`)是可选功能,如果开启会限制推送时间范围
-        - 测试时建议关闭此功能,避免因不在推送时间窗口而收不到消息
-
-        #### 3️⃣ **检查配置细节** 🔍
-        部分问题可能是配置细节导致,建议检查:
-        - 配置文件的缩进格式是否正确(YAML 格式要求严格,必须使用空格而非 Tab)
-        - Webhook 地址是否完整复制(注意不要有多余或缺失的字符)
-        - 环境变量是否正确设置
-        - 文件路径是否正确
-
-        #### 4️⃣ **遇到困难时的建议** 💡
-        - 如果尝试 30 分钟以上仍无进展,可以考虑换个思路
-        - 建议重新从头阅读相关文档章节
-        - 或尝试其他部署方式(如从 Docker 切换到 GitHub Actions)
-
-        #### 5️⃣ **根据部署方式提供完整信息** 📦
-
-        **如果是 GitHub Actions 部署:**
-        1. **必须提供** Actions 工作流链接(如:`https://github.com/你的用户名/TrendRadar/actions/workflows/crawler.yml`)
-        2. **查看执行日志的步骤:**
-           - 打开你的仓库页面
-           - 点击顶部的 **Actions** 标签
-           - 点击左侧的 **Crawler** 工作流
-           - 点击最近一次运行记录
-           - 点击 **Run crawler** 查看详细日志
-           - **截图完整的日志内容**(特别是错误部分)
-        3. 提供 `config.yaml` 配置内容(隐藏敏感信息)
-
-        **如果是 Docker 部署:**
-        1. 提供项目目录结构截图(运行 `ls -la` 或打开文件管理器)
-        2. 提供 Docker 容器日志(运行 `docker logs 容器名`)
-        3. 提供容器状态(运行 `docker ps -a`)
-        4. 提供 `.env` 文件内容(隐藏敏感信息)
-
-        **如果是本地运行:**
-        1. 提供完整的错误日志截图
-        2. 提供 `config.yaml` 配置内容
-        3. 提供 Python 版本(运行 `python --version`)
+        **简单的描述 + 关键截图** 是最有效的沟通方式。
 
   - type: dropdown
     id: bug-category
     attributes:
-      label: 🏷️ 遇到了什么问题
-      options:
-        - 数据获取问题(获取不到新闻、请求失败等)
-        - 关键词筛选问题(关键词不生效、匹配异常等)
-        - 通知推送问题(收不到消息、推送失败等)
-        - 配置设置问题(配置文件错误、参数不生效等)
-        - 部署运行问题(Docker、GitHub Actions等)
-        - 性能问题(运行慢、卡顿等)
-        - 其他问题
-    validations:
-      required: true
-
-  - type: dropdown
-    id: environment
-    attributes:
-      label: 🖥️ 使用环境
+      label: 🏷️ 问题类别
       options:
-        - 本地运行(直接在电脑上运行)
-        - Docker 容器运行
-        - GitHub Actions 自动运行
-        - 其他方式
+        - AI 分析相关(报错、内容异常、提示词失效等)
+        - 数据获取相关(爬不到新闻、平台失效等)
+        - 通知推送相关(收不到消息、推送报错等)
+        - 部署运行相关(Docker、Actions、Python 报错)
+        - 其他
     validations:
       required: true
 
   - type: textarea
     id: bug-description
     attributes:
-      label: 📝 详细描述问题
-      description: 请详细说明遇到的问题(建议配合截图说明)
+      label: 📝 描述发生了什么
       placeholder: |
-        请清楚地描述:
-        - 具体发生了什么问题
-        - 问题出现时的情况
-        - 这个问题影响了什么功能
-
-        💡 提示:上传问题截图能提供更多信息。
+        请描述:
+        1. 你在做什么?
+        2. 出现了什么错误?(如果是 AI 问题,请贴出分析失败的错误提示)
+        3. 建议上传一张截图,这比文字更有力!
     validations:
       required: true
 
-  - type: dropdown
-    id: system-info
-    attributes:
-      label: 💻 系统信息
-      description: 你的电脑系统
-      options:
-        - Windows 10
-        - Windows 11
-        - macOS
-        - Ubuntu/Linux
-        - 其他系统
-        - 不确定
-    validations:
-      required: false
-
-  - type: textarea
-    id: reproduction-steps
-    attributes:
-      label: 🔄 怎么重现这个问题
-      description: 如何让这个问题重新出现?(可选,但建议填写)
-      placeholder: |
-        请按步骤描述(建议每个步骤都配截图):
-        1. 我点击了...
-        2. 然后设置了...
-        3. 接着出现了...
-
-        💡 操作过程的截图特别有用!
-    validations:
-      required: false
-
-  - type: textarea
-    id: expected-behavior
-    attributes:
-      label: ✅ 期望的正常情况
-      description: 正常情况下应该是什么样的?(可选)
-      placeholder: |
-        描述你期望看到的正常结果...
-        如果有参考图片就更好了!
-    validations:
-      required: false
-
   - type: textarea
     id: error-logs
     attributes:
-      label: 📋 错误信息
-      description: 程序显示的错误信息或日志(如果有的话)
+      label: 📋 错误日志/配置(可选)
       placeholder: |
-        如果程序显示了错误信息,请完整复制到这里:
-
+        贴出相关的错误日志或 config.yaml 片段(记得隐藏 API Key 等敏感信息):
         ```
-        错误信息内容...
+        在这里贴内容...
         ```
-
     validations:
       required: false
 
-  - type: textarea
-    id: config-info
-    attributes:
-      label: ⚙️ 相关配置
-      description: 与问题相关的配置内容(请隐藏敏感信息如 webhook 地址)
-      placeholder: |
-        相关的配置内容(记得隐藏敏感信息):
-
-        ```yaml
-        notification:
-          enable_notification: true
-          webhooks:
-            feishu_url: "***隐藏***"
-        ```
-
-  - type: textarea
-    id: screenshots
-    attributes:
-      label: 📷 截图补充
-      description: 上传相关截图(强烈推荐!)
-      placeholder: |
-        请拖拽截图到这里,建议包含:
-        - 错误界面截图
-        - 配置设置截图
-        - 操作步骤截图
-
-        💡 截图是最直观的问题说明方式!
-
-  - type: textarea
-    id: additional-context
+  - type: dropdown
+    id: environment
     attributes:
-      label: 📎 其他补充信息
-      description: 其他可能有用的信息
-      placeholder: |
-        - 网络环境特殊情况
-        - 之前是否正常工作过
-        - 最近有没有改动什么设置
-        - 其他你觉得可能相关的信息
+      label: 🖥️ 使用环境
+      options:
+        - Docker (本地/NAS)
+        - GitHub Actions
+        - 本地 Python 运行
+        - MCP Server 客户端 (Cherry Studio等)
+    validations:
+      required: true

+ 18 - 75
.github/ISSUE_TEMPLATE/02-feature-request.yml

@@ -1,96 +1,39 @@
 # yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
 
 name: 💡 我有个想法
-description: 建议新功能或改进现有功能
+description: 建议新功能、推送样式改进或体验优化
 title: "[建议] "
 labels: ["enhancement"]
 body:
   - type: markdown
     attributes:
       value: |
-        ### 💝 温馨提醒
-
-        感谢你的创意想法!如果这个项目对你有帮助,欢迎给项目点个 ⭐ **Star**!
-
-        好的建议让项目变得更加实用。**欢迎用截图或示例**来展示你的想法!
-
-  - type: dropdown
-    id: feature-category
-    attributes:
-      label: 🏷️ 建议类别
-      options:
-        - 数据源相关(新增平台、改进抓取等)
-        - 分析功能相关(算法改进、筛选优化等)
-        - 通知方式相关(新增通知渠道、消息格式等)
-        - 配置管理相关(设置优化、界面改进等)
-        - 部署运维相关(安装简化、监控告警等)
-        - 数据展示相关(报告格式、图表可视化等)
-        - 性能优化相关
-        - 用户体验改进
-        - 其他想法
-    validations:
-      required: true
+        ### 💝 欢迎分享你的创意
+        你的好点子能让 TrendRadar 变得更好!
+        
+        目前主要关注以下方向的改进:
+        - ✨ **AI 分析能力**:更智能的解读、更丰富的分析维度
+        - 🎨 **推送体验**:更好看的排版、更合理的信息展示
+        - 🛠️ **易用性优化**:配置更简单、运行更稳定
+        
+        *注:目前暂不接受新爬虫平台的接入申请,感谢理解。*
 
   - type: textarea
     id: feature-description
     attributes:
-      label: 💭 详细描述你的想法
-      description: 请详细描述你希望添加的功能
+      label: 💭 你的想法是什么?
       placeholder: |
-        请详细描述:
-        - 你希望增加什么功能
-        - 这个功能应该怎么使用
-        - 使用后能达到什么效果
-
-        💡 提示:如果有类似功能的截图作为参考就更好了!
+        请简要描述:
+        - 你希望增加什么功能?
+        - 它能解决什么问题?
+        - 如果有参考的图片或工具,欢迎上传截图。
     validations:
       required: true
 
   - type: textarea
     id: use-case
     attributes:
-      label: 🎯 什么时候会用到这个功能
-      description: 这个功能在什么场景下使用?
-      placeholder: |
-        例如:
-        - 当我需要...的时候
-        - 在...情况下会很方便
-        - 可以解决...问题
-        - 能够帮助...用户
+      label: 🎯 使用场景(可选)
+      placeholder: 例如:当我在...的时候,如果能...就太棒了。
     validations:
-      required: true
-
-  - type: textarea
-    id: implementation-ideas
-    attributes:
-      label: 🛠️ 实现想法(可选)
-      description: 如果你有实现思路,欢迎分享
-      placeholder: |
-        - 功能界面应该怎么设计
-        - 配置应该怎么设置
-        - 参考哪些类似的工具或网站
-        - 其他实现建议
-
-  - type: textarea
-    id: mockups-examples
-    attributes:
-      label: 📷 功能示意图(推荐)
-      description: 上传功能示意图、参考截图或手绘草图
-      placeholder: |
-        请上传:
-        - 功能界面的设计图(手绘也可以)
-        - 类似功能的参考截图
-        - 使用流程的示意图
-
-        💡 可视化的说明最容易理解!
-
-  - type: textarea
-    id: additional-context
-    attributes:
-      label: 📎 其他补充说明
-      description: 其他想要补充的内容
-      placeholder: |
-        - 相关的参考资料链接
-        - 类似功能的其他工具
-        - 更多使用场景说明
-        - 其他相关想法
+      required: false

+ 45 - 0
.github/ISSUE_TEMPLATE/03-ai-and-config.yml

@@ -0,0 +1,45 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
+
+name: ✨ AI 提示词分享与配置求助
+description: 分享你调优的 ai_analysis_prompt.txt 或寻求设置帮助
+title: "[AI/配置] "
+labels: ["config", "AI"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        ### ✨ 提示词分享计划
+        欢迎在此分享你精心调优的 `ai_analysis_prompt.txt` 内容!
+        优秀的提示词可以让 AI 分析更精准、更有趣。
+
+        ---
+        如果是**寻求配置帮助**,请尽量贴出你的错误表现。
+
+  - type: dropdown
+    id: category
+    attributes:
+      label: 🏷️ 目的
+      options:
+        - 分享我的 AI 提示词 (ai_analysis_prompt.txt)
+        - 寻求 AI 分析设置帮助
+        - 寻求基础功能配置帮助 (Webhook/RSS等)
+    validations:
+      required: true
+
+  - type: textarea
+    id: share-content
+    attributes:
+      label: 📄 内容描述
+      placeholder: |
+        - 如果是分享:请贴出你的提示词代码块,并简述它的分析风格。
+        - 如果是求助:请贴出你的配置片段(隐藏 Key)和遇到的现象。
+    validations:
+      required: true
+
+  - type: textarea
+    id: screenshots
+    attributes:
+      label: 📷 效果截图(推荐)
+      placeholder: 拖拽分析结果截图或配置截图到这里。
+    validations:
+      required: false

+ 0 - 195
.github/ISSUE_TEMPLATE/03-config-help.yml

@@ -1,195 +0,0 @@
-# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
-
-name: ⚙️ 设置遇到困难
-description: 配置相关的问题或需要帮助
-title: "[设置] "
-labels: ["配置", "帮助"]
-body:
-  - type: markdown
-    attributes:
-      value: |
-        遇到设置问题时,**请尽可能详细描述你的问题**,并上传配置文件和错误信息的截图,这样能帮助更快定位和解决问题。
-
-        建议先查看项目说明文档,大部分常见问题都有详细说明。
-
-        ---
-
-        ### 📋 配置问题自查清单(提问前建议阅读)
-
-        #### 1️⃣ **优先查看文档** 📚
-        绝大部分配置问题在文档中都有详细说明,建议先查看相关章节:
-        - [🚀 快速开始](https://github.com/sansan0/TrendRadar#-快速开始)
-        - [📝 frequency_words.txt 配置](https://github.com/sansan0/TrendRadar#frequencywordstxt-配置教程)
-        - [🐳 Docker 部署指南](https://github.com/sansan0/TrendRadar#-docker-部署)
-        - [🤖 AI 分析配置](https://github.com/sansan0/TrendRadar#-ai-智能分析部署)
-
-        #### 2️⃣ **测试推送的常见误区** ⚠️
-        测试消息推送时,请检查以下配置:
-
-        **必须检查的配置项:**
-        - ❌ **错误**:`report.mode` 设置为 `incremental`(增量模式仅在有新内容时推送)
-        - ✅ **正确**:`report.mode` 设置为 `daily` 或 `current`
-        - ❌ **错误**:`notification.push_window.enabled` 设置为 `true`(推送时间窗口会限制推送时间)
-        - ✅ **正确**:`notification.push_window.enabled` 设置为 `false`(测试时建议关闭)
-
-        **说明:**
-        - 推送时间窗口控制是可选功能,开启后只在指定时间范围内推送
-        - 如果当前时间不在设定的窗口范围内,将不会收到推送消息
-        - 测试时建议先关闭此功能
-
-        #### 3️⃣ **YAML 格式很严格** 📏
-        配置文件最常见的 3 个错误:
-        ```yaml
-        # ❌ 错误示例 1:缩进不对(必须用空格,不能用Tab)
-        notification:
-        enable_notification: true  # ← 错误:缺少缩进
-
-        # ❌ 错误示例 2:冒号后面没有空格
-        enable_notification:true  # ← 错误:冒号后需要空格
-
-        # ✅ 正确示例:
-        notification:
-          enable_notification: true  # ← 正确:2空格缩进 + 冒号后有空格
-        ```
-
-        #### 4️⃣ **根据部署方式准备信息** 📦
-
-        **如果你是 GitHub Actions 部署:**
-        1. **必须提供** Actions 工作流链接(格式:`https://github.com/你的用户名/TrendRadar/actions/workflows/crawler.yml`)
-        2. **如何查看并截图执行日志:**
-           ```
-           步骤 1:打开你的仓库,点击顶部 "Actions" 标签
-           步骤 2:点击左侧 "Crawler" 工作流
-           步骤 3:点击最近一次运行记录(最上面的那个)
-           步骤 4:点击展开 "Run crawler" 步骤
-           步骤 5:截图完整的日志内容(特别是红色错误部分)
-           ```
-        3. 提供 `config.yaml` 配置内容(记得隐藏 webhook 地址)
-
-        **如果你是 Docker 部署:**
-        1. 提供项目目录结构截图(运行 `ls -la` 或打开文件管理器)
-        2. 提供 Docker 日志(运行 `docker logs 容器名`)
-        3. 提供容器状态(运行 `docker ps -a`)
-        4. 提供 `.env` 文件内容(隐藏敏感信息)
-
-        **如果你是本地运行:**
-        1. 提供完整的错误信息截图
-        2. 提供 `config.yaml` 配置内容
-        3. 提供 Python 版本(运行 `python --version`)
-
-        #### 5️⃣ **遇到困难时的建议** 🤔
-        - 如果尝试 30 分钟以上仍无进展,建议考虑换个思路
-        - 可以尝试:
-          1. 重新从头阅读相关文档章节
-          2. 尝试其他部署方式(如从 Docker 切换到 GitHub Actions)
-          3. 对比文档示例,检查差异之处
-
-        #### 6️⃣ **提问时请尽量提供以下信息** 📋
-        为了更快地帮你定位问题,建议提供:
-        - ✅ 配置文件内容(请隐藏 webhook 等敏感信息)
-        - ✅ 完整的错误日志截图
-        - ✅ 部署方式(本地运行/Docker/GitHub Actions)
-        - ✅ 已经尝试过的解决方法
-        - ✅ 具体的问题现象(请避免只说"不生效"或"没反应",尽量描述具体表现)
-
-  - type: dropdown
-    id: config-type
-    attributes:
-      label: 🏷️ 配置问题类别
-      options:
-        - 基础配置问题(config.yaml 设置)
-        - 通知配置问题(webhook、消息推送等)
-        - 部署配置问题(Docker、GitHub Actions等)
-        - 关键词配置问题(frequency_words.txt 设置)
-        - 环境配置问题(Python、依赖包等)
-        - 其他配置问题
-    validations:
-      required: true
-
-  - type: dropdown
-    id: environment
-    attributes:
-      label: 🖥️ 使用环境
-      options:
-        - 本地运行(直接在电脑上运行)
-        - Docker 容器运行
-        - GitHub Actions 自动运行
-        - 其他方式
-    validations:
-      required: true
-
-  - type: textarea
-    id: problem-description
-    attributes:
-      label: 📝 详细描述问题
-      description: 请详细描述你遇到的设置问题
-      placeholder: |
-        请详细描述:
-        - 遇到的具体问题是什么
-        - 你希望达到什么效果
-        - 已经尝试了哪些方法
-        - 参考了哪些文档或教程
-
-        💡 问题截图能提供更多信息!
-    validations:
-      required: true
-
-  - type: textarea
-    id: config-content
-    attributes:
-      label: 📄 配置内容
-      description: 请提供相关的配置内容(记得隐藏敏感信息如 webhook 地址)
-      placeholder: |
-        请贴出相关的配置内容(记得隐藏 webhook 地址等敏感信息):
-
-        ```yaml
-        notification:
-          enable_notification: true
-          webhooks:
-            feishu_url: "***隐藏***"
-            dingtalk_url: "***隐藏***"
-        ```
-
-        💡 配置文件截图也很有用!
-    validations:
-      required: false
-
-  - type: textarea
-    id: error-messages
-    attributes:
-      label: ❌ 错误信息(如果有的话)
-      description: 如果程序显示了错误信息,请贴出来
-      placeholder: |
-        如果有错误信息,请完整复制到这里:
-
-        ```
-        错误信息内容...
-        ```
-
-        💡 错误信息的截图也很重要!
-
-  - type: textarea
-    id: screenshots
-    attributes:
-      label: 📷 相关截图(强烈推荐)
-      description: 上传配置界面、错误信息等截图
-      placeholder: |
-        请上传相关截图,特别是:
-        - 配置文件内容截图
-        - 错误信息截图
-        - 操作界面截图
-        - 期望效果的参考图
-
-        💡 截图是最直观的问题展示方式!
-
-  - type: textarea
-    id: additional-info
-    attributes:
-      label: 📎 其他补充信息
-      description: 其他可能有用的信息
-      placeholder: |
-        - 操作系统版本(如 Windows 11、macOS)
-        - Python 版本信息
-        - 网络环境特殊情况
-        - 具体使用场景说明
-        - 其他你觉得相关的信息

+ 10 - 3
.github/workflows/crawler.yml

@@ -151,9 +151,16 @@ jobs:
           NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
           BARK_URL: ${{ secrets.BARK_URL }}
           SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-          STORAGE_BACKEND: auto
-          LOCAL_RETENTION_DAYS: ${{ secrets.LOCAL_RETENTION_DAYS }}
-          REMOTE_RETENTION_DAYS: ${{ secrets.REMOTE_RETENTION_DAYS }}
+          # 通用Webhook配置
+          GENERIC_WEBHOOK_URL: ${{ secrets.GENERIC_WEBHOOK_URL }}
+          GENERIC_WEBHOOK_TEMPLATE: ${{ secrets.GENERIC_WEBHOOK_TEMPLATE }}
+          # AI 分析配置
+          AI_ANALYSIS_ENABLED: ${{ secrets.AI_ANALYSIS_ENABLED }}
+          AI_API_KEY: ${{ secrets.AI_API_KEY }}
+          AI_PROVIDER: ${{ secrets.AI_PROVIDER }}
+          AI_MODEL: ${{ secrets.AI_MODEL }}
+          AI_BASE_URL: ${{ secrets.AI_BASE_URL }}
+          # 远程存储配置
           S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
           S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
           S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}

+ 356 - 133
README-EN.md

@@ -13,8 +13,8 @@ Deploy in <strong>30 seconds</strong> — Say goodbye to endless scrolling, only
 [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
 [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v4.7.0-blue.svg)](https://github.com/sansan0/TrendRadar)
-[![MCP](https://img.shields.io/badge/MCP-v2.0.1-green.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.0.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![MCP](https://img.shields.io/badge/MCP-v3.1.5-green.svg)](https://github.com/sansan0/TrendRadar)
 [![RSS](https://img.shields.io/badge/RSS-Feed_Support-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 
 [![WeWork](https://img.shields.io/badge/WeWork-Notification-00D4AA?style=flat-square)](https://work.weixin.qq.com/)
@@ -26,12 +26,14 @@ Deploy in <strong>30 seconds</strong> — Say goodbye to endless scrolling, only
 [![ntfy](https://img.shields.io/badge/ntfy-Notification-00D4AA?style=flat-square)](https://github.com/binwiederhier/ntfy)
 [![Bark](https://img.shields.io/badge/Bark-Notification-00D4AA?style=flat-square)](https://github.com/Finb/Bark)
 [![Slack](https://img.shields.io/badge/Slack-Notification-00D4AA?style=flat-square)](https://slack.com/)
+[![Generic Webhook](https://img.shields.io/badge/Generic-Webhook-607D8B?style=flat-square&logo=webhook&logoColor=white)](#)
 
 
 [![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-Automation-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![GitHub Pages](https://img.shields.io/badge/GitHub_Pages-Deployment-4285F4?style=flat-square&logo=github&logoColor=white)](https://sansan0.github.io/TrendRadar)
 [![Docker](https://img.shields.io/badge/Docker-Deployment-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/r/wantcat/trendradar)
 [![MCP Support](https://img.shields.io/badge/MCP-AI_Analysis-FF6B6B?style=flat-square&logo=ai&logoColor=white)](https://modelcontextprotocol.io/)
+[![AI Analysis Push](https://img.shields.io/badge/AI-Analysis_Push-FF6B6B?style=flat-square&logo=openai&logoColor=white)](#)
 
 </div>
 
@@ -61,12 +63,19 @@ Deploy in <strong>30 seconds</strong> — Say goodbye to endless scrolling, only
 
 <br>
 
-- Thanks to **bug reporters**, your feedback makes this project better 😉
 - Thanks to **stargazers**, your stars and forks are the best support for open source 😍
-- Thanks to **followers**, your interactions make the content more meaningful 😎
 
 <details>
-<summary>👉 Click to view <strong>Acknowledgments</strong> (Current <strong>🔥73🔥</strong> supporters)</summary>
+<summary>👉 Click to view <strong>Acknowledgments</strong> (Angel Round Honor Roll 🔥73+🔥 supporters)</summary>
+
+### Acknowledgments to Early Supporters
+
+> 💡 **Special Note**:
+>
+> 1. **About the List**: The table below records supporters from the early stage (Angel Round) of the project. Due to the manual nature of statistics in the early days, **there may be omissions or incomplete records. If anyone was missed, it was unintentional, and we ask for your kind understanding**.
+> 2. **Future Plan**: To focus limited energy back on code development and feature iteration, **this list will no longer be manually maintained as of today**.
+>
+> Whether your name is on the list or not, your every bit of support is the cornerstone that allows TrendRadar to be where it is today. 🙏
 
 ### Infrastructure Support
 
@@ -92,7 +101,8 @@ After communication, the author indicated no concerns about server pressure, but
 
 > Thanks to **financial supporters**. Your generosity has transformed into snacks and drinks beside my keyboard, accompanying every iteration of this project
 >
-> **"One-yuan appreciation"** has been suspended. If you still want to support the author, please visit the [official account](#-faq--support) article and click "Like Author" at the bottom.
+> **Return of "One-Yuan Appreciation"**:
+> With the release of v5.0.0, the project enters a new phase. To support growing API costs and caffeine consumption, the "One-Yuan Appreciation" channel is now reopened. Every bit of your kindness translates into Tokens and motivation in the code world. 🚀 [Support Now](#-faq--support)
 
 | Supporter | Amount (CNY) | Date | Note |
 | :-------: | :----------: | :--: | :--: |
@@ -143,12 +153,51 @@ After communication, the author indicated no concerns about server pressure, but
 >**📌 Check Latest Updates**: **[Original Repository Changelog](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-changelog)**:
 - **Tip**: Check [Changelog] to understand specific [Features]
 
+### 2026/01/10 - v5.0.0
+
+> **Dev Anecdote**:
+> A salute to a certain 'C' model provider that accompanied me for over two years, only to slap me with `"This organization has been disabled"` right after I renewed my subscription.
+
+**✨ "Five Major Sections" Content Refactoring**
+
+This update refactors the push message structure into five distinct core sections:
+
+1.  **📊 Trending News**: Aggregated trending topics from across the web, precisely filtered by your keywords.
+2.  **📰 RSS Feeds**: Your personalized subscription content, supporting keyword-based grouping.
+3.  **🆕 New Items**: Real-time capture of brand new trending topics since the last run (marked with 🆕).
+4.  **📋 Independent Display**: Complete trending lists or RSS feeds from specified platforms, **completely unaffected by keyword filtering**.
+5.  **✨ AI Analysis**: Deep insights driven by AI, including trend overview, popularity trends, and **critically important** sentiment analysis.
+
+**✨ AI Smart Analysis Push Feature**
+
+- **AI Analysis Integration**: Use AI models to deeply analyze push content, automatically generate trending insights, keyword analysis, cross-platform correlation, potential impact assessment
+- **Sentiment Analysis**: New deep sentiment recognition to accurately capture positive, negative, controversial, or concerned public opinions (v5.0.0 key enhancement)
+- **Multi AI Provider Support**: Supports DeepSeek (default, cost-effective), OpenAI, Google Gemini, and any OpenAI-compatible API
+- **Two Push Modes**: `only_analysis` (AI analysis only), `both` (push both)
+- **Custom Prompts**: Customize AI analysis role and output format via `config/ai_analysis_prompt.txt`
+- **Multi-dimensional Analysis**: AI can analyze ranking changes, trending duration, cross-platform performance, trend prediction
+
+### 2026/01/10 - mcp-v3.0.0~v3.1.5
+
+- **Breaking Change**: All tool return values unified to `{success, summary, data, error}` structure
+- **Async Consistency**: All 21 tool functions wrapped with `asyncio.to_thread()` for sync calls
+- **MCP Resources**: Added 4 resources (platforms, rss-feeds, available-dates, keywords)
+- **RSS Enhancement**: `get_latest_rss` supports multi-day queries (days param), cross-date URL deduplication
+- **Regex Matching Fix**: `get_trending_topics` supports `/pattern/` regex syntax and `display_name`
+- **Cache Optimization**: Added `make_cache_key()` function with param sorting + MD5 hash for consistency
+- **New check_version Tool**: Check TrendRadar and MCP Server version updates simultaneously
+
+
+<details>
+<summary>👉 Click to expand: <strong>Historical Updates</strong></summary>
+
+
 ### 2026/01/02 - v4.7.0
 
 - **Fix RSS HTML Display**: Fixed RSS data format mismatch causing rendering issues, now displays correctly grouped by keyword
 - **New Regex Syntax**: Keyword config supports `/pattern/` regex syntax, solves English substring mismatch issues (e.g., `ai` matching `training`) [📖 View Syntax Details](#keyword-basic-syntax)
 - **New Display Name Syntax**: Use `=> alias` to give complex regex a friendly name, cleaner push notifications (e.g., `/\bai\b/ => AI Related`)
-- **Can't Write Regex?** README now includes AI prompt guide - just tell ChatGPT/Claude/DeepSeek what you want to match
+- **Can't Write Regex?** README now includes AI prompt guide - just tell ChatGPT/Gemini/DeepSeek what you want to match
 
 
 ### 2025/12/30 - mcp-v2.0.0
@@ -158,10 +207,6 @@ After communication, the author indicated no concerns about server pressure, but
 - **Unified Search**: `search_news` supports `include_rss` parameter to search both trending and RSS
 
 
-<details>
-<summary>👉 Click to expand: <strong>Historical Updates</strong></summary>
-
-
 ### 2026/01/01 - v4.6.0
 
 - **Fix RSS HTML Display**: Merged RSS content into trending HTML page, grouped by source
@@ -430,7 +475,7 @@ After communication, the author indicated no concerns about server pressure, but
 
 ### 2025/10/20 - v3.0.0
 
-**Major Update - AI Analysis Feature Launched** 🤖
+**Major Update - AI Analysis Feature Launched** 
 
 - **Core Features**:
   - New MCP (Model Context Protocol) based AI analysis server
@@ -792,6 +837,27 @@ Supports **WeWork** (+ WeChat push solution), **Feishu**, **DingTalk**, **Telegr
 - **Local Running**: Python environment direct execution
 
 
+### **AI Analysis Push (v5.0.0 New)**
+
+Use AI models to deeply analyze push content, automatically generate trending insights report
+
+- **Smart Analysis**: Automatically analyze trending topics, keyword popularity, cross-platform correlation, potential impact
+- **Multi Provider**: Supports DeepSeek, OpenAI, Gemini, and OpenAI-compatible APIs
+- **Flexible Push**: Choose original content only, AI analysis only, or both
+- **Custom Prompts**: Customize analysis perspective via `config/ai_analysis_prompt.txt`
+
+> 💡 Detailed configuration tutorial: [AI Analysis Configuration](#12-ai-analysis-configuration)
+
+### **Independent Display Section (v5.0.0 New)**
+
+Provide complete trending display for specified platforms, unaffected by keyword filtering
+
+- **Full Trending**: Specified platforms show complete trending list, for users who want to see full rankings
+- **RSS Independent Display**: RSS source content can be fully displayed, not limited by keywords
+- **Flexible Configuration**: Support configuring display platforms, RSS sources, max count
+
+> 💡 Detailed configuration tutorial: [Report Configuration - Independent Display](#7-report-configuration)
+
 ### **AI Smart Analysis (v3.0.0 New)**
 
 AI conversational analysis system based on MCP (Model Context Protocol), enabling deep data mining with natural language.
@@ -847,9 +913,9 @@ Transform from "algorithm recommendation captivity" to "actively getting the inf
 * **Recommended**: Configure cloud storage service (Cloudflare R2 free tier is sufficient, Alibaba Cloud OSS, Tencent Cloud COS, etc.)
 * **Note**: Requires periodic check-in renewal (every 7 days)
 
-1️⃣ **Get project code**
+### 1️⃣ Step 1: Get project code
 
-   Click the green **[Use this template]** button at the top right of this repository page → Select "Create a new repository".
+   Click the green **[Use this template]** button in the upper right corner of this repository → select "Create a new repository".
 
    > ⚠️ Note:
    > - Any mention of "Fork" in this document can be understood as "Use this template"
@@ -857,9 +923,9 @@ Transform from "algorithm recommendation captivity" to "actively getting the inf
 
    <br>
 
-2️⃣ **Setup GitHub Secrets**:
+### 2️⃣ Step 2: Setup GitHub Secrets
 
-   In your forked repo, go to `Settings` > `Secrets and variables` > `Actions` > `New repository secret`
+   In your Forked repository, go to `Settings` > `Secrets and variables` > `Actions` > `New repository secret`
 
    **📌 Important Instructions (Please Read Carefully):**
 
@@ -961,9 +1027,6 @@ Method 1 discovered and suggested by **ziventian**, thanks to them. Default is p
 {
   "message_type": "text",
   "content": {
-    "total_titles": "{{Content}}",
-    "timestamp": "{{Content}}",
-    "report_type": "{{Content}}",
     "text": "{{Content}}"
   }
 }
@@ -975,7 +1038,7 @@ Method 1 discovered and suggested by **ziventian**, thanks to them. Default is p
 
 8. Most critical part, click + button, select "Webhook Trigger", then arrange as shown in image
 
-![Feishu Bot Config Example](_image/image.png)
+![Feishu Bot Config Example](_image/feishu.png)
 
 9. After configuration, put Webhook address from step 4 into GitHub Secrets `FEISHU_WEBHOOK_URL`
 
@@ -999,9 +1062,6 @@ Method 1 discovered and suggested by **ziventian**, thanks to them. Default is p
 {
   "message_type": "text",
   "content": {
-    "total_titles": "{{Content}}",
-    "timestamp": "{{Content}}",
-    "report_type": "{{Content}}",
     "text": "{{Content}}"
   }
 }
@@ -1013,7 +1073,7 @@ Method 1 discovered and suggested by **ziventian**, thanks to them. Default is p
 
 9. Most critical part, click + button, select "Webhook Trigger", then arrange as shown in image
 
-![Feishu Bot Config Example](_image/image.png)
+![Feishu Bot Config Example](_image/feishu.png)
 
 10. After configuration, put Webhook address from step 5 into GitHub Secrets `FEISHU_WEBHOOK_URL`
 
@@ -1463,15 +1523,82 @@ Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack
 
 </details>
 
+<details>
+<summary>👉 Click to expand: <strong>Generic Webhook Push</strong> (Supports Discord, Matrix, IFTTT, etc.)</summary>
+<br>
+
+**GitHub Secret Configuration (⚠️ Name must be exact):**
+- **Name**: `GENERIC_WEBHOOK_URL` (copy and paste this name, don't type manually)
+- **Secret**: Your Webhook URL
+
+- **Name**: `GENERIC_WEBHOOK_TEMPLATE` (optional, copy and paste this name)
+- **Secret**: JSON template string, supports `{title}` and `{content}` placeholders
+
+<br>
+
+**Generic Webhook Introduction:**
+
+Generic Webhook supports any platform that accepts HTTP POST requests, including but not limited to:
+- **Discord**: Push to channels via Webhook
+- **Matrix**: Push via Webhook bridge
+- **IFTTT**: Trigger automation workflows
+- **Custom Services**: Any custom service supporting Webhooks
+
+**Configuration Examples:**
+
+### Discord Configuration
+
+1. **Get Webhook URL**:
+   - Go to Discord Server Settings → Integrations → Webhooks
+   - Create new Webhook, copy URL
+
+2. **Configure Template**:
+   ```json
+   {"content": "{content}"}
+   ```
+
+3. **GitHub Secret Configuration**:
+   - `GENERIC_WEBHOOK_URL`: Discord Webhook URL
+   - `GENERIC_WEBHOOK_TEMPLATE`: `{"content": "{content}"}`
+
+### Custom Templates
+
+Templates support two placeholders:
+- `{title}` - Message title
+- `{content}` - Message content
+
+**Template Examples**:
+```json
+# Default format (used when empty)
+{"title": "{title}", "content": "{content}"}
+
+# Discord format
+{"content": "{content}"}
+
+# Custom format
+{"text": "{content}", "username": "TrendRadar"}
+```
+
+---
+
+**Notes:**
+- ✅ Supports Markdown format (same as WeWork format)
+- ✅ Supports automatic batch sending
+- ✅ Supports multi-account configuration (use `;` separator)
+- ⚠️ Template must be valid JSON format
+- ⚠️ Different platforms have different message format requirements, please refer to target platform documentation
+
+</details>
+
 > ⚠️ Note:
 > - For first deployment, suggest completing **GitHub Secrets** configuration first (choose one push platform), then jump to [Step 3] to test push success.
 > - **Don't modify** `config/config.yaml` and `frequency_words.txt` temporarily, adjust these configs after push test succeeds as needed.
 
    <br>
 
-3️⃣ **Manual Test News Push**:
+### 3️⃣ Step 3: Manual Test News Push
 
-   > ⚠️ Note:
+   > ⚠️ Reminder:
    > - Complete Step 1-2 first, then test immediately! Test success first, then adjust configuration (Step 4) as needed.
    > - IMPORTANT: Enter your own forked project, not this project!
 
@@ -1497,9 +1624,9 @@ Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack
 
    <br>
 
-4️⃣ **Configuration Notes (Optional)**:
+### 4️⃣ Step 4: Configuration Notes (Optional)
 
-   Default configuration works normally. Only adjust if you need personalization, understanding these three files:
+   The default configuration is ready to use. If you need personalized adjustments, just understand the following files:
 
    | File | Purpose |
    |------|---------|
@@ -1511,9 +1638,9 @@ Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack
 
    <br>
 
-5️⃣ **GitHub Actions Check-In Mechanism & Remote Cloud Storage Configuration**:
+### 5️⃣ Step 5: GitHub Actions Check-In & Remote Cloud Storage
 
-   **v4.0.0 Important Change**: Introduced "Activity Detection" mechanism—GitHub Actions requires periodic check-in to remain active.
+   **v4.0.0 Important Change**: Introduced the "Activity Detection" mechanism; GitHub Actions need periodic check-ins to maintain operation.
 
    - **Running Cycle**: Valid for **7 days**—service will automatically suspend when countdown ends.
    - **Renewal Method**: Manually trigger the "Check In" workflow on the Actions page to reset the 7-day validity period.
@@ -1586,9 +1713,22 @@ Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack
 
    <br>
 
-6️⃣ **🎉 Deployment Success! Share Your Experience**
+### 6️⃣ Step 6: Enable AI Analysis Push
+
+   This is a core feature of v5.0.0, letting AI summarize and analyze news for you. Highly recommended.
+
+   **Configuration Method:**
+   Add the following to GitHub Secrets (or `.env` / `config.yaml`):
+   - `AI_API_KEY`: Your API Key (Supports DeepSeek, OpenAI, etc.)
+   - `AI_PROVIDER`: Provider name (e.g., `deepseek`, `openai`)
+
+   That's it! No complex deployment needed. You'll see the smart analysis report in the next push.
+
+   <br>
+
+### 7️⃣ Step 7: 🎉 Deployment Success!
 
-   Congratulations on completing the TrendRadar configuration! You can now start tracking trending news.
+   Congratulations! Now you can start enjoying the efficient information flow brought by TrendRadar.
 
    💬 Many users are sharing their experiences on the official account, we look forward to your insights~
 
@@ -1600,16 +1740,18 @@ Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack
 
    <br>
 
-7️⃣ **🤖 Want Smarter Analysis? Try AI-Enhanced Features** (Optional)
+### 8️⃣ Step 8: Advanced: Choose Your AI Assistant
 
-   Basic configuration already meets daily needs, but if you want:
+   TrendRadar provides two ways to use AI to meet different needs:
 
-   - Let AI automatically analyze trending topics and data insights
-   - Search and query news using natural language
-   - Get sentiment analysis, topic prediction, and deep analytics
-   - Directly access data in AI tools like Claude, Cursor, etc.
+   | Feature | ✨ AI Analysis Push (Step 6) | 🧠 AI Smart Analysis |
+   | :--- | :--- | :--- |
+   | **Mode** | **Passive Receipt** (Daily Report) | **Active Conversation** (Deep Research) |
+   | **Scenario** | "What's big today?" | "Analyze AI industry changes over the past week" |
+   | **Deployment** | Minimalist (Just add Key) | Advanced (Requires Local/Docker) |
+   | **Client** | Mobile | PC |
 
-   👉 **Learn More**: [AI Analysis](#-ai-analysis) — Unlock hidden capabilities and make trend tracking more efficient!
+   👉 **Conclusion**: Start with **AI Analysis Push** for daily needs; if you are a data analyst or need deep mining, try **[MCP Smart Analysis](#-ai-analysis)**.
 
 <br>
 
@@ -1809,7 +1951,7 @@ machine learning
 
 **💡 Can't Write Regex? Let AI Help!**
 
-If you're not familiar with regular expressions, just ask ChatGPT / Claude / DeepSeek to generate one:
+If you're not familiar with regular expressions, just ask ChatGPT / Gemini / DeepSeek to generate one:
 
 > I need a Python regex to match the word "ai" but not match "ai" in "training".
 > Please give me the regex in `/pattern/` format without extra explanation.
@@ -2249,6 +2391,7 @@ TrendRadar provides two independent Docker images, deploy according to your need
    # Download config file templates
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/config.yaml -P config/
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/frequency_words.txt -P config/
+   wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/ai_analysis_prompt.txt -P config/
 
    # Download docker compose config
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/docker/.env -P docker/
@@ -2260,16 +2403,18 @@ TrendRadar provides two independent Docker images, deploy according to your need
 current directory/
 ├── config/
 │   ├── config.yaml
-│   └── frequency_words.txt
+│   ├── frequency_words.txt
+│   └── ai_analysis_prompt.txt    # AI analysis prompt (v5.0.0 new, optional)
 └── docker/
     ├── .env
     └── docker-compose.yml
 ```
 
 2. **Config File Description**:
-   - `config/config.yaml` - Application main config (report mode, push settings, etc.)
+   - `config/config.yaml` - Application main config (report mode, push settings, AI analysis, etc.)
    - `config/frequency_words.txt` - Keyword config (set your interested trending keywords)
-   - `.env` - Environment variable config (webhook URLs and scheduled tasks)
+   - `config/ai_analysis_prompt.txt` - AI prompt config (customize AI analysis role and output format, v5.0.0 new)
+   - `.env` - Environment variable config (webhook URLs, API Keys, scheduled tasks)
 
    **⚙️ Environment Variable Override Mechanism (v3.0.5+)**
 
@@ -2280,13 +2425,14 @@ current directory/
    | `ENABLE_CRAWLER` | `advanced.crawler.enabled` | `true` / `false` | Enable crawler |
    | `ENABLE_NOTIFICATION` | `notification.enabled` | `true` / `false` | Enable notification |
    | `REPORT_MODE` | `report.mode` | `daily` / `incremental` / `current`| Report mode |
-   | `MAX_ACCOUNTS_PER_CHANNEL` | `advanced.max_accounts_per_channel` | `3` | Maximum accounts per channel |
-   | `PUSH_WINDOW_ENABLED` | `notification.push_window.enabled` | `true` / `false` | Push time window switch |
-   | `PUSH_WINDOW_START` | `notification.push_window.start` | `08:00` | Push start time |
-   | `PUSH_WINDOW_END` | `notification.push_window.end` | `22:00` | Push end time |
+   | `DISPLAY_MODE` | `report.display_mode` | `keyword` / `platform` | Display mode |
    | `ENABLE_WEBSERVER` | - | `true` / `false` | Auto-start web server |
-   | `WEBSERVER_PORT` | - | `8080` | Web server port (default 8080) |
-   | `FEISHU_WEBHOOK_URL` | `notification.channels.feishu.webhook_url` | `https://...` | Feishu Webhook (supports multi-account, use `;` separator) |
+   | `WEBSERVER_PORT` | - | `8080` | Web server port |
+   | `FEISHU_WEBHOOK_URL` | `notification.channels.feishu.webhook_url` | `https://...` | Feishu Webhook (multi-account use `;` separator) |
+   | `AI_ANALYSIS_ENABLED` | `ai_analysis.enabled` | `true` / `false` | Enable AI analysis (v5.0.0 new) |
+   | `AI_API_KEY` | `ai_analysis.api_key` | `sk-xxx...` | AI API Key (v5.0.0 new) |
+   | `AI_PROVIDER` | `ai_analysis.provider` | `deepseek` / `openai` / `gemini` | AI provider (v5.0.0 new) |
+   | `S3_*` | `storage.remote.*` | - | Remote storage config (5 params) |
 
    **Config Priority**: Environment Variables > config.yaml
 
@@ -2323,7 +2469,7 @@ current directory/
 
    > 💡 **Tips**:
    > - Most users only need to start `trendradar` for news push functionality
-   > - Only start `trendradar-mcp` when using Claude/ChatGPT for AI dialogue analysis
+   > - Only need to start `trendradar-mcp` when using ChatGPT/Gemini for AI dialogue analysis
    > - Both services are independent and can be flexibly combined
 
 4. **Check Running Status**:
@@ -2703,6 +2849,39 @@ SORT_BY_POSITION_FIRST=true
 MAX_NEWS_PER_KEYWORD=10
 ```
 
+#### Independent Display Section Configuration (v5.0.0 New)
+
+Provides full trending list display for specified platforms, unaffected by `frequency_words.txt` keyword filtering.
+
+**Configuration Location:** `notification.standalone_display` section in `config/config.yaml`
+
+```yaml
+notification:
+  standalone_display:
+    enabled: false                    # Enable or not
+    platforms: ["zhihu", "weibo"]     # Trending platform ID list
+    rss_feeds: ["hacker-news"]        # RSS feed ID list
+    max_items: 20                     # Max display count per source (0=unlimited)
+```
+
+**Use Cases:**
+- Want to view the complete trending ranking of a platform (like Zhihu) instead of just keyword-matched content
+- Subscribed to RSS feeds with few updates (like personal blogs) and want full push every time
+
+**Effect Example:**
+```
+📋 Independent Display Section (Total 15 items)
+
+Zhihu Trending (10 items):
+  1. [Zhihu] How to view OpenAI releasing Sora?
+  2. [Zhihu] 2024 postgraduate entrance exam scores released...
+  ...
+
+Hacker News (5 items):
+  1. [Hacker News] Launch HN: TrendRadar...
+  ...
+```
+
 </details>
 
 ### 8. Push Window Configuration
@@ -3293,9 +3472,126 @@ export TIMEZONE=Asia/Shanghai
 
 </details>
 
+### 12. AI Analysis Configuration
+
+<details id="ai-analysis-config">
+<summary>👉 Click to expand: <strong>AI Analysis Push Configuration Guide</strong></summary>
 <br>
 
-## 🤖 AI Analysis
+#### Feature Overview
+
+v5.0.0 adds AI analysis push feature, using AI models to deeply analyze push content and automatically generate trending insights report.
+
+**Analysis includes**:
+- Trending topic overview
+- Keyword popularity analysis
+- Cross-platform correlation analysis
+- Potential impact assessment
+- Signals worth attention
+- Summary and recommendations
+
+#### Configuration Location
+
+**Config file**: `ai_analysis` section in `config/config.yaml`
+
+```yaml
+ai_analysis:
+  enabled: false                    # Enable AI analysis
+  provider: "deepseek"              # AI provider
+  api_key: ""                       # API Key (recommend using AI_API_KEY env var)
+  model: "deepseek-chat"            # Model name
+  base_url: ""                      # Custom API endpoint (optional)
+  timeout: 90                       # Request timeout (seconds)
+  push_mode: "both"                 # Push mode
+  max_news_for_analysis: 50         # Max news items to analyze
+  include_rss: true                 # Include RSS content
+  prompt_file: "ai_analysis_prompt.txt"  # Prompt config file
+```
+
+#### Supported AI Providers
+
+| Provider | provider value | Default endpoint |
+|----------|---------------|------------------|
+| **DeepSeek** | `deepseek` | https://api.deepseek.com/v1/chat/completions |
+| **OpenAI** | `openai` | https://api.openai.com/v1/chat/completions |
+| **Google Gemini** | `gemini` | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
+| **Custom** | `custom` | Requires base_url |
+
+> 💡 **Tip**: When using `custom` provider, `base_url` must be the complete API address (e.g., `https://api.example.com/v1/chat/completions`)
+
+#### Push Mode Description
+
+| Mode | Description |
+|------|-------------|
+| `only_analysis` | Push AI analysis result only, no original content |
+| `both` | Push both (default), AI analysis appended after original content |
+
+> 💡 **Tip**: If you don't need AI analysis, set `enabled` to `false` instead of using `push_mode`
+
+#### Environment Variable Support
+
+| Environment Variable | Description | Example |
+|---------------------|-------------|---------|
+| `AI_ANALYSIS_ENABLED` | Enable AI analysis | `true` / `false` |
+| `AI_API_KEY` | AI API Key | `sk-xxx...` |
+| `AI_PROVIDER` | AI provider | `deepseek` / `openai` / `gemini` / `custom` |
+| `AI_MODEL` | Model name | `deepseek-chat` |
+| `AI_BASE_URL` | Complete API address (required for custom) | `https://api.example.com/v1/chat/completions` |
+
+#### Custom Prompts
+
+Edit `config/ai_analysis_prompt.txt` to customize AI analysis role and output format.
+
+**File structure**:
+```
+[system]
+System prompt, define AI role and analysis principles
+...
+
+[user]
+User prompt template, supports variable substitution
+...
+```
+
+**Available variables**:
+- `{report_mode}` - Current report mode
+- `{report_type}` - Report type description
+- `{current_time}` - Current time
+- `{news_count}` - Trending news count
+- `{rss_count}` - RSS news count
+- `{keywords}` - Matched keywords list
+- `{platforms}` - Data source platforms list
+- `{news_content}` - News content
+
+#### Quick Enable Example
+
+**Method 1: Config file**
+
+```yaml
+ai_analysis:
+  enabled: true
+  provider: "deepseek"
+  api_key: "sk-your-api-key"
+  model: "deepseek-chat"
+  push_mode: "both"
+```
+
+**Method 2: Environment variables (Recommended)**
+
+```bash
+# GitHub Actions: Add to Secrets
+# Docker: Add to .env file
+AI_ANALYSIS_ENABLED=true
+AI_API_KEY=sk-your-api-key
+AI_PROVIDER=deepseek
+AI_MODEL=deepseek-chat
+```
+
+</details>
+
+<br>
+
+## ✨ AI Analysis
 
 TrendRadar v3.0.0 added **MCP (Model Context Protocol)** based AI analysis feature, allowing natural language conversations with news data for deep analysis.
 
@@ -3358,43 +3654,6 @@ TrendRadar MCP service supports standard Model Context Protocol (MCP), can conne
 - Windows paths use double backslashes: `C:\\Users\\YourName\\TrendRadar`
 - Remember to restart after saving
 
-<details>
-<summary><b>👉 Click to expand: Claude Desktop</b></summary>
-
-#### Config File Method
-
-Edit Claude Desktop's MCP config file:
-
-**Windows**:
-`%APPDATA%\Claude\claude_desktop_config.json`
-
-**Mac**:
-`~/Library/Application Support/Claude/claude_desktop_config.json`
-
-**Config Content**:
-```json
-{
-  "mcpServers": {
-    "trendradar": {
-      "command": "uv",
-      "args": [
-        "--directory",
-        "/path/to/TrendRadar",
-        "run",
-        "python",
-        "-m",
-        "mcp_server.server"
-      ],
-      "env": {},
-      "disabled": false,
-      "alwaysAllow": []
-    }
-  }
-}
-```
-
-</details>
-
 <details>
 <summary><b>👉 Click to expand: Cursor</b></summary>
 
@@ -3528,38 +3787,6 @@ Search "Bitcoin" related news and analyze sentiment
 
 </details>
 
-<details>
-<summary><b>👉 Click to expand: Claude Code CLI</b></summary>
-
-#### HTTP Mode Configuration
-
-```bash
-# 1. Start HTTP service
-# Windows: start-http.bat
-# Mac/Linux: ./start-http.sh
-
-# 2. Add MCP server
-claude mcp add --transport http trendradar http://localhost:3333/mcp
-
-# 3. Verify connection (ensure service started)
-claude mcp list
-```
-
-#### Usage Examples
-
-```bash
-# Query news
-claude "Search today's Zhihu trending news, top 10"
-
-# Trend analysis
-claude "Analyze 'artificial intelligence' topic popularity trend for the past week"
-
-# Data comparison
-claude "Compare Zhihu and Weibo platform attention on 'Bitcoin'"
-```
-
-</details>
-
 <details>
 <summary><b>👉 Click to expand: MCP Inspector</b> (Debug Tool)</summary>
 <br>
@@ -3725,24 +3952,20 @@ Any client supporting Model Context Protocol can connect to TrendRadar:
 
 ## ☕ FAQ & Support
 
-> If you want to support this project, you can search **Tencent Charity** on WeChat and donate to **Education Support Programs** as you wish
->
-> Thanks to those who participated in the **one-yuan donation**! You are listed in the **Acknowledgments** at the top. Your support gives more motivation to open source maintenance. Personal donation QR code has been removed.
+> If this project is helpful to you, you can choose the following ways to support:
+> 1. **Public Welfare**: Search for **Tencent Charity** on WeChat and donate to **Education Support** related projects as you wish.
 >
-> 🎯 Interested in sponsoring this project? Your banner will be displayed in the Sponsors section at the top.
-
-- **GitHub Issues**: Suitable for targeted answers. Please provide complete info when asking (screenshots, error logs, system environment, etc.)
-- **WeChat Official Account**: Suitable for quick consultation. Suggest priority to communicate in public comment area of related articles. If private message, please use polite language 😉
-- **Contact**: path@linux.do
+> 2. **Sponsor the Developer**: Your sponsorship will be used to replenish caffeine for the carbon-based organism and Token consumption for the silicon-based organism.
 
 
-<div align="center">
+- **GitHub Issues**: Suitable for targeted answers. Please provide complete info when asking (screenshots, error logs, system environment, etc.).
+- **Official Account**: Suitable for quick consultation. Suggest priority to communicate in public comment area of related articles. If private message, please use polite language 😉
+- **Contact**: path@linux.do
 
-| WeChat Official Account |
-|:---:|
-| <img src="_image/weixin.png" width="400" title="Silicon Tea Room"/> |
 
-</div>
+| Official Account | WeChat Appreciation | Alipay Appreciation |
+|:---:|:---:|:---:| 
+| <img src="_image/weixin.png" width="300" title="Silicon Tea Room"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="WeChat Pay"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="Alipay"/> |
 
 <br>
 

+ 140 - 268
README-MCP-FAQ-EN.md

@@ -6,7 +6,38 @@
 
 # TrendRadar MCP Tool Usage Q&A
 
-> AI Query Guide - How to Use News Trend Analysis Tools Through Conversation
+> AI Query Guide - How to Use News Trend Analysis Tools Through Natural Conversation (v3.1.5)
+
+---
+
+## 📋 Tools Overview
+
+| Category | Tool Name | Description |
+|:--------:|-----------|-------------|
+| **Date** | `resolve_date_range` | Parse "this week", "last 7 days" to standard dates |
+| **Query** | `get_latest_news` | Get the latest batch of trending news |
+| | `get_news_by_date` | Query historical news by date range |
+| | `get_trending_topics` | Get trending topics statistics (auto-extract supported) |
+| **RSS** | `get_latest_rss` | Get latest RSS subscription content |
+| | `search_rss` | Search keywords in RSS data |
+| | `get_rss_feeds_status` | View RSS feed config and data status |
+| **Search** | `search_news` | Unified search (keyword/fuzzy/entity, RSS optional) |
+| | `find_related_news` | Find news similar to a given title |
+| **Analysis** | `analyze_topic_trend` | Topic trend analysis (hotness/lifecycle/viral/predict) |
+| | `analyze_data_insights` | Data insights (platform compare/activity/co-occurrence) |
+| | `analyze_sentiment` | News sentiment analysis |
+| | `aggregate_news` | Cross-platform news aggregation & dedup |
+| | `compare_periods` | Period comparison (week-over-week/month-over-month) |
+| | `generate_summary_report` | Generate daily/weekly summary reports |
+| **System** | `get_current_config` | Get current system configuration |
+| | `get_system_status` | Get system running status |
+| | `check_version` | Check version updates (TrendRadar + MCP Server) |
+| | `trigger_crawl` | Manually trigger a crawl task |
+| **Storage** | `sync_from_remote` | Pull data from remote storage to local |
+| | `get_storage_status` | Get storage config and status |
+| | `list_available_dates` | List available dates (local/remote) |
+
+---
 
 ## ⚙️ Default Settings Explanation (Important!)
 
@@ -19,9 +50,9 @@ The following optimization strategies are adopted by default, mainly to save AI
 | **URL Links** | Default no links (saves ~160 tokens/item) | Say "need links" or "include URLs" |
 | **Keyword List** | Default does not use frequency_words.txt to filter news | Only used when calling "trending topics" tool |
 
-**⚠️ Important:** The choice of AI model directly affects the tool call effectiveness. The smarter the AI, the more accurate the calls. When you remove the above restrictions, for example, from querying today to querying a week, first you need to have a week's data locally, and secondly, token consumption may multiply (why "may", for example, if I query "analyze 'Apple' trend in the last week", if there isn't much Apple news in that week, then token consumption may actually be less).
+**⚠️ Important:** The choice of AI model directly affects the tool call effectiveness. The smarter the AI, the more accurate the calls. When you remove the above restrictions, for example, from querying today to querying a week, first you need to have a week's data locally, and secondly, token consumption may multiply.
 
-**💡 Tip:** This project provides a dedicated date parsing tool `resolve_date_range`, which can accurately parse natural language date expressions like "last 7 days", "this week", ensuring all AI models get consistent date ranges. Recommended to use this tool first, see Q18 below for details.
+**💡 Tip:** This project provides a dedicated date parsing tool that can accurately parse natural language date expressions like "last 7 days", "this week", ensuring all AI models get consistent date ranges. See Q18 below for details.
 
 
 ## 💰 AI Models
@@ -65,6 +96,8 @@ Now you can start using this project and enjoy stable and fast AI services!
 After testing one query, please immediately check the [SiliconFlow Billing](https://cloud.siliconflow.cn/me/bills) to see the consumption and have an estimate in mind.
 
 
+---
+
 ## Basic Queries
 
 ### Q1: How to view the latest news?
@@ -76,12 +109,10 @@ After testing one query, please immediately check the [SiliconFlow Billing](http
 - "Get the latest 10 news from Zhihu and Weibo"
 - "View latest news, need links included"
 
-**Tool called:** `get_latest_news`
-
 **Tool return behavior:**
 
-- MCP tool returns the latest 50 news items from all platforms to AI
-- Does not include URL links (saves tokens)
+- Tool returns the latest 50 news items from all platforms
+- Does not include URL links by default (saves tokens)
 
 **AI display behavior (Important):**
 
@@ -108,19 +139,17 @@ After testing one query, please immediately check the [SiliconFlow Billing](http
 - "News from last Monday"
 - "Show me the latest news" (automatically queries today)
 
-**Tool called:** `get_news_by_date`
-
 **Supported date formats:**
 
 - Relative dates: today, yesterday, day before yesterday, 3 days ago
-- Days of week: last Monday, this Wednesday, last monday
+- Days of week: last Monday, this Wednesday
 - Absolute dates: 2025-10-10, October 10
 
 **Tool return behavior:**
 
 - Automatically queries today when date not specified (saves tokens)
-- MCP tool returns 50 news items from all platforms to AI
-- Does not include URL links
+- Tool returns 50 news items from all platforms
+- Does not include URL links by default
 
 **AI display behavior (Important):**
 
@@ -137,24 +166,12 @@ After testing one query, please immediately check the [SiliconFlow Billing](http
 - "Automatically analyze what hot topics are in today's news" (auto extract)
 - "See what are the hottest words in the news" (auto extract)
 
-**Tool called:** `get_trending_topics`
-
 **Two extraction modes:**
 
 | Mode | Description | Example Question |
 |------|------|---------|
-| **keywords** | Count preset followed words (based on `config/frequency_words.txt`, default) | "How many times did my followed words appear" |
-| **auto_extract** | Auto-extract high-frequency words from news titles (no preset needed) | "Auto-analyze hot topics" |
-
-**Usage examples:**
-
-```
-# Use preset followed words (default mode)
-get_trending_topics(mode="current")
-
-# Auto-extract high-frequency words (new feature)
-get_trending_topics(extract_mode="auto_extract", top_n=20)
-```
+| **Preset keywords** | Count preset followed words (based on config file, default) | "How many times did my followed words appear" |
+| **Auto extract** | Auto-extract high-frequency words from news titles (no preset needed) | "Auto-analyze hot topics" |
 
 ---
 
@@ -168,14 +185,16 @@ get_trending_topics(extract_mode="auto_extract", top_n=20)
 - "Get the latest articles from Hacker News"
 - "View latest 20 items from all RSS feeds"
 - "Get RSS feeds, need to include summaries"
-
-**Tool called:** `get_latest_rss`
+- "Show me RSS content from the last week" (multi-day query support)
+- "Get Hacker News articles from last 7 days"
 
 **Tool return behavior:**
 
-- MCP tool returns the latest 50 RSS items to AI
+- Returns today's RSS items by default (up to 50)
+- Supports `days` parameter for multi-day queries (1-30 days)
 - Does not include summaries by default (saves tokens)
 - Sorted by publication time in descending order
+- Auto-deduplication across dates (by URL)
 
 **AI display behavior (Important):**
 
@@ -185,22 +204,10 @@ get_trending_topics(extract_mode="auto_extract", top_n=20)
 **Can be adjusted:**
 
 - Specify RSS feed: like "only Hacker News"
+- Specify days: like "last 7 days", "past week"
 - Adjust quantity: like "return top 20"
 - Include summary: like "need summaries"
 
-**Usage examples:**
-
-```
-# Get latest content from all RSS feeds
-get_latest_rss()
-
-# Get from specified feed
-get_latest_rss(feeds=['hacker-news'])
-
-# Include summaries
-get_latest_rss(include_summary=True, limit=20)
-```
-
 ---
 
 ### Q4.2: How to search content in RSS feeds?
@@ -211,13 +218,11 @@ get_latest_rss(include_summary=True, limit=20)
 - "Search RSS content about 'machine learning' from last 7 days"
 - "Search 'Python' in Hacker News"
 
-**Tool called:** `search_rss`
-
 **Tool return behavior:**
 
 - Searches RSS item titles using keywords
 - Default searches last 7 days of data
-- MCP tool returns up to 50 results to AI
+- Tool returns up to 50 results
 
 **Can be adjusted:**
 
@@ -225,16 +230,6 @@ get_latest_rss(include_summary=True, limit=20)
 - Adjust days: like "search last 14 days"
 - Include summary: like "need summaries"
 
-**Usage examples:**
-
-```
-# Search all RSS feeds
-search_rss(keyword="AI")
-
-# Search specified feed and days
-search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
-```
-
 ---
 
 ### Q4.3: How to view RSS feed status?
@@ -245,16 +240,14 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - "How much data has RSS crawled"
 - "Which RSS feeds have data"
 
-**Tool called:** `get_rss_feeds_status`
-
 **Return information:**
 
 | Field | Description |
 |-------|-------------|
-| **available_dates** | List of dates with RSS data |
-| **total_dates** | Total date count |
-| **today_feeds** | Today's data statistics by RSS feed |
-| **generated_at** | Generation time |
+| **Available dates** | List of dates with RSS data |
+| **Total date count** | How many days of data total |
+| **Today's feed stats** | Today's data statistics by RSS feed |
+| **Generation time** | Status generation time |
 
 ---
 
@@ -271,15 +264,13 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - "Find news about 'Tesla' from January 1 to 7, 2025"
 - "Find the link to the news 'iPhone 16 release'"
 
-**Tool called:** `search_news`
-
 **Tool return behavior:**
 
 - Uses keyword mode search
 - Default searches today's data
 - AI automatically converts relative time like "last 7 days", "last week" to specific date ranges
-- MCP tool returns up to 50 results to AI
-- Does not include URL links
+- Tool returns up to 50 results
+- Does not include URL links by default
 
 **AI display behavior (Important):**
 
@@ -295,18 +286,6 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - Adjust sorting: like "sort by weight"
 - Include links: like "need links"
 
-**Recommended usage flow:**
-
-```
-User: Search for news about "AI breakthrough" in the last 7 days
-Recommended steps:
-1. First call resolve_date_range("last 7 days") to get precise date range
-2. Then call search_news with the date range
-
-User: Find "Tesla" reports from January 2025
-AI: (date_range={"start": "2025-01-01", "end": "2025-01-31"})
-```
-
 ---
 
 ### Q4.4: How to search both hot news and RSS content simultaneously?
@@ -317,8 +296,6 @@ AI: (date_range={"start": "2025-01-01", "end": "2025-01-31"})
 - "Find news about 'artificial intelligence', also search RSS subscriptions"
 - "Search for 'Tesla', both hot news and RSS"
 
-**Tool called:** `search_news` (with `include_rss=True`)
-
 **Tool return behavior:**
 
 - Hot news results and RSS results are **displayed separately**
@@ -326,44 +303,11 @@ AI: (date_range={"start": "2025-01-01", "end": "2025-01-31"})
 - RSS results do not affect hot news ranking display
 - Default returns 50 hot news + 20 RSS items
 
-**Return structure:**
-
-```json
-{
-  "results": [
-    // Hot news (sorted by rank)
-    {"title": "...", "platform": "zhihu", "rank": 1, ...}
-  ],
-  "rss": [
-    // RSS content (separate section)
-    {"title": "...", "feed_name": "Hacker News", ...}
-  ],
-  "summary": {
-    "total_found": 15,
-    "rss_found": 8,
-    "include_rss": true
-  }
-}
-```
-
 **Can be adjusted:**
 
-- RSS count: like "return 10 RSS items" (`rss_limit=10`)
+- RSS count: like "return 10 RSS items"
 - Only search hot news: don't say "including RSS" (default behavior)
-- Only search RSS: use `search_rss` tool
-
-**Code examples:**
-
-```python
-# Search both hot news and RSS
-search_news(query="AI", include_rss=True)
-
-# Adjust RSS return count
-search_news(query="AI", include_rss=True, rss_limit=10)
-
-# Only search hot news (default)
-search_news(query="AI")
-```
+- Only search RSS: say "only search in RSS"
 
 ---
 
@@ -376,22 +320,20 @@ search_news(query="AI")
 - "Search for historical reports about 'Tesla' from last week" (history)
 - "See if there are reports similar to this news in the last 7 days" (history)
 
-**Tool called:** `find_related_news`
-
 **Supported time ranges:**
 
 | Method | Description | Example |
 |--------|-------------|---------|
 | Not specified | Only query today's data (default) | "Find similar news" |
-| Preset values | yesterday, last_week, last_month | "Find related news from yesterday" |
-| Date range | `{"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}` | "Find related reports from Jan 1 to 7" |
+| Preset values | yesterday, last week, last month | "Find related news from yesterday" |
+| Date range | Specify start and end dates | "Find related reports from Jan 1 to 7" |
 
 **Tool return behavior:**
 
 - Similarity threshold 0.5 (adjustable)
-- MCP tool returns up to 50 results to AI
+- Tool returns up to 50 results
 - Sorted by similarity
-- Does not include URL links
+- Does not include URL links by default
 
 **AI display behavior (Important):**
 
@@ -418,32 +360,21 @@ search_news(query="AI")
 - "Predict potential hot topics coming up"
 - "Analyze the lifecycle of 'Bitcoin' in December 2024"
 
-**Tool called:** `analyze_topic_trend`
+**Four analysis modes:**
+
+| Mode | Description | Example Question |
+|------|------|---------|
+| **Heat trend** | Track topic heat changes | "Analyze 'AI' heat trend" |
+| **Lifecycle** | Complete cycle from emergence to disappearance | "See if 'XX' is a flash in the pan or sustained hot topic" |
+| **Anomaly detection** | Identify suddenly viral topics | "What topics suddenly went viral today" |
+| **Prediction** | Predict future hot topics | "Predict upcoming hot topics" |
 
 **Tool return behavior:**
 
-- Supports multiple analysis modes: heat trend, lifecycle, anomaly detection, prediction
 - AI automatically converts relative time like "last week" to specific date ranges
 - Default analyzes last 7 days of data
 - Statistics by day granularity
 
-**AI display behavior:**
-
-- Usually displays trend analysis results and charts
-- AI may summarize key findings
-
-**Recommended usage flow:**
-
-```
-User: Analyze the lifecycle of 'artificial intelligence' in the last week
-Recommended steps:
-1. First call resolve_date_range("last week") to get precise date range
-2. Then call analyze_topic_trend with the date range
-
-User: See if 'Bitcoin' in December 2024 is a flash in the pan or sustained hot topic
-AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
-```
-
 ---
 
 ## Data Insights
@@ -456,8 +387,6 @@ AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "See which platform updates most frequently"
 - "Analyze which keywords often appear together"
 
-**Tool called:** `analyze_data_insights`
-
 **Three insight modes:**
 
 | Mode | Function | Example Question |
@@ -468,15 +397,10 @@ AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
 
 **Tool return behavior:**
 
-- Platform compare mode
+- Default uses platform compare mode
 - Analyzes today's data
 - Keyword co-occurrence minimum frequency 3 times
 
-**AI display behavior:**
-
-- Usually displays analysis results and statistical data
-- AI may summarize insight findings
-
 ---
 
 ## Sentiment Analysis
@@ -490,14 +414,12 @@ AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "Analyze different platforms' sentiment towards 'artificial intelligence'"
 - "See the sentiment of 'Bitcoin' within a week, choose the top 20 most important"
 
-**Tool called:** `analyze_sentiment`
-
 **Tool return behavior:**
 
-- Analyzes today's data
-- MCP tool returns up to 50 news items to AI
+- Default analyzes today's data
+- Tool returns up to 50 news items
 - Sorted by weight (prioritizing important news)
-- Does not include URL links
+- Does not include URL links by default
 
 **AI display behavior (Important):**
 
@@ -522,8 +444,6 @@ AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "Show me deduplicated hotspot news"
 - "Which news are cross-platform hot topics"
 
-**Tool called:** `aggregate_news`
-
 **Tool functionality:**
 
 - Automatically identifies the same event reported by different platforms
@@ -535,33 +455,20 @@ AI: (date_range={"start": "2024-12-01", "end": "2024-12-31"})
 
 | Field | Description |
 |-------|-------------|
-| **representative_title** | Representative title |
-| **platforms** | List of covered platforms |
-| **platform_count** | Number of covered platforms |
-| **is_cross_platform** | Whether it's cross-platform news |
-| **best_rank** | Best ranking |
-| **aggregate_weight** | Comprehensive weight |
-| **sources** | Details from each platform source |
+| **Representative title** | Representative title of this news group |
+| **Covered platforms** | Which platforms reported this news |
+| **Platform count** | How many platforms covered |
+| **Is cross-platform** | Whether it's a cross-platform hot topic |
+| **Best rank** | Best ranking across platforms |
+| **Comprehensive weight** | Comprehensive heat score |
+| **Platform sources** | Detailed info from each platform |
 
 **Can be adjusted:**
 
 - Specify time: like "from last week"
-- Adjust similarity threshold: like "stricter matching" (0.8) or "looser matching" (0.5)
+- Adjust similarity threshold: like "stricter matching" or "looser matching"
 - Specify platform: like "only Zhihu and Weibo"
 
-**Usage examples:**
-
-```
-# Default aggregate today's news
-aggregate_news()
-
-# Stricter similarity matching
-aggregate_news(similarity_threshold=0.8)
-
-# Specify date range
-aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
-```
-
 ---
 
 ### Q10: How to generate daily or weekly hotspot summaries?
@@ -572,8 +479,6 @@ aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
 - "Give me a weekly hotspot summary"
 - "Generate news analysis report for the past 7 days"
 
-**Tool called:** `generate_summary_report`
-
 **Report types:**
 
 - Daily summary: Summarizes the day's hotspot news
@@ -590,39 +495,20 @@ aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
 - "Analyze 'artificial intelligence' heat difference in two periods"
 - "Compare platform activity changes"
 
-**Tool called:** `compare_periods`
-
 **Three comparison modes:**
 
 | Mode | Description | Use Case |
 |------|-------------|----------|
-| **overview** | Overall overview | News count change, keyword change, TOP news comparison |
-| **topic_shift** | Topic change analysis | Rising topics, falling topics, newly appeared topics |
-| **platform_activity** | Platform activity comparison | News count change by platform, fastest/slowest growing platforms |
+| **Overview** | News count change, keyword change, TOP news comparison | Quick understanding of overall changes |
+| **Topic shift** | Rising topics, falling topics, newly appeared topics | Analyze hotspot migration |
+| **Platform activity** | News count change by platform | Understand platform dynamics |
 
 **Time period presets:**
 
-- `today` / `yesterday`: Today/Yesterday
-- `this_week` / `last_week`: This week/Last week
-- `this_month` / `last_month`: This month/Last month
-- Or use custom date range: `{"start": "2025-01-01", "end": "2025-01-07"}`
-
-**Usage examples:**
-
-```
-# Week-over-week analysis
-compare_periods(period1="last_week", period2="this_week")
-
-# Topic shift analysis
-compare_periods(period1="last_month", period2="this_month", compare_type="topic_shift")
-
-# Focus on specific topic
-compare_periods(
-    period1={"start": "2025-01-01", "end": "2025-01-07"},
-    period2={"start": "2025-01-08", "end": "2025-01-14"},
-    topic="artificial intelligence"
-)
-```
+- Today / Yesterday
+- This week / Last week
+- This month / Last month
+- Or use custom date range
 
 ---
 
@@ -637,8 +523,6 @@ compare_periods(
 - "What platforms are available?"
 - "What's the current weight configuration?"
 
-**Tool called:** `get_current_config`
-
 **Can query:**
 
 - Available platform list
@@ -657,8 +541,6 @@ compare_periods(
 - "When was the last crawl?"
 - "How many days of historical data?"
 
-**Tool called:** `get_system_status`
-
 **Return information:**
 
 - System version and status
@@ -668,6 +550,35 @@ compare_periods(
 
 ---
 
+### Q13.1: How to check for version updates?
+
+**You can ask like this:**
+
+- "Check for version updates"
+- "Is there a new version?"
+- "Is the current version up to date?"
+
+**Return information:**
+
+Will check both components' versions simultaneously:
+
+| Component | Description |
+|-----------|-------------|
+| **TrendRadar** | Core crawler and analysis engine |
+| **MCP Server** | AI conversation tool service |
+
+For each component, you'll get:
+- Currently installed version
+- Latest available version
+- Whether an update is needed
+- Update recommendation
+
+**Can be adjusted:**
+
+- If GitHub access is slow, say "check version updates, use proxy http://127.0.0.1:10801"
+
+---
+
 ### Q14: How to manually trigger a crawl task?
 
 **You can ask like this:**
@@ -677,8 +588,6 @@ compare_periods(
 - "Trigger a crawl and save data" (persistent)
 - "Get real-time data from 36Kr but don't save" (temporary query)
 
-**Tool called:** `trigger_crawl`
-
 **Two modes:**
 
 | Mode | Purpose | Example |
@@ -688,9 +597,9 @@ compare_periods(
 
 **Tool return behavior:**
 
-- Temporary crawl mode (no save)
-- Crawls all platforms
-- Does not include URL links
+- Default is temporary crawl mode (no save)
+- Default crawls all platforms
+- Does not include URL links by default
 
 **AI display behavior (Important):**
 
@@ -715,8 +624,6 @@ compare_periods(
 - "Pull data from remote storage to local"
 - "Sync last 30 days of news data"
 
-**Tool called:** `sync_from_remote`
-
 **Use cases:**
 
 - Crawler deployed in the cloud (e.g., GitHub Actions), data stored remotely (e.g., Cloudflare R2)
@@ -724,18 +631,18 @@ compare_periods(
 
 **Return information:**
 
-- synced_files: Number of successfully synced files
-- synced_dates: List of successfully synced dates
-- skipped_dates: Skipped dates (already exist locally)
-- failed_dates: Failed dates and error information
+- Number of successfully synced files
+- List of successfully synced dates
+- Skipped dates (already exist locally)
+- Failed dates and error information
 
 **Prerequisites:**
 
-Need to configure remote storage in `config/config.yaml` or set environment variables:
-- `S3_ENDPOINT_URL`: Service endpoint
-- `S3_BUCKET_NAME`: Bucket name
-- `S3_ACCESS_KEY_ID`: Access key ID
-- `S3_SECRET_ACCESS_KEY`: Secret access key
+Need to configure remote storage in config file or set environment variables:
+- Service endpoint URL
+- Bucket name
+- Access key ID
+- Secret access key
 
 ---
 
@@ -748,8 +655,6 @@ Need to configure remote storage in `config/config.yaml` or set environment vari
 - "How much data is stored locally"
 - "Is remote storage configured"
 
-**Tool called:** `get_storage_status`
-
 **Return information:**
 
 | Category | Information |
@@ -769,21 +674,19 @@ Need to configure remote storage in `config/config.yaml` or set environment vari
 - "Compare local and remote data dates"
 - "Which dates only exist remotely"
 
-**Tool called:** `list_available_dates`
-
 **Three query modes:**
 
 | Mode | Description | Example Question |
 |------|-------------|------------------|
-| **local** | View local only | "What dates are available locally" |
-| **remote** | View remote only | "What dates are in remote" |
-| **both** | Compare both (default) | "Compare local and remote data" |
+| **Local** | View local only | "What dates are available locally" |
+| **Remote** | View remote only | "What dates are in remote" |
+| **Compare** | Compare both (default) | "Compare local and remote data" |
 
-**Return information (both mode):**
+**Return information (compare mode):**
 
-- only_local: Dates only existing locally
-- only_remote: Dates only existing remotely (useful for deciding which dates to sync)
-- both: Dates existing in both places
+- Dates only existing locally
+- Dates only existing remotely (useful for deciding which dates to sync)
+- Dates existing in both places
 
 ---
 
@@ -796,8 +699,6 @@ Need to configure remote storage in `config/config.yaml` or set environment vari
 - "Last month's date range"
 - "Help me convert 'last 30 days' to specific dates"
 
-**Tool called:** `resolve_date_range`
-
 **Why is this tool needed?**
 
 Users often use natural language like "this week", "last 7 days" to express dates, but different AI models calculating dates on their own will produce inconsistent results. This tool uses server-side precise time calculations to ensure all AI models get consistent date ranges.
@@ -812,41 +713,12 @@ Users often use natural language like "this week", "last 7 days" to express date
 | Last N Days | 最近7天、最近30天 | last 7 days, last 30 days |
 | Dynamic | 最近N天 (any number) | last N days |
 
-**Return format:**
-
-```json
-{
-  "success": true,
-  "expression": "this week",
-  "date_range": {
-    "start": "2025-11-18",
-    "end": "2025-11-26"
-  },
-  "current_date": "2025-11-26",
-  "description": "This week (Monday to Sunday, 11-18 to 11-26)"
-}
-```
-
-**Recommended usage flow:**
-
-```
-User: Analyze AI's sentiment this week
-Recommended steps:
-1. AI first calls resolve_date_range("this week") → gets {"start": "2025-11-18", "end": "2025-11-26"}
-2. AI calls analyze_sentiment(topic="AI", date_range=date_range from previous step)
-
-User: Check Tesla news from last 7 days
-Recommended steps:
-1. AI calls resolve_date_range("last 7 days") → gets precise date range
-2. AI calls search_news(query="Tesla", date_range=date_range from previous step)
-```
-
 **Usage advantages:**
 
 - ✅ **Consistency**: All AI models get the same date range
-- ✅ **Accuracy**: Based on server-side Python `datetime.now()` calculation
-- ✅ **Standardization**: Returns standard `YYYY-MM-DD` format
-- ✅ **Flexibility**: Supports Chinese/English, dynamic days (last N days)
+- ✅ **Accuracy**: Based on server-side precise time calculation
+- ✅ **Standardization**: Returns standard date format
+- ✅ **Flexibility**: Supports Chinese/English, dynamic days
 
 ---
 

+ 140 - 267
README-MCP-FAQ.md

@@ -6,7 +6,38 @@
 
 # TrendRadar MCP 工具使用问答
 
-> AI 提问指南 - 如何通过对话使用新闻热点分析工具
+> AI 提问指南 - 如何通过自然对话使用新闻热点分析工具(v3.1.5)
+
+---
+
+## 📋 工具一览
+
+| 分类 | 工具名称 | 功能简介 |
+|:----:|---------|---------|
+| **日期** | `resolve_date_range` | 解析"本周"、"最近7天"等自然语言为标准日期 |
+| **查询** | `get_latest_news` | 获取最新一批爬取的热榜新闻 |
+| | `get_news_by_date` | 按日期范围查询历史新闻 |
+| | `get_trending_topics` | 获取热点话题统计(支持自动提取) |
+| **RSS** | `get_latest_rss` | 获取最新 RSS 订阅内容 |
+| | `search_rss` | 在 RSS 数据中搜索关键词 |
+| | `get_rss_feeds_status` | 查看 RSS 源配置和数据状态 |
+| **搜索** | `search_news` | 统一搜索(关键词/模糊/实体,可含RSS) |
+| | `find_related_news` | 查找与指定标题相似的新闻 |
+| **分析** | `analyze_topic_trend` | 话题趋势分析(热度/生命周期/爆火/预测) |
+| | `analyze_data_insights` | 数据洞察(平台对比/活跃度/关键词共现) |
+| | `analyze_sentiment` | 新闻情感倾向分析 |
+| | `aggregate_news` | 跨平台新闻聚合去重 |
+| | `compare_periods` | 时期对比分析(周环比/月环比) |
+| | `generate_summary_report` | 生成每日/每周摘要报告 |
+| **系统** | `get_current_config` | 获取当前系统配置 |
+| | `get_system_status` | 获取系统运行状态 |
+| | `check_version` | 检查版本更新(TrendRadar + MCP Server) |
+| | `trigger_crawl` | 手动触发一次爬取任务 |
+| **存储** | `sync_from_remote` | 从远程存储拉取数据到本地 |
+| | `get_storage_status` | 获取存储配置和状态 |
+| | `list_available_dates` | 列出本地/远程可用的日期 |
+
+---
 
 ## ⚙️ 默认设置说明(重要!)
 
@@ -19,9 +50,9 @@
 | **URL 链接**   | 默认不返回链接(节省约 160 tokens/条)  | 说"需要链接"或"包含 URL"              |
 | **关键词列表** | 默认不使用 frequency_words.txt 过滤新闻 | 只有调用"趋势话题"工具时才使用        |
 
-**⚠️ 重要:** AI 模型的选择直接影响工具调用效果,AI 越智能,调用越准确。当你解除上面的限制,比如从今天的查询,放宽到一周的查询,首先你要在本地有一周的数据,其次,token 消耗量可能会倍增(为什么说可能,比如我查询 分析'苹果'最近一周的热度趋势,如果一周中没多少苹果的新闻,那么 token消耗量可能反而很少)
+**⚠️ 重要:** AI 模型的选择直接影响工具调用效果,AI 越智能,调用越准确。当你解除上面的限制,比如从今天的查询,放宽到一周的查询,首先你要在本地有一周的数据,其次,token 消耗量可能会倍增
 
-**💡 提示:** 本项目提供了专门的日期解析工具 `resolve_date_range`,可以准确解析"最近7天"、"本周"等自然语言日期表达式,确保所有 AI 模型获得一致的日期范围。推荐优先使用该工具,详见下方 Q18。
+**💡 提示:** 本项目提供了专门的日期解析工具,可以准确解析"最近7天"、"本周"等自然语言日期表达式,确保所有 AI 模型获得一致的日期范围。详见下方 Q18。
 
 
 ## 💰 AI 模型
@@ -65,6 +96,8 @@
 在你测试一次询问后,请立刻去 [硅基流动账单](https://cloud.siliconflow.cn/me/bills) 查询这一次的消耗量,心底有个估算。
 
 
+---
+
 ## 基础查询
 
 ### Q1: 如何查看最新的新闻?
@@ -76,12 +109,10 @@
 - "获取知乎和微博的最新 10 条新闻"
 - "查看最新新闻,需要包含链接"
 
-**调用的工具:** `get_latest_news`
-
 **工具返回行为:**
 
-- MCP 工具会返回所有平台的最新 50 条新闻给 AI
-- 不包含 URL 链接(节省 token)
+- 工具会返回所有平台的最新 50 条新闻
+- 默认不包含 URL 链接(节省 token)
 
 **AI 展示行为(重要):**
 
@@ -108,8 +139,6 @@
 - "上周一的新闻"
 - "给我看看最新新闻"(自动查询今天)
 
-**调用的工具:** `get_news_by_date`
-
 **支持的日期格式:**
 
 - 相对日期:今天、昨天、前天、3 天前
@@ -119,8 +148,8 @@
 **工具返回行为:**
 
 - 不指定日期时自动查询今天(节省 token)
-- MCP 工具会返回所有平台的 50 条新闻给 AI
-- 不包含 URL 链接
+- 工具会返回所有平台的 50 条新闻
+- 默认不包含 URL 链接
 
 **AI 展示行为(重要):**
 
@@ -137,24 +166,12 @@
 - "自动分析今天新闻里有哪些热门话题"(自动提取)
 - "看看新闻里最热门的词是什么"(自动提取)
 
-**调用的工具:** `get_trending_topics`
-
 **两种提取模式:**
 
 | 模式 | 说明 | 示例问法 |
 |------|------|---------|
-| **keywords** | 统计预设关注词(基于 `config/frequency_words.txt`,默认) | "我的关注词出现了多少次" |
-| **auto_extract** | 自动从新闻标题提取高频词(无需预设) | "自动分析热门话题" |
-
-**使用示例:**
-
-```
-# 使用预设关注词(默认模式)
-get_trending_topics(mode="current")
-
-# 自动提取高频词(新功能)
-get_trending_topics(extract_mode="auto_extract", top_n=20)
-```
+| **预设关注词** | 统计你预先设定的关注词(基于配置文件,默认) | "我的关注词出现了多少次" |
+| **自动提取** | 自动从新闻标题提取高频词(无需预设) | "自动分析热门话题" |
 
 ---
 
@@ -168,14 +185,16 @@ get_trending_topics(extract_mode="auto_extract", top_n=20)
 - "获取 Hacker News 的最新文章"
 - "查看所有 RSS 源的最新 20 条"
 - "获取 RSS 订阅,需要包含摘要"
-
-**调用的工具:** `get_latest_rss`
+- "看看最近一周的 RSS 内容"(支持多日查询)
+- "获取 Hacker News 最近 7 天的文章"
 
 **工具返回行为:**
 
-- MCP 工具会返回最新 50 条 RSS 条目给 AI
+- 默认返回今天的 RSS 条目(最多 50 条)
+- 支持 `days` 参数获取多日数据(1-30天)
 - 默认不包含摘要(节省 token)
 - 按发布时间倒序排列
+- 跨日期自动去重(按 URL)
 
 **AI 展示行为(重要):**
 
@@ -185,22 +204,10 @@ get_trending_topics(extract_mode="auto_extract", top_n=20)
 **可以调整:**
 
 - 指定 RSS 源:如"只看 Hacker News"
+- 指定天数:如"最近 7 天"、"最近一周"
 - 调整数量:如"返回前 20 条"
 - 包含摘要:如"需要摘要"
 
-**使用示例:**
-
-```
-# 获取所有 RSS 源的最新内容
-get_latest_rss()
-
-# 获取指定源
-get_latest_rss(feeds=['hacker-news'])
-
-# 包含摘要
-get_latest_rss(include_summary=True, limit=20)
-```
-
 ---
 
 ### Q4.2: 如何搜索 RSS 订阅中的内容?
@@ -211,13 +218,11 @@ get_latest_rss(include_summary=True, limit=20)
 - "搜索最近 7 天 RSS 中关于'机器学习'的内容"
 - "在 Hacker News 中搜索'Python'"
 
-**调用的工具:** `search_rss`
-
 **工具返回行为:**
 
 - 使用关键词搜索 RSS 条目的标题
 - 默认搜索最近 7 天的数据
-- MCP 工具会返回最多 50 条结果给 AI
+- 工具会返回最多 50 条结果
 
 **可以调整:**
 
@@ -225,16 +230,6 @@ get_latest_rss(include_summary=True, limit=20)
 - 调整天数:如"搜索最近 14 天"
 - 包含摘要:如"需要摘要"
 
-**使用示例:**
-
-```
-# 搜索所有 RSS 源
-search_rss(keyword="AI")
-
-# 搜索指定源和天数
-search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
-```
-
 ---
 
 ### Q4.3: 如何查看 RSS 源的状态?
@@ -245,16 +240,14 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - "RSS 抓取了多少数据"
 - "哪些 RSS 源有数据"
 
-**调用的工具:** `get_rss_feeds_status`
-
 **返回信息:**
 
 | 字段 | 说明 |
 |------|------|
-| **available_dates** | 有 RSS 数据的日期列表 |
-| **total_dates** | 总日期数 |
-| **today_feeds** | 今日各 RSS 源的数据统计 |
-| **generated_at** | 生成时间 |
+| **可用日期** | 有 RSS 数据的日期列表 |
+| **总日期数** | 总共有多少天的数据 |
+| **今日各源统计** | 今日各 RSS 源的数据统计 |
+| **生成时间** | 状态生成时间 |
 
 ---
 
@@ -271,15 +264,13 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - "查找2025年1月1日到7日'特斯拉'的相关新闻"
 - "查找'iPhone 16 发布'这条新闻的链接"
 
-**调用的工具:** `search_news`
-
 **工具返回行为:**
 
 - 使用关键词模式搜索
 - 默认搜索今天的数据
 - AI会自动将"最近7天"、"上周"等相对时间转换为具体日期范围
-- MCP 工具会返回最多 50 条结果给 AI
-- 不包含 URL 链接
+- 工具会返回最多 50 条结果
+- 默认不包含 URL 链接
 
 **AI 展示行为(重要):**
 
@@ -295,18 +286,6 @@ search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
 - 调整排序:如"按权重排序"
 - 包含链接:如"需要链接"
 
-**推荐使用流程:**
-
-```
-用户:搜索最近7天关于"人工智能突破"的新闻
-推荐步骤:
-1. 先调用 resolve_date_range("最近7天") 获取精确日期范围
-2. 再调用 search_news 并传入日期范围
-
-用户:查找2025年1月的"特斯拉"报道
-AI:(date_range={"start": "2025-01-01", "end": "2025-01-31"})
-```
-
 ---
 
 ### Q4.4: 如何同时搜索热榜和 RSS 内容?
@@ -317,8 +296,6 @@ AI:(date_range={"start": "2025-01-01", "end": "2025-01-31"})
 - "查找'人工智能'的新闻,同时搜索 RSS 订阅"
 - "搜索'特斯拉',热榜和 RSS 都要"
 
-**调用的工具:** `search_news`(设置 `include_rss=True`)
-
 **工具返回行为:**
 
 - 热榜结果和 RSS 结果**分开展示**
@@ -326,44 +303,11 @@ AI:(date_range={"start": "2025-01-01", "end": "2025-01-31"})
 - RSS 结果不影响热榜的排名展示
 - 默认返回热榜 50 条 + RSS 20 条
 
-**返回结构:**
-
-```json
-{
-  "results": [
-    // 热榜新闻(按排名排序)
-    {"title": "...", "platform": "zhihu", "rank": 1, ...}
-  ],
-  "rss": [
-    // RSS 内容(独立区块)
-    {"title": "...", "feed_name": "Hacker News", ...}
-  ],
-  "summary": {
-    "total_found": 15,
-    "rss_found": 8,
-    "include_rss": true
-  }
-}
-```
-
 **可以调整:**
 
-- RSS 数量:如"RSS 返回 10 条"(`rss_limit=10`)
+- RSS 数量:如"RSS 返回 10 条"
 - 只搜索热榜:不说"包括 RSS"(默认行为)
-- 只搜索 RSS:使用 `search_rss` 工具
-
-**代码示例:**
-
-```python
-# 同时搜索热榜和 RSS
-search_news(query="AI", include_rss=True)
-
-# 调整 RSS 返回数量
-search_news(query="AI", include_rss=True, rss_limit=10)
-
-# 只搜索热榜(默认)
-search_news(query="AI")
-```
+- 只搜索 RSS:说"只在 RSS 中搜索"
 
 ---
 
@@ -376,22 +320,20 @@ search_news(query="AI")
 - "搜索上周关于'ChatGPT'的相关报道"(历史)
 - "看看最近7天有没有和这条新闻相似的报道"(历史)
 
-**调用的工具:** `find_related_news`
-
 **支持的时间范围:**
 
 | 方式 | 说明 | 示例 |
 |------|------|------|
 | 不指定 | 只查询今天的数据(默认) | "找相似新闻" |
-| 预设值 | yesterday, last_week, last_month | "查找昨天的相关新闻" |
-| 日期范围 | `{"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}` | "查找1月1日到7日的相关报道" |
+| 预设值 | 昨天、上周、上个月 | "查找昨天的相关新闻" |
+| 日期范围 | 指定开始和结束日期 | "查找1月1日到7日的相关报道" |
 
 **工具返回行为:**
 
 - 相似度阈值 0.5(可调整)
-- MCP 工具会返回最多 50 条结果给 AI
+- 工具会返回最多 50 条结果
 - 按相似度排序
-- 不包含 URL 链接
+- 默认不包含 URL 链接
 
 **AI 展示行为(重要):**
 
@@ -418,32 +360,21 @@ search_news(query="AI")
 - "预测接下来可能的热点话题"
 - "分析'比特币'在2024年12月的生命周期"
 
-**调用的工具:** `analyze_topic_trend`
+**四种分析模式:**
+
+| 模式 | 说明 | 示例问法 |
+|------|------|---------|
+| **热度趋势** | 追踪话题热度变化 | "分析'AI'的热度趋势" |
+| **生命周期** | 从出现到消失的完整周期 | "看看'XX'是昙花一现还是持续热点" |
+| **异常检测** | 识别突然爆火的话题 | "今天有哪些突然爆火的话题" |
+| **预测** | 预测未来可能的热点 | "预测接下来可能的热点" |
 
 **工具返回行为:**
 
-- 支持多种分析模式:热度趋势、生命周期、异常检测、预测
 - AI会自动将"最近一周"等相对时间转换为具体日期范围
 - 默认分析最近7天数据
 - 按天粒度统计
 
-**AI 展示行为:**
-
-- 通常会展示趋势分析结果和图表
-- AI 可能会总结关键发现
-
-**推荐使用流程:**
-
-```
-用户:分析'人工智能'最近一周的生命周期
-推荐步骤:
-1. 先调用 resolve_date_range("最近一周") 获取精确日期范围
-2. 再调用 analyze_topic_trend 并传入日期范围
-
-用户:看看'比特币'在2024年12月是昙花一现还是持续热点
-AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
-```
-
 ---
 
 ## 数据洞察
@@ -456,8 +387,6 @@ AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "看看哪个平台更新最频繁"
 - "分析一下哪些关键词经常一起出现"
 
-**调用的工具:** `analyze_data_insights`
-
 **三种洞察模式:**
 
 | 模式           | 功能             | 示例问法                   |
@@ -468,15 +397,10 @@ AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
 
 **工具返回行为:**
 
-- 平台对比模式
+- 默认使用平台对比模式
 - 分析今天的数据
 - 关键词共现最小频次 3 次
 
-**AI 展示行为:**
-
-- 通常会展示分析结果和统计数据
-- AI 可能会总结洞察发现
-
 ---
 
 ## 情感分析
@@ -490,14 +414,12 @@ AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "分析各平台对'人工智能'的情感态度"
 - "看看'比特币'一周内的情感倾向,选择前 20 条最重要的"
 
-**调用的工具:** `analyze_sentiment`
-
 **工具返回行为:**
 
-- 分析今天的数据
-- MCP 工具会返回最多 50 条新闻给 AI
+- 默认分析今天的数据
+- 工具会返回最多 50 条新闻
 - 按权重排序(优先展示重要新闻)
-- 不包含 URL 链接
+- 默认不包含 URL 链接
 
 **AI 展示行为(重要):**
 
@@ -522,8 +444,6 @@ AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
 - "给我看去重后的热点新闻"
 - "哪些新闻是跨平台热点"
 
-**调用的工具:** `aggregate_news`
-
 **工具功能:**
 
 - 自动识别不同平台报道的同一事件
@@ -535,33 +455,20 @@ AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"})
 
 | 字段 | 说明 |
 |------|------|
-| **representative_title** | 代表性标题 |
-| **platforms** | 覆盖的平台列表 |
-| **platform_count** | 覆盖平台数量 |
-| **is_cross_platform** | 是否跨平台新闻 |
-| **best_rank** | 最佳排名 |
-| **aggregate_weight** | 综合权重 |
-| **sources** | 各平台来源详情 |
+| **代表性标题** | 这组新闻的代表标题 |
+| **覆盖平台** | 哪些平台报道了这条新闻 |
+| **平台数量** | 覆盖了多少个平台 |
+| **是否跨平台** | 是否为跨平台热点 |
+| **最佳排名** | 在各平台的最佳排名 |
+| **综合权重** | 综合热度评分 |
+| **各平台来源** | 各平台的详细信息 |
 
 **可以调整:**
 
 - 指定时间:如"最近一周的"
-- 调整相似度阈值:如"更严格匹配"(0.8)或"宽松匹配"(0.5)
+- 调整相似度阈值:如"更严格匹配"或"宽松匹配"
 - 指定平台:如"只看知乎和微博"
 
-**使用示例:**
-
-```
-# 默认聚合今天的新闻
-aggregate_news()
-
-# 更严格的相似度匹配
-aggregate_news(similarity_threshold=0.8)
-
-# 指定日期范围
-aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
-```
-
 ---
 
 ### Q10: 如何生成每日或每周的热点摘要?
@@ -572,8 +479,6 @@ aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
 - "给我一份本周的热点总结"
 - "生成过去 7 天的新闻分析报告"
 
-**调用的工具:** `generate_summary_report`
-
 **报告类型:**
 
 - 每日摘要:总结当天的热点新闻
@@ -590,39 +495,20 @@ aggregate_news(date_range={"start": "2025-01-01", "end": "2025-01-07"})
 - "分析'人工智能'在两个时期的热度差异"
 - "对比各平台活跃度的变化"
 
-**调用的工具:** `compare_periods`
-
 **三种对比模式:**
 
 | 模式 | 说明 | 适用场景 |
 |------|------|---------|
-| **overview** | 总体概览 | 新闻数量变化、关键词变化、TOP新闻对比 |
-| **topic_shift** | 话题变化分析 | 上升话题、下降话题、新出现话题 |
-| **platform_activity** | 平台活跃度对比 | 各平台新闻数量变化、增长最快/最慢的平台 |
+| **总体概览** | 新闻数量变化、关键词变化、TOP新闻对比 | 快速了解整体变化 |
+| **话题变化** | 上升话题、下降话题、新出现话题 | 分析热点转移 |
+| **平台活跃度** | 各平台新闻数量变化 | 了解平台动态 |
 
 **时间段预设值:**
 
-- `today` / `yesterday`: 今天/昨天
-- `this_week` / `last_week`: 本周/上周
-- `this_month` / `last_month`: 本月/上月
-- 或使用自定义日期范围:`{"start": "2025-01-01", "end": "2025-01-07"}`
-
-**使用示例:**
-
-```
-# 周环比分析
-compare_periods(period1="last_week", period2="this_week")
-
-# 话题变化分析
-compare_periods(period1="last_month", period2="this_month", compare_type="topic_shift")
-
-# 聚焦特定话题
-compare_periods(
-    period1={"start": "2025-01-01", "end": "2025-01-07"},
-    period2={"start": "2025-01-08", "end": "2025-01-14"},
-    topic="人工智能"
-)
-```
+- 今天 / 昨天
+- 本周 / 上周
+- 本月 / 上月
+- 或使用自定义日期范围
 
 ---
 
@@ -637,8 +523,6 @@ compare_periods(
 - "有哪些可用的平台?"
 - "当前的权重配置是什么?"
 
-**调用的工具:** `get_current_config`
-
 **可以查询:**
 
 - 可用平台列表
@@ -657,8 +541,6 @@ compare_periods(
 - "最后一次爬取是什么时候?"
 - "有多少天的历史数据?"
 
-**调用的工具:** `get_system_status`
-
 **返回信息:**
 
 - 系统版本和状态
@@ -668,6 +550,35 @@ compare_periods(
 
 ---
 
+### Q13.1: 如何检查版本更新?
+
+**你可以这样问:**
+
+- "检查版本更新"
+- "有没有新版本?"
+- "当前版本是最新的吗?"
+
+**返回信息:**
+
+会同时检查两个组件的版本:
+
+| 组件 | 说明 |
+|------|------|
+| **TrendRadar** | 核心爬虫和分析引擎 |
+| **MCP Server** | AI 对话工具服务 |
+
+每个组件会告诉你:
+- 当前安装的版本
+- 最新可用的版本
+- 是否需要更新
+- 更新建议
+
+**可以调整:**
+
+- 如果访问 GitHub 较慢,可以说"检查版本更新,使用代理 http://127.0.0.1:10801"
+
+---
+
 ### Q14: 如何手动触发爬取任务?
 
 **你可以这样问:**
@@ -677,8 +588,6 @@ compare_periods(
 - "触发一次爬取并保存数据"(持久化)
 - "获取 36 氪 的实时数据但不保存"(临时查询)
 
-**调用的工具:** `trigger_crawl`
-
 **两种模式:**
 
 | 模式           | 用途                 | 示例                 |
@@ -688,9 +597,9 @@ compare_periods(
 
 **工具返回行为:**
 
-- 临时爬取模式(不保存)
-- 爬取所有平台
-- 不包含 URL 链接
+- 默认为临时爬取模式(不保存)
+- 默认爬取所有平台
+- 默认不包含 URL 链接
 
 **AI 展示行为(重要):**
 
@@ -715,8 +624,6 @@ compare_periods(
 - "拉取远程存储的数据到本地"
 - "同步最近 30 天的新闻数据"
 
-**调用的工具:** `sync_from_remote`
-
 **使用场景:**
 
 - 爬虫部署在云端(如 GitHub Actions),数据存储到远程(如 Cloudflare R2)
@@ -724,18 +631,18 @@ compare_periods(
 
 **返回信息:**
 
-- synced_files: 成功同步的文件数量
-- synced_dates: 成功同步的日期列表
-- skipped_dates: 跳过的日期(本地已存在)
-- failed_dates: 失败的日期及错误信息
+- 成功同步的文件数量
+- 成功同步的日期列表
+- 跳过的日期(本地已存在)
+- 失败的日期及错误信息
 
 **前提条件:**
 
-需要在 `config/config.yaml` 中配置远程存储或设置环境变量:
-- `S3_ENDPOINT_URL`: 服务端点
-- `S3_BUCKET_NAME`: 存储桶名称
-- `S3_ACCESS_KEY_ID`: 访问密钥 ID
-- `S3_SECRET_ACCESS_KEY`: 访问密钥
+需要在配置文件中配置远程存储或设置环境变量:
+- 服务端点 URL
+- 存储桶名称
+- 访问密钥 ID
+- 访问密钥
 
 ---
 
@@ -748,8 +655,6 @@ compare_periods(
 - "本地有多少数据"
 - "远程存储配置了吗"
 
-**调用的工具:** `get_storage_status`
-
 **返回信息:**
 
 | 类别 | 信息 |
@@ -769,21 +674,19 @@ compare_periods(
 - "对比本地和远程的数据日期"
 - "哪些日期只在远程有"
 
-**调用的工具:** `list_available_dates`
-
 **三种查询模式:**
 
 | 模式 | 说明 | 示例问法 |
 |------|------|---------|
-| **local** | 仅查看本地 | "本地有哪些日期" |
-| **remote** | 仅查看远程 | "远程有哪些日期" |
-| **both** | 对比两者(默认) | "对比本地和远程的数据" |
+| **本地** | 仅查看本地 | "本地有哪些日期" |
+| **远程** | 仅查看远程 | "远程有哪些日期" |
+| **对比** | 对比两者(默认) | "对比本地和远程的数据" |
 
-**返回信息(both 模式):**
+**返回信息(对比模式):**
 
-- only_local: 仅本地存在的日期
-- only_remote: 仅远程存在的日期(可用于决定同步哪些日期)
-- both: 两边都存在的日期
+- 仅本地存在的日期
+- 仅远程存在的日期(可用于决定同步哪些日期)
+- 两边都存在的日期
 
 ---
 
@@ -796,8 +699,6 @@ compare_periods(
 - "上月的日期范围"
 - "帮我把'最近30天'转换为具体日期"
 
-**调用的工具:** `resolve_date_range`
-
 **为什么需要这个工具?**
 
 用户经常使用"本周"、"最近7天"等自然语言表达日期,但不同的 AI 模型自行计算日期时会产生不一致的结果。此工具使用服务器端的精确时间计算,确保所有 AI 模型获得一致的日期范围。
@@ -812,41 +713,12 @@ compare_periods(
 | 最近N天 | 最近7天、最近30天 | last 7 days, last 30 days |
 | 动态 | 最近N天(任意数字) | last N days |
 
-**返回格式:**
-
-```json
-{
-  "success": true,
-  "expression": "本周",
-  "date_range": {
-    "start": "2025-11-18",
-    "end": "2025-11-26"
-  },
-  "current_date": "2025-11-26",
-  "description": "本周(周一到周日,11-18 至 11-26)"
-}
-```
-
-**推荐使用流程:**
-
-```
-用户:分析 AI 本周的情感倾向
-推荐步骤:
-1. AI 先调用 resolve_date_range("本周") → 获取 {"start": "2025-11-18", "end": "2025-11-26"}
-2. AI 调用 analyze_sentiment(topic="AI", date_range=上一步返回的date_range)
-
-用户:看看最近7天的特斯拉新闻
-推荐步骤:
-1. AI 调用 resolve_date_range("最近7天") → 获取精确日期范围
-2. AI 调用 search_news(query="特斯拉", date_range=上一步返回的date_range)
-```
-
 **使用优势:**
 
 - ✅ **一致性**:所有 AI 模型获得相同的日期范围
-- ✅ **准确性**:基于服务器端 Python `datetime.now()` 计算
-- ✅ **标准化**:返回标准 `YYYY-MM-DD` 格式
-- ✅ **灵活性**:支持中英文、动态天数(最近N天)
+- ✅ **准确性**:基于服务器端精确时间计算
+- ✅ **标准化**:返回标准日期格式
+- ✅ **灵活性**:支持中英文、动态天数
 
 ---
 
@@ -879,3 +751,4 @@ compare_periods(
 1. 查看最新:"查询今天关于'iPhone'的新闻"
 2. 查找历史:"查找上周与'iPhone'相关的历史新闻"
 3. 找相似报道:"找出和'iPhone 发布会'相似的新闻"
+

+ 384 - 152
README.md

@@ -13,8 +13,8 @@
 [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
 [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v4.7.0-blue.svg)](https://github.com/sansan0/TrendRadar)
-[![MCP](https://img.shields.io/badge/MCP-v2.0.1-green.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.0.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![MCP](https://img.shields.io/badge/MCP-v3.1.5-green.svg)](https://github.com/sansan0/TrendRadar)
 [![RSS](https://img.shields.io/badge/RSS-订阅源支持-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 
 [![企业微信通知](https://img.shields.io/badge/企业微信-通知-00D4AA?style=flat-square)](https://work.weixin.qq.com/)
@@ -26,12 +26,14 @@
 [![ntfy通知](https://img.shields.io/badge/ntfy-通知-00D4AA?style=flat-square)](https://github.com/binwiederhier/ntfy)
 [![Bark通知](https://img.shields.io/badge/Bark-通知-00D4AA?style=flat-square)](https://github.com/Finb/Bark)
 [![Slack通知](https://img.shields.io/badge/Slack-通知-00D4AA?style=flat-square)](https://slack.com/)
+[![通用Webhook](https://img.shields.io/badge/通用-Webhook-607D8B?style=flat-square&logo=webhook&logoColor=white)](#)
 
 
 [![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-自动化-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![GitHub Pages](https://img.shields.io/badge/GitHub_Pages-部署-4285F4?style=flat-square&logo=github&logoColor=white)](https://sansan0.github.io/TrendRadar)
 [![Docker](https://img.shields.io/badge/Docker-部署-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/r/wantcat/trendradar)
 [![MCP Support](https://img.shields.io/badge/MCP-AI分析支持-FF6B6B?style=flat-square&logo=ai&logoColor=white)](https://modelcontextprotocol.io/)
+[![AI分析推送](https://img.shields.io/badge/AI-分析推送-FF6B6B?style=flat-square&logo=openai&logoColor=white)](#)
 
 </div>
 
@@ -61,12 +63,19 @@
 
 <br>
 
-- 感谢**耐心反馈 bug** 的贡献者,你们的每一条反馈让项目更加完善😉;  
-- 感谢**为项目点 star** 的观众们,**fork** 你所欲也,**star** 我所欲也,两者得兼😍是对开源精神最好的支持; 
-- 感谢**关注[公众号](#问题答疑与交流)** 的读者们,你们的留言、点赞、分享和推荐等积极互动让内容更有温度😎。 
+- 感谢**为项目点 star** 的观众们,**fork** 你所欲也,**star** 我所欲也,两者得兼😍是对开源精神最好的支持
 
 <details>
-<summary>👉 点击展开:<strong>致谢名单</strong> (当前 <strong>🔥73🔥</strong> 位)</summary>
+<summary>👉 点击展开:<strong>致谢名单</strong> (天使轮荣誉榜 🔥73+🔥 位)</summary>
+
+### 早期支持者致谢
+
+> 💡 **特别说明**:
+>
+> 1. **关于名单**:下方表格记录了项目起步阶段(天使轮)的支持者。因早期人工统计繁琐,**难免存在疏漏或记录不全的情况,如有遗漏,实非本意,万望海涵**。
+> 2. **未来规划**:为了将有限的精力回归代码与功能迭代,**即日起不再人工维护此名单**。
+>
+> 无论名字是否上榜,你们的每一份支持都是 TrendRadar 能够走到今天的基石。🙏
 
 ### 基础设施支持
 
@@ -92,9 +101,8 @@
 
 > 感谢**给予资金支持**的朋友们,你们的慷慨已化身为键盘旁的零食饮料,陪伴着项目的每一次迭代。
 >
-> **"一元点赞"已暂停**,如仍想支持作者,可前往[公众号](#问题答疑与交流)文章底部点击"喜欢作者"。
->
-> 一位可爱猫头像的朋友,不知你从哪个角落翻到了我的收款码,三连了 1.8,心意已收到,感谢厚爱
+> **关于"一元点赞"的回归**:
+> 随着 v5.0.0 版本的发布,项目迈入了一个新的阶段。为了支持日益增长的 API 成本和咖啡因消耗,"一元点赞"通道现已重新开启。你的每一份心意,都将转化为代码世界里的 Token 和动力。🚀 [前往支持](#问题答疑与交流)
 
 |           点赞人            |  金额  |  日期  |             备注             |
 | :-------------------------: | :----: | :----: | :-----------------------: |
@@ -192,12 +200,71 @@
 > **📌 查看最新更新**:**[原仓库更新日志](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-更新日志)** :
 - **提示**:建议查看【历史更新】,明确具体的【功能内容】
 
+### 2026/01/10 - v5.0.0
+
+> **开发小插曲**:         
+> 致敬那个陪伴我两年多、却在刚续费后反手弹出 `"This organization has been disabled"` 的某 C 厂模型
+
+**✨ 推送内容“五大板块”重构**
+
+本次更新对推送消息进行了区域化重构,现在推送内容清晰地划分为五大核心板块:
+
+1.  **📊 热榜新闻**:根据您的关键词精准筛选后的全网热点聚合。
+2.  **📰 RSS 订阅**:您的个性化订阅源内容,支持按关键词分组。
+3.  **🆕 本次新增**:实时捕捉自上次运行以来的全新热点(带 🆕 标记)。
+4.  **📋 独立展示区**:指定平台的完整热榜或 RSS 源展示,**完全不受关键词过滤限制**。
+5.  **✨ AI 分析板块**:由 AI 驱动的深度洞察,包含趋势概述、热度走势及**极其重要**的情感倾向分析。
+
+**✨ AI 智能分析推送功能**
+
+- **AI 分析集成**:使用 AI 大模型对推送内容进行深度分析,自动生成热点趋势概述、关键词热度分析、跨平台关联、潜在影响评估等
+- **情感倾向分析**:新增深度情感识别,精准捕捉舆论的正负面、争议或担忧情绪
+- **多 AI 提供商支持**:支持 DeepSeek(默认,性价比高)、OpenAI、Google Gemini 及任意 OpenAI 兼容接口
+- **两种推送模式**:`only_analysis`(仅 AI 分析)、`both`(两者都推送)
+- **自定义提示词**:通过 `config/ai_analysis_prompt.txt` 文件自定义 AI 分析角色和输出格式
+- **多维度数据分析**:AI 可分析排名变化、热度持续时间、跨平台表现、趋势预测等
+
+**📋 独立展示区功能**
+
+- **完整热榜展示**:指定平台的完整热榜单独展示,不受关键词过滤影响
+- **RSS 独立展示**:RSS 源内容可完整展示,适合内容较少的订阅源
+- **灵活配置**:支持配置展示平台列表、RSS 源列表、最大展示条数
+
+**📊 推送体验重构**
+
+- **排版升级**:重新设计并统一各渠道统计头部,强化区块组织,消息层次一目了然
+- **配置简化**:优化飞书等通知渠道的配置逻辑,上手更简单
+- **热度趋势箭头**:新增 🔺(上升)、🔻(下降)、➖(持平) 趋势标识,直观展示热度变化
+- **通用 Webhook**:支持自定义 Webhook URL 和 JSON 模板,轻松适配 Discord、Matrix、IFTTT 等任意平台
+
+**🔧 配置优化**
+
+- **频率词配置增强**:新增 `[组别名]` 语法,支持 `#` 注释行,配置更清晰(感谢 [@songge8](https://github.com/sansan0/TrendRadar/issues/752) 提出的建议)
+- **环境变量支持**:AI 分析相关配置支持环境变量覆盖(`AI_API_KEY`、`AI_PROVIDER` 等)
+
+> 💡 详细配置教程见 [AI 分析配置](#12-ai-分析配置)
+
+### 2026/01/10 - mcp-v3.0.0~v3.1.5
+
+- **Breaking Change**:所有工具返回值统一为 `{success, summary, data, error}` 结构
+- **异步一致性**:所有 21 个工具函数使用 `asyncio.to_thread()` 包装同步调用
+- **MCP Resources**:新增 4 个资源(platforms、rss-feeds、available-dates、keywords)
+- **RSS 增强**:`get_latest_rss` 支持多日查询(days 参数),跨日期 URL 去重
+- **正则匹配修复**:`get_trending_topics` 支持 `/pattern/` 正则语法和 `display_name`
+- **缓存优化**:新增 `make_cache_key()` 函数,参数排序+MD5 哈希确保一致性
+- **新增 check_version 工具**:支持同时检查 TrendRadar 和 MCP Server 版本更新
+
+
+<details>
+<summary>👉 点击展开:<strong>历史更新</strong></summary>
+
+
 ### 2026/01/02 - v4.7.0
 
 - **修复 RSS HTML 显示**:修复 RSS 数据格式不匹配导致的渲染问题,现在按关键词分组正确显示
 - **新增正则表达式语法**:关键词配置支持 `/pattern/` 正则语法,解决英文子字符串误匹配问题(如 `ai` 匹配 `training`)[📖 查看语法详解](#关键词基础语法)
 - **新增显示名称语法**:使用 `=> 备注` 给复杂的正则表达式起个好记的名字,推送消息显示更清晰(如 `/\bai\b/ => AI相关`)
-- **不会写正则?** README 新增 AI 生成正则的引导,告诉 ChatGPT/Claude/DeepSeek 你想匹配什么,让 AI 帮你写
+- **不会写正则?** README 新增 AI 生成正则的引导,告诉 ChatGPT/Gemini/DeepSeek 你想匹配什么,让 AI 帮你写
 
 
 ### 2025/12/30 - mcp-v2.0.0
@@ -207,10 +274,6 @@
 - **统一搜索**:`search_news` 支持 `include_rss` 参数同时搜索热榜和 RSS
 
 
-<details>
-<summary>👉 点击展开:<strong>历史更新</strong></summary>
-
-
 ### 2026/01/01 - v4.6.0
 
 - **修复 RSS HTML 显示**:将 RSS 内容合并到热榜 HTML 页面,按源分组显示
@@ -475,7 +538,7 @@
 
 ### 2025/10/20 - v3.0.0
 
-**重大更新 - AI 分析功能上线** 🤖
+**重大更新 - AI 分析功能上线** 
 
 - **核心功能**:
   - 新增基于 MCP (Model Context Protocol) 的 AI 分析服务器
@@ -828,6 +891,27 @@ frequency_words.txt 文件增加了一个【必须词】功能,使用 + 号
 - **本地运行**:Windows/Mac/Linux 直接运行
 
 
+### **AI 分析推送(v5.0.0 新增)**
+
+使用 AI 大模型对推送内容进行深度分析,自动生成热点洞察报告
+
+- **智能分析**:自动分析热点趋势、关键词热度、跨平台关联、潜在影响
+- **多提供商**:支持 DeepSeek、OpenAI、Gemini 及 OpenAI 兼容接口
+- **灵活推送**:可选仅原始内容、仅 AI 分析、或两者都推送
+- **自定义提示词**:通过 `config/ai_analysis_prompt.txt` 自定义分析角度
+
+> 💡 详细配置教程见 [AI 分析配置](#12-ai-分析配置)
+
+### **独立展示区(v5.0.0 新增)**
+
+为指定平台提供完整热榜展示,不受关键词过滤影响
+
+- **完整热榜**:指定平台的热榜完整展示,适合想看完整排名的用户
+- **RSS 独立展示**:RSS 源内容可完整展示,不受关键词限制
+- **灵活配置**:支持配置展示平台、RSS 源、最大条数
+
+> 💡 详细配置教程见 [报告配置 - 独立展示区](#7-报告配置)
+
 ### **AI 智能分析(v3.0.0 新增)**
 
 基于 MCP (Model Context Protocol) 协议的 AI 对话分析系统,让你用自然语言深度挖掘新闻数据
@@ -872,18 +956,17 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
 #### 🅰️ 方案一:Docker 部署(推荐 🔥)
 
-* **特点**:比 GitHub Actions 更稳定
+* **特点**:比 GitHub Actions 更稳定,数据本地存储(无需配置云存储)
 * **适用**:有自己的服务器、NAS 或长期运行的电脑
-
-👉 **[跳转到 Docker 部署教程](#6-docker-部署)**
+* **注意**:你需要阅读了解下方的基础配置流程,然后跳转到 Docker 教程进行部署。
 
 #### 🅱️ 方案二:GitHub Actions 部署(本章节内容 ⬇️)
 
-* **特点**:数据存储在 **远程云存储**(不再写入 Git 仓库
-* **推荐**:配置云存储服务(Cloudflare R2 免费额度足够、阿里云 OSS、腾讯云 COS 等)
-* **注意**:需定期签到续期(7天一次)
+* **特点**:无服务器,数据存储在 **远程云存储**(推荐配置
+* **适用**:没有服务器的用户,利用 GitHub 免费资源
+* **注意**:需配置云存储以获得完整体验,且需定期签到续期
 
-1️⃣ **获取项目代码**
+### 1️⃣ 第一步:获取项目代码
 
    点击本仓库页面右上角的绿色 **[Use this template]** 按钮 → 选择 "Create a new repository"。
 
@@ -893,7 +976,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    <br>
 
-2️⃣ **设置 GitHub Secrets**:
+### 2️⃣ 第二步:设置 GitHub Secrets
 
    在你 Fork 后的仓库中,进入 `Settings` > `Secrets and variables` > `Actions` > `New repository secret`
 
@@ -997,9 +1080,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
    {
      "message_type": "text",
      "content": {
-       "total_titles": "{{内容}}",
-       "timestamp": "{{内容}}",
-       "report_type": "{{内容}}",
        "text": "{{内容}}"
      }
    }
@@ -1011,7 +1091,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    8. 最关键的部分来了,点击 + 按钮,选择"Webhook 触发",然后按照下面的图片摆放
 
-   ![飞书机器人配置示例](_image/image.png)
+   ![飞书机器人配置示例](_image/feishu.png)
 
    9. 配置完成后,将第 4 步复制的 Webhook 地址配置到 GitHub Secrets 中的 `FEISHU_WEBHOOK_URL`
 
@@ -1035,9 +1115,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
    {
      "message_type": "text",
      "content": {
-       "total_titles": "{{内容}}",
-       "timestamp": "{{内容}}",
-       "report_type": "{{内容}}",
        "text": "{{内容}}"
      }
    }
@@ -1049,7 +1126,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    9. 最关键的部分来了,点击 + 按钮,选择"Webhook 触发",然后按照下面的图片摆放
 
-   ![飞书机器人配置示例](_image/image.png)
+   ![飞书机器人配置示例](_image/feishu.png)
 
    10. 配置完成后,将第 5 步复制的 Webhook 地址配置到 GitHub Secrets 中的 `FEISHU_WEBHOOK_URL`
 
@@ -1499,9 +1576,76 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    </details>
 
+   <details>
+   <summary>👉 点击展开:<strong>通用 Webhook 推送</strong>(支持 Discord、Matrix、IFTTT 等)</summary>
+   <br>
+
+   **GitHub Secret 配置(⚠️ Name 名称必须严格一致):**
+   - **Name(名称)**:`GENERIC_WEBHOOK_URL`(请复制粘贴此名称,不要手打)
+   - **Secret(值)**:你的 Webhook URL
+
+   - **Name(名称)**:`GENERIC_WEBHOOK_TEMPLATE`(可选配置,请复制粘贴此名称)
+   - **Secret(值)**:JSON 模板字符串,支持 `{title}` 和 `{content}` 占位符
+
+   <br>
+
+   **通用 Webhook 简介:**
+
+   通用 Webhook 支持任意接受 HTTP POST 请求的平台,包括但不限于:
+   - **Discord**:通过 Webhook 推送到频道
+   - **Matrix**:通过 Webhook 桥接推送
+   - **IFTTT**:触发自动化流程
+   - **自建服务**:任何支持 Webhook 的自定义服务
+
+   **配置示例:**
+
+   ### Discord 配置
+
+   1. **获取 Webhook URL**:
+      - 进入 Discord 服务器设置 → 整合 → Webhooks
+      - 创建新 Webhook,复制 URL
+
+   2. **配置模板**:
+      ```json
+      {"content": "{content}"}
+      ```
+
+   3. **GitHub Secret 配置**:
+      - `GENERIC_WEBHOOK_URL`:Discord Webhook URL
+      - `GENERIC_WEBHOOK_TEMPLATE`:`{"content": "{content}"}`
+
+   ### 自定义模板
+
+   模板支持两个占位符:
+   - `{title}` - 消息标题
+   - `{content}` - 消息内容
+
+   **模板示例**:
+   ```json
+   # 默认格式(留空时使用)
+   {"title": "{title}", "content": "{content}"}
+
+   # Discord 格式
+   {"content": "{content}"}
+
+   # 自定义格式
+   {"text": "{content}", "username": "TrendRadar"}
+   ```
+
+   ---
+
+   **注意事项:**
+   - ✅ 支持 Markdown 格式(与企业微信格式一致)
+   - ✅ 支持自动分批推送
+   - ✅ 支持多账号配置(用 `;` 分隔)
+   - ⚠️ 模板必须是有效的 JSON 格式
+   - ⚠️ 不同平台对消息格式要求不同,请参考目标平台文档
+
+   </details>
+
    <br>
 
-3️⃣ **手动测试新闻推送**:
+### 3️⃣ 第三步:手动测试新闻推送
 
    > ⚠️ 提醒:
    > - 完成第 1-2 步后,请立即测试!测试成功后再根据需要调整配置(第 4 步)
@@ -1530,21 +1674,22 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    <br>
 
-4️⃣ **配置说明(可选)**:
+### 4️⃣ 第四步:配置说明(可选)
 
-   默认配置已可正常使用,如需个性化调整,了解以下三个文件即可:
+   默认配置已可正常使用,如需个性化调整,了解以下文件即可:
 
    | 文件 | 作用 |
    |------|------|
    | `config/config.yaml` | 主配置文件:推送模式、时间窗口、平台列表、热点权重等 |
    | `config/frequency_words.txt` | 关键词文件:设置你关心的词汇,筛选推送内容 |
+   | `config/ai_analysis_prompt.txt` | AI 提示词模板:自定义 AI 分析师的角色和分析维度 |
    | `.github/workflows/crawler.yml` | 执行频率:控制多久运行一次(⚠️ 谨慎修改) |
 
    👉 **详细配置教程**:[配置详解](#配置详解)
 
    <br>
 
-5️⃣ **GitHub Actions 签到续期机制 & 远程云存储配置**:
+### 5️⃣ 第五步:远程云存储 & 签到配置
 
    **v4.0.0 重要变更**:引入「活跃度检测」机制,GitHub Actions 需定期签到以维持运行。
 
@@ -1557,26 +1702,21 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    ---
 
-   **你也可以不配置远程云存储**,但此时项目处于**轻量模式**,部分高级功能不可用。
+   **关于远程云存储配置(请根据部署方式选择):**
 
-   **两种部署模式对比:**
+   - **GitHub Actions 用户**:
+     - **现状**:Actions 每次运行都是全新环境,不保存文件。如果不配置云存储,项目将运行在**轻量模式**(无增量推送、无历史追踪)。
+     - **建议**:配置远程云存储以获得完整体验。
 
-   | 模式 | 配置要求 | 功能范围 |
-   |------|---------|---------|
-   | **轻量模式** | 无需配置存储 | 实时抓取 + 关键词筛选 + 多渠道推送 |
-   | **完整模式** | 配置远程云存储 | 轻量模式 + 新增检测 + 趋势追踪 + 增量推送 + AI分析 |
-
-   **轻量模式说明**:
-   - ✅ 可用:实时新闻抓取、关键词筛选、热点权重排序、当前榜单推送
-   - ❌ 不可用:新增新闻检测(🆕)、热度趋势追踪、增量模式、每日汇总累积、MCP AI分析
-
-   **完整模式说明**:配置远程云存储后即可解锁全部功能,继续按下方步骤配置即可。
+   - **Docker / 本地用户**:
+     - **现状**:数据默认保存在本地硬盘。
+     - **建议**:云存储为可选项,可作为异地备份。
 
    <details>
-   <summary>👉 点击展开:<strong>远程云存储配置(决定功能完整性)(可选)</strong></summary>
+   <summary>👉 点击展开:<strong>远程云存储配置教程(以 Cloudflare R2 为例)</strong></summary>
    <br>
 
-   **⚠️ 以 Cloudflare R2 为例的配置前置条件:**
+   **⚠️ 前置条件(重要):**
 
    根据 Cloudflare 平台规则,开通 R2 需绑定支付方式。
 
@@ -1586,9 +1726,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    ---
 
-   **GitHub Secret 配置:**
-
-   **必需配置(4 项):**
+   **GitHub Secret 配置(需添加 4 项):**
 
    | Name(名称) | Secret(值)说明 |
    |-------------|-----------------|
@@ -1607,7 +1745,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    <br>
 
-   **如何获取凭据(以 Cloudflare R2 为例):**
+   **详细操作步骤(获取凭据):**
 
    1. **进入 R2 概览**:
       - 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)。
@@ -1633,30 +1771,40 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
    <br>
 
-6️⃣ **🎉 部署成功!分享你的使用体验**
+### 6️⃣ 第六步:开启 AI 分析推送
 
-   恭喜你完成了 TrendRadar 的配置!现在你可以开始追踪热点资讯了
+   这是 v5.0.0 的核心功能,让 AI 帮你总结和分析新闻,建议尝试
 
-   💬 有更多小伙伴在公众号交流使用心得,期待你的分享~
+   **配置方法:**
+   在 GitHub Secrets (或 `.env` / `config.yaml`) 中添加:
+   - `AI_API_KEY`: 你的 API Key(支持 DeepSeek、OpenAI 等)
+   - `AI_PROVIDER`: 服务商名称(如 `deepseek`, `openai`)
 
-   - 想了解更多玩法和高级技巧?
-   - 遇到问题需要快速解答?
-   - 有好的想法想要交流?
+   就这样,无需复杂部署,下次推送时你就会看到智能分析报告了。
 
-   👉 欢迎关注公众号「**[硅基茶水间](#问题答疑与交流)**」,你的点赞和留言都是项目持续更新的动力。
+   <br>
+
+### 7️⃣ 第七步:🎉 部署成功!
+
+   恭喜!现在你可以开始享受 TrendRadar 带来的高效信息流了。
+
+   💬 **加入社区**:欢迎关注公众号「**[硅基茶水间](#问题答疑与交流)**」,分享你的使用心得和高级玩法。
 
    <br>
 
-7️⃣ **想要更智能的分析?试试 AI 增强功能**(可选)
+### 8️⃣ 第八步:进阶:选择你的 AI 助手
 
-   基础配置已经能满足日常使用,但如果你想要:
+   TrendRadar 提供了两种 AI 使用方式,满足不同需求
 
-   - 让 AI 自动分析热点趋势和数据洞察
-   - 通过自然语言搜索和查询新闻
-   - 获得情感分析、话题预测等深度分析
-   - 在 Claude、Cursor 等 AI 工具中直接调用数据
+   | 特性 | ✨ AI 分析推送 | 🧠 AI 智能分析 |
+   | :--- | :--- | :--- |
+   | **模式** | **被动接收** (每日日报) | **主动对话** (深度调研) |
+   | **场景** | "今天有什么大事?" | "分析一下过去一周 AI 行业的变化" |
+   | **部署** | 极简 (填 Key 即可) | 进阶 (需本地运行/Docker) |
+   | **客户端** | 手机 |  电脑 |
+  
 
-   👉 **了解更多**:[AI 智能分析](#-ai-智能分析) — 解锁项目的隐藏能力,让热点追踪更高效!
+   👉 **结论**:先用 **AI 分析推送** 满足日常需求;如果你是数据分析师或需要深度挖掘,再尝试 **[AI 智能分析](#-ai-智能分析)**。
 
 <br>
 
@@ -1856,7 +2004,7 @@ AI
 
 **💡 不会写正则?让 AI 帮你生成!**
 
-如果你不熟悉正则表达式,可以直接让 ChatGPT / Claude / DeepSeek 帮你生成。只需告诉 AI:
+如果你不熟悉正则表达式,可以直接让 ChatGPT / Gemini / DeepSeek 帮你生成。只需告诉 AI:
 
 > 我需要一个 Python 正则表达式,用于匹配英文单词 "ai",但不匹配 "training" 中的 "ai"。
 > 请直接给出正则表达式,格式为 `/pattern/`,不需要额外解释。
@@ -2291,6 +2439,7 @@ TrendRadar 提供两个独立的 Docker 镜像,可根据需求选择部署:
    # 下载配置文件模板
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/config.yaml -P config/
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/frequency_words.txt -P config/
+   wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/config/ai_analysis_prompt.txt -P config/
 
    # 下载 docker compose 配置
    wget https://raw.githubusercontent.com/sansan0/TrendRadar/master/docker/.env  -P docker/
@@ -2302,7 +2451,8 @@ TrendRadar 提供两个独立的 Docker 镜像,可根据需求选择部署:
 当前目录/
 ├── config/
 │   ├── config.yaml
-│   └── frequency_words.txt
+│   ├── frequency_words.txt
+│   └── ai_analysis_prompt.txt    # AI 分析提示词(v5.0.0 新增,可选)
 └── docker/
     ├── .env
     └── docker-compose.yml
@@ -2311,9 +2461,10 @@ TrendRadar 提供两个独立的 Docker 镜像,可根据需求选择部署:
 2. **配置文件说明**:
 
    **配置分工原则(v4.6.0 优化)**:
-   - `config/config.yaml` - **功能配置**(报告模式、推送设置、存储格式、推送窗口等)
+   - `config/config.yaml` - **功能配置**(报告模式、推送设置、存储格式、推送窗口、AI 分析等)
    - `config/frequency_words.txt` - **关键词配置**(设置你关心的热点词汇)
-   - `docker/.env` - **敏感信息 + Docker 特有配置**(webhook URLs、S3 密钥、定时任务)
+   - `config/ai_analysis_prompt.txt` - **AI 提示词配置**(自定义 AI 分析角色和输出格式,v5.0.0 新增)
+   - `docker/.env` - **敏感信息 + Docker 特有配置**(webhook URLs、API Key、S3 密钥、定时任务)
 
    > 💡 **配置修改生效**:修改 `config.yaml` 后,执行 `docker compose up -d` 重启容器即可生效
 
@@ -2330,6 +2481,9 @@ TrendRadar 提供两个独立的 Docker 镜像,可根据需求选择部署:
    | `ENABLE_WEBSERVER` | - | `true` / `false` | 是否自动启动 Web 服务器 |
    | `WEBSERVER_PORT` | - | `8080` | Web 服务器端口 |
    | `FEISHU_WEBHOOK_URL` | `notification.channels.feishu.webhook_url` | `https://...` | 飞书 Webhook(多账号用 `;` 分隔) |
+   | `AI_ANALYSIS_ENABLED` | `ai_analysis.enabled` | `true` / `false` | 是否启用 AI 分析(v5.0.0 新增) |
+   | `AI_API_KEY` | `ai_analysis.api_key` | `sk-xxx...` | AI API Key(v5.0.0 新增) |
+   | `AI_PROVIDER` | `ai_analysis.provider` | `deepseek` / `openai` / `gemini` | AI 提供商(v5.0.0 新增) |
    | `S3_*` | `storage.remote.*` | - | 远程存储配置(5 个参数) |
 
    **配置优先级**:环境变量 > config.yaml
@@ -2367,7 +2521,7 @@ TrendRadar 提供两个独立的 Docker 镜像,可根据需求选择部署:
 
    > 💡 **提示**:
    > - 大多数用户只需启动 `trendradar` 即可实现新闻推送功能
-   > - 只有需要使用 Claude/ChatGPT 进行 AI 对话分析时,才需启动 `trendradar-mcp`
+   > - 只有需要使用 ChatGPT/Gemini 进行 AI 对话分析时,才需启动 `trendradar-mcp`
    > - 两个服务相互独立,可根据需求灵活组合
 
 4. **查看运行状态**:
@@ -2734,6 +2888,39 @@ SORT_BY_POSITION_FIRST=true
 MAX_NEWS_PER_KEYWORD=10
 ```
 
+#### 独立展示区配置(v5.0.0 新增)
+
+为指定平台提供完整热榜展示,不受 `frequency_words.txt` 关键词过滤影响。
+
+**配置位置:** `config/config.yaml` 的 `notification.standalone_display` 部分
+
+```yaml
+notification:
+  standalone_display:
+    enabled: false                    # 是否启用
+    platforms: ["zhihu", "weibo"]     # 热榜平台 ID 列表
+    rss_feeds: ["hacker-news"]        # RSS 源 ID 列表
+    max_items: 20                     # 每个源最多展示条数(0=不限制)
+```
+
+**适用场景:**
+- 想完整查看某个平台(如知乎)的热榜排名,而不是只看匹配关键词的内容
+- 订阅了更新较少的 RSS 源(如个人博客),希望每次更新都完整推送
+
+**效果示例:**
+```
+📋 独立展示区 (共 15 条)
+
+知乎热榜 (10 条):
+  1. [知乎] 如何看待 OpenAI 发布 Sora?
+  2. [知乎] 2024 年考研分数线公布...
+  ...
+
+Hacker News (5 条):
+  1. [Hacker News] Launch HN: TrendRadar...
+  ...
+```
+
 </details>
 
 ### 8. 推送时间窗口配置
@@ -3232,9 +3419,126 @@ app:
 
 </details>
 
+### 12. AI 分析配置
+
+<details id="ai-analysis-config">
+<summary>👉 点击展开:<strong>AI 分析推送配置详解</strong></summary>
+<br>
+
+#### 功能概述
+
+v5.0.0 新增 AI 分析推送功能,使用 AI 大模型对推送内容进行深度分析,自动生成热点洞察报告。
+
+**分析内容包括**:
+- 热点趋势概述
+- 关键词热度分析
+- 跨平台关联分析
+- 潜在影响评估
+- 值得关注的信号
+- 总结与建议
+
+#### 配置位置
+
+**配置文件**:`config/config.yaml` 的 `ai_analysis` 部分
+
+```yaml
+ai_analysis:
+  enabled: false                    # 是否启用 AI 分析
+  provider: "deepseek"              # AI 提供商
+  api_key: ""                       # API Key(建议使用环境变量 AI_API_KEY)
+  model: "deepseek-chat"            # 模型名称
+  base_url: ""                      # 自定义 API 端点(可选)
+  timeout: 90                       # 请求超时(秒)
+  push_mode: "both"                 # 推送模式
+  max_news_for_analysis: 50         # 最多分析多少条新闻
+  include_rss: true                 # 是否包含 RSS 内容
+  prompt_file: "ai_analysis_prompt.txt"  # 提示词配置文件
+```
+
+#### 支持的 AI 提供商
+
+| 提供商 | provider 值 | 默认端点 |
+|-------|------------|---------|
+| **DeepSeek** | `deepseek` | https://api.deepseek.com/v1/chat/completions |
+| **OpenAI** | `openai` | https://api.openai.com/v1/chat/completions |
+| **Google Gemini** | `gemini` | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
+| **自定义** | `custom` | 需填写 base_url |
+
+> 💡 **提示**:使用 `custom` 提供商时,`base_url` 需填写完整的 API 地址(如 `https://api.example.com/v1/chat/completions`)
+
+#### 推送模式说明
+
+| 模式 | 说明 |
+|------|------|
+| `only_analysis` | 仅推送 AI 分析结果,不推送原始内容 |
+| `both` | 两者都推送(默认),AI 分析追加在原始内容后 |
+
+> 💡 **提示**:如果不需要 AI 分析功能,请将 `enabled` 设为 `false`,无需使用 `push_mode` 控制
+
+#### 环境变量支持
+
+| 环境变量 | 说明 | 示例 |
+|---------|------|------|
+| `AI_ANALYSIS_ENABLED` | 是否启用 AI 分析 | `true` / `false` |
+| `AI_API_KEY` | AI API Key | `sk-xxx...` |
+| `AI_PROVIDER` | AI 提供商 | `deepseek` / `openai` / `gemini` / `custom` |
+| `AI_MODEL` | 模型名称 | `deepseek-chat` |
+| `AI_BASE_URL` | 完整 API 地址(custom 提供商必填) | `https://api.example.com/v1/chat/completions` |
+
+#### 自定义提示词
+
+编辑 `config/ai_analysis_prompt.txt` 文件可自定义 AI 分析角色和输出格式。
+
+**文件结构**:
+```
+[system]
+系统提示词,定义 AI 角色和分析原则
+...
+
+[user]
+用户提示词模板,支持变量替换
+...
+```
+
+**可用变量**:
+- `{report_mode}` - 当前报告模式
+- `{report_type}` - 报告类型描述
+- `{current_time}` - 当前时间
+- `{news_count}` - 热榜新闻条数
+- `{rss_count}` - RSS 新闻条数
+- `{keywords}` - 匹配的关键词列表
+- `{platforms}` - 数据来源平台列表
+- `{news_content}` - 新闻内容
+
+#### 快速启用示例
+
+**方式一:配置文件**
+
+```yaml
+ai_analysis:
+  enabled: true
+  provider: "deepseek"
+  api_key: "sk-your-api-key"
+  model: "deepseek-chat"
+  push_mode: "both"
+```
+
+**方式二:环境变量(推荐)**
+
+```bash
+# GitHub Actions: 添加到 Secrets
+# Docker: 添加到 .env 文件
+AI_ANALYSIS_ENABLED=true
+AI_API_KEY=sk-your-api-key
+AI_PROVIDER=deepseek
+AI_MODEL=deepseek-chat
+```
+
+</details>
+
 <br>
 
-## 🤖 AI 智能分析
+##  AI 智能分析
 
 TrendRadar v3.0.0 新增了基于 **MCP (Model Context Protocol)** 的 AI 分析功能,让你可以通过自然语言与新闻数据对话,进行深度分析。
 
@@ -3294,43 +3598,6 @@ TrendRadar MCP 服务支持标准的 Model Context Protocol (MCP) 协议,可
 - Windows 路径使用双反斜杠:`C:\\Users\\YourName\\TrendRadar`
 - 保存后记得重启
 
-<details>
-<summary>👉 点击展开:<b>Claude Desktop</b></summary>
-
-#### 配置文件方式
-
-编辑 Claude Desktop 的 MCP 配置文件:
-
-**Windows**:
-`%APPDATA%\Claude\claude_desktop_config.json`
-
-**Mac**:
-`~/Library/Application Support/Claude/claude_desktop_config.json`
-
-**配置内容**:
-```json
-{
-  "mcpServers": {
-    "trendradar": {
-      "command": "uv",
-      "args": [
-        "--directory",
-        "/path/to/TrendRadar",
-        "run",
-        "python",
-        "-m",
-        "mcp_server.server"
-      ],
-      "env": {},
-      "disabled": false,
-      "alwaysAllow": []
-    }
-  }
-}
-```
-
-</details>
-
 <details>
 <summary>👉 点击展开:<b>Cursor</b></summary>
 
@@ -3464,38 +3731,6 @@ TrendRadar MCP 服务支持标准的 Model Context Protocol (MCP) 协议,可
 
 </details>
 
-<details>
-<summary>👉 点击展开:<b>Claude Code CLI</b></summary>
-
-#### HTTP 模式配置
-
-```bash
-# 1. 启动 HTTP 服务
-# Windows: start-http.bat
-# Mac/Linux: ./start-http.sh
-
-# 2. 添加 MCP 服务器
-claude mcp add --transport http trendradar http://localhost:3333/mcp
-
-# 3. 验证连接(确保服务已启动)
-claude mcp list
-```
-
-#### 使用示例
-
-```bash
-# 查询新闻
-claude "搜索今天知乎的热点新闻,前10条"
-
-# 趋势分析
-claude "分析'人工智能'这个话题最近一周的热度趋势"
-
-# 数据对比
-claude "对比知乎和微博平台对'比特币'的关注度"
-```
-
-</details>
-
 <details>
 <summary>👉 点击展开:<b>MCP Inspector</b>(调试工具)</summary>
 <br>
@@ -3662,24 +3897,21 @@ MCP Inspector 是官方调试工具,用于测试 MCP 连接:
 
 ## ☕问题答疑与交流
 
-> 如果你想支持本项目,可通过微信搜索**腾讯公益**,对里面的**助学**相关的项目随心捐助
+> 如果本项目对你有帮助,你可以选择以下方式支持:  
+> 1. **公益助学**:微信搜索**腾讯公益**,对里面的**助学**相关的项目随心捐。
 >
-> 感谢参与过**一元点赞**的朋友,已收录至顶部**致谢名单**!你们的支持让开源维护更有动力,个人打赏码现已移除。
->
-> 🎯 如果你有兴趣赞助本项目,你的 Banner 将展示在顶部赞助商位置
+> 2. **赞助开发者**:你的赞助将用于补充碳基生物的咖啡因和硅基生物的 Token 消耗。
+
 
 - **GitHub Issues**:适合针对性强的解答。提问时请提供完整信息(截图、错误日志、系统环境等)。
 - **公众号交流**:适合快速咨询。建议优先在相关文章下的公共留言区交流,如私信,请文明礼貌用语😉
 - **联系方式**:path@linux.do
 
 
-<div align="center">
+|公众号关注 |微信点赞 | 支付宝点赞 |
+|:---:|:---:|:---:| 
+| <img src="_image/weixin.png" width="300" title="硅基茶水间"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="微信支付"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="支付宝支付"/> |
 
-|公众号关注 |
-|:---:|
-| <img src="_image/weixin.png" width="400" title="硅基茶水间"/> |
-
-</div>
 
 
 <br>

BIN
_image/feishu.png


+ 98 - 0
config/ai_analysis_prompt.txt

@@ -0,0 +1,98 @@
+# ═══════════════════════════════════════════════════════════════
+#                    TrendRadar AI 分析提示词配置
+# ═══════════════════════════════════════════════════════════════
+#
+# 此文件定义 AI 分析热点新闻时使用的提示词模板
+# 你可以根据需要自定义分析角度和输出格式
+#
+# 可用变量(在分析时会被替换):
+#   {report_mode}     - 当前报告模式 (daily/current/incremental)
+#   {report_type}     - 报告类型描述
+#   {current_time}    - 当前时间
+#   {news_count}      - 热榜新闻条数
+#   {rss_count}       - RSS 新闻条数
+#   {keywords}        - 匹配的关键词列表
+#   {platforms}       - 数据来源平台列表
+#
+# ═══════════════════════════════════════════════════════════════
+
+[system]
+你是一位专业的新闻分析师和趋势观察者。你的任务是分析热点新闻数据,提供有价值的洞察。
+
+分析原则:
+1. 客观中立 - 基于事实分析,避免主观臆断
+2. 深度洞察 - 挖掘表面现象背后的趋势和规律
+3. 实用价值 - 提供可操作的见解和建议
+4. 简洁明了 - 用精炼的语言表达核心观点
+
+## 数据来源说明
+
+本系统从多个热榜平台(如微博、知乎、今日头条等)和 RSS 订阅源抓取新闻数据。
+数据经过 frequency_words.txt 中定义的关键词过滤,只保留匹配的新闻。
+
+## 数据字段说明
+
+### 热榜新闻字段
+每条热榜新闻包含以下维度:
+- 来源: 新闻所在的热榜平台(如微博热搜、知乎热榜、今日头条等)
+- 标题: 新闻标题内容
+- 排名: 该新闻在来源平台热榜中的排名范围,格式为"最高排名-最低排名"(如"1"表示排名稳定在第1,"3-8"表示最高冲到第3名、最低跌到第8名)
+- 时间: 该新闻在热榜上出现的时间段,格式为"首次出现时间~最后出现时间"(如"09:30~12:45"表示从9:30首次上榜到12:45最后一次出现)
+- 出现次数: 在监控时间段内,该新闻被抓取到的次数(次数越多说明在热榜上停留时间越长,热度越持久)
+
+### RSS 新闻字段
+每条 RSS 新闻包含:
+- 来源: RSS 订阅源名称
+- 标题: 文章标题
+- 发布时间: 文章的原始发布时间
+
+## 分析要点
+
+利用这些数据维度,你可以分析:
+1. 热度强度: 排名越靠前(数字越小)、出现次数越多,热度越高
+2. 持续时间: 时间跨度大、出现次数多,说明话题持续发酵
+3. 排名波动: 排名范围大(如"1-20")说明热度不稳定,范围小(如"2-4")说明热度稳定
+4. 跨平台热度: 同一话题在多个平台出现,说明影响力更广
+5. 新兴趋势: 排名快速上升或首次出现的话题
+6. 时效性: RSS 发布时间可判断信息新鲜度
+
+[user]
+请分析以下热点新闻数据:
+
+## 数据概览
+- 报告模式:{report_mode}
+- 报告类型:{report_type}
+- 分析时间:{current_time}
+- 热榜新闻:{news_count} 条
+- RSS 新闻:{rss_count} 条
+- 数据来源:{platforms}
+
+## 匹配关键词
+{keywords}
+
+## 新闻内容
+{news_content}
+
+---
+
+请基于上述数据进行多维度分析,以 JSON 格式返回结果:
+
+```json
+{
+  "summary": "核心热点概况(用简练语言概括当前最主要的核心事件,避免提及具体排名数据,80字以内)",
+  "keyword_analysis": "热度走势分析(结合排名波动、出现次数和时间跨度,分析核心话题的爆发力与持久性,80字以内)",
+  "sentiment": "情感倾向分析(极其重要:深入分析公众对核心话题的情感反馈,如:正面、负面、担忧、中性或争议,并简述原因,80字以内)",
+  "cross_platform": "跨平台联动分析(分析话题在多平台同步热搜的程度及其影响力差异,60字以内)",
+  "impact": "潜在影响评估(评估话题对社会舆论、行业动态或公众决策的冲击,60字以内)",
+  "signals": "异常与弱信号捕捉(关注排名骤升、首次出现或反直觉的波动,60字以内)",
+  "conclusion": "结论与建议(给出1-2条具有参考价值的操作性建议,40字以内)"
+}
+```
+
+要求:
+- 必须返回有效的 JSON 格式
+- 分析要结合排名、出现次数、时间跨度等数据维度
+- 情感倾向分析是重点,请确保能够准确捕捉舆论风向
+- 每个字段都要填写,如无明显发现可写"暂无明显特征"
+- 使用中文
+- 保持简洁,避免冗余内容在不同字段间重复

+ 74 - 3
config/config.yaml

@@ -168,6 +168,18 @@ notification:
     end: "22:00"                      # 结束时间(北京时间)
     once_per_day: true                # true=窗口内只推送一次,false=窗口内每次执行都推送
 
+  # 📋 独立展示区配置(可选功能)
+  # 用途:将指定平台的完整热榜/RSS 单独展示,不受关键词过滤影响
+  # 适用场景:
+  #   - 想完整查看某个平台的热榜排名
+  #   - RSS 源内容较少,希望全部展示而非只显示关键词匹配的
+  # 注意:同一新闻可能同时出现在关键词匹配区和独立展示区
+  standalone_display:
+    enabled: false                    # 是否启用独立展示区
+    platforms: []                     # 热榜平台 ID 列表(如 ["zhihu", "weibo"])
+    rss_feeds: []                     # RSS 源 ID 列表(如 ["hacker-news"])
+    max_items: 20                     # 每个源最多展示条数(0=不限制)
+
   # 推送渠道配置
   channels:
     feishu:
@@ -202,6 +214,12 @@ notification:
     slack:
       webhook_url: ""                 # Slack Incoming Webhook URL
 
+    generic_webhook:
+      webhook_url: ""                 # 通用 Webhook URL(支持 Discord、Matrix、IFTTT 等)
+      payload_template: ""            # JSON 模板,支持 {title} 和 {content} 占位符
+                                      # 示例:{"content": "{content}"}
+                                      # 留空则使用默认格式:{"title": "{title}", "content": "{content}"}
+
 
 # ===============================================================
 # 6. 存储配置
@@ -247,22 +265,75 @@ storage:
 
 
 # ===============================================================
-# 7. 高级设置(一般无需修改)
+# 7. AI 分析功能
+#
+# 使用 AI 大模型对推送内容进行深度分析
+# 支持 OpenAI、Anthropic、DeepSeek等兼容接口
+# ===============================================================
+ai_analysis:
+  enabled: true                    # 是否启用 AI 分析
+
+  # AI 提供商配置
+  # 支持的提供商:
+  #   - deepseek: DeepSeek(默认)
+  #   - openai: OpenAI
+  #   - gemini: Google Gemini
+  #   - custom: 自定义 OpenAI 兼容接口(需填写完整 base_url)
+  provider: "deepseek"              # 提供商
+  api_key: ""                       # API Key(建议使用环境变量 AI_API_KEY)
+
+  model: "deepseek-chat"            # 模型名称
+                                    # DeepSeek: deepseek-chat, deepseek-reasoner
+                                    # OpenAI: o3-mini, o1, gpt-4o
+                                    # Gemini: gemini-2.5-flash, gemini-2.5-pro
+
+  base_url: ""                      # 完整 API 地址(可选)
+                                    # 留空则使用提供商默认端点
+                                    # 其他提供商必须填写完整 URL
+                                    # 示例: https://api.openai.com/v1/chat/completions
+
+  timeout: 90                       # 请求超时(秒)
+
+  # 推送模式(仅在 enabled: true 时生效)
+  # - only_analysis: 仅推送 AI 分析结果(若开启了“独立展示区”则一并保留,屏蔽原始热榜/RSS 列表)
+  # - both: 两者都推送(分析追加在原始内容后)
+  # 注:如果不需要 AI 分析,请将上方 enabled 设为 false,无需使用 push_mode 控制
+  push_mode: "both"
+
+  # 分析选项
+  max_news_for_analysis: 50         # 参与分析的新闻数量上限(控制成本关键项)
+                                    # api 成本估算 (仅供参考)
+                                      # 按默认推送频率和模型
+                                      # GitHub Action 约 0.1 元/天
+                                      # Docker 部署约 0.2 元/天
+
+  include_rss: false                # 是否包含 RSS 内容进行分析
+
+  # 提示词配置文件路径(相对于 config 目录)
+  prompt_file: "ai_analysis_prompt.txt"
+
+
+# ===============================================================
+# 8. 高级设置(一般无需修改)
 # ===============================================================
 advanced:
+  # 调试模式
+  debug: false
+
   # 版本检查
   version_check_url: "https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/version"
+  mcp_version_check_url: "https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/version_mcp"
 
   # 爬虫设置
   crawler:
     enabled: true                     # 是否启用爬取新闻功能
-    request_interval: 1000            # 请求间隔(毫秒)
+    request_interval: 2000            # 请求间隔(毫秒)
     use_proxy: false                  # 是否启用代理
     default_proxy: "http://127.0.0.1:10801"
 
   # RSS 设置
   rss:
-    request_interval: 2000            # 请求间隔(毫秒)
+    request_interval: 1000            # 请求间隔(毫秒)
     timeout: 15                       # 请求超时(秒)
     use_proxy: false                  # 是否使用代理
     proxy_url: ""                     # RSS 专属代理(留空则使用 crawler.default_proxy)

+ 204 - 26
config/frequency_words.txt

@@ -1,25 +1,172 @@
+# ═══════════════════════════════════════════════════════════════
+#                    TrendRadar 频率词配置文件
+# ═══════════════════════════════════════════════════════════════
+#
+# 这个文件用来设置你想关注的新闻关键词。
+# 系统会自动抓取包含这些关键词的热榜新闻推送给你。
+#
+# 文件分为两个区域:
+#   [GLOBAL_FILTER]  - 全局过滤区:排除不想看的内容
+#   [WORD_GROUPS]    - 词组定义区:设置想关注的关键词
+#
+# ═══════════════════════════════════════════════════════════════
+
+
+# ───────────────────────────────────────────────────────────────
+#                        全局过滤区
+# ───────────────────────────────────────────────────────────────
+# 在这里写入你不想看到的词,每行一个。
+# 包含这些词的新闻会被自动排除,不会出现在推送中。
+#
+# 使用方法:
+#   震惊              直接写词,包含"震惊"的新闻会被过滤
+#   /赌博|博彩/       用 /.../ 包裹可以匹配多个词(用 | 分隔)
+
+[GLOBAL_FILTER]
+# 过滤标题党
+震惊
+
+
+
+# ───────────────────────────────────────────────────────────────
+#                        词组定义区
+# ───────────────────────────────────────────────────────────────
+# 在这里写入你想关注的关键词。
+# 每个词组用空行分隔,同一词组内的关键词是"或"的关系。
+#
+# ┌─────────────────────────────────────────────────────────────┐
+# │                      语法总览(快速参考)                      │
+# └─────────────────────────────────────────────────────────────┘
+#
+# 关键词语法:
+#   关键词            普通关键词,标题包含即匹配
+#   /正则/            正则表达式匹配(自动忽略大小写)
+#   关键词 => 别名    给关键词指定显示别名
+#   [组别名]          词组第一行,给整组指定别名
+#   +关键词           必须词,所有必须词都要匹配才算匹配
+#   !关键词           过滤词,匹配则排除该条新闻(仅限当前词组)
+#   @数字             限制该词组最多显示多少条
+#
+# 显示名称优先级:
+#   1. 有组别名 → 显示组别名
+#   2. 没有组别名 → 显示各行别名拼接(用 " / " 连接)
+#   3. 没有别名 → 显示关键词本身
+#
+#
+# ┌─────────────────────────────────────────────────────────────┐
+# │                      基础用法(推荐新手)                      │
+# └─────────────────────────────────────────────────────────────┘
+#
+# 1. 最简单:直接写关键词
+#    ────────────────────
+#    华为
+#
+#    效果:匹配所有包含"华为"的新闻
+#
+#
+# 2. 多个关键词归为一组
+#    ────────────────────
+#    华为
+#    鸿蒙
+#    任正非
+#
+#    效果:匹配包含"华为"或"鸿蒙"或"任正非"的新闻,统一显示为"华为 / 鸿蒙 / 任正非"
+#
+#
+# 3. 给词组起个名字(推荐)
+#    ────────────────────
+#    [华为]
+#    华为
+#    鸿蒙
+#    任正非
+#
+#    效果:同上,但显示名称为"华为"(更简洁)
+#
+#
+# ┌─────────────────────────────────────────────────────────────┐
+# │                      进阶用法(可选)                         │
+# └─────────────────────────────────────────────────────────────┘
+#
+# 4. 用正则表达式匹配多个词(一行搞定)
+#    ────────────────────
+#    /华为|鸿蒙|任正非/ => 华为
+#
+#    效果:匹配包含"华为"或"鸿蒙"或"任正非"的新闻,显示为"华为"
+#    说明:/.../ 里用 | 分隔多个词,=> 后面是显示名称
+#
+#    💡 不懂正则?问 AI:
+#       "帮我写一个正则表达式,匹配包含'华为'或'鸿蒙'或'任正非'的文本,
+#        格式要求:/正则/ => 显示名称"
+#
+#
+# 5. 精确匹配英文单词(避免误匹配)
+#    ────────────────────
+#    /\bAI\b/i => AI
+#
+#    说明:\b 表示单词边界,避免匹配到 "MAIL" 中的 "AI"
+#          /i 表示忽略大小写,"ai"、"AI"、"Ai" 都能匹配
+#
+#    💡 不懂正则?问 AI:
+#       "帮我写一个正则表达式,精确匹配英文单词'AI'(不匹配 MAIL 中的 AI),
+#        忽略大小写,格式要求:/正则/i => 显示名称"
+#
+#
+# 6. 排除特定内容
+#    ────────────────────
+#    [苹果公司]
+#    苹果
+#    !水果
+#    !果园
+#
+#    效果:匹配"苹果"但排除包含"水果"或"果园"的新闻
+#    说明:! 开头的词表示"排除"
+#
+#
+# 7. 限制显示条数
+#    ────────────────────
+#    [科技新闻]
+#    科技
+#    @5
+#
+#    效果:最多显示 5 条匹配的新闻
+#    说明:@数字 表示限制条数
+#
+#
+# 8. 必须同时包含多个词
+#    ────────────────────
+#    +苹果
+#    +发布会
+#
+#    效果:必须同时包含"苹果"和"发布会"才匹配
+#    说明:+ 开头的词表示"必须包含"
+#
+# ───────────────────────────────────────────────────────────────
+
+[WORD_GROUPS]
+
+# ═══════════════════════════════════════════════════════════════
+#                         企业与品牌
+# ═══════════════════════════════════════════════════════════════
+
 /胖东来|于东来/ => 胖东来
 
 /DeepSeek|梁文锋/i => DeepSeek
 
 /华为|鸿蒙|HarmonyOS|任正非/i => 华为
 
-/比亚迪|王传福/ => 比亚迪
+/比亚迪|王传福|byd/i => 比亚迪
 
 /大疆|\bDJI\b/i => 大疆
 
-/宇树|王兴兴/ => 宇树机器人
+/宇树|王兴兴|Unitree/ => 宇树机器人
 
-/智元|灵犀|稚晖君|彭志辉/ => 智元机器人
+/智元|灵犀|稚晖君|彭志辉|AgiBot/ => 智元机器人
+/众擎|EngineAI|赵同阳/ => 众擎机器人
 
 /黑神话|冯骥/ => 黑神话悟空
 
 /影之刃零|梁其伟/ => 影之刃零
 
-/哪吒|杨宇/ => 哪吒电影
-!车
-!餐
-
 /三体|流浪地球|刘慈欣|郭帆/ => 三体/流浪地球
 
 申奥
@@ -28,46 +175,77 @@
 
 /字节|bytedance|张一鸣/i => 字节跳动
 
-/特斯拉|马斯克/ => 特斯拉
+/qwen|minimax|glm/ => 国产开源模型
 
-/微软|\bMicrosoft\b/i => 微软
+/特斯拉|马斯克/ => 特斯拉
 
 /英伟达|\bNVIDIA\b|黄仁勋/i => 英伟达
-
 /\bAMD\b/i => AMD
 
+/微软|\bMicrosoft\b/i => 微软
 /谷歌|\bgoogle\b|\bgemini\b|\bdeepMind\b/i => 谷歌
+/\biphone\b|\bipad\b|\bmac\b|\bios\b/i => 苹果
 
 /\bchatgpt\b|\bopenai\b|\bsora\b/i => OpenAI
+/\bclaude\b|anthropic/i => Claude
 
-/\bclaude\b|Anthropic/i => Claude
 
-/\biphone\b|\bipad\b|\bmac\b|\bios\b/i => 苹果产品
-
-/(?<![a-zA-Z])ai(?![a-zA-Z])/i => AI 相关
-人工智能
-
-自动驾驶
-
-机器人
+# ═══════════════════════════════════════════════════════════════
+#                         国家与地区
+# ═══════════════════════════════════════════════════════════════
 
+[中国]
 国产
 中国
 
-美国
+[东亚]
 日本
+朝鲜
 韩国
 
+[北美]
+美国
+加拿大
+
+[西欧]
+法国
+英国
+
+/俄罗斯|俄国/ => 俄罗斯
+
+印度
+
+
+# ═══════════════════════════════════════════════════════════════
+#                         科技领域
+# ═══════════════════════════════════════════════════════════════
+
+[AI 相关]
+/(?<![a-zA-Z])ai(?![a-zA-Z])/i
+人工智能
+
+[芯片]
 芯片
 光刻机
+半导体
 
-科技
-
+/水电|雅鲁藏布江/ => 水电
+/光伏|太阳能/ => 光伏
 核能
+能源
+
+/自动驾驶|无人驾驶|智驾/ => 自动驾驶
+
+机器人
+/机械狗|四足/ => 机器狗
+具身智能
 
-水电站
-雅鲁藏布江
+/月球|登月|火星|宇宙|飞船|航天|空间站|卫星/ => 航天
 
-新质生产力
+# 前沿科技
+量子
+脑机
+基因
 
-/月球|登月|火星|宇宙|飞船|航空/ => 航天航空
+# 产业政策
+生产力

+ 22 - 0
docker/.env

@@ -62,6 +62,28 @@ BARK_URL=
 # Slack 推送配置(多账号用 ; 分隔)
 SLACK_WEBHOOK_URL=
 
+# 通用 Webhook 配置(多账号用 ; 分隔)
+# 支持 Discord、Matrix、IFTTT 等任意支持 Webhook 的平台
+GENERIC_WEBHOOK_URL=
+# JSON 模板,支持 {title} 和 {content} 占位符
+# 示例:{"content": "{content}"}
+GENERIC_WEBHOOK_TEMPLATE=
+
+# ============================================
+# AI 分析配置
+# ============================================
+
+# 是否启用 AI 分析 (true/false)
+AI_ANALYSIS_ENABLED=false
+# AI API Key(必填,启用 AI 分析时需要)
+AI_API_KEY=
+# AI 提供商 (deepseek|openai|gemini|custom)
+AI_PROVIDER=deepseek
+# 模型名称
+AI_MODEL=deepseek-chat
+# 自定义 API 端点(使用 custom 提供商时必填)
+AI_BASE_URL=
+
 # ============================================
 # 远程存储配置(S3 兼容协议,支持 R2/OSS/COS/S3 等)
 # ============================================

+ 9 - 0
docker/docker-compose-build.yml

@@ -44,6 +44,15 @@ services:
       - BARK_URL=${BARK_URL:-}
       # Slack配置
       - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-}
+      # 通用Webhook配置
+      - GENERIC_WEBHOOK_URL=${GENERIC_WEBHOOK_URL:-}
+      - GENERIC_WEBHOOK_TEMPLATE=${GENERIC_WEBHOOK_TEMPLATE:-}
+      # AI 分析配置
+      - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
+      - AI_API_KEY=${AI_API_KEY:-}
+      - AI_PROVIDER=${AI_PROVIDER:-}
+      - AI_MODEL=${AI_MODEL:-}
+      - AI_BASE_URL=${AI_BASE_URL:-}
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 9 - 0
docker/docker-compose.yml

@@ -42,6 +42,15 @@ services:
       - BARK_URL=${BARK_URL:-}
       # Slack配置
       - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-}
+      # 通用Webhook配置
+      - GENERIC_WEBHOOK_URL=${GENERIC_WEBHOOK_URL:-}
+      - GENERIC_WEBHOOK_TEMPLATE=${GENERIC_WEBHOOK_TEMPLATE:-}
+      # AI 分析配置
+      - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
+      - AI_API_KEY=${AI_API_KEY:-}
+      - AI_PROVIDER=${AI_PROVIDER:-}
+      - AI_MODEL=${AI_MODEL:-}
+      - AI_BASE_URL=${AI_BASE_URL:-}
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 6 - 0
docker/manage.py

@@ -296,6 +296,12 @@ def show_config():
         "NTFY_TOKEN",
         "BARK_URL",
         "SLACK_WEBHOOK_URL",
+        # AI 分析配置
+        "AI_ANALYSIS_ENABLED",
+        "AI_API_KEY",
+        "AI_PROVIDER",
+        "AI_MODEL",
+        "AI_BASE_URL",
         # 远程存储配置
         "S3_BUCKET_NAME",
         "S3_ACCESS_KEY_ID",

+ 1 - 1
mcp_server/__init__.py

@@ -5,4 +5,4 @@ TrendRadar MCP Server
 
 """
 
-__version__ = "2.0.1"
+__version__ = "3.1.5"

+ 212 - 30
mcp_server/server.py

@@ -5,10 +5,12 @@ TrendRadar MCP Server - FastMCP 2.0 实现
 支持 stdio 和 HTTP 两种传输模式。
 """
 
+import asyncio
 import json
 from typing import List, Optional, Dict, Union
 
-from fastmcp import FastMCP
+from fastmcp import FastMCP, Context
+from fastmcp.server.dependencies import get_context
 
 from .tools.data_query import DataQueryTools
 from .tools.analytics import AnalyticsTools
@@ -26,6 +28,9 @@ mcp = FastMCP('trendradar-news')
 # 全局工具实例(在第一次请求时初始化)
 _tools_instances = {}
 
+# Session-level 工具实例存储(用于 Context 管理)
+_session_tools: Dict[str, Dict] = {}
+
 
 def _get_tools(project_root: Optional[str] = None):
     """获取或创建工具实例(单例模式)"""
@@ -39,6 +44,108 @@ def _get_tools(project_root: Optional[str] = None):
     return _tools_instances
 
 
+def _get_tools_with_context(ctx: Optional[Context] = None) -> Dict:
+    """
+    获取工具实例(支持 Session 隔离)
+
+    如果提供了 Context,则为每个 session 创建独立的工具实例。
+    这样可以避免不同会话之间的状态污染。
+
+    Args:
+        ctx: FastMCP Context 对象
+
+    Returns:
+        工具实例字典
+    """
+    if ctx is None:
+        return _get_tools()
+
+    # 获取 session ID(如果有的话)
+    session_id = getattr(ctx, 'session_id', None) or 'default'
+
+    if session_id not in _session_tools:
+        # 为新 session 创建工具实例
+        _session_tools[session_id] = {
+            'data': DataQueryTools(),
+            'analytics': AnalyticsTools(),
+            'search': SearchTools(),
+            'config': ConfigManagementTools(),
+            'system': SystemManagementTools(),
+            'storage': StorageSyncTools(),
+        }
+
+    return _session_tools[session_id]
+
+
+# ==================== MCP Resources ====================
+
+@mcp.resource("config://platforms")
+async def get_platforms_resource() -> str:
+    """
+    获取支持的平台列表
+
+    返回 config.yaml 中配置的所有平台信息,包括 ID 和名称。
+    """
+    tools = _get_tools()
+    config = await asyncio.to_thread(
+        tools['config'].get_current_config, section="crawler"
+    )
+    return json.dumps({
+        "platforms": config.get("platforms", []),
+        "description": "TrendRadar 支持的热榜平台列表"
+    }, ensure_ascii=False, indent=2)
+
+
+@mcp.resource("config://rss-feeds")
+async def get_rss_feeds_resource() -> str:
+    """
+    获取 RSS 订阅源列表
+
+    返回当前配置的所有 RSS 源信息。
+    """
+    tools = _get_tools()
+    status = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
+    return json.dumps({
+        "feeds": status.get("today_feeds", {}),
+        "description": "TrendRadar 支持的 RSS 订阅源列表"
+    }, ensure_ascii=False, indent=2)
+
+
+@mcp.resource("data://available-dates")
+async def get_available_dates_resource() -> str:
+    """
+    获取可用的数据日期范围
+
+    返回本地存储中可查询的日期列表。
+    """
+    tools = _get_tools()
+    result = await asyncio.to_thread(
+        tools['storage'].list_available_dates, source="local"
+    )
+    return json.dumps({
+        "dates": result.get("data", {}).get("local", {}).get("dates", []),
+        "description": "本地存储中可查询的日期列表"
+    }, ensure_ascii=False, indent=2)
+
+
+@mcp.resource("config://keywords")
+async def get_keywords_resource() -> str:
+    """
+    获取关注词配置
+
+    返回 frequency_words.txt 中配置的关注词分组。
+    """
+    tools = _get_tools()
+    config = await asyncio.to_thread(
+        tools['config'].get_current_config, section="keywords"
+    )
+    return json.dumps({
+        "word_groups": config.get("word_groups", []),
+        "total_groups": config.get("total_groups", 0),
+        "description": "TrendRadar 关注词配置"
+    }, ensure_ascii=False, indent=2)
+
+
 # ==================== 日期解析工具(优先调用)====================
 
 @mcp.tool
@@ -93,7 +200,7 @@ async def resolve_date_range(
         2. search_news(query="特斯拉", date_range={"start": "2025-11-20", "end": "2025-11-26"})
     """
     try:
-        result = DateParser.resolve_date_range_expression(expression)
+        result = await asyncio.to_thread(DateParser.resolve_date_range_expression, expression)
         return json.dumps(result, ensure_ascii=False, indent=2)
     except MCPError as e:
         return json.dumps({
@@ -146,7 +253,10 @@ async def get_latest_news(
     **注意**:如果用户询问"为什么只显示了部分",说明他们需要完整数据
     """
     tools = _get_tools()
-    result = tools['data'].get_latest_news(platforms=platforms, limit=limit, include_url=include_url)
+    result = await asyncio.to_thread(
+        tools['data'].get_latest_news,
+        platforms=platforms, limit=limit, include_url=include_url
+    )
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -176,7 +286,10 @@ async def get_trending_topics(
         - 自动提取热点: get_trending_topics(extract_mode="auto_extract", top_n=20)
     """
     tools = _get_tools()
-    result = tools['data'].get_trending_topics(top_n=top_n, mode=mode, extract_mode=extract_mode)
+    result = await asyncio.to_thread(
+        tools['data'].get_trending_topics,
+        top_n=top_n, mode=mode, extract_mode=extract_mode
+    )
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -185,11 +298,12 @@ async def get_trending_topics(
 @mcp.tool
 async def get_latest_rss(
     feeds: Optional[List[str]] = None,
+    days: int = 1,
     limit: int = 50,
     include_summary: bool = False
 ) -> str:
     """
-    获取最新的 RSS 订阅数据
+    获取最新的 RSS 订阅数据(支持多日查询)
 
     RSS 数据与热榜新闻分开存储,按时间流展示,适合获取特定来源的最新内容。
 
@@ -197,6 +311,10 @@ async def get_latest_rss(
         feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr']
                - 不指定时:返回所有已配置 RSS 源的数据
                - 支持的 RSS 源来自 config/config.yaml 的 rss.feeds 配置
+        days: 获取最近 N 天的数据,默认 1(仅今天),最大 30 天
+              - 1: 仅今天(默认)
+              - 7: 最近一周
+              - 30: 最近一个月
         limit: 返回条数限制,默认50,最大500
         include_summary: 是否包含文章摘要,默认False(节省token)
 
@@ -209,17 +327,22 @@ async def get_latest_rss(
             - url: 文章链接
             - published_at: 发布时间
             - author: 作者(如有)
+            - date: 数据日期
             - summary: 摘要(仅当 include_summary=True)
         - total: 返回条数
         - feeds: 请求的 RSS 源列表
 
     Examples:
-        - 获取所有 RSS 最新内容: get_latest_rss()
-        - 获取指定源: get_latest_rss(feeds=['hacker-news'])
-        - 包含摘要: get_latest_rss(include_summary=True, limit=20)
+        - 获取今天所有 RSS: get_latest_rss()
+        - 获取最近一周: get_latest_rss(days=7)
+        - 获取指定源最近7天: get_latest_rss(feeds=['hacker-news'], days=7)
+        - 包含摘要: get_latest_rss(include_summary=True, days=7, limit=20)
     """
     tools = _get_tools()
-    result = tools['data'].get_latest_rss(feeds=feeds, limit=limit, include_summary=include_summary)
+    result = await asyncio.to_thread(
+        tools['data'].get_latest_rss,
+        feeds=feeds, days=days, limit=limit, include_summary=include_summary
+    )
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -252,7 +375,8 @@ async def search_rss(
         - search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
     """
     tools = _get_tools()
-    result = tools['data'].search_rss(
+    result = await asyncio.to_thread(
+        tools['data'].search_rss,
         keyword=keyword,
         feeds=feeds,
         days=days,
@@ -281,7 +405,7 @@ async def get_rss_feeds_status() -> str:
         - get_rss_feeds_status()  # 查看所有 RSS 源状态
     """
     tools = _get_tools()
-    result = tools['data'].get_rss_feeds_status()
+    result = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -325,7 +449,8 @@ async def get_news_by_date(
     **注意**:如果用户询问"为什么只显示了部分",说明他们需要完整数据
     """
     tools = _get_tools()
-    result = tools['data'].get_news_by_date(
+    result = await asyncio.to_thread(
+        tools['data'].get_news_by_date,
         date_range=date_range,
         platforms=platforms,
         limit=limit,
@@ -388,7 +513,8 @@ async def analyze_topic_trend(
         2. analyze_topic_trend(topic="特斯拉", analysis_type="lifecycle", date_range=...)
     """
     tools = _get_tools()
-    result = tools['analytics'].analyze_topic_trend_unified(
+    result = await asyncio.to_thread(
+        tools['analytics'].analyze_topic_trend_unified,
         topic=topic,
         analysis_type=analysis_type,
         date_range=date_range,
@@ -434,7 +560,8 @@ async def analyze_data_insights(
         - analyze_data_insights(insight_type="keyword_cooccur", min_frequency=5, top_n=15)
     """
     tools = _get_tools()
-    result = tools['analytics'].analyze_data_insights_unified(
+    result = await asyncio.to_thread(
+        tools['analytics'].analyze_data_insights_unified,
         insight_type=insight_type,
         topic=topic,
         date_range=date_range,
@@ -497,7 +624,8 @@ async def analyze_sentiment(
     - 仅在用户明确要求"总结"或"挑重点"时才进行筛选
     """
     tools = _get_tools()
-    result = tools['analytics'].analyze_sentiment(
+    result = await asyncio.to_thread(
+        tools['analytics'].analyze_sentiment,
         topic=topic,
         platforms=platforms,
         date_range=date_range,
@@ -546,7 +674,8 @@ async def find_related_news(
     - 仅在用户明确要求"总结"时才进行筛选
     """
     tools = _get_tools()
-    result = tools['search'].find_related_news_unified(
+    result = await asyncio.to_thread(
+        tools['search'].find_related_news_unified,
         reference_title=reference_title,
         date_range=date_range,
         threshold=threshold,
@@ -575,7 +704,8 @@ async def generate_summary_report(
         JSON格式的摘要报告,包含Markdown格式内容
     """
     tools = _get_tools()
-    result = tools['analytics'].generate_summary_report(
+    result = await asyncio.to_thread(
+        tools['analytics'].generate_summary_report,
         report_type=report_type,
         date_range=date_range
     )
@@ -635,7 +765,8 @@ async def aggregate_news(
     - 可优先展示 platform_count > 1 的新闻
     """
     tools = _get_tools()
-    result = tools['analytics'].aggregate_news(
+    result = await asyncio.to_thread(
+        tools['analytics'].aggregate_news,
         date_range=date_range,
         platforms=platforms,
         similarity_threshold=similarity_threshold,
@@ -693,7 +824,8 @@ async def compare_periods(
           )
     """
     tools = _get_tools()
-    result = tools['analytics'].compare_periods(
+    result = await asyncio.to_thread(
+        tools['analytics'].compare_periods,
         period1=period1,
         period2=period2,
         topic=topic,
@@ -785,7 +917,8 @@ async def search_news(
     - 当include_rss=True时,热榜和RSS结果分开展示,RSS在热榜之后
     """
     tools = _get_tools()
-    result = tools['search'].search_news_unified(
+    result = await asyncio.to_thread(
+        tools['search'].search_news_unified,
         query=query,
         search_mode=search_mode,
         date_range=date_range,
@@ -821,7 +954,7 @@ async def get_current_config(
         JSON格式的配置信息
     """
     tools = _get_tools()
-    result = tools['config'].get_current_config(section=section)
+    result = await asyncio.to_thread(tools['config'].get_current_config, section=section)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -836,7 +969,52 @@ async def get_system_status() -> str:
         JSON格式的系统状态信息
     """
     tools = _get_tools()
-    result = tools['system'].get_system_status()
+    result = await asyncio.to_thread(tools['system'].get_system_status)
+    return json.dumps(result, ensure_ascii=False, indent=2)
+
+
+@mcp.tool
+async def check_version(
+    proxy_url: Optional[str] = None
+) -> str:
+    """
+    检查版本更新(同时检查 TrendRadar 和 MCP Server)
+
+    比较本地版本与 GitHub 远程版本,判断是否需要更新。
+    远程版本 URL 从 config.yaml 获取:
+    - version_check_url: TrendRadar 版本
+    - mcp_version_check_url: MCP Server 版本
+
+    Args:
+        proxy_url: 可选的代理URL,用于访问 GitHub(如 http://127.0.0.1:7890)
+
+    Returns:
+        JSON格式的版本检查结果,包含:
+        - success: 是否成功
+        - summary:
+            - description: 结果描述
+            - any_update: 是否有任何组件需要更新
+        - data:
+            - trendradar: TrendRadar 版本检查结果
+                - name: 组件名称
+                - current_version: 当前本地版本(如 "5.0.0")
+                - remote_version: 远程最新版本
+                - need_update: 是否需要更新
+                - message: 状态描述
+            - mcp: MCP Server 版本检查结果
+                - name: 组件名称
+                - current_version: 当前本地版本(如 "3.1.4")
+                - remote_version: 远程最新版本
+                - need_update: 是否需要更新
+                - message: 状态描述
+            - any_update: 是否有任何组件需要更新
+
+    Examples:
+        - check_version()  # 直接检查两个组件的版本
+        - check_version(proxy_url="http://127.0.0.1:7890")  # 使用代理访问 GitHub
+    """
+    tools = _get_tools()
+    result = await asyncio.to_thread(tools['system'].check_version, proxy_url=proxy_url)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -871,7 +1049,10 @@ async def trigger_crawl(
         - 使用默认平台: trigger_crawl()  # 爬取config.yaml中配置的所有平台
     """
     tools = _get_tools()
-    result = tools['system'].trigger_crawl(platforms=platforms, save_to_local=save_to_local, include_url=include_url)
+    result = await asyncio.to_thread(
+        tools['system'].trigger_crawl,
+        platforms=platforms, save_to_local=save_to_local, include_url=include_url
+    )
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -914,7 +1095,7 @@ async def sync_from_remote(
         - S3_SECRET_ACCESS_KEY: 访问密钥
     """
     tools = _get_tools()
-    result = tools['storage'].sync_from_remote(days=days)
+    result = await asyncio.to_thread(tools['storage'].sync_from_remote, days=days)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -948,7 +1129,7 @@ async def get_storage_status() -> str:
         - get_storage_status()  # 查看所有存储状态
     """
     tools = _get_tools()
-    result = tools['storage'].get_storage_status()
+    result = await asyncio.to_thread(tools['storage'].get_storage_status)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -992,7 +1173,7 @@ async def list_available_dates(
         - list_available_dates(source="remote")  # 仅查看远程
     """
     tools = _get_tools()
-    result = tools['storage'].list_available_dates(source=source)
+    result = await asyncio.to_thread(tools['storage'].list_available_dates, source=source)
     return json.dumps(result, ensure_ascii=False, indent=2)
 
 
@@ -1065,12 +1246,13 @@ def run_server(
     print("    === 配置与系统管理 ===")
     print("    15. get_current_config      - 获取当前系统配置")
     print("    16. get_system_status       - 获取系统运行状态")
-    print("    17. trigger_crawl           - 手动触发爬取任务")
+    print("    17. check_version           - 检查版本更新(对比本地与远程版本)")
+    print("    18. trigger_crawl           - 手动触发爬取任务")
     print()
     print("    === 存储同步工具 ===")
-    print("    18. sync_from_remote        - 从远程存储拉取数据到本地")
-    print("    19. get_storage_status      - 获取存储配置和状态")
-    print("    20. list_available_dates    - 列出本地/远程可用日期")
+    print("    19. sync_from_remote        - 从远程存储拉取数据到本地")
+    print("    20. get_storage_status      - 获取存储配置和状态")
+    print("    21. list_available_dates    - 列出本地/远程可用日期")
     print("=" * 60)
     print()
 

+ 48 - 0
mcp_server/services/cache_service.py

@@ -4,11 +4,59 @@
 实现TTL缓存机制,提升数据访问性能。
 """
 
+import hashlib
+import json
 import time
 from typing import Any, Optional
 from threading import Lock
 
 
+def make_cache_key(namespace: str, **params) -> str:
+    """
+    生成结构化缓存 key
+
+    通过对参数排序和哈希,确保相同参数组合总是生成相同的 key。
+
+    Args:
+        namespace: 缓存命名空间,如 "latest_news", "trending_topics"
+        **params: 缓存参数
+
+    Returns:
+        格式化的缓存 key,如 "latest_news:a1b2c3d4"
+
+    Examples:
+        >>> make_cache_key("latest_news", platforms=["zhihu"], limit=50)
+        'latest_news:8f14e45f'
+        >>> make_cache_key("search", query="AI", mode="keyword")
+        'search:3c6e0b8a'
+    """
+    if not params:
+        return namespace
+
+    # 对参数进行规范化处理
+    normalized_params = {}
+    for k, v in params.items():
+        if v is None:
+            continue  # 跳过 None 值
+        elif isinstance(v, (list, tuple)):
+            # 列表排序后转为字符串
+            normalized_params[k] = json.dumps(sorted(v) if all(isinstance(i, str) for i in v) else list(v), ensure_ascii=False)
+        elif isinstance(v, dict):
+            # 字典按键排序后转为字符串
+            normalized_params[k] = json.dumps(v, sort_keys=True, ensure_ascii=False)
+        else:
+            normalized_params[k] = str(v)
+
+    # 排序参数并生成哈希
+    sorted_params = sorted(normalized_params.items())
+    param_str = "&".join(f"{k}={v}" for k, v in sorted_params)
+
+    # 使用 MD5 生成短哈希(取前8位)
+    hash_value = hashlib.md5(param_str.encode('utf-8')).hexdigest()[:8]
+
+    return f"{namespace}:{hash_value}"
+
+
 class CacheService:
     """缓存服务类"""
 

+ 80 - 42
mcp_server/services/data_service.py

@@ -382,16 +382,26 @@ class DataService:
         for platform_id, titles in titles_to_process.items():
             for title in titles.keys():
                 if extract_mode == "keywords":
-                    # 基于预设关键词统计
+                    # 基于预设关键词统计(支持正则匹配)
+                    from trendradar.core.frequency import _word_matches
+
                     word_groups = self.parser.parse_frequency_words()
+                    title_lower = title.lower()
+
                     for group in word_groups:
                         all_words = group.get("required", []) + group.get("normal", [])
-                        for word in all_words:
-                            if word and word in title:
-                                word_frequency[word] += 1
-                                if word not in keyword_to_news:
-                                    keyword_to_news[word] = []
-                                keyword_to_news[word].append(title)
+                        # 检查是否匹配词组中的任意一个词
+                        matched = any(_word_matches(word_config, title_lower) for word_config in all_words)
+
+                        if matched:
+                            # 使用组的 display_name(组别名或行别名拼接)
+                            display_key = group.get("display_name") or group.get("group_key", "")
+
+                            word_frequency[display_key] += 1
+                            if display_key not in keyword_to_news:
+                                keyword_to_news[display_key] = []
+                            keyword_to_news[display_key].append(title)
+                            break  # 每个标题只计入第一个匹配的词组
 
                 elif extract_mode == "auto_extract":
                     # 自动提取关键词
@@ -674,62 +684,82 @@ class DataService:
     def get_latest_rss(
         self,
         feeds: Optional[List[str]] = None,
+        days: int = 1,
         limit: int = 50,
         include_summary: bool = False
     ) -> List[Dict]:
         """
-        获取最新的 RSS 数据
+        获取最新的 RSS 数据(支持多日查询)
 
         Args:
             feeds: RSS 源 ID 列表,None 表示所有源
+            days: 获取最近 N 天的数据,默认 1(仅今天),最大 30 天
             limit: 返回条数限制
             include_summary: 是否包含摘要,默认 False(节省 token)
 
         Returns:
-            RSS 条目列表
+            RSS 条目列表(按 URL 去重)
 
         Raises:
             DataNotFoundError: 数据不存在
         """
-        cache_key = f"latest_rss:{','.join(feeds or [])}:{limit}:{include_summary}"
+        days = min(max(days, 1), 30)  # 限制 1-30 天
+        cache_key = f"latest_rss:{','.join(feeds or [])}:{days}:{limit}:{include_summary}"
         cached = self.cache.get(cache_key, ttl=900)
         if cached:
             return cached
 
-        # 读取今天的 RSS 数据
-        all_items, id_to_name, timestamps = self.parser.read_all_titles_for_date(
-            date=None,
-            platform_ids=feeds,
-            db_type="rss"
-        )
+        rss_list = []
+        seen_urls = set()  # 跨日期 URL 去重
+        today = datetime.now()
 
-        # 获取最新的抓取时间
-        if timestamps:
-            latest_timestamp = max(timestamps.values())
-            fetch_time = datetime.fromtimestamp(latest_timestamp)
-        else:
-            fetch_time = datetime.now()
+        for i in range(days):
+            target_date = today - timedelta(days=i)
 
-        # 转换为列表
-        rss_list = []
-        for feed_id, items in all_items.items():
-            feed_name = id_to_name.get(feed_id, feed_id)
+            try:
+                all_items, id_to_name, timestamps = self.parser.read_all_titles_for_date(
+                    date=target_date,
+                    platform_ids=feeds,
+                    db_type="rss"
+                )
 
-            for title, info in items.items():
-                rss_item = {
-                    "title": title,
-                    "feed_id": feed_id,
-                    "feed_name": feed_name,
-                    "url": info.get("url", ""),
-                    "published_at": info.get("published_at", ""),
-                    "author": info.get("author", ""),
-                    "fetch_time": fetch_time.strftime("%Y-%m-%d %H:%M:%S")
-                }
+                # 获取抓取时间
+                if timestamps:
+                    latest_timestamp = max(timestamps.values())
+                    fetch_time = datetime.fromtimestamp(latest_timestamp)
+                else:
+                    fetch_time = target_date
 
-                if include_summary:
-                    rss_item["summary"] = info.get("summary", "")
+                # 转换为列表
+                for feed_id, items in all_items.items():
+                    feed_name = id_to_name.get(feed_id, feed_id)
 
-                rss_list.append(rss_item)
+                    for title, info in items.items():
+                        # 跨日期 URL 去重
+                        url = info.get("url", "")
+                        if url and url in seen_urls:
+                            continue
+                        if url:
+                            seen_urls.add(url)
+
+                        rss_item = {
+                            "title": title,
+                            "feed_id": feed_id,
+                            "feed_name": feed_name,
+                            "url": url,
+                            "published_at": info.get("published_at", ""),
+                            "author": info.get("author", ""),
+                            "date": target_date.strftime("%Y-%m-%d"),
+                            "fetch_time": fetch_time.strftime("%Y-%m-%d %H:%M:%S") if isinstance(fetch_time, datetime) else target_date.strftime("%Y-%m-%d")
+                        }
+
+                        if include_summary:
+                            rss_item["summary"] = info.get("summary", "")
+
+                        rss_list.append(rss_item)
+
+            except DataNotFoundError:
+                continue
 
         # 按发布时间排序(最新的在前)
         rss_list.sort(key=lambda x: x.get("published_at", ""), reverse=True)
@@ -751,7 +781,7 @@ class DataService:
         include_summary: bool = False
     ) -> List[Dict]:
         """
-        搜索 RSS 数据
+        搜索 RSS 数据(跨日期自动去重)
 
         Args:
             keyword: 搜索关键词
@@ -761,7 +791,7 @@ class DataService:
             include_summary: 是否包含摘要
 
         Returns:
-            匹配的 RSS 条目列表
+            匹配的 RSS 条目列表(按 URL 去重)
         """
         cache_key = f"search_rss:{keyword}:{','.join(feeds or [])}:{days}:{limit}:{include_summary}"
         cached = self.cache.get(cache_key, ttl=900)
@@ -769,6 +799,7 @@ class DataService:
             return cached
 
         results = []
+        seen_urls = set()  # 用于 URL 去重
         today = datetime.now()
 
         for i in range(days):
@@ -785,6 +816,13 @@ class DataService:
                     feed_name = id_to_name.get(feed_id, feed_id)
 
                     for title, info in items.items():
+                        # 跨日期去重:如果 URL 已出现过则跳过
+                        url = info.get("url", "")
+                        if url and url in seen_urls:
+                            continue
+                        if url:
+                            seen_urls.add(url)
+
                         # 关键词匹配(标题或摘要)
                         summary = info.get("summary", "")
                         if keyword.lower() in title.lower() or keyword.lower() in summary.lower():
@@ -792,7 +830,7 @@ class DataService:
                                 "title": title,
                                 "feed_id": feed_id,
                                 "feed_name": feed_name,
-                                "url": info.get("url", ""),
+                                "url": url,
                                 "published_at": info.get("published_at", ""),
                                 "author": info.get("author", ""),
                                 "date": target_date.strftime("%Y-%m-%d")

+ 5 - 1
mcp_server/services/parser_service.py

@@ -374,12 +374,16 @@ class ParserService:
         解析关键词配置文件
 
         复用 trendradar.core.frequency 的解析逻辑,支持:
+        - # 开头的注释行
         - 空行分隔词组
+        - [组别名] 作为词组第一行,给整组指定别名
         - +前缀必须词、!前缀过滤词、@数量限制
         - /pattern/ 正则表达式语法
-        - => 备注 显示名称语法
+        - => 别名 显示名称语法
         - [GLOBAL_FILTER] 全局过滤区域
 
+        显示名称优先级:组别名 > 行别名拼接 > 关键词拼接
+
         Args:
             words_file: 关键词文件路径,默认为 config/frequency_words.txt
 

+ 88 - 56
mcp_server/tools/analytics.py

@@ -367,22 +367,23 @@ class AnalyticsTools:
 
             return {
                 "success": True,
-                "topic": topic,
-                "date_range": {
-                    "start": start_date.strftime("%Y-%m-%d"),
-                    "end": end_date.strftime("%Y-%m-%d"),
-                    "total_days": total_days
-                },
-                "granularity": granularity,
-                "trend_data": trend_data,
-                "statistics": {
+                "summary": {
+                    "description": f"话题「{topic}」的热度趋势分析",
+                    "topic": topic,
+                    "date_range": {
+                        "start": start_date.strftime("%Y-%m-%d"),
+                        "end": end_date.strftime("%Y-%m-%d"),
+                        "total_days": total_days
+                    },
+                    "granularity": granularity,
                     "total_mentions": sum(counts),
                     "average_mentions": round(sum(counts) / len(counts), 2) if counts else 0,
                     "peak_count": max_count,
                     "peak_time": peak_time,
-                    "change_rate": round(change_rate, 2)
+                    "change_rate": round(change_rate, 2),
+                    "trend_direction": "上升" if change_rate > 10 else "下降" if change_rate < -10 else "稳定"
                 },
-                "trend_direction": "上升" if change_rate > 10 else "下降" if change_rate < -10 else "稳定"
+                "data": trend_data
             }
 
         except MCPError as e:
@@ -608,10 +609,13 @@ class AnalyticsTools:
 
             return {
                 "success": True,
-                "cooccurrence_pairs": result_pairs,
-                "total_pairs": len(result_pairs),
-                "min_frequency": min_frequency,
-                "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                "summary": {
+                    "description": "关键词共现分析结果",
+                    "total": len(result_pairs),
+                    "min_frequency": min_frequency,
+                    "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                },
+                "data": result_pairs
             }
 
         except MCPError as e:
@@ -779,8 +783,9 @@ class AnalyticsTools:
                 "success": True,
                 "method": "ai_prompt_generation",
                 "summary": {
+                    "description": "情感分析数据和AI提示词",
                     "total_found": len(deduplicated_news),
-                    "returned_count": len(selected_news),
+                    "returned": len(selected_news),
                     "requested_limit": limit,
                     "duplicates_removed": len(all_news_items) - len(deduplicated_news),
                     "topic": topic,
@@ -789,7 +794,7 @@ class AnalyticsTools:
                     "sorted_by_weight": sort_by_weight
                 },
                 "ai_prompt": ai_prompt,
-                "news_sample": selected_news,
+                "data": selected_news,
                 "usage_note": "请将 ai_prompt 字段的内容发送给 AI 进行情感分析"
             }
 
@@ -993,13 +998,14 @@ class AnalyticsTools:
             result = {
                 "success": True,
                 "summary": {
+                    "description": "相似新闻搜索结果",
                     "total_found": len(similar_items),
-                    "returned_count": len(result_items),
+                    "returned": len(result_items),
                     "requested_limit": limit,
                     "threshold": threshold,
                     "reference_title": reference_title
                 },
-                "similar_news": result_items
+                "data": result_items
             }
 
             if len(similar_items) < limit:
@@ -1123,12 +1129,15 @@ class AnalyticsTools:
 
             return {
                 "success": True,
-                "entity": entity,
-                "entity_type": entity_type or "auto",
-                "related_news": result_news,
-                "total_found": len(related_news),
-                "returned_count": len(result_news),
-                "sorted_by_weight": sort_by_weight,
+                "summary": {
+                    "description": f"实体「{entity}」相关新闻",
+                    "entity": entity,
+                    "entity_type": entity_type or "auto",
+                    "total_found": len(related_news),
+                    "returned": len(result_news),
+                    "sorted_by_weight": sort_by_weight
+                },
+                "data": result_news,
                 "related_keywords": [
                     {"keyword": k, "count": v}
                     for k, v in entity_context.most_common(10)
@@ -1717,18 +1726,26 @@ class AnalyticsTools:
             if not viral_topics:
                 return {
                     "success": True,
-                    "viral_topics": [],
-                    "total_detected": 0,
+                    "summary": {
+                        "description": "异常热度检测结果",
+                        "total": 0,
+                        "threshold": threshold,
+                        "time_window": time_window
+                    },
+                    "data": [],
                     "message": f"未检测到热度增长超过 {threshold} 倍的话题"
                 }
 
             return {
                 "success": True,
-                "viral_topics": viral_topics,
-                "total_detected": len(viral_topics),
-                "threshold": threshold,
-                "time_window": time_window,
-                "detection_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                "summary": {
+                    "description": "异常热度检测结果",
+                    "total": len(viral_topics),
+                    "threshold": threshold,
+                    "time_window": time_window,
+                    "detection_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                },
+                "data": viral_topics
             }
 
         except MCPError as e:
@@ -1886,11 +1903,15 @@ class AnalyticsTools:
 
             return {
                 "success": True,
-                "predicted_topics": predicted_topics[:20],  # 返回TOP 20
-                "total_predicted": len(predicted_topics),
-                "lookahead_hours": lookahead_hours,
-                "confidence_threshold": confidence_threshold,
-                "prediction_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+                "summary": {
+                    "description": "热点话题预测结果",
+                    "total": len(predicted_topics),
+                    "returned": min(20, len(predicted_topics)),
+                    "lookahead_hours": lookahead_hours,
+                    "confidence_threshold": confidence_threshold,
+                    "prediction_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+                },
+                "data": predicted_topics[:20],  # 返回TOP 20
                 "note": "预测基于历史趋势,实际结果可能有偏差"
             }
 
@@ -2071,8 +2092,12 @@ class AnalyticsTools:
             if not all_news:
                 return {
                     "success": True,
-                    "aggregated_news": [],
-                    "total": 0,
+                    "summary": {
+                        "description": "跨平台新闻聚合结果",
+                        "total": 0,
+                        "returned": 0
+                    },
+                    "data": [],
                     "message": "未找到新闻数据"
                 }
 
@@ -2100,9 +2125,10 @@ class AnalyticsTools:
             return {
                 "success": True,
                 "summary": {
+                    "description": "跨平台新闻聚合结果",
                     "original_count": total_original,
                     "aggregated_count": total_aggregated,
-                    "returned_count": len(results),
+                    "returned": len(results),
                     "deduplication_rate": f"{dedup_rate * 100:.1f}%",
                     "similarity_threshold": similarity_threshold,
                     "date_range": {
@@ -2110,7 +2136,7 @@ class AnalyticsTools:
                         "end": end_date.strftime("%Y-%m-%d")
                     }
                 },
-                "aggregated_news": results,
+                "data": results,
                 "statistics": {
                     "platform_coverage": dict(platform_coverage),
                     "multi_platform_news": len([a for a in aggregated if len(a["platforms"]) > 1]),
@@ -2282,27 +2308,33 @@ class AnalyticsTools:
 
             # 根据对比类型执行不同的分析
             if compare_type == "overview":
-                result = self._compare_overview(data1, data2, date_range1, date_range2, top_n)
+                analysis_result = self._compare_overview(data1, data2, date_range1, date_range2, top_n)
             elif compare_type == "topic_shift":
-                result = self._compare_topic_shift(data1, data2, date_range1, date_range2, top_n)
+                analysis_result = self._compare_topic_shift(data1, data2, date_range1, date_range2, top_n)
             else:  # platform_activity
-                result = self._compare_platform_activity(data1, data2, date_range1, date_range2)
-
-            result["success"] = True
-            result["compare_type"] = compare_type
-            result["periods"] = {
-                "period1": {
-                    "start": date_range1[0].strftime("%Y-%m-%d"),
-                    "end": date_range1[1].strftime("%Y-%m-%d")
+                analysis_result = self._compare_platform_activity(data1, data2, date_range1, date_range2)
+
+            result = {
+                "success": True,
+                "summary": {
+                    "description": f"时期对比分析({compare_type})",
+                    "compare_type": compare_type,
+                    "periods": {
+                        "period1": {
+                            "start": date_range1[0].strftime("%Y-%m-%d"),
+                            "end": date_range1[1].strftime("%Y-%m-%d")
+                        },
+                        "period2": {
+                            "start": date_range2[0].strftime("%Y-%m-%d"),
+                            "end": date_range2[1].strftime("%Y-%m-%d")
+                        }
+                    }
                 },
-                "period2": {
-                    "start": date_range2[0].strftime("%Y-%m-%d"),
-                    "end": date_range2[1].strftime("%Y-%m-%d")
-                }
+                "data": analysis_result
             }
 
             if topic:
-                result["topic_filter"] = topic
+                result["summary"]["topic_filter"] = topic
 
             return result
 

+ 41 - 21
mcp_server/tools/data_query.py

@@ -68,10 +68,14 @@ class DataQueryTools:
             )
 
             return {
-                "news": news_list,
-                "total": len(news_list),
-                "platforms": platforms,
-                "success": True
+                "success": True,
+                "summary": {
+                    "description": "最新一批爬取的新闻数据",
+                    "total": len(news_list),
+                    "returned": len(news_list),
+                    "platforms": platforms or "全部平台"
+                },
+                "data": news_list
             }
 
         except MCPError as e:
@@ -287,12 +291,16 @@ class DataQueryTools:
             )
 
             return {
-                "news": news_list,
-                "total": len(news_list),
-                "date": target_date.strftime("%Y-%m-%d"),
-                "date_range": date_range,
-                "platforms": platforms,
-                "success": True
+                "success": True,
+                "summary": {
+                    "description": f"按日期查询的新闻({target_date.strftime('%Y-%m-%d')})",
+                    "total": len(news_list),
+                    "returned": len(news_list),
+                    "date": target_date.strftime("%Y-%m-%d"),
+                    "date_range": date_range,
+                    "platforms": platforms or "全部平台"
+                },
+                "data": news_list
             }
 
         except MCPError as e:
@@ -316,14 +324,16 @@ class DataQueryTools:
     def get_latest_rss(
         self,
         feeds: Optional[List[str]] = None,
+        days: int = 1,
         limit: Optional[int] = None,
         include_summary: bool = False
     ) -> Dict:
         """
-        获取最新的 RSS 数据
+        获取最新的 RSS 数据(支持多日查询)
 
         Args:
             feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr']
+            days: 获取最近 N 天的数据,默认 1(仅今天),最大 30 天
             limit: 返回条数限制,默认50
             include_summary: 是否包含摘要,默认False(节省token)
 
@@ -335,15 +345,21 @@ class DataQueryTools:
 
             rss_list = self.data_service.get_latest_rss(
                 feeds=feeds,
+                days=days,
                 limit=limit,
                 include_summary=include_summary
             )
 
             return {
-                "rss": rss_list,
-                "total": len(rss_list),
-                "feeds": feeds,
-                "success": True
+                "success": True,
+                "summary": {
+                    "description": f"最近 {days} 天的 RSS 订阅数据" if days > 1 else "最新的 RSS 订阅数据",
+                    "total": len(rss_list),
+                    "returned": len(rss_list),
+                    "days": days,
+                    "feeds": feeds or "全部订阅源"
+                },
+                "data": rss_list
             }
 
         except MCPError as e:
@@ -397,12 +413,16 @@ class DataQueryTools:
             )
 
             return {
-                "rss": rss_list,
-                "total": len(rss_list),
-                "keyword": keyword,
-                "feeds": feeds,
-                "days": days,
-                "success": True
+                "success": True,
+                "summary": {
+                    "description": f"RSS 搜索结果(关键词: {keyword})",
+                    "total": len(rss_list),
+                    "returned": len(rss_list),
+                    "keyword": keyword,
+                    "feeds": feeds or "全部订阅源",
+                    "days": days
+                },
+                "data": rss_list
             }
 
         except MCPError as e:

+ 11 - 19
mcp_server/tools/search_tools.py

@@ -26,14 +26,6 @@ class SearchTools:
             project_root: 项目根目录
         """
         self.data_service = DataService(project_root)
-        # 中文停用词列表
-        self.stopwords = {
-            '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一',
-            '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有',
-            '看', '好', '自己', '这', '那', '来', '被', '与', '为', '对', '将', '从',
-            '以', '及', '等', '但', '或', '而', '于', '中', '由', '可', '可以', '已',
-            '已经', '还', '更', '最', '再', '因为', '所以', '如果', '虽然', '然而'
-        }
 
     def search_news_unified(
         self,
@@ -211,8 +203,9 @@ class SearchTools:
             result = {
                 "success": True,
                 "summary": {
+                    "description": f"新闻搜索结果({search_mode}模式)",
                     "total_found": len(all_matches),
-                    "returned_count": len(results),
+                    "returned": len(results),
                     "requested_limit": limit,
                     "search_mode": search_mode,
                     "query": query,
@@ -220,7 +213,7 @@ class SearchTools:
                     "time_range": time_range_desc,
                     "sort_by": sort_by
                 },
-                "results": results
+                "data": results
             }
 
             if search_mode == "fuzzy":
@@ -477,11 +470,8 @@ class SearchTools:
         # 使用正则表达式分词(中文和英文)
         words = re.findall(r'[\w]+', text)
 
-        # 过滤停用词和短词
-        keywords = [
-            word for word in words
-            if word and len(word) >= min_length and word not in self.stopwords
-        ]
+        # 过滤短词
+        keywords = [word for word in words if word and len(word) >= min_length]
 
         return keywords
 
@@ -703,8 +693,9 @@ class SearchTools:
             result = {
                 "success": True,
                 "summary": {
+                    "description": "历史相关新闻搜索结果",
                     "total_found": len(all_related_news),
-                    "returned_count": len(results),
+                    "returned": len(results),
                     "requested_limit": limit,
                     "threshold": threshold,
                     "reference_title": reference_title,
@@ -715,7 +706,7 @@ class SearchTools:
                         "end": search_end.strftime("%Y-%m-%d")
                     }
                 },
-                "results": results,
+                "data": results,
                 "statistics": {
                     "platform_distribution": dict(platform_distribution),
                     "date_distribution": dict(date_distribution),
@@ -881,8 +872,9 @@ class SearchTools:
             return {
                 "success": True,
                 "summary": {
+                    "description": "相关新闻搜索结果",
                     "total_found": len(all_related_news),
-                    "returned_count": len(results),
+                    "returned": len(results),
                     "reference_title": reference_title,
                     "threshold": threshold,
                     "date_range": {
@@ -890,7 +882,7 @@ class SearchTools:
                         "end": max(search_dates).strftime("%Y-%m-%d")
                     } if search_dates else None
                 },
-                "results": results,
+                "data": results,
                 "statistics": {
                     "platform_distribution": dict(platform_dist),
                     "date_distribution": dict(date_dist)

+ 119 - 39
mcp_server/tools/storage_sync.py

@@ -148,21 +148,58 @@ class StorageSyncTools:
 
         return None
 
-    def _get_local_dates(self) -> List[str]:
-        """获取本地可用的日期列表"""
+    def _get_local_dates(self, db_type: str = "news") -> List[str]:
+        """
+        获取本地可用的日期列表
+
+        存储结构: output/{db_type}/{date}.db
+        例如: output/news/2025-12-30.db, output/rss/2025-12-30.db
+
+        Args:
+            db_type: 数据库类型 ("news" 或 "rss"),默认 "news"
+
+        Returns:
+            日期列表(按时间倒序)
+        """
         local_dir = self._get_local_data_dir()
-        dates = []
+        dates = set()
 
         if not local_dir.exists():
-            return dates
+            return []
+
+        # 扫描 output/{db_type}/{date}.db 文件
+        type_dir = local_dir / db_type
+        if type_dir.exists():
+            for item in type_dir.iterdir():
+                if item.is_file() and item.suffix == ".db":
+                    # 从文件名解析日期 (2025-12-30.db -> 2025-12-30)
+                    date_str = item.stem  # 去除 .db 后缀
+                    folder_date = self._parse_date_folder_name(date_str)
+                    if folder_date:
+                        dates.add(folder_date.strftime("%Y-%m-%d"))
+
+        return sorted(list(dates), reverse=True)
+
+    def _get_all_local_dates(self) -> Dict[str, List[str]]:
+        """
+        获取所有本地可用的日期列表(包括 news 和 rss)
 
-        for item in local_dir.iterdir():
-            if item.is_dir() and not item.name.startswith('.'):
-                folder_date = self._parse_date_folder_name(item.name)
-                if folder_date:
-                    dates.append(folder_date.strftime("%Y-%m-%d"))
+        Returns:
+            {
+                "news": ["2025-12-30", ...],
+                "rss": ["2025-12-30", ...],
+                "all": ["2025-12-30", ...]  # 合并去重
+            }
+        """
+        news_dates = set(self._get_local_dates("news"))
+        rss_dates = set(self._get_local_dates("rss"))
+        all_dates = news_dates | rss_dates
 
-        return sorted(dates, reverse=True)
+        return {
+            "news": sorted(list(news_dates), reverse=True),
+            "rss": sorted(list(rss_dates), reverse=True),
+            "all": sorted(list(all_dates), reverse=True)
+        }
 
     def _calculate_dir_size(self, path: Path) -> int:
         """计算目录大小(字节)"""
@@ -261,10 +298,17 @@ class StorageSyncTools:
 
             return {
                 "success": True,
-                "synced_files": len(synced_dates),
-                "synced_dates": synced_dates,
-                "skipped_dates": skipped_dates,
-                "failed_dates": failed_dates,
+                "summary": {
+                    "description": "远程存储同步结果",
+                    "synced_files": len(synced_dates),
+                    "skipped_count": len(skipped_dates),
+                    "failed_count": len(failed_dates)
+                },
+                "data": {
+                    "synced_dates": synced_dates,
+                    "skipped_dates": skipped_dates,
+                    "failed_dates": failed_dates
+                },
                 "message": f"成功同步 {len(synced_dates)} 天数据" + (
                     f",跳过 {len(skipped_dates)} 天(本地已存在)" if skipped_dates else ""
                 ) + (
@@ -301,16 +345,29 @@ class StorageSyncTools:
             local_config = storage_config.get("local", {})
             local_dir = self._get_local_data_dir()
             local_size = self._calculate_dir_size(local_dir)
-            local_dates = self._get_local_dates()
+
+            # 获取分类的日期列表
+            all_dates = self._get_all_local_dates()
+            news_dates = all_dates["news"]
+            rss_dates = all_dates["rss"]
+            combined_dates = all_dates["all"]
 
             local_status = {
                 "data_dir": local_config.get("data_dir", "output"),
                 "retention_days": local_config.get("retention_days", 0),
                 "total_size": f"{local_size / 1024 / 1024:.2f} MB",
                 "total_size_bytes": local_size,
-                "date_count": len(local_dates),
-                "earliest_date": local_dates[-1] if local_dates else None,
-                "latest_date": local_dates[0] if local_dates else None,
+                "date_count": len(combined_dates),
+                "earliest_date": combined_dates[-1] if combined_dates else None,
+                "latest_date": combined_dates[0] if combined_dates else None,
+                "news": {
+                    "date_count": len(news_dates),
+                    "dates": news_dates[:10],  # 最近 10 天
+                },
+                "rss": {
+                    "date_count": len(rss_dates),
+                    "dates": rss_dates[:10],  # 最近 10 天
+                },
             }
 
             # 远程存储状态
@@ -350,10 +407,15 @@ class StorageSyncTools:
 
             return {
                 "success": True,
-                "backend": storage_config.get("backend", "auto"),
-                "local": local_status,
-                "remote": remote_status,
-                "pull": pull_status,
+                "summary": {
+                    "description": "存储配置和状态信息",
+                    "backend": storage_config.get("backend", "auto")
+                },
+                "data": {
+                    "local": local_status,
+                    "remote": remote_status,
+                    "pull": pull_status
+                }
             }
 
         except MCPError as e:
@@ -384,24 +446,38 @@ class StorageSyncTools:
             日期列表字典
         """
         try:
-            result = {
-                "success": True,
+            data_result = {}
+            summary_info = {
+                "description": "可用日期列表",
+                "source": source
             }
 
             # 本地日期
             if source in ("local", "both"):
-                local_dates = self._get_local_dates()
-                result["local"] = {
-                    "dates": local_dates,
-                    "count": len(local_dates),
-                    "earliest": local_dates[-1] if local_dates else None,
-                    "latest": local_dates[0] if local_dates else None,
+                all_dates = self._get_all_local_dates()
+                news_dates = all_dates["news"]
+                rss_dates = all_dates["rss"]
+                combined_dates = all_dates["all"]
+
+                data_result["local"] = {
+                    "dates": combined_dates,
+                    "count": len(combined_dates),
+                    "earliest": combined_dates[-1] if combined_dates else None,
+                    "latest": combined_dates[0] if combined_dates else None,
+                    "news": {
+                        "dates": news_dates,
+                        "count": len(news_dates),
+                    },
+                    "rss": {
+                        "dates": rss_dates,
+                        "count": len(rss_dates),
+                    },
                 }
 
             # 远程日期
             if source in ("remote", "both"):
                 if not self._has_remote_config():
-                    result["remote"] = {
+                    data_result["remote"] = {
                         "configured": False,
                         "dates": [],
                         "count": 0,
@@ -414,7 +490,7 @@ class StorageSyncTools:
                     if remote_backend:
                         try:
                             remote_dates = remote_backend.list_remote_dates()
-                            result["remote"] = {
+                            data_result["remote"] = {
                                 "configured": True,
                                 "dates": remote_dates,
                                 "count": len(remote_dates),
@@ -422,7 +498,7 @@ class StorageSyncTools:
                                 "latest": remote_dates[0] if remote_dates else None,
                             }
                         except Exception as e:
-                            result["remote"] = {
+                            data_result["remote"] = {
                                 "configured": True,
                                 "dates": [],
                                 "count": 0,
@@ -431,7 +507,7 @@ class StorageSyncTools:
                                 "error": str(e)
                             }
                     else:
-                        result["remote"] = {
+                        data_result["remote"] = {
                             "configured": True,
                             "dates": [],
                             "count": 0,
@@ -441,17 +517,21 @@ class StorageSyncTools:
                         }
 
             # 如果同时查询两者,计算差异
-            if source == "both" and "local" in result and "remote" in result:
-                local_set = set(result["local"]["dates"])
-                remote_set = set(result["remote"].get("dates", []))
+            if source == "both" and "local" in data_result and "remote" in data_result:
+                local_set = set(data_result["local"]["dates"])
+                remote_set = set(data_result["remote"].get("dates", []))
 
-                result["comparison"] = {
+                data_result["comparison"] = {
                     "only_local": sorted(list(local_set - remote_set), reverse=True),
                     "only_remote": sorted(list(remote_set - local_set), reverse=True),
                     "both": sorted(list(local_set & remote_set), reverse=True),
                 }
 
-            return result
+            return {
+                "success": True,
+                "summary": summary_info,
+                "data": data_result
+            }
 
         except MCPError as e:
             return {

+ 196 - 10
mcp_server/tools/system.py

@@ -47,8 +47,11 @@ class SystemManagementTools:
             status = self.data_service.get_system_status()
 
             return {
-                **status,
-                "success": True
+                "success": True,
+                "summary": {
+                    "description": "系统运行状态和健康检查信息"
+                },
+                "data": status
             }
 
         except MCPError as e:
@@ -232,14 +235,17 @@ class SystemManagementTools:
 
             result = {
                 "success": True,
-                "task_id": f"crawl_{int(time.time())}",
-                "status": "completed",
-                "crawl_time": current_time.strftime("%Y-%m-%d %H:%M:%S"),
-                "platforms": list(results.keys()),
-                "total_news": len(news_response_data),
-                "failed_platforms": failed_ids,
-                "data": news_response_data,
-                "saved_to_local": save_success and save_to_local
+                "summary": {
+                    "description": "爬取任务执行结果",
+                    "task_id": f"crawl_{int(time.time())}",
+                    "status": "completed",
+                    "crawl_time": current_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "total_news": len(news_response_data),
+                    "platforms": list(results.keys()),
+                    "failed_platforms": failed_ids,
+                    "saved_to_local": save_success and save_to_local
+                },
+                "data": news_response_data
             }
 
             if save_success:
@@ -367,3 +373,183 @@ class SystemManagementTools:
             .replace('"', "&quot;")
             .replace("'", "&#x27;")
         )
+
+    def check_version(self, proxy_url: Optional[str] = None) -> Dict:
+        """
+        检查版本更新
+
+        同时检查 TrendRadar 和 MCP Server 两个组件的版本更新。
+        远程版本 URL 从 config.yaml 获取:
+        - version_check_url: TrendRadar 版本
+        - mcp_version_check_url: MCP Server 版本
+
+        Args:
+            proxy_url: 可选的代理URL,用于访问远程版本
+
+        Returns:
+            版本检查结果字典,包含:
+            - success: 是否成功
+            - trendradar: TrendRadar 版本检查结果
+            - mcp: MCP Server 版本检查结果
+            - any_update: 是否有任何组件需要更新
+
+        Example:
+            >>> tools = SystemManagementTools()
+            >>> result = tools.check_version()
+            >>> print(result['data']['any_update'])
+        """
+        import yaml
+        import requests
+
+        def parse_version(version_str: str):
+            """将版本号字符串解析为元组"""
+            try:
+                parts = version_str.strip().split(".")
+                if len(parts) != 3:
+                    raise ValueError("版本号格式不正确")
+                return int(parts[0]), int(parts[1]), int(parts[2])
+            except:
+                return 0, 0, 0
+
+        def check_single_version(
+            name: str,
+            local_version: str,
+            remote_url: str,
+            proxies: Optional[Dict],
+            headers: Dict
+        ) -> Dict:
+            """检查单个组件的版本"""
+            try:
+                response = requests.get(
+                    remote_url, proxies=proxies, headers=headers, timeout=10
+                )
+                response.raise_for_status()
+                remote_version = response.text.strip()
+
+                local_tuple = parse_version(local_version)
+                remote_tuple = parse_version(remote_version)
+                need_update = local_tuple < remote_tuple
+
+                if need_update:
+                    message = f"发现新版本 {remote_version},当前版本 {local_version},建议更新"
+                elif local_tuple > remote_tuple:
+                    message = f"当前版本 {local_version} 高于远程版本 {remote_version}(可能是开发版本)"
+                else:
+                    message = f"当前版本 {local_version} 已是最新版本"
+
+                return {
+                    "success": True,
+                    "name": name,
+                    "current_version": local_version,
+                    "remote_version": remote_version,
+                    "need_update": need_update,
+                    "current_parsed": list(local_tuple),
+                    "remote_parsed": list(remote_tuple),
+                    "message": message
+                }
+            except requests.exceptions.Timeout:
+                return {
+                    "success": False,
+                    "name": name,
+                    "current_version": local_version,
+                    "error": "获取远程版本超时"
+                }
+            except requests.exceptions.RequestException as e:
+                return {
+                    "success": False,
+                    "name": name,
+                    "current_version": local_version,
+                    "error": f"网络请求失败: {str(e)}"
+                }
+            except Exception as e:
+                return {
+                    "success": False,
+                    "name": name,
+                    "current_version": local_version,
+                    "error": str(e)
+                }
+
+        try:
+            # 导入本地版本
+            from trendradar import __version__ as trendradar_version
+            from mcp_server import __version__ as mcp_version
+
+            # 从配置文件获取远程版本 URL
+            config_path = self.project_root / "config" / "config.yaml"
+            if not config_path.exists():
+                return {
+                    "success": False,
+                    "error": {
+                        "code": "CONFIG_NOT_FOUND",
+                        "message": f"配置文件不存在: {config_path}"
+                    }
+                }
+
+            with open(config_path, "r", encoding="utf-8") as f:
+                config_data = yaml.safe_load(f)
+
+            advanced_config = config_data.get("advanced", {})
+            trendradar_url = advanced_config.get(
+                "version_check_url",
+                "https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/version"
+            )
+            mcp_url = advanced_config.get(
+                "mcp_version_check_url",
+                "https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/version_mcp"
+            )
+
+            # 配置代理
+            proxies = None
+            if proxy_url:
+                proxies = {"http": proxy_url, "https": proxy_url}
+
+            # 请求头
+            headers = {
+                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+                "Accept": "text/plain, */*",
+                "Cache-Control": "no-cache",
+            }
+
+            # 检查两个版本
+            trendradar_result = check_single_version(
+                "TrendRadar", trendradar_version, trendradar_url, proxies, headers
+            )
+            mcp_result = check_single_version(
+                "MCP Server", mcp_version, mcp_url, proxies, headers
+            )
+
+            # 判断是否有任何更新
+            any_update = (
+                (trendradar_result.get("success") and trendradar_result.get("need_update", False)) or
+                (mcp_result.get("success") and mcp_result.get("need_update", False))
+            )
+
+            return {
+                "success": True,
+                "summary": {
+                    "description": "版本检查结果(TrendRadar + MCP Server)",
+                    "any_update": any_update
+                },
+                "data": {
+                    "trendradar": trendradar_result,
+                    "mcp": mcp_result,
+                    "any_update": any_update
+                }
+            }
+
+        except ImportError as e:
+            return {
+                "success": False,
+                "error": {
+                    "code": "IMPORT_ERROR",
+                    "message": f"无法导入版本信息: {str(e)}"
+                }
+            }
+        except Exception as e:
+            return {
+                "success": False,
+                "error": {
+                    "code": "INTERNAL_ERROR",
+                    "message": str(e)
+                }
+            }

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 name = "trendradar"
-version = "4.6.0"
+version = "5.0.0"
 description = "TrendRadar - 热点新闻聚合与分析工具"
 requires-python = ">=3.10"
 dependencies = [

+ 1 - 1
trendradar/__init__.py

@@ -9,5 +9,5 @@ TrendRadar - 热点新闻聚合与分析工具
 
 from trendradar.context import AppContext
 
-__version__ = "4.7.0"
+__version__ = "5.0.0"
 __all__ = ["AppContext", "__version__"]

+ 242 - 8
trendradar/__main__.py

@@ -20,6 +20,7 @@ from trendradar.core.analyzer import convert_keyword_stats_to_platform_stats
 from trendradar.crawler import DataFetcher
 from trendradar.storage import convert_crawl_results_to_news_data
 from trendradar.utils.time import is_within_days
+from trendradar.ai import AIAnalyzer, AIAnalysisResult
 
 
 def check_version_update(
@@ -234,6 +235,62 @@ class NewsAnalyzer:
             )
             return has_matched_news or has_new_news
 
+    def _run_ai_analysis(
+        self,
+        stats: List[Dict],
+        rss_items: Optional[List[Dict]],
+        mode: str,
+        report_type: str,
+        id_to_name: Optional[Dict],
+    ) -> Optional[AIAnalysisResult]:
+        """执行 AI 分析"""
+        ai_config = self.ctx.config.get("AI_ANALYSIS", {})
+        if not ai_config.get("ENABLED", False):
+            return None
+
+        print("[AI] 正在进行 AI 分析...")
+        try:
+            analyzer = AIAnalyzer(ai_config, self.ctx.get_time)
+
+            # 提取平台列表
+            platforms = list(id_to_name.values()) if id_to_name else []
+
+            # 提取关键词列表
+            keywords = [s.get("word", "") for s in stats if s.get("word")] if stats else []
+
+            result = analyzer.analyze(
+                stats=stats,
+                rss_stats=rss_items,
+                report_mode=mode,
+                report_type=report_type,
+                platforms=platforms,
+                keywords=keywords,
+            )
+
+            if result.success:
+                if result.error:
+                    # 成功但有警告(如 JSON 解析问题但使用了原始文本)
+                    print(f"[AI] 分析完成(有警告: {result.error})")
+                else:
+                    print("[AI] 分析完成")
+            else:
+                print(f"[AI] 分析失败: {result.error}")
+
+            return result
+        except Exception as e:
+            import traceback
+            error_type = type(e).__name__
+            error_msg = str(e)
+            # 截断过长的错误消息
+            if len(error_msg) > 200:
+                error_msg = error_msg[:200] + "..."
+            print(f"[AI] 分析出错 ({error_type}): {error_msg}")
+            # 详细错误日志到 stderr
+            import sys
+            print(f"[AI] 详细错误堆栈:", file=sys.stderr)
+            traceback.print_exc(file=sys.stderr)
+            return AIAnalysisResult(success=False, error=f"{error_type}: {error_msg}")
+
     def _load_analysis_data(
         self,
         quiet: bool = False,
@@ -293,6 +350,150 @@ class NewsAnalyzer:
                 }
         return title_info
 
+    def _prepare_standalone_data(
+        self,
+        results: Dict,
+        id_to_name: Dict,
+        title_info: Optional[Dict] = None,
+        rss_items: Optional[List[Dict]] = None,
+    ) -> Optional[Dict]:
+        """
+        从原始数据中提取独立展示区数据
+
+        Args:
+            results: 原始爬取结果 {platform_id: {title: title_data}}
+            id_to_name: 平台 ID 到名称的映射
+            title_info: 标题元信息(含排名历史、时间等)
+            rss_items: RSS 条目列表
+
+        Returns:
+            独立展示数据字典,如果未启用返回 None
+        """
+        standalone_config = self.ctx.config.get("STANDALONE_DISPLAY", {})
+        if not standalone_config.get("ENABLED", False):
+            return None
+
+        platform_ids = standalone_config.get("PLATFORMS", [])
+        rss_feed_ids = standalone_config.get("RSS_FEEDS", [])
+        max_items = standalone_config.get("MAX_ITEMS", 20)
+
+        if not platform_ids and not rss_feed_ids:
+            return None
+
+        standalone_data = {
+            "platforms": [],
+            "rss_feeds": [],
+        }
+
+        # 找出最新批次时间(类似 current 模式的过滤逻辑)
+        latest_time = None
+        if title_info:
+            for source_titles in title_info.values():
+                for title_data in source_titles.values():
+                    last_time = title_data.get("last_time", "")
+                    if last_time:
+                        if latest_time is None or last_time > latest_time:
+                            latest_time = last_time
+
+        # 提取热榜平台数据
+        for platform_id in platform_ids:
+            if platform_id not in results:
+                continue
+
+            platform_name = id_to_name.get(platform_id, platform_id)
+            platform_titles = results[platform_id]
+
+            items = []
+            for title, title_data in platform_titles.items():
+                # 获取元信息(如果有 title_info)
+                meta = {}
+                if title_info and platform_id in title_info and title in title_info[platform_id]:
+                    meta = title_info[platform_id][title]
+
+                # 只保留当前在榜的话题(last_time 等于最新时间)
+                if latest_time and meta:
+                    if meta.get("last_time") != latest_time:
+                        continue
+
+                # 使用当前热榜的排名数据(title_data)进行排序
+                # title_data 包含的是爬虫返回的当前排名,用于保证独立展示区的顺序与热榜一致
+                current_ranks = title_data.get("ranks", [])
+                current_rank = current_ranks[-1] if current_ranks else 0
+
+                # 用于显示的排名范围:合并历史排名和当前排名
+                historical_ranks = meta.get("ranks", []) if meta else []
+                # 合并去重,保持顺序
+                all_ranks = historical_ranks.copy()
+                for rank in current_ranks:
+                    if rank not in all_ranks:
+                        all_ranks.append(rank)
+                display_ranks = all_ranks if all_ranks else current_ranks
+
+                item = {
+                    "title": title,
+                    "url": title_data.get("url", ""),
+                    "mobileUrl": title_data.get("mobileUrl", ""),
+                    "rank": current_rank,  # 用于排序的当前排名
+                    "ranks": display_ranks,  # 用于显示的排名范围(历史+当前)
+                    "first_time": meta.get("first_time", ""),
+                    "last_time": meta.get("last_time", ""),
+                    "count": meta.get("count", 1),
+                }
+                items.append(item)
+
+            # 按当前排名排序
+            items.sort(key=lambda x: x["rank"] if x["rank"] > 0 else 9999)
+
+            # 限制条数
+            if max_items > 0:
+                items = items[:max_items]
+
+            if items:
+                standalone_data["platforms"].append({
+                    "id": platform_id,
+                    "name": platform_name,
+                    "items": items,
+                })
+
+        # 提取 RSS 数据
+        if rss_items and rss_feed_ids:
+            # 按 feed_id 分组
+            feed_items_map = {}
+            for item in rss_items:
+                feed_id = item.get("feed_id", "")
+                if feed_id in rss_feed_ids:
+                    if feed_id not in feed_items_map:
+                        feed_items_map[feed_id] = {
+                            "name": item.get("feed_name", feed_id),
+                            "items": [],
+                        }
+                    feed_items_map[feed_id]["items"].append({
+                        "title": item.get("title", ""),
+                        "url": item.get("url", ""),
+                        "published_at": item.get("published_at", ""),
+                        "author": item.get("author", ""),
+                    })
+
+            # 限制条数并添加到结果
+            for feed_id in rss_feed_ids:
+                if feed_id in feed_items_map:
+                    feed_data = feed_items_map[feed_id]
+                    items = feed_data["items"]
+                    if max_items > 0:
+                        items = items[:max_items]
+                    if items:
+                        standalone_data["rss_feeds"].append({
+                            "id": feed_id,
+                            "name": feed_data["name"],
+                            "items": items,
+                        })
+
+        # 如果没有任何数据,返回 None
+        if not standalone_data["platforms"] and not standalone_data["rss_feeds"]:
+            return None
+
+        return standalone_data
+
     def _run_analysis_pipeline(
         self,
         data_source: Dict,
@@ -361,8 +562,9 @@ class NewsAnalyzer:
         html_file_path: Optional[str] = None,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """统一的通知发送逻辑,包含所有判断条件,支持热榜+RSS合并推送"""
+        """统一的通知发送逻辑,包含所有判断条件,支持热榜+RSS合并推送+AI分析+独立展示区"""
         has_notification = self._has_notification_configured()
         cfg = self.ctx.config
 
@@ -373,7 +575,8 @@ class NewsAnalyzer:
 
         # 计算热榜匹配条数
         news_count = sum(len(stat.get("titles", [])) for stat in stats) if stats else 0
-        rss_count = len(rss_items) if rss_items else 0
+        # rss_items 是统计列表 [{"word": "xx", "count": 5, ...}],需累加 count
+        rss_count = sum(stat.get("count", 0) for stat in rss_items) if rss_items else 0
 
         if (
             cfg["ENABLE_NOTIFICATION"]
@@ -409,13 +612,21 @@ class NewsAnalyzer:
                     else:
                         print(f"推送窗口控制:今天首次推送")
 
+            # AI 分析(如果启用)
+            ai_result = None
+            ai_config = cfg.get("AI_ANALYSIS", {})
+            if ai_config.get("ENABLED", False):
+                ai_result = self._run_ai_analysis(
+                    stats, rss_items, mode, report_type, id_to_name
+                )
+
             # 准备报告数据
             report_data = self.ctx.prepare_report(stats, failed_ids, new_titles, id_to_name, mode)
 
             # 是否发送版本更新信息
             update_info_to_send = self.update_info if cfg["SHOW_VERSION_UPDATE"] else None
 
-            # 使用 NotificationDispatcher 发送到所有渠道(合并热榜+RSS)
+            # 使用 NotificationDispatcher 发送到所有渠道(合并热榜+RSS+AI分析+独立展示区
             dispatcher = self.ctx.create_notification_dispatcher()
             results = dispatcher.dispatch_all(
                 report_data=report_data,
@@ -426,6 +637,8 @@ class NewsAnalyzer:
                 html_file_path=html_file_path,
                 rss_items=rss_items,
                 rss_new_items=rss_new_items,
+                ai_analysis=ai_result,
+                standalone_data=standalone_data,
             )
 
             if not results:
@@ -512,7 +725,12 @@ class NewsAnalyzer:
         if html_file:
             print(f"{summary_type}报告已生成: {html_file}")
 
-        # 发送通知(合并RSS)
+        # 准备独立展示区数据
+        standalone_data = self._prepare_standalone_data(
+            all_results, id_to_name, title_info, rss_items
+        )
+
+        # 发送通知(合并RSS+独立展示区)
         self._send_notification_if_needed(
             stats,
             mode_strategy["summary_report_type"],
@@ -523,6 +741,7 @@ class NewsAnalyzer:
             html_file_path=html_file,
             rss_items=rss_items,
             rss_new_items=rss_new_items,
+            standalone_data=standalone_data,
         )
 
         return html_file
@@ -1040,9 +1259,13 @@ class NewsAnalyzer:
                 if html_file:
                     print(f"HTML报告已生成: {html_file}")
 
-                # 发送实时通知(使用完整历史数据的统计结果,合并RSS)
+                # 发送实时通知(使用完整历史数据的统计结果,合并RSS+独立展示区
                 summary_html = None
                 if mode_strategy["should_send_realtime"]:
+                    # 准备独立展示区数据
+                    standalone_data = self._prepare_standalone_data(
+                        all_results, combined_id_to_name, historical_title_info, rss_items
+                    )
                     self._send_notification_if_needed(
                         stats,
                         mode_strategy["realtime_report_type"],
@@ -1053,6 +1276,7 @@ class NewsAnalyzer:
                         html_file_path=html_file,
                         rss_items=rss_items,
                         rss_new_items=rss_new_items,
+                        standalone_data=standalone_data,
                     )
             else:
                 print("❌ 严重错误:无法读取刚保存的数据文件")
@@ -1075,9 +1299,13 @@ class NewsAnalyzer:
             if html_file:
                 print(f"HTML报告已生成: {html_file}")
 
-            # 发送实时通知(如果需要,合并RSS)
+            # 发送实时通知(如果需要,合并RSS+独立展示区
             summary_html = None
             if mode_strategy["should_send_realtime"]:
+                # 准备独立展示区数据
+                standalone_data = self._prepare_standalone_data(
+                    results, id_to_name, title_info, rss_items
+                )
                 self._send_notification_if_needed(
                     stats,
                     mode_strategy["realtime_report_type"],
@@ -1088,6 +1316,7 @@ class NewsAnalyzer:
                     html_file_path=html_file,
                     rss_items=rss_items,
                     rss_new_items=rss_new_items,
+                    standalone_data=standalone_data,
                 )
 
         # 生成汇总报告(如果需要)
@@ -1145,7 +1374,8 @@ class NewsAnalyzer:
 
         except Exception as e:
             print(f"分析流程执行出错: {e}")
-            raise
+            if self.ctx.config.get("DEBUG", False):
+                raise
         finally:
             # 清理资源(包括过期数据清理和数据库连接关闭)
             self.ctx.cleanup()
@@ -1153,8 +1383,11 @@ class NewsAnalyzer:
 
 def main():
     """主程序入口"""
+    debug_mode = False
     try:
         analyzer = NewsAnalyzer()
+        # 获取 debug 配置
+        debug_mode = analyzer.ctx.config.get("DEBUG", False)
         analyzer.run()
     except FileNotFoundError as e:
         print(f"❌ 配置文件错误: {e}")
@@ -1164,7 +1397,8 @@ def main():
         print("\n参考项目文档进行正确配置")
     except Exception as e:
         print(f"❌ 程序运行错误: {e}")
-        raise
+        if debug_mode:
+            raise
 
 
 if __name__ == "__main__":

+ 27 - 0
trendradar/ai/__init__.py

@@ -0,0 +1,27 @@
+# coding=utf-8
+"""
+TrendRadar AI 分析模块
+
+提供 AI 大模型对热点新闻的深度分析功能
+"""
+
+from .analyzer import AIAnalyzer, AIAnalysisResult
+from .formatter import (
+    get_ai_analysis_renderer,
+    render_ai_analysis_markdown,
+    render_ai_analysis_feishu,
+    render_ai_analysis_dingtalk,
+    render_ai_analysis_html,
+    render_ai_analysis_plain,
+)
+
+__all__ = [
+    "AIAnalyzer",
+    "AIAnalysisResult",
+    "get_ai_analysis_renderer",
+    "render_ai_analysis_markdown",
+    "render_ai_analysis_feishu",
+    "render_ai_analysis_dingtalk",
+    "render_ai_analysis_html",
+    "render_ai_analysis_plain",
+]

+ 503 - 0
trendradar/ai/analyzer.py

@@ -0,0 +1,503 @@
+# coding=utf-8
+"""
+AI 分析器模块
+
+调用 AI 大模型对热点新闻进行深度分析
+支持 OpenAI、Google Gemini、Azure OpenAI 等兼容接口
+"""
+
+import json
+import os
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Callable, Dict, List, Optional
+
+
+@dataclass
+class AIAnalysisResult:
+    """AI 分析结果"""
+    summary: str = ""                    # 热点趋势概述
+    keyword_analysis: str = ""           # 关键词热度分析
+    sentiment: str = ""                  # 情感倾向分析
+    cross_platform: str = ""             # 跨平台关联
+    impact: str = ""                     # 潜在影响评估
+    signals: str = ""                    # 值得关注的信号
+    conclusion: str = ""                 # 总结与建议
+    raw_response: str = ""               # 原始响应
+    success: bool = False                # 是否成功
+    error: str = ""                      # 错误信息
+    # 新闻数量统计
+    total_news: int = 0                  # 总新闻数(热榜+RSS)
+    analyzed_news: int = 0               # 实际分析的新闻数
+    max_news_limit: int = 0              # 分析上限配置值
+    hotlist_count: int = 0               # 热榜新闻数
+    rss_count: int = 0                   # RSS 新闻数
+
+
+class AIAnalyzer:
+    """AI 分析器"""
+
+    def __init__(self, config: Dict[str, Any], get_time_func: Callable):
+        """
+        初始化 AI 分析器
+
+        Args:
+            config: AI 分析配置
+            get_time_func: 获取当前时间的函数
+        """
+        self.config = config
+        self.get_time_func = get_time_func
+
+        # 从配置或环境变量获取 API Key
+        self.api_key = config.get("API_KEY") or os.environ.get("AI_API_KEY", "")
+        self.provider = config.get("PROVIDER", "openai")
+        self.model = config.get("MODEL", "gpt-4o-mini")
+        self.base_url = config.get("BASE_URL", "")
+        self.timeout = config.get("TIMEOUT", 90)
+        self.max_news = config.get("MAX_NEWS_FOR_ANALYSIS", 50)
+        self.include_rss = config.get("INCLUDE_RSS", True)
+        self.push_mode = config.get("PUSH_MODE", "both")
+
+        # 加载提示词模板
+        self.system_prompt, self.user_prompt_template = self._load_prompt_template(
+            config.get("PROMPT_FILE", "ai_analysis_prompt.txt")
+        )
+
+    def _load_prompt_template(self, prompt_file: str) -> tuple:
+        """加载提示词模板"""
+        config_dir = Path(__file__).parent.parent.parent / "config"
+        prompt_path = config_dir / prompt_file
+
+        if not prompt_path.exists():
+            print(f"[AI] 提示词文件不存在: {prompt_path}")
+            return "", ""
+
+        content = prompt_path.read_text(encoding="utf-8")
+
+        # 解析 [system] 和 [user] 部分
+        system_prompt = ""
+        user_prompt = ""
+
+        if "[system]" in content and "[user]" in content:
+            parts = content.split("[user]")
+            system_part = parts[0]
+            user_part = parts[1] if len(parts) > 1 else ""
+
+            # 提取 system 内容
+            if "[system]" in system_part:
+                system_prompt = system_part.split("[system]")[1].strip()
+
+            user_prompt = user_part.strip()
+        else:
+            # 整个文件作为 user prompt
+            user_prompt = content
+
+        return system_prompt, user_prompt
+
+    def analyze(
+        self,
+        stats: List[Dict],
+        rss_stats: Optional[List[Dict]] = None,
+        report_mode: str = "daily",
+        report_type: str = "当日汇总",
+        platforms: Optional[List[str]] = None,
+        keywords: Optional[List[str]] = None,
+    ) -> AIAnalysisResult:
+        """
+        执行 AI 分析
+
+        Args:
+            stats: 热榜统计数据
+            rss_stats: RSS 统计数据
+            report_mode: 报告模式
+            report_type: 报告类型
+            platforms: 平台列表
+            keywords: 关键词列表
+
+        Returns:
+            AIAnalysisResult: 分析结果
+        """
+        if not self.api_key:
+            return AIAnalysisResult(
+                success=False,
+                error="未配置 AI API Key,请在 config.yaml 或环境变量 AI_API_KEY 中设置"
+            )
+
+        # 准备新闻内容并获取统计数据
+        news_content, hotlist_total, rss_total, analyzed_count = self._prepare_news_content(stats, rss_stats)
+        total_news = hotlist_total + rss_total
+
+        if not news_content:
+            return AIAnalysisResult(
+                success=False,
+                error="没有可分析的新闻内容",
+                total_news=total_news,
+                hotlist_count=hotlist_total,
+                rss_count=rss_total,
+                analyzed_news=0,
+                max_news_limit=self.max_news
+            )
+
+        # 构建提示词
+        current_time = self.get_time_func().strftime("%Y-%m-%d %H:%M:%S")
+
+        # 提取关键词
+        if not keywords:
+            keywords = [s.get("word", "") for s in stats if s.get("word")] if stats else []
+
+        # 使用安全的字符串替换,避免模板中其他花括号(如 JSON 示例)被误解析
+        user_prompt = self.user_prompt_template
+        user_prompt = user_prompt.replace("{report_mode}", report_mode)
+        user_prompt = user_prompt.replace("{report_type}", report_type)
+        user_prompt = user_prompt.replace("{current_time}", current_time)
+        user_prompt = user_prompt.replace("{news_count}", str(hotlist_total))
+        user_prompt = user_prompt.replace("{rss_count}", str(rss_total))
+        user_prompt = user_prompt.replace("{platforms}", ", ".join(platforms) if platforms else "多平台")
+        user_prompt = user_prompt.replace("{keywords}", ", ".join(keywords[:20]) if keywords else "无")
+        user_prompt = user_prompt.replace("{news_content}", news_content)
+
+        # 调用 AI API
+        try:
+            response = self._call_ai_api(user_prompt)
+            result = self._parse_response(response)
+            # 填充统计数据
+            result.total_news = total_news
+            result.hotlist_count = hotlist_total
+            result.rss_count = rss_total
+            result.analyzed_news = analyzed_count
+            result.max_news_limit = self.max_news
+            return result
+        except Exception as e:
+            import requests
+            error_type = type(e).__name__
+            error_msg = str(e)
+
+            # 针对不同错误类型提供更友好的提示
+            if isinstance(e, requests.exceptions.Timeout):
+                friendly_msg = f"AI API 请求超时({self.timeout}秒),请检查网络或增加超时时间"
+            elif isinstance(e, requests.exceptions.ConnectionError):
+                friendly_msg = f"无法连接到 AI API ({self.base_url or self.provider}),请检查网络和 API 地址"
+            elif isinstance(e, requests.exceptions.HTTPError):
+                status_code = e.response.status_code if hasattr(e, 'response') and e.response else "未知"
+                if status_code == 401:
+                    friendly_msg = "AI API 认证失败,请检查 API Key 是否正确"
+                elif status_code == 429:
+                    friendly_msg = "AI API 请求频率过高,请稍后重试"
+                elif status_code == 500:
+                    friendly_msg = "AI API 服务器内部错误,请稍后重试"
+                else:
+                    friendly_msg = f"AI API 返回错误 (HTTP {status_code}): {error_msg[:100]}"
+            else:
+                # 截断过长的错误消息
+                if len(error_msg) > 150:
+                    error_msg = error_msg[:150] + "..."
+                friendly_msg = f"AI 分析失败 ({error_type}): {error_msg}"
+
+            return AIAnalysisResult(
+                success=False,
+                error=friendly_msg
+            )
+
+    def _prepare_news_content(
+        self,
+        stats: List[Dict],
+        rss_stats: Optional[List[Dict]] = None,
+    ) -> tuple:
+        """
+        准备新闻内容文本(增强版)
+
+        热榜新闻包含:来源、标题、排名范围、时间范围、出现次数
+        RSS 包含:来源、标题、发布时间
+
+        Returns:
+            tuple: (content_str, hotlist_total, rss_total, analyzed_count)
+        """
+        lines = []
+        count = 0
+
+        # 计算总新闻数
+        hotlist_total = sum(len(s.get("titles", [])) for s in stats) if stats else 0
+        rss_total = sum(len(s.get("titles", [])) for s in rss_stats) if rss_stats else 0
+
+        # 热榜内容
+        if stats:
+            lines.append("### 热榜新闻")
+            lines.append("格式: [来源] 标题 | 排名:最高-最低 | 时间:首次~末次 | 出现:N次")
+            for stat in stats:
+                word = stat.get("word", "")
+                titles = stat.get("titles", [])
+                if word and titles:
+                    lines.append(f"\n**{word}** ({len(titles)}条)")
+                    for t in titles:
+                        if not isinstance(t, dict):
+                            continue
+                        title = t.get("title", "")
+                        if not title:
+                            continue
+
+                        # 来源
+                        source = t.get("source_name", t.get("source", ""))
+
+                        # 排名范围
+                        ranks = t.get("ranks", [])
+                        if ranks:
+                            min_rank = min(ranks)
+                            max_rank = max(ranks)
+                            rank_str = f"{min_rank}" if min_rank == max_rank else f"{min_rank}-{max_rank}"
+                        else:
+                            rank_str = "-"
+
+                        # 时间范围(简化显示)
+                        first_time = t.get("first_time", "")
+                        last_time = t.get("last_time", "")
+                        time_str = self._format_time_range(first_time, last_time)
+
+                        # 出现次数
+                        appear_count = t.get("count", 1)
+
+                        # 构建行:[来源] 标题 | 排名:X-Y | 时间:首次~末次 | 出现:N次
+                        if source:
+                            line = f"- [{source}] {title}"
+                        else:
+                            line = f"- {title}"
+                        line += f" | 排名:{rank_str} | 时间:{time_str} | 出现:{appear_count}次"
+                        lines.append(line)
+
+                        count += 1
+                        if count >= self.max_news:
+                            break
+                if count >= self.max_news:
+                    break
+
+        # RSS 内容(仅在启用时提交)
+        if self.include_rss and rss_stats and count < self.max_news:
+            lines.append("\n### RSS 订阅")
+            lines.append("格式: [来源] 标题 | 发布时间")
+            for stat in rss_stats:
+                word = stat.get("word", "")
+                titles = stat.get("titles", [])
+                if word and titles:
+                    lines.append(f"\n**{word}** ({len(titles)}条)")
+                    for t in titles:
+                        if not isinstance(t, dict):
+                            continue
+                        title = t.get("title", "")
+                        if not title:
+                            continue
+
+                        # 来源
+                        source = t.get("source_name", t.get("feed_name", ""))
+
+                        # 发布时间
+                        time_display = t.get("time_display", "")
+
+                        # 构建行:[来源] 标题 | 发布时间
+                        if source:
+                            line = f"- [{source}] {title}"
+                        else:
+                            line = f"- {title}"
+                        if time_display:
+                            line += f" | {time_display}"
+                        lines.append(line)
+
+                        count += 1
+                        if count >= self.max_news:
+                            break
+                if count >= self.max_news:
+                    break
+
+        return "\n".join(lines), hotlist_total, rss_total, count
+
+    def _format_time_range(self, first_time: str, last_time: str) -> str:
+        """格式化时间范围(简化显示,只保留时分)"""
+        def extract_time(time_str: str) -> str:
+            if not time_str:
+                return "-"
+            # 尝试提取 HH:MM 部分
+            # 格式可能是 "2026-01-04 12:30:00" 或 "12:30" 等
+            if " " in time_str:
+                parts = time_str.split(" ")
+                if len(parts) >= 2:
+                    time_part = parts[1]
+                    if ":" in time_part:
+                        return time_part[:5]  # HH:MM
+            elif ":" in time_str:
+                return time_str[:5]
+            return time_str[:5] if len(time_str) >= 5 else time_str
+
+        first = extract_time(first_time)
+        last = extract_time(last_time)
+
+        if first == last or last == "-":
+            return first
+        return f"{first}~{last}"
+
+    def _call_ai_api(self, user_prompt: str) -> str:
+        """调用 AI API"""
+        if self.provider == "gemini":
+            return self._call_gemini(user_prompt)
+        return self._call_openai_compatible(user_prompt)
+
+    def _get_api_url(self) -> str:
+        """获取完整 API URL"""
+        if self.base_url:
+            return self.base_url
+
+        # 预设完整端点
+        urls = {
+            "deepseek": "https://api.deepseek.com/v1/chat/completions",
+            "openai": "https://api.openai.com/v1/chat/completions",
+        }
+        url = urls.get(self.provider)
+        if not url:
+            raise ValueError(f"{self.provider} 需要配置 base_url(完整 API 地址)")
+        return url
+
+    def _call_openai_compatible(self, user_prompt: str) -> str:
+        """调用 OpenAI 兼容接口"""
+        import requests
+
+        url = self._get_api_url()
+
+        headers = {
+            "Authorization": f"Bearer {self.api_key}",
+            "Content-Type": "application/json",
+        }
+
+        messages = []
+        if self.system_prompt:
+            messages.append({"role": "system", "content": self.system_prompt})
+        messages.append({"role": "user", "content": user_prompt})
+
+        payload = {
+            "model": self.model,
+            "messages": messages,
+            "temperature": 0.7,
+            "max_tokens": 2000,
+        }
+
+        response = requests.post(
+            url,
+            headers=headers,
+            json=payload,
+            timeout=self.timeout,
+        )
+        response.raise_for_status()
+
+        data = response.json()
+        return data["choices"][0]["message"]["content"]
+
+    def _call_gemini(self, user_prompt: str) -> str:
+        """调用 Google Gemini API"""
+        import requests
+
+        # Gemini API URL 格式: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
+        model = self.model or "gemini-1.5-flash"
+        url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={self.api_key}"
+
+        headers = {
+            "Content-Type": "application/json",
+        }
+
+        # 构建 Gemini 格式的消息
+        contents = []
+        if self.system_prompt:
+            contents.append({
+                "role": "user",
+                "parts": [{"text": f"System instruction: {self.system_prompt}"}]
+            })
+            contents.append({
+                "role": "model",
+                "parts": [{"text": "Understood. I will follow these instructions."}]
+            })
+        contents.append({
+            "role": "user",
+            "parts": [{"text": user_prompt}]
+        })
+
+        payload = {
+            "contents": contents,
+            "generationConfig": {
+                "temperature": 0.7,
+                "maxOutputTokens": 2000,
+            }
+        }
+
+        response = requests.post(
+            url,
+            headers=headers,
+            json=payload,
+            timeout=self.timeout,
+        )
+        response.raise_for_status()
+
+        data = response.json()
+        return data["candidates"][0]["content"]["parts"][0]["text"]
+
+    def _parse_response(self, response: str) -> AIAnalysisResult:
+        """解析 AI 响应"""
+        result = AIAnalysisResult(raw_response=response)
+
+        if not response or not response.strip():
+            result.error = "AI 返回空响应"
+            return result
+
+        # 尝试解析 JSON
+        try:
+            # 提取 JSON 部分
+            json_str = response
+
+            # 尝试提取 ```json ... ``` 代码块
+            if "```json" in response:
+                parts = response.split("```json", 1)
+                if len(parts) > 1:
+                    code_block = parts[1]
+                    # 查找结束的 ```
+                    end_idx = code_block.find("```")
+                    if end_idx != -1:
+                        json_str = code_block[:end_idx]
+                    else:
+                        json_str = code_block  # 没有结束标记,使用剩余内容
+            # 尝试提取 ``` ... ``` 代码块
+            elif "```" in response:
+                parts = response.split("```", 2)  # 最多分割2次
+                if len(parts) >= 2:
+                    json_str = parts[1]
+
+            # 清理 JSON 字符串
+            json_str = json_str.strip()
+            if not json_str:
+                raise ValueError("提取的 JSON 内容为空")
+
+            data = json.loads(json_str)
+
+            result.summary = data.get("summary", "")
+            result.keyword_analysis = data.get("keyword_analysis", "")
+            result.sentiment = data.get("sentiment", "")
+            result.cross_platform = data.get("cross_platform", "")
+            result.impact = data.get("impact", "")
+            result.signals = data.get("signals", "")
+            result.conclusion = data.get("conclusion", "")
+            result.success = True
+
+        except json.JSONDecodeError as e:
+            # JSON 解析失败,记录详细错误但仍使用原始文本
+            error_context = json_str[max(0, e.pos - 30):e.pos + 30] if json_str and e.pos else ""
+            result.error = f"JSON 解析错误 (位置 {e.pos}): {e.msg}"
+            if error_context:
+                result.error += f",上下文: ...{error_context}..."
+            # 使用原始响应作为 summary
+            result.summary = response[:1000] if len(response) > 1000 else response
+            result.success = True  # 仍标记为成功,因为有内容可展示
+        except (IndexError, KeyError, TypeError, ValueError) as e:
+            # 其他解析错误
+            result.error = f"响应解析错误: {type(e).__name__}: {str(e)}"
+            result.summary = response[:1000] if len(response) > 1000 else response
+            result.success = True
+        except Exception as e:
+            # 未知错误
+            result.error = f"解析时发生未知错误: {type(e).__name__}: {str(e)}"
+            result.summary = response[:1000] if len(response) > 1000 else response
+            result.success = True
+
+        return result

+ 220 - 0
trendradar/ai/formatter.py

@@ -0,0 +1,220 @@
+# coding=utf-8
+"""
+AI 分析结果格式化模块
+
+将 AI 分析结果格式化为各推送渠道的样式
+"""
+
+import html as html_lib
+from .analyzer import AIAnalysisResult
+
+
+def _escape_html(text: str) -> str:
+    """转义 HTML 特殊字符,防止 XSS 攻击"""
+    return html_lib.escape(text) if text else ""
+
+
+def render_ai_analysis_markdown(result: AIAnalysisResult) -> str:
+    """渲染为通用 Markdown 格式(Telegram、企业微信、ntfy、Bark、Slack)"""
+    if not result.success:
+        return f"⚠️ AI 分析失败: {result.error}"
+
+    lines = ["**✨ AI 热点分析**", ""]
+
+    if result.summary:
+        lines.extend(["**趋势概述**", result.summary, ""])
+
+    if result.keyword_analysis:
+        lines.extend(["**热度走势**", result.keyword_analysis, ""])
+
+    if result.sentiment:
+        lines.extend(["**情感倾向**", result.sentiment, ""])
+
+    if result.cross_platform:
+        lines.extend(["**跨平台关联**", result.cross_platform, ""])
+
+    if result.impact:
+        lines.extend(["**潜在影响**", result.impact, ""])
+
+    if result.signals:
+        lines.extend(["**值得关注**", result.signals, ""])
+
+    if result.conclusion:
+        lines.extend(["**总结建议**", result.conclusion])
+
+    return "\n".join(lines)
+
+
+def render_ai_analysis_feishu(result: AIAnalysisResult) -> str:
+    """渲染为飞书卡片 Markdown 格式"""
+    if not result.success:
+        return f"⚠️ AI 分析失败: {result.error}"
+
+    lines = ["**✨ AI 热点分析**", ""]
+
+    if result.summary:
+        lines.extend(["**趋势概述**", result.summary, ""])
+
+    if result.keyword_analysis:
+        lines.extend(["**热度走势**", result.keyword_analysis, ""])
+
+    if result.sentiment:
+        lines.extend(["**情感倾向**", result.sentiment, ""])
+
+    if result.cross_platform:
+        lines.extend(["**跨平台关联**", result.cross_platform, ""])
+
+    if result.impact:
+        lines.extend(["**潜在影响**", result.impact, ""])
+
+    if result.signals:
+        lines.extend(["**值得关注**", result.signals, ""])
+
+    if result.conclusion:
+        lines.extend(["**总结建议**", result.conclusion])
+
+    return "\n".join(lines)
+
+
+def render_ai_analysis_dingtalk(result: AIAnalysisResult) -> str:
+    """渲染为钉钉 Markdown 格式"""
+    if not result.success:
+        return f"⚠️ AI 分析失败: {result.error}"
+
+    lines = ["### ✨ AI 热点分析", ""]
+
+    if result.summary:
+        lines.extend(["#### 趋势概述", result.summary, ""])
+
+    if result.keyword_analysis:
+        lines.extend(["#### 热度走势", result.keyword_analysis, ""])
+
+    if result.sentiment:
+        lines.extend(["#### 情感倾向", result.sentiment, ""])
+
+    if result.cross_platform:
+        lines.extend(["#### 跨平台关联", result.cross_platform, ""])
+
+    if result.impact:
+        lines.extend(["#### 潜在影响", result.impact, ""])
+
+    if result.signals:
+        lines.extend(["#### 值得关注", result.signals, ""])
+
+    if result.conclusion:
+        lines.extend(["#### 总结建议", result.conclusion])
+
+    return "\n".join(lines)
+
+
+def render_ai_analysis_html(result: AIAnalysisResult) -> str:
+    """渲染为 HTML 格式(邮件)"""
+    if not result.success:
+        return f'<div class="ai-error">⚠️ AI 分析失败: {_escape_html(result.error)}</div>'
+
+    html_parts = ['<div class="ai-analysis">', '<h3>✨ AI 热点分析</h3>']
+
+    if result.summary:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>趋势概述</h4>',
+            f'<p>{_escape_html(result.summary)}</p>',
+            '</div>'
+        ])
+
+    if result.keyword_analysis:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>热度走势</h4>',
+            f'<p>{_escape_html(result.keyword_analysis)}</p>',
+            '</div>'
+        ])
+
+    if result.sentiment:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>情感倾向</h4>',
+            f'<p>{_escape_html(result.sentiment)}</p>',
+            '</div>'
+        ])
+
+    if result.cross_platform:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>跨平台关联</h4>',
+            f'<p>{_escape_html(result.cross_platform)}</p>',
+            '</div>'
+        ])
+
+    if result.impact:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>潜在影响</h4>',
+            f'<p>{_escape_html(result.impact)}</p>',
+            '</div>'
+        ])
+
+    if result.signals:
+        html_parts.extend([
+            '<div class="ai-section">',
+            '<h4>值得关注</h4>',
+            f'<p>{_escape_html(result.signals)}</p>',
+            '</div>'
+        ])
+
+    if result.conclusion:
+        html_parts.extend([
+            '<div class="ai-section ai-conclusion">',
+            '<h4>总结建议</h4>',
+            f'<p>{_escape_html(result.conclusion)}</p>',
+            '</div>'
+        ])
+
+    html_parts.append('</div>')
+    return "\n".join(html_parts)
+
+
+def render_ai_analysis_plain(result: AIAnalysisResult) -> str:
+    """渲染为纯文本格式"""
+    if not result.success:
+        return f"AI 分析失败: {result.error}"
+
+    lines = ["【AI 热点分析】", ""]
+
+    if result.summary:
+        lines.extend(["[趋势概述]", result.summary, ""])
+
+    if result.keyword_analysis:
+        lines.extend(["[热度走势]", result.keyword_analysis, ""])
+
+    if result.sentiment:
+        lines.extend(["[情感倾向]", result.sentiment, ""])
+
+    if result.cross_platform:
+        lines.extend(["[跨平台关联]", result.cross_platform, ""])
+
+    if result.impact:
+        lines.extend(["[潜在影响]", result.impact, ""])
+
+    if result.signals:
+        lines.extend(["[值得关注]", result.signals, ""])
+
+    if result.conclusion:
+        lines.extend(["[总结建议]", result.conclusion])
+
+    return "\n".join(lines)
+
+
+def get_ai_analysis_renderer(channel: str):
+    """根据渠道获取对应的渲染函数"""
+    renderers = {
+        "feishu": render_ai_analysis_feishu,
+        "dingtalk": render_ai_analysis_dingtalk,
+        "wework": render_ai_analysis_markdown,
+        "telegram": render_ai_analysis_markdown,
+        "email": render_ai_analysis_html,
+        "ntfy": render_ai_analysis_markdown,
+        "bark": render_ai_analysis_plain,
+        "slack": render_ai_analysis_markdown,
+    }
+    return renderers.get(channel, render_ai_analysis_markdown)

+ 14 - 1
trendradar/context.py

@@ -374,8 +374,12 @@ class AppContext:
         mode: str = "daily",
         rss_items: Optional[list] = None,
         rss_new_items: Optional[list] = None,
+        ai_content: Optional[str] = None,
+        standalone_data: Optional[Dict] = None,
+        ai_stats: Optional[Dict] = None,
+        report_type: str = "热点分析报告",
     ) -> List[str]:
-        """分批处理消息内容(支持热榜+RSS合并)
+        """分批处理消息内容(支持热榜+RSS合并+AI分析+独立展示区
 
         Args:
             report_data: 报告数据
@@ -385,6 +389,10 @@ class AppContext:
             mode: 报告模式
             rss_items: RSS 统计条目列表
             rss_new_items: RSS 新增条目列表
+            ai_content: AI 分析内容(已渲染的字符串)
+            standalone_data: 独立展示区数据
+            ai_stats: AI 分析统计数据
+            report_type: 报告类型
 
         Returns:
             分批后的消息内容列表
@@ -407,6 +415,11 @@ class AppContext:
             rss_new_items=rss_new_items,
             timezone=self.config.get("TIMEZONE", "Asia/Shanghai"),
             display_mode=self.display_mode,
+            ai_content=ai_content,
+            standalone_data=standalone_data,
+            rank_threshold=self.rank_threshold,
+            ai_stats=ai_stats,
+            report_type=report_type,
         )
 
     # === 通知发送 ===

+ 28 - 8
trendradar/core/frequency.py

@@ -9,7 +9,8 @@
 - 全局过滤词([GLOBAL_FILTER] 区域)
 - 最大显示数量(@前缀)
 - 正则表达式(/pattern/ 语法)
-- 显示名称(=> 备注 语法)
+- 显示名称(=> 别名 语法)
+- 组别名([组别名] 语法,作为词组第一行)
 """
 
 import os
@@ -136,7 +137,8 @@ def load_frequency_words(
     current_section = "WORD_GROUPS"
 
     for group in word_groups:
-        lines = [line.strip() for line in group.split("\n") if line.strip()]
+        # 过滤空行和注释行(# 开头)
+        lines = [line.strip() for line in group.split("\n") if line.strip() and not line.strip().startswith("#")]
 
         if not lines:
             continue
@@ -161,6 +163,15 @@ def load_frequency_words(
 
         # 处理词组区域
         words = lines
+        group_alias = None  # 组别名([别名] 语法)
+
+        # 检查第一行是否为组别名(非区域标记)
+        if words and words[0].startswith("[") and words[0].endswith("]"):
+            potential_alias = words[0][1:-1].strip()
+            # 排除区域标记(GLOBAL_FILTER, WORD_GROUPS)
+            if potential_alias.upper() not in ("GLOBAL_FILTER", "WORD_GROUPS"):
+                group_alias = potential_alias
+                words = words[1:]  # 移除组别名行
 
         group_required_words = []
         group_normal_words = []
@@ -196,12 +207,21 @@ def load_frequency_words(
             else:
                 group_key = " ".join(w["word"] for w in group_required_words)
 
-            # 提取显示名称:优先使用第一个有 display_name 的词
-            display_name = None
-            for w in group_normal_words + group_required_words:
-                if w.get("display_name"):
-                    display_name = w["display_name"]
-                    break
+            # 生成显示名称
+            # 优先级:组别名 > 行别名拼接 > 关键词拼接
+            if group_alias:
+                # 有组别名,直接使用
+                display_name = group_alias
+            else:
+                # 没有组别名,拼接每行的显示名(行别名或关键词本身)
+                all_words = group_normal_words + group_required_words
+                display_parts = []
+                for w in all_words:
+                    # 优先使用行别名,否则使用关键词本身
+                    part = w.get("display_name") or w["word"]
+                    display_parts.append(part)
+                # 用 " / " 拼接多个词
+                display_name = " / ".join(display_parts) if display_parts else None
 
             processed_groups.append(
                 {

+ 62 - 0
trendradar/core/loader.py

@@ -33,6 +33,17 @@ def _get_env_int(key: str, default: int = 0) -> int:
         return default
 
 
+def _get_env_int_or_none(key: str) -> Optional[int]:
+    """从环境变量获取整数值,未设置时返回 None"""
+    value = os.environ.get(key, "").strip()
+    if not value:
+        return None
+    try:
+        return int(value)
+    except ValueError:
+        return None
+
+
 def _get_env_str(key: str, default: str = "") -> str:
     """从环境变量获取字符串值"""
     return os.environ.get(key, "").strip() or default
@@ -46,6 +57,7 @@ def _load_app_config(config_data: Dict) -> Dict:
         "VERSION_CHECK_URL": advanced.get("version_check_url", ""),
         "SHOW_VERSION_UPDATE": app_config.get("show_version_update", True),
         "TIMEZONE": _get_env_str("TIMEZONE") or app_config.get("timezone", "Asia/Shanghai"),
+        "DEBUG": _get_env_bool("DEBUG") if _get_env_bool("DEBUG") is not None else advanced.get("debug", False),
     }
 
 
@@ -174,6 +186,40 @@ def _load_rss_config(config_data: Dict) -> Dict:
     }
 
 
+def _load_standalone_display_config(config_data: Dict) -> Dict:
+    """加载独立展示区配置"""
+    notification = config_data.get("notification", {})
+    standalone = notification.get("standalone_display", {})
+
+    return {
+        "ENABLED": standalone.get("enabled", False),
+        "PLATFORMS": standalone.get("platforms", []),
+        "RSS_FEEDS": standalone.get("rss_feeds", []),
+        "MAX_ITEMS": standalone.get("max_items", 20),
+    }
+
+
+def _load_ai_analysis_config(config_data: Dict) -> Dict:
+    """加载 AI 分析配置"""
+    ai_config = config_data.get("ai_analysis", {})
+
+    enabled_env = _get_env_bool("AI_ANALYSIS_ENABLED")
+    timeout_env = _get_env_int_or_none("AI_TIMEOUT")
+
+    return {
+        "ENABLED": enabled_env if enabled_env is not None else ai_config.get("enabled", False),
+        "PROVIDER": _get_env_str("AI_PROVIDER") or ai_config.get("provider", "deepseek"),
+        "API_KEY": _get_env_str("AI_API_KEY") or ai_config.get("api_key", ""),
+        "MODEL": _get_env_str("AI_MODEL") or ai_config.get("model", "deepseek-chat"),
+        "BASE_URL": _get_env_str("AI_BASE_URL") or ai_config.get("base_url", ""),
+        "TIMEOUT": timeout_env if timeout_env is not None else ai_config.get("timeout", 90),
+        "PUSH_MODE": _get_env_str("AI_PUSH_MODE") or ai_config.get("push_mode", "both"),
+        "MAX_NEWS_FOR_ANALYSIS": ai_config.get("max_news_for_analysis", 50),
+        "INCLUDE_RSS": ai_config.get("include_rss", True),
+        "PROMPT_FILE": ai_config.get("prompt_file", "ai_analysis_prompt.txt"),
+    }
+
+
 def _load_storage_config(config_data: Dict) -> Dict:
     """加载存储配置"""
     storage = config_data.get("storage", {})
@@ -226,6 +272,7 @@ def _load_webhook_config(config_data: Dict) -> Dict:
     ntfy = channels.get("ntfy", {})
     bark = channels.get("bark", {})
     slack = channels.get("slack", {})
+    generic = channels.get("generic_webhook", {})
 
     return {
         # 飞书
@@ -252,6 +299,9 @@ def _load_webhook_config(config_data: Dict) -> Dict:
         "BARK_URL": _get_env_str("BARK_URL") or bark.get("url", ""),
         # Slack
         "SLACK_WEBHOOK_URL": _get_env_str("SLACK_WEBHOOK_URL") or slack.get("webhook_url", ""),
+        # 通用 Webhook
+        "GENERIC_WEBHOOK_URL": _get_env_str("GENERIC_WEBHOOK_URL") or generic.get("url", ""),
+        "GENERIC_WEBHOOK_TEMPLATE": _get_env_str("GENERIC_WEBHOOK_TEMPLATE") or generic.get("template", ""),
     }
 
 
@@ -324,6 +374,12 @@ def _print_notification_sources(config: Dict) -> None:
         slack_source = "环境变量" if os.environ.get("SLACK_WEBHOOK_URL") else "配置文件"
         notification_sources.append(f"Slack({slack_source}, {count}个账号)")
 
+    if config.get("GENERIC_WEBHOOK_URL"):
+        accounts = parse_multi_account_config(config["GENERIC_WEBHOOK_URL"])
+        count = min(len(accounts), max_accounts)
+        source = "环境变量" if os.environ.get("GENERIC_WEBHOOK_URL") else "配置文件"
+        notification_sources.append(f"通用Webhook({source}, {count}个账号)")
+
     if notification_sources:
         print(f"通知渠道配置来源: {', '.join(notification_sources)}")
         print(f"每个渠道最大账号数: {max_accounts}")
@@ -382,6 +438,12 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
     # RSS 配置
     config["RSS"] = _load_rss_config(config_data)
 
+    # AI 分析配置
+    config["AI_ANALYSIS"] = _load_ai_analysis_config(config_data)
+
+    # 独立展示区配置
+    config["STANDALONE_DISPLAY"] = _load_standalone_display_config(config_data)
+
     # 存储配置
     config["STORAGE"] = _load_storage_config(config_data)
 

+ 190 - 32
trendradar/notification/dispatcher.py

@@ -10,7 +10,9 @@
     results = dispatcher.dispatch_all(report_data, report_type, ...)
 """
 
-from typing import Any, Callable, Dict, List, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
 
 from trendradar.core.config import (
     get_account_at_index,
@@ -28,6 +30,7 @@ from .senders import (
     send_to_slack,
     send_to_telegram,
     send_to_wework,
+    send_to_generic_webhook,
 )
 from .renderer import (
     render_rss_feishu_content,
@@ -35,6 +38,10 @@ from .renderer import (
     render_rss_markdown_content,
 )
 
+# 类型检查时导入,运行时不导入(避免循环导入)
+if TYPE_CHECKING:
+    from trendradar.ai import AIAnalysisResult
+
 
 class NotificationDispatcher:
     """
@@ -73,9 +80,11 @@ class NotificationDispatcher:
         html_file_path: Optional[str] = None,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        standalone_data: Optional[Dict] = None,
     ) -> Dict[str, bool]:
         """
-        分发通知到所有已配置的渠道(支持热榜+RSS合并推送)
+        分发通知到所有已配置的渠道(支持热榜+RSS合并推送+AI分析+独立展示区
 
         Args:
             report_data: 报告数据(由 prepare_report_data 生成)
@@ -86,52 +95,72 @@ class NotificationDispatcher:
             html_file_path: HTML 报告文件路径(邮件使用)
             rss_items: RSS 统计条目列表(用于 RSS 统计区块)
             rss_new_items: RSS 新增条目列表(用于 RSS 新增区块)
+            ai_analysis: AI 分析结果(可选)
+            standalone_data: 独立展示区数据(可选)
 
         Returns:
             Dict[str, bool]: 每个渠道的发送结果,key 为渠道名,value 为是否成功
         """
         results = {}
 
+        # 获取 AI 推送模式
+        ai_config = self.config.get("AI_ANALYSIS", {})
+        ai_push_mode = ai_config.get("PUSH_MODE", "both")
+
         # 飞书
         if self.config.get("FEISHU_WEBHOOK_URL"):
             results["feishu"] = self._send_feishu(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # 钉钉
         if self.config.get("DINGTALK_WEBHOOK_URL"):
             results["dingtalk"] = self._send_dingtalk(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # 企业微信
         if self.config.get("WEWORK_WEBHOOK_URL"):
             results["wework"] = self._send_wework(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # Telegram(需要配对验证)
         if self.config.get("TELEGRAM_BOT_TOKEN") and self.config.get("TELEGRAM_CHAT_ID"):
             results["telegram"] = self._send_telegram(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # ntfy(需要配对验证)
         if self.config.get("NTFY_SERVER_URL") and self.config.get("NTFY_TOPIC"):
             results["ntfy"] = self._send_ntfy(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # Bark
         if self.config.get("BARK_URL"):
             results["bark"] = self._send_bark(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # Slack
         if self.config.get("SLACK_WEBHOOK_URL"):
             results["slack"] = self._send_slack(
-                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
+            )
+
+        # 通用 Webhook
+        if self.config.get("GENERIC_WEBHOOK_URL"):
+            results["generic_webhook"] = self._send_generic_webhook(
+                report_data, report_type, update_info, proxy_url, mode, rss_items, rss_new_items,
+                ai_analysis, ai_push_mode, standalone_data
             )
 
         # 邮件(保持原有逻辑,已支持多收件人)
@@ -140,7 +169,7 @@ class NotificationDispatcher:
             and self.config.get("EMAIL_PASSWORD")
             and self.config.get("EMAIL_TO")
         ):
-            results["email"] = self._send_email(report_type, html_file_path)
+            results["email"] = self._send_email(report_type, html_file_path, ai_analysis, ai_push_mode)
 
         return results
 
@@ -187,8 +216,15 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到飞书(多账号,支持热榜+RSS合并)"""
+        """发送到飞书(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        # 根据 AI 推送模式决定是否发送原始内容
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         return self._send_to_multi_accounts(
             channel_name="飞书",
             config_value=self.config["FEISHU_WEBHOOK_URL"],
@@ -204,8 +240,11 @@ class NotificationDispatcher:
                 batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                 split_content_func=self.split_content_func,
                 get_time_func=self.get_time_func,
-                rss_items=rss_items,
-                rss_new_items=rss_new_items,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
             ),
         )
 
@@ -218,8 +257,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到钉钉(多账号,支持热榜+RSS合并)"""
+        """发送到钉钉(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         return self._send_to_multi_accounts(
             channel_name="钉钉",
             config_value=self.config["DINGTALK_WEBHOOK_URL"],
@@ -234,8 +279,11 @@ class NotificationDispatcher:
                 batch_size=self.config.get("DINGTALK_BATCH_SIZE", 20000),
                 batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                 split_content_func=self.split_content_func,
-                rss_items=rss_items,
-                rss_new_items=rss_new_items,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
             ),
         )
 
@@ -248,8 +296,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到企业微信(多账号,支持热榜+RSS合并)"""
+        """发送到企业微信(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         return self._send_to_multi_accounts(
             channel_name="企业微信",
             config_value=self.config["WEWORK_WEBHOOK_URL"],
@@ -265,8 +319,11 @@ class NotificationDispatcher:
                 batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                 msg_type=self.config.get("WEWORK_MSG_TYPE", "markdown"),
                 split_content_func=self.split_content_func,
-                rss_items=rss_items,
-                rss_new_items=rss_new_items,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
             ),
         )
 
@@ -279,8 +336,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到 Telegram(多账号,需验证 token 和 chat_id 配对,支持热榜+RSS合并)"""
+        """发送到 Telegram(多账号,需验证 token 和 chat_id 配对,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         telegram_tokens = parse_multi_account_config(self.config["TELEGRAM_BOT_TOKEN"])
         telegram_chat_ids = parse_multi_account_config(self.config["TELEGRAM_CHAT_ID"])
 
@@ -318,8 +381,11 @@ class NotificationDispatcher:
                     batch_size=self.config.get("MESSAGE_BATCH_SIZE", 4000),
                     batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                     split_content_func=self.split_content_func,
-                    rss_items=rss_items,
-                    rss_new_items=rss_new_items,
+                    rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                    rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                    ai_analysis=ai_analysis,
+                    ai_push_mode=ai_push_mode,
+                    standalone_data=standalone_data,
                 )
                 results.append(result)
 
@@ -334,8 +400,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到 ntfy(多账号,需验证 topic 和 token 配对,支持热榜+RSS合并)"""
+        """发送到 ntfy(多账号,需验证 topic 和 token 配对,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         ntfy_server_url = self.config["NTFY_SERVER_URL"]
         ntfy_topics = parse_multi_account_config(self.config["NTFY_TOPIC"])
         ntfy_tokens = parse_multi_account_config(self.config.get("NTFY_TOKEN", ""))
@@ -372,8 +444,11 @@ class NotificationDispatcher:
                     account_label=account_label,
                     batch_size=3800,
                     split_content_func=self.split_content_func,
-                    rss_items=rss_items,
-                    rss_new_items=rss_new_items,
+                    rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                    rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                    ai_analysis=ai_analysis,
+                    ai_push_mode=ai_push_mode,
+                    standalone_data=standalone_data,
                 )
                 results.append(result)
 
@@ -388,8 +463,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到 Bark(多账号,支持热榜+RSS合并)"""
+        """发送到 Bark(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         return self._send_to_multi_accounts(
             channel_name="Bark",
             config_value=self.config["BARK_URL"],
@@ -404,8 +485,11 @@ class NotificationDispatcher:
                 batch_size=self.config.get("BARK_BATCH_SIZE", 3600),
                 batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                 split_content_func=self.split_content_func,
-                rss_items=rss_items,
-                rss_new_items=rss_new_items,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
             ),
         )
 
@@ -418,8 +502,14 @@ class NotificationDispatcher:
         mode: str,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
     ) -> bool:
-        """发送到 Slack(多账号,支持热榜+RSS合并)"""
+        """发送到 Slack(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
         return self._send_to_multi_accounts(
             channel_name="Slack",
             config_value=self.config["SLACK_WEBHOOK_URL"],
@@ -434,17 +524,83 @@ class NotificationDispatcher:
                 batch_size=self.config.get("SLACK_BATCH_SIZE", 4000),
                 batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
                 split_content_func=self.split_content_func,
-                rss_items=rss_items,
-                rss_new_items=rss_new_items,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
             ),
         )
 
+    def _send_generic_webhook(
+        self,
+        report_data: Dict,
+        report_type: str,
+        update_info: Optional[Dict],
+        proxy_url: Optional[str],
+        mode: str,
+        rss_items: Optional[List[Dict]] = None,
+        rss_new_items: Optional[List[Dict]] = None,
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
+        standalone_data: Optional[Dict] = None,
+    ) -> bool:
+        """发送到通用 Webhook(多账号,支持热榜+RSS合并+AI分析+独立展示区)"""
+        if ai_push_mode == "only_analysis" and ai_analysis:
+            report_data = {"stats": [], "failed_ids": [], "new_titles": {}, "id_to_name": {}}
+
+        urls = parse_multi_account_config(self.config.get("GENERIC_WEBHOOK_URL", ""))
+        templates = parse_multi_account_config(self.config.get("GENERIC_WEBHOOK_TEMPLATE", ""))
+
+        if not urls:
+            return False
+
+        urls = limit_accounts(urls, self.max_accounts, "通用Webhook")
+        results = []
+
+        for i, url in enumerate(urls):
+            if not url:
+                continue
+
+            template = ""
+            if templates:
+                if i < len(templates):
+                    template = templates[i]
+                elif len(templates) == 1:
+                    template = templates[0] # 共用一个模板
+
+            account_label = f"账号{i+1}" if len(urls) > 1 else ""
+
+            result = send_to_generic_webhook(
+                webhook_url=url,
+                payload_template=template,
+                report_data=report_data,
+                report_type=report_type,
+                update_info=update_info,
+                proxy_url=proxy_url,
+                mode=mode,
+                account_label=account_label,
+                batch_size=self.config.get("MESSAGE_BATCH_SIZE", 4000),
+                batch_interval=self.config.get("BATCH_SEND_INTERVAL", 1.0),
+                split_content_func=self.split_content_func,
+                rss_items=rss_items if ai_push_mode != "only_analysis" else None,
+                rss_new_items=rss_new_items if ai_push_mode != "only_analysis" else None,
+                ai_analysis=ai_analysis,
+                ai_push_mode=ai_push_mode,
+                standalone_data=standalone_data,
+            )
+            results.append(result)
+
+        return any(results) if results else False
+
     def _send_email(
         self,
         report_type: str,
         html_file_path: Optional[str],
+        ai_analysis: Optional[AIAnalysisResult] = None,
+        ai_push_mode: str = "both",
     ) -> bool:
-        """发送邮件(保持原有逻辑,已支持多收件人)"""
+        """发送邮件(保持原有逻辑,已支持多收件人,支持AI分析)"""
         return send_to_email(
             from_email=self.config["EMAIL_FROM"],
             password=self.config["EMAIL_PASSWORD"],
@@ -454,6 +610,8 @@ class NotificationDispatcher:
             custom_smtp_server=self.config.get("EMAIL_SMTP_SERVER", ""),
             custom_smtp_port=self.config.get("EMAIL_SMTP_PORT", ""),
             get_time_func=self.get_time_func,
+            ai_analysis=ai_analysis,
+            ai_push_mode=ai_push_mode,
         )
 
     # === RSS 通知方法 ===

+ 330 - 20
trendradar/notification/senders.py

@@ -17,13 +17,14 @@
 
 import smtplib
 import time
+import json
 from datetime import datetime
 from email.header import Header
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from email.utils import formataddr, formatdate, make_msgid
 from pathlib import Path
-from typing import Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional
 from urllib.parse import urlparse
 
 import requests
@@ -32,6 +33,19 @@ from .batch import add_batch_headers, get_max_batch_header_size
 from .formatters import convert_markdown_to_mrkdwn, strip_markdown
 
 
+def _render_ai_analysis(ai_analysis: Any, channel: str, ai_push_mode: str) -> str:
+    """渲染 AI 分析内容为指定渠道格式"""
+    if not ai_analysis:
+        return ""
+
+    try:
+        from trendradar.ai.formatter import get_ai_analysis_renderer
+        renderer = get_ai_analysis_renderer(channel)
+        return renderer(ai_analysis)
+    except ImportError:
+        return ""
+
+
 # === SMTP 邮件配置 ===
 SMTP_CONFIGS = {
     # Gmail(使用 STARTTLS)
@@ -75,9 +89,12 @@ def send_to_feishu(
     get_time_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到飞书(支持分批发送,支持热榜+RSS合并)
+    发送到飞书(支持分批发送,支持热榜+RSS合并+独立展示区
 
     Args:
         webhook_url: 飞书 Webhook URL
@@ -105,6 +122,21 @@ def send_to_feishu(
     # 日志前缀
     log_prefix = f"飞书{account_label}" if account_label else "飞书"
 
+    # 渲染 AI 分析内容(如果有)
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "feishu", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 预留批次头部空间,避免添加头部后超限
     header_reserve = get_max_batch_header_size("feishu")
     batches = split_content_func(
@@ -115,6 +147,10 @@ def send_to_feishu(
         mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -129,17 +165,10 @@ def send_to_feishu(
             f"发送{log_prefix}第 {i}/{len(batches)} 批次,大小:{content_size} 字节 [{report_type}]"
         )
 
-        total_titles = sum(
-            len(stat["titles"]) for stat in report_data["stats"] if stat["count"] > 0
-        )
-        now = get_time_func() if get_time_func else datetime.now()
-
+        # 飞书 webhook 只显示 content.text,所有信息都整合到 text 中
         payload = {
             "msg_type": "text",
             "content": {
-                "total_titles": total_titles,
-                "timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
-                "report_type": report_type,
                 "text": batch_content,
             },
         }
@@ -172,6 +201,7 @@ def send_to_feishu(
             return False
 
     print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
     return True
 
 
@@ -189,9 +219,12 @@ def send_to_dingtalk(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到钉钉(支持分批发送,支持热榜+RSS合并)
+    发送到钉钉(支持分批发送,支持热榜+RSS合并+独立展示区
 
     Args:
         webhook_url: 钉钉 Webhook URL
@@ -218,6 +251,21 @@ def send_to_dingtalk(
     # 日志前缀
     log_prefix = f"钉钉{account_label}" if account_label else "钉钉"
 
+    # 渲染 AI 分析内容(如果有)
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "dingtalk", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 预留批次头部空间,避免添加头部后超限
     header_reserve = get_max_batch_header_size("dingtalk")
     batches = split_content_func(
@@ -228,6 +276,10 @@ def send_to_dingtalk(
         mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -276,6 +328,7 @@ def send_to_dingtalk(
             return False
 
     print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
     return True
 
 
@@ -294,9 +347,12 @@ def send_to_wework(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到企业微信(支持分批发送,支持 markdown 和 text 两种格式,支持热榜+RSS合并)
+    发送到企业微信(支持分批发送,支持 markdown 和 text 两种格式,支持热榜+RSS合并+独立展示区
 
     Args:
         webhook_url: 企业微信 Webhook URL
@@ -335,12 +391,31 @@ def send_to_wework(
     # text 模式使用 wework_text,markdown 模式使用 wework
     header_format_type = "wework_text" if is_text_mode else "wework"
 
+    # 渲染 AI 分析内容(如果有)
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "wework", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 获取分批内容,预留批次头部空间
     header_reserve = get_max_batch_header_size(header_format_type)
     batches = split_content_func(
         report_data, "wework", update_info, max_bytes=batch_size - header_reserve, mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -391,6 +466,7 @@ def send_to_wework(
             return False
 
     print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
     return True
 
 
@@ -409,9 +485,12 @@ def send_to_telegram(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到 Telegram(支持分批发送,支持热榜+RSS合并)
+    发送到 Telegram(支持分批发送,支持热榜+RSS合并+独立展示区
 
     Args:
         bot_token: Telegram Bot Token
@@ -441,12 +520,31 @@ def send_to_telegram(
     # 日志前缀
     log_prefix = f"Telegram{account_label}" if account_label else "Telegram"
 
+    # 渲染 AI 分析内容(如果有)
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "telegram", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 获取分批内容,预留批次头部空间
     header_reserve = get_max_batch_header_size("telegram")
     batches = split_content_func(
         report_data, "telegram", update_info, max_bytes=batch_size - header_reserve, mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -494,6 +592,7 @@ def send_to_telegram(
             return False
 
     print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
     return True
 
 
@@ -507,6 +606,8 @@ def send_to_email(
     custom_smtp_port: Optional[int] = None,
     *,
     get_time_func: Callable = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
 ) -> bool:
     """
     发送邮件通知
@@ -533,6 +634,12 @@ def send_to_email(
         with open(html_file_path, "r", encoding="utf-8") as f:
             html_content = f.read()
 
+        # 追加 AI 分析内容到 HTML
+        if ai_analysis:
+            ai_content = _render_ai_analysis(ai_analysis, "email", ai_push_mode)
+            if ai_content:
+                html_content = html_content.replace("</body>", f"{ai_content}</body>")
+
         domain = from_email.split("@")[-1].lower()
 
         if custom_smtp_server and custom_smtp_port:
@@ -668,9 +775,12 @@ def send_to_ntfy(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到 ntfy(支持分批发送,严格遵守4KB限制,支持热榜+RSS合并)
+    发送到 ntfy(支持分批发送,严格遵守4KB限制,支持热榜+RSS合并+独立展示区
 
     Args:
         server_url: ntfy 服务器 URL
@@ -724,12 +834,31 @@ def send_to_ntfy(
     if proxy_url:
         proxies = {"http": proxy_url, "https": proxy_url}
 
+    # 渲染 AI 分析内容(如果有),合并到主内容中
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "ntfy", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 获取分批内容,预留批次头部空间
     header_reserve = get_max_batch_header_size("ntfy")
     batches = split_content_func(
         report_data, "ntfy", update_info, max_bytes=batch_size - header_reserve, mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -825,14 +954,14 @@ def send_to_ntfy(
     # 判断整体发送是否成功
     if success_count == total_batches:
         print(f"{log_prefix}所有 {total_batches} 批次发送完成 [{report_type}]")
-        return True
     elif success_count > 0:
         print(f"{log_prefix}部分发送成功:{success_count}/{total_batches} 批次 [{report_type}]")
-        return True  # 部分成功也视为成功
     else:
         print(f"{log_prefix}发送完全失败 [{report_type}]")
         return False
 
+    return True
+
 
 def send_to_bark(
     bark_url: str,
@@ -848,9 +977,12 @@ def send_to_bark(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到 Bark(支持分批发送,使用 markdown 格式,支持热榜+RSS合并)
+    发送到 Bark(支持分批发送,使用 markdown 格式,支持热榜+RSS合并+独立展示区
 
     Args:
         bark_url: Bark URL(包含 device_key)
@@ -888,12 +1020,31 @@ def send_to_bark(
     # 构建正确的 API 端点
     api_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/push"
 
+    # 渲染 AI 分析内容(如果有),合并到主内容中
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "bark", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 获取分批内容,预留批次头部空间
     header_reserve = get_max_batch_header_size("bark")
     batches = split_content_func(
         report_data, "bark", update_info, max_bytes=batch_size - header_reserve, mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -976,14 +1127,14 @@ def send_to_bark(
     # 判断整体发送是否成功
     if success_count == total_batches:
         print(f"{log_prefix}所有 {total_batches} 批次发送完成 [{report_type}]")
-        return True
     elif success_count > 0:
         print(f"{log_prefix}部分发送成功:{success_count}/{total_batches} 批次 [{report_type}]")
-        return True  # 部分成功也视为成功
     else:
         print(f"{log_prefix}发送完全失败 [{report_type}]")
         return False
 
+    return True
+
 
 def send_to_slack(
     webhook_url: str,
@@ -999,9 +1150,12 @@ def send_to_slack(
     split_content_func: Callable = None,
     rss_items: Optional[list] = None,
     rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
 ) -> bool:
     """
-    发送到 Slack(支持分批发送,使用 mrkdwn 格式,支持热榜+RSS合并)
+    发送到 Slack(支持分批发送,使用 mrkdwn 格式,支持热榜+RSS合并+独立展示区
 
     Args:
         webhook_url: Slack Webhook URL
@@ -1028,12 +1182,31 @@ def send_to_slack(
     # 日志前缀
     log_prefix = f"Slack{account_label}" if account_label else "Slack"
 
+    # 渲染 AI 分析内容(如果有),合并到主内容中
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        ai_content = _render_ai_analysis(ai_analysis, "slack", ai_push_mode)
+        # 提取 AI 分析统计数据(只要 AI 分析成功就显示)
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
     # 获取分批内容,预留批次头部空间
     header_reserve = get_max_batch_header_size("slack")
     batches = split_content_func(
         report_data, "slack", update_info, max_bytes=batch_size - header_reserve, mode=mode,
         rss_items=rss_items,
         rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
     )
 
     # 统一添加批次头部(已预留空间,不会超限)
@@ -1076,4 +1249,141 @@ def send_to_slack(
             return False
 
     print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
+    return True
+
+
+def send_to_generic_webhook(
+    webhook_url: str,
+    payload_template: Optional[str],
+    report_data: Dict,
+    report_type: str,
+    update_info: Optional[Dict] = None,
+    proxy_url: Optional[str] = None,
+    mode: str = "daily",
+    account_label: str = "",
+    *,
+    batch_size: int = 4000,
+    batch_interval: float = 1.0,
+    split_content_func: Optional[Callable] = None,
+    rss_items: Optional[list] = None,
+    rss_new_items: Optional[list] = None,
+    ai_analysis: Any = None,
+    ai_push_mode: str = "both",
+    standalone_data: Optional[Dict] = None,
+) -> bool:
+    """
+    发送到通用 Webhook(支持分批发送,支持自定义 JSON 模板,支持热榜+RSS合并+独立展示区)
+
+    Args:
+        webhook_url: Webhook URL
+        payload_template: JSON 模板字符串,支持 {title} 和 {content} 占位符
+        report_data: 报告数据
+        report_type: 报告类型
+        update_info: 更新信息(可选)
+        proxy_url: 代理 URL(可选)
+        mode: 报告模式 (daily/current)
+        account_label: 账号标签(多账号时显示)
+        batch_size: 批次大小(字节)
+        batch_interval: 批次发送间隔(秒)
+        split_content_func: 内容分批函数
+        rss_items: RSS 统计条目列表(可选,用于合并推送)
+        rss_new_items: RSS 新增条目列表(可选,用于新增区块)
+
+    Returns:
+        bool: 发送是否成功
+    """
+    if split_content_func is None:
+        raise ValueError("split_content_func is required")
+
+    headers = {"Content-Type": "application/json"}
+    proxies = None
+    if proxy_url:
+        proxies = {"http": proxy_url, "https": proxy_url}
+
+    # 日志前缀
+    log_prefix = f"通用Webhook{account_label}" if account_label else "通用Webhook"
+
+    # 渲染 AI 分析内容(如果有)
+    ai_content = None
+    ai_stats = None
+    if ai_analysis:
+        # 通用 Webhook 使用 markdown 格式渲染 AI 分析
+        ai_content = _render_ai_analysis(ai_analysis, "wework", ai_push_mode)
+        # 提取 AI 分析统计数据
+        if getattr(ai_analysis, "success", False):
+            ai_stats = {
+                "total_news": getattr(ai_analysis, "total_news", 0),
+                "analyzed_news": getattr(ai_analysis, "analyzed_news", 0),
+                "max_news_limit": getattr(ai_analysis, "max_news_limit", 0),
+                "hotlist_count": getattr(ai_analysis, "hotlist_count", 0),
+                "rss_count": getattr(ai_analysis, "rss_count", 0),
+            }
+
+    # 获取分批内容
+    # 使用 'wework' 作为 format_type 以获取 markdown 格式的通用输出
+    # 预留一定空间给模板外壳
+    template_overhead = 200 
+    batches = split_content_func(
+        report_data, "wework", update_info, max_bytes=batch_size - template_overhead, mode=mode,
+        rss_items=rss_items,
+        rss_new_items=rss_new_items,
+        ai_content=ai_content,
+        standalone_data=standalone_data,
+        ai_stats=ai_stats,
+        report_type=report_type,
+    )
+
+    # 统一添加批次头部
+    batches = add_batch_headers(batches, "wework", batch_size)
+
+    print(f"{log_prefix}消息分为 {len(batches)} 批次发送 [{report_type}]")
+
+    # 逐批发送
+    for i, batch_content in enumerate(batches, 1):
+        content_size = len(batch_content.encode("utf-8"))
+        print(
+            f"发送{log_prefix}第 {i}/{len(batches)} 批次,大小:{content_size} 字节 [{report_type}]"
+        )
+
+        try:
+            # 构建 payload
+            if payload_template:
+                # 简单的字符串替换
+                # 注意:content 可能包含 JSON 特殊字符,需要先转义
+                json_content = json.dumps(batch_content)[1:-1] # 去掉首尾引号
+                json_title = json.dumps(report_type)[1:-1]
+                
+                payload_str = payload_template.replace("{content}", json_content).replace("{title}", json_title)
+                
+                # 尝试解析为 JSON 对象以验证有效性
+                try:
+                    payload = json.loads(payload_str)
+                except json.JSONDecodeError as e:
+                    print(f"{log_prefix} JSON 模板解析失败: {e}")
+                    # 回退到默认格式
+                    payload = {"title": report_type, "content": batch_content}
+            else:
+                # 默认格式
+                payload = {"title": report_type, "content": batch_content}
+
+            response = requests.post(
+                webhook_url, headers=headers, json=payload, proxies=proxies, timeout=30
+            )
+            
+            if response.status_code >= 200 and response.status_code < 300:
+                print(f"{log_prefix}第 {i}/{len(batches)} 批次发送成功 [{report_type}]")
+                if i < len(batches):
+                    time.sleep(batch_interval)
+            else:
+                print(
+                    f"{log_prefix}第 {i}/{len(batches)} 批次发送失败 [{report_type}],状态码:{response.status_code}, 响应: {response.text}"
+                )
+                return False
+        except Exception as e:
+            print(f"{log_prefix}第 {i}/{len(batches)} 批次发送出错 [{report_type}]:{e}")
+            return False
+
+    print(f"{log_prefix}所有 {len(batches)} 批次发送完成 [{report_type}]")
+
     return True

+ 479 - 17
trendradar/notification/splitter.py

@@ -9,7 +9,8 @@ from datetime import datetime
 from typing import Dict, List, Optional, Callable
 
 from trendradar.report.formatter import format_title_for_platform
-from trendradar.utils.time import format_iso_time_friendly
+from trendradar.report.helpers import format_rank_display
+from trendradar.utils.time import format_iso_time_friendly, convert_time_for_display
 
 
 # 默认批次大小配置
@@ -35,11 +36,18 @@ def split_content_into_batches(
     rss_new_items: Optional[list] = None,
     timezone: str = "Asia/Shanghai",
     display_mode: str = "keyword",
+    ai_content: Optional[str] = None,
+    standalone_data: Optional[Dict] = None,
+    rank_threshold: int = 10,
+    ai_stats: Optional[Dict] = None,
+    report_type: str = "热点分析报告",
 ) -> List[str]:
-    """分批处理消息内容,确保词组标题+至少第一条新闻的完整性(支持热榜+RSS合并)
+    """分批处理消息内容,确保词组标题+至少第一条新闻的完整性(支持热榜+RSS合并+AI分析+独立展示区
 
     热榜统计与RSS统计并列显示,热榜新增与RSS新增并列显示。
     reverse_content_order 控制统计和新增的前后顺序。
+    AI分析内容默认放在最后(footer之前)。
+    独立展示区放在新增区块之后、失败ID之前。
 
     Args:
         report_data: 报告数据字典,包含 stats, new_titles, failed_ids, total_new_count
@@ -55,6 +63,9 @@ def split_content_into_batches(
         rss_new_items: RSS 新增条目列表(可选,用于新增区块)
         timezone: 时区名称(用于 RSS 时间格式化)
         display_mode: 显示模式 (keyword=按关键词分组, platform=按平台分组)
+        ai_content: AI 分析内容(已渲染的字符串,可选)
+        standalone_data: 独立展示区数据(可选),包含 platforms 和 rss_feeds 列表
+        ai_stats: AI 分析统计数据(可选),包含 total_news, analyzed_news, max_news_limit 等
 
     Returns:
         分批后的消息内容列表
@@ -74,27 +85,64 @@ def split_content_into_batches(
 
     batches = []
 
-    total_titles = sum(
+    total_hotlist_count = sum(
         len(stat["titles"]) for stat in report_data["stats"] if stat["count"] > 0
     )
+    total_titles = total_hotlist_count
+    
+    # 累加 RSS 条目数
+    if rss_items:
+        total_titles += sum(stat.get("count", 0) for stat in rss_items)
+
     now = get_time_func() if get_time_func else datetime.now()
 
+    # 构建头部信息
     base_header = ""
+    
+    # 准备 AI 分析统计行(如果存在)
+    ai_stats_line = ""
+    if ai_stats and ai_stats.get("analyzed_news", 0) > 0:
+        analyzed_news = ai_stats.get("analyzed_news", 0)
+        if format_type in ("wework", "bark", "ntfy", "feishu", "dingtalk"):
+            ai_stats_line = f"**AI 分析数:** {analyzed_news}\n"
+        elif format_type == "slack":
+            ai_stats_line = f"*AI 分析数:* {analyzed_news}\n"
+        elif format_type == "telegram":
+            ai_stats_line = f"AI 分析数: {analyzed_news}\n"
+
+    # 构建统一的头部(总是显示总新闻数、时间和类型)
     if format_type in ("wework", "bark"):
-        base_header = f"**总新闻数:** {total_titles}\n\n\n\n"
+        base_header = f"**总新闻数:** {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"**类型:** {report_type}\n\n"
     elif format_type == "telegram":
-        base_header = f"总新闻数: {total_titles}\n\n"
+        base_header = f"总新闻数: {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"时间: {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"类型: {report_type}\n\n"
     elif format_type == "ntfy":
-        base_header = f"**总新闻数:** {total_titles}\n\n"
+        base_header = f"**总新闻数:** {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"**类型:** {report_type}\n\n"
     elif format_type == "feishu":
-        base_header = ""
+        base_header = f"**总新闻数:** {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"**类型:** {report_type}\n\n"
+        base_header += "---\n\n"
     elif format_type == "dingtalk":
-        base_header = f"**总新闻数:** {total_titles}\n\n"
-        base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
-        base_header += f"**类型:** 热点分析报告\n\n"
+        base_header = f"**总新闻数:** {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"**类型:** {report_type}\n\n"
         base_header += "---\n\n"
     elif format_type == "slack":
-        base_header = f"*总新闻数:* {total_titles}\n\n"
+        base_header = f"*总新闻数:* {total_titles}\n"
+        base_header += ai_stats_line
+        base_header += f"*时间:* {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
+        base_header += f"*类型:* {report_type}\n\n"
 
     base_footer = ""
     if format_type in ("wework", "bark"):
@@ -127,25 +175,30 @@ def split_content_into_batches(
     stats_header = ""
     if report_data["stats"]:
         if format_type in ("wework", "bark"):
-            stats_header = f"📊 **{stats_title}**\n\n"
+            stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
         elif format_type == "telegram":
-            stats_header = f"📊 {stats_title}\n\n"
+            stats_header = f"📊 {stats_title} (共 {total_hotlist_count} 条)\n\n"
         elif format_type == "ntfy":
-            stats_header = f"📊 **{stats_title}**\n\n"
+            stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
         elif format_type == "feishu":
-            stats_header = f"📊 **{stats_title}**\n\n"
+            stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
         elif format_type == "dingtalk":
-            stats_header = f"📊 **{stats_title}**\n\n"
+            stats_header = f"📊 **{stats_title}** (共 {total_hotlist_count} 条)\n\n"
         elif format_type == "slack":
-            stats_header = f"📊 *{stats_title}*\n\n"
+            stats_header = f"📊 *{stats_title}* (共 {total_hotlist_count} 条)\n\n"
 
     current_batch = base_header
     current_batch_has_content = False
 
+    # 当没有热榜数据时的处理
+    # 注意:如果有 ai_content,不应该返回"暂无匹配"消息,而应该继续处理 AI 内容
     if (
         not report_data["stats"]
         and not report_data["new_titles"]
         and not report_data["failed_ids"]
+        and not ai_content  # 有 AI 内容时不返回"暂无匹配"
+        and not rss_items  # 有 RSS 内容时也不返回
+        and not standalone_data  # 有独立展示区数据时也不返回
     ):
         if mode == "incremental":
             mode_text = "增量模式下暂无新增匹配的热点词汇"
@@ -571,6 +624,14 @@ def split_content_into_batches(
                 max_bytes, current_batch, current_batch_has_content, batches, timezone
             )
 
+    # 5. 处理独立展示区(如果有)
+    if standalone_data:
+        current_batch, current_batch_has_content, batches = _process_standalone_section(
+            standalone_data, format_type, feishu_separator, base_header, base_footer,
+            max_bytes, current_batch, current_batch_has_content, batches, timezone,
+            rank_threshold
+        )
+
     if report_data["failed_ids"]:
         failed_header = ""
         if format_type == "wework":
@@ -618,6 +679,41 @@ def split_content_into_batches(
                 current_batch = test_content
                 current_batch_has_content = True
 
+    # 处理 AI 分析内容(放在最后,footer 之前)
+    if ai_content:
+        # 添加 AI 分析区块分隔符
+        ai_separator = ""
+        if format_type == "feishu":
+            ai_separator = f"\n{feishu_separator}\n\n"
+        elif format_type == "dingtalk":
+            ai_separator = "\n---\n\n"
+        elif format_type in ("wework", "bark"):
+            ai_separator = "\n\n\n\n"
+        elif format_type in ("telegram", "ntfy", "slack"):
+            ai_separator = "\n\n"
+
+        # 尝试将 AI 内容添加到当前批次
+        test_content = current_batch + ai_separator + ai_content
+        if (
+            len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
+            < max_bytes
+        ):
+            current_batch = test_content
+            current_batch_has_content = True
+        else:
+            # 当前批次容纳不下,开启新批次
+            if current_batch_has_content:
+                batches.append(current_batch + base_footer)
+            # AI 内容可能很长,需要考虑是否需要进一步分割
+            ai_with_header = base_header + ai_content
+            if len(ai_with_header.encode("utf-8")) + len(base_footer.encode("utf-8")) < max_bytes:
+                current_batch = ai_with_header
+                current_batch_has_content = True
+            else:
+                # AI 内容过长,直接添加(可能会超限,但保持完整性)
+                current_batch = ai_with_header
+                current_batch_has_content = True
+
     # 完成最后批次
     if current_batch_has_content:
         batches.append(current_batch + base_footer)
@@ -1050,3 +1146,369 @@ def _format_rss_item_line(
 
     item_line += "\n"
     return item_line
+
+
+def _process_standalone_section(
+    standalone_data: Dict,
+    format_type: str,
+    feishu_separator: str,
+    base_header: str,
+    base_footer: str,
+    max_bytes: int,
+    current_batch: str,
+    current_batch_has_content: bool,
+    batches: List[str],
+    timezone: str = "Asia/Shanghai",
+    rank_threshold: int = 10,
+) -> tuple:
+    """处理独立展示区区块
+
+    独立展示区显示指定平台的完整热榜或 RSS 源内容,不受关键词过滤影响。
+    热榜按原始排名排序,RSS 按发布时间排序。
+
+    Args:
+        standalone_data: 独立展示数据,格式:
+            {
+                "platforms": [{"id": "zhihu", "name": "知乎热榜", "items": [...]}],
+                "rss_feeds": [{"id": "hacker-news", "name": "Hacker News", "items": [...]}]
+            }
+        format_type: 格式类型
+        feishu_separator: 飞书分隔符
+        base_header: 基础头部
+        base_footer: 基础尾部
+        max_bytes: 最大字节数
+        current_batch: 当前批次内容
+        current_batch_has_content: 当前批次是否有内容
+        batches: 已完成的批次列表
+        timezone: 时区名称
+
+    Returns:
+        (current_batch, current_batch_has_content, batches) 元组
+    """
+    if not standalone_data:
+        return current_batch, current_batch_has_content, batches
+
+    platforms = standalone_data.get("platforms", [])
+    rss_feeds = standalone_data.get("rss_feeds", [])
+
+    if not platforms and not rss_feeds:
+        return current_batch, current_batch_has_content, batches
+
+    # 计算总条目数
+    total_platform_items = sum(len(p.get("items", [])) for p in platforms)
+    total_rss_items = sum(len(f.get("items", [])) for f in rss_feeds)
+    total_items = total_platform_items + total_rss_items
+
+    # 独立展示区标题
+    section_header = ""
+    if format_type == "feishu":
+        section_header = f"\n{feishu_separator}\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
+    elif format_type == "dingtalk":
+        section_header = f"\n---\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
+    elif format_type == "telegram":
+        section_header = f"\n\n📋 独立展示区 (共 {total_items} 条)\n\n"
+    elif format_type == "slack":
+        section_header = f"\n\n📋 *独立展示区* (共 {total_items} 条)\n\n"
+    else:
+        section_header = f"\n\n📋 **独立展示区** (共 {total_items} 条)\n\n"
+
+    # 添加区块标题
+    test_content = current_batch + section_header
+    if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) < max_bytes:
+        current_batch = test_content
+        current_batch_has_content = True
+    else:
+        if current_batch_has_content:
+            batches.append(current_batch + base_footer)
+        current_batch = base_header + section_header
+        current_batch_has_content = True
+
+    # 处理热榜平台
+    for platform in platforms:
+        platform_name = platform.get("name", platform.get("id", ""))
+        items = platform.get("items", [])
+        if not items:
+            continue
+
+        # 平台标题
+        platform_header = ""
+        if format_type in ("wework", "bark"):
+            platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
+        elif format_type == "telegram":
+            platform_header = f"{platform_name} ({len(items)} 条):\n\n"
+        elif format_type == "ntfy":
+            platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
+        elif format_type == "feishu":
+            platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
+        elif format_type == "dingtalk":
+            platform_header = f"**{platform_name}** ({len(items)} 条):\n\n"
+        elif format_type == "slack":
+            platform_header = f"*{platform_name}* ({len(items)} 条):\n\n"
+
+        # 构建第一条新闻
+        first_item_line = ""
+        if items:
+            first_item_line = _format_standalone_platform_item(items[0], 1, format_type, rank_threshold)
+
+        # 原子性检查
+        platform_with_first = platform_header + first_item_line
+        test_content = current_batch + platform_with_first
+
+        if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
+            if current_batch_has_content:
+                batches.append(current_batch + base_footer)
+            current_batch = base_header + section_header + platform_with_first
+            current_batch_has_content = True
+            start_index = 1
+        else:
+            current_batch = test_content
+            current_batch_has_content = True
+            start_index = 1
+
+        # 处理剩余条目
+        for j in range(start_index, len(items)):
+            item_line = _format_standalone_platform_item(items[j], j + 1, format_type, rank_threshold)
+
+            test_content = current_batch + item_line
+            if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
+                if current_batch_has_content:
+                    batches.append(current_batch + base_footer)
+                current_batch = base_header + section_header + platform_header + item_line
+                current_batch_has_content = True
+            else:
+                current_batch = test_content
+                current_batch_has_content = True
+
+        current_batch += "\n"
+
+    # 处理 RSS 源
+    for feed in rss_feeds:
+        feed_name = feed.get("name", feed.get("id", ""))
+        items = feed.get("items", [])
+        if not items:
+            continue
+
+        # RSS 源标题
+        feed_header = ""
+        if format_type in ("wework", "bark"):
+            feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
+        elif format_type == "telegram":
+            feed_header = f"{feed_name} ({len(items)} 条):\n\n"
+        elif format_type == "ntfy":
+            feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
+        elif format_type == "feishu":
+            feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
+        elif format_type == "dingtalk":
+            feed_header = f"**{feed_name}** ({len(items)} 条):\n\n"
+        elif format_type == "slack":
+            feed_header = f"*{feed_name}* ({len(items)} 条):\n\n"
+
+        # 构建第一条 RSS
+        first_item_line = ""
+        if items:
+            first_item_line = _format_standalone_rss_item(items[0], 1, format_type, timezone)
+
+        # 原子性检查
+        feed_with_first = feed_header + first_item_line
+        test_content = current_batch + feed_with_first
+
+        if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
+            if current_batch_has_content:
+                batches.append(current_batch + base_footer)
+            current_batch = base_header + section_header + feed_with_first
+            current_batch_has_content = True
+            start_index = 1
+        else:
+            current_batch = test_content
+            current_batch_has_content = True
+            start_index = 1
+
+        # 处理剩余条目
+        for j in range(start_index, len(items)):
+            item_line = _format_standalone_rss_item(items[j], j + 1, format_type, timezone)
+
+            test_content = current_batch + item_line
+            if len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8")) >= max_bytes:
+                if current_batch_has_content:
+                    batches.append(current_batch + base_footer)
+                current_batch = base_header + section_header + feed_header + item_line
+                current_batch_has_content = True
+            else:
+                current_batch = test_content
+                current_batch_has_content = True
+
+        current_batch += "\n"
+
+    return current_batch, current_batch_has_content, batches
+
+
+def _format_standalone_platform_item(item: Dict, index: int, format_type: str, rank_threshold: int = 10) -> str:
+    """格式化独立展示区的热榜条目(复用热点词汇统计区样式)
+
+    Args:
+        item: 热榜条目,包含 title, url, rank, ranks, first_time, last_time, count
+        index: 序号
+        format_type: 格式类型
+        rank_threshold: 排名高亮阈值
+
+    Returns:
+        格式化后的条目行字符串
+    """
+    title = item.get("title", "")
+    url = item.get("url", "") or item.get("mobileUrl", "")
+    ranks = item.get("ranks", [])
+    rank = item.get("rank", 0)
+    first_time = item.get("first_time", "")
+    last_time = item.get("last_time", "")
+    count = item.get("count", 1)
+
+    # 使用 format_rank_display 格式化排名(复用热点词汇统计区逻辑)
+    # 如果没有 ranks 列表,用单个 rank 构造
+    if not ranks and rank > 0:
+        ranks = [rank]
+    rank_display = format_rank_display(ranks, rank_threshold, format_type) if ranks else ""
+
+    # 构建时间显示(用 ~ 连接范围,与热点词汇统计区一致)
+    # 将 HH-MM 格式转换为 HH:MM 格式
+    time_display = ""
+    if first_time and last_time and first_time != last_time:
+        first_time_display = convert_time_for_display(first_time)
+        last_time_display = convert_time_for_display(last_time)
+        time_display = f"{first_time_display}~{last_time_display}"
+    elif first_time:
+        time_display = convert_time_for_display(first_time)
+
+    # 构建次数显示(格式为 (N次),与热点词汇统计区一致)
+    count_display = f"({count}次)" if count > 1 else ""
+
+    # 根据格式类型构建条目行(复用热点词汇统计区样式)
+    if format_type == "feishu":
+        if url:
+            item_line = f"  {index}. [{title}]({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if rank_display:
+            item_line += f" {rank_display}"
+        if time_display:
+            item_line += f" <font color='grey'>- {time_display}</font>"
+        if count_display:
+            item_line += f" <font color='green'>{count_display}</font>"
+
+    elif format_type == "dingtalk":
+        if url:
+            item_line = f"  {index}. [{title}]({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if rank_display:
+            item_line += f" {rank_display}"
+        if time_display:
+            item_line += f" - {time_display}"
+        if count_display:
+            item_line += f" {count_display}"
+
+    elif format_type == "telegram":
+        if url:
+            item_line = f"  {index}. {title} ({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if rank_display:
+            item_line += f" {rank_display}"
+        if time_display:
+            item_line += f" - {time_display}"
+        if count_display:
+            item_line += f" {count_display}"
+
+    elif format_type == "slack":
+        if url:
+            item_line = f"  {index}. <{url}|{title}>"
+        else:
+            item_line = f"  {index}. {title}"
+        if rank_display:
+            item_line += f" {rank_display}"
+        if time_display:
+            item_line += f" _{time_display}_"
+        if count_display:
+            item_line += f" {count_display}"
+
+    else:
+        # wework, bark, ntfy
+        if url:
+            item_line = f"  {index}. [{title}]({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if rank_display:
+            item_line += f" {rank_display}"
+        if time_display:
+            item_line += f" - {time_display}"
+        if count_display:
+            item_line += f" {count_display}"
+
+    item_line += "\n"
+    return item_line
+
+
+def _format_standalone_rss_item(
+    item: Dict, index: int, format_type: str, timezone: str = "Asia/Shanghai"
+) -> str:
+    """格式化独立展示区的 RSS 条目
+
+    Args:
+        item: RSS 条目,包含 title, url, published_at, author
+        index: 序号
+        format_type: 格式类型
+        timezone: 时区名称
+
+    Returns:
+        格式化后的条目行字符串
+    """
+    title = item.get("title", "")
+    url = item.get("url", "")
+    published_at = item.get("published_at", "")
+    author = item.get("author", "")
+
+    # 使用友好时间格式
+    friendly_time = ""
+    if published_at:
+        friendly_time = format_iso_time_friendly(published_at, timezone, include_date=True)
+
+    # 构建元信息
+    meta_parts = []
+    if friendly_time:
+        meta_parts.append(friendly_time)
+    if author:
+        meta_parts.append(author)
+    meta_str = ", ".join(meta_parts)
+
+    # 根据格式类型构建条目行
+    if format_type == "feishu":
+        if url:
+            item_line = f"  {index}. [{title}]({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if meta_str:
+            item_line += f" <font color='grey'>- {meta_str}</font>"
+    elif format_type == "telegram":
+        if url:
+            item_line = f"  {index}. {title} ({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if meta_str:
+            item_line += f" - {meta_str}"
+    elif format_type == "slack":
+        if url:
+            item_line = f"  {index}. <{url}|{title}>"
+        else:
+            item_line = f"  {index}. {title}"
+        if meta_str:
+            item_line += f" _{meta_str}_"
+    else:
+        # wework, bark, ntfy, dingtalk
+        if url:
+            item_line = f"  {index}. [{title}]({url})"
+        else:
+            item_line = f"  {index}. {title}"
+        if meta_str:
+            item_line += f" `{meta_str}`"
+
+    item_line += "\n"
+    return item_line

+ 20 - 4
trendradar/report/helpers.py

@@ -113,13 +113,29 @@ def format_rank_display(ranks: List[int], rank_threshold: int, format_type: str)
         highlight_end = "**"
 
     # 生成排名显示
+    rank_str = ""
     if min_rank <= rank_threshold:
         if min_rank == max_rank:
-            return f"{highlight_start}[{min_rank}]{highlight_end}"
+            rank_str = f"{highlight_start}[{min_rank}]{highlight_end}"
         else:
-            return f"{highlight_start}[{min_rank} - {max_rank}]{highlight_end}"
+            rank_str = f"{highlight_start}[{min_rank} - {max_rank}]{highlight_end}"
     else:
         if min_rank == max_rank:
-            return f"[{min_rank}]"
+            rank_str = f"[{min_rank}]"
         else:
-            return f"[{min_rank} - {max_rank}]"
+            rank_str = f"[{min_rank} - {max_rank}]"
+
+    # 计算热度趋势
+    trend_arrow = ""
+    if len(ranks) >= 2:
+        prev_rank = ranks[-2]
+        curr_rank = ranks[-1]
+        if curr_rank < prev_rank:
+            trend_arrow = "🔺"  # 排名上升(数值变小)
+        elif curr_rank > prev_rank:
+            trend_arrow = "🔻"  # 排名下降(数值变大)
+        else:
+            trend_arrow = "➖"  # 排名持平
+    # len(ranks) == 1 时不显示趋势箭头(新上榜由 is_new 字段在 formatter.py 中处理)
+
+    return f"{rank_str} {trend_arrow}" if trend_arrow else rank_str

+ 289 - 4
trendradar/report/html.py

@@ -9,6 +9,7 @@ from datetime import datetime
 from typing import Dict, List, Optional, Callable
 
 from trendradar.report.helpers import html_escape
+from trendradar.utils.time import convert_time_for_display
 
 
 def render_html_content(
@@ -23,6 +24,7 @@ def render_html_content(
     rss_items: Optional[List[Dict]] = None,
     rss_new_items: Optional[List[Dict]] = None,
     display_mode: str = "keyword",
+    standalone_data: Optional[Dict] = None,
 ) -> str:
     """渲染HTML内容
 
@@ -37,6 +39,7 @@ def render_html_content(
         rss_items: RSS 统计条目列表(可选)
         rss_new_items: RSS 新增条目列表(可选)
         display_mode: 显示模式 ("keyword"=按关键词分组, "platform"=按平台分组)
+        standalone_data: 独立展示区数据(可选),包含 platforms 和 rss_feeds
 
     Returns:
         渲染后的 HTML 字符串
@@ -592,6 +595,60 @@ def render_html_content(
                 -webkit-box-orient: vertical;
                 overflow: hidden;
             }
+
+            /* 独立展示区样式 - 复用热点词汇统计区样式 */
+            .standalone-section {
+                margin-top: 32px;
+                padding-top: 24px;
+                border-top: 2px solid #e5e7eb;
+            }
+
+            .standalone-section-header {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                margin-bottom: 20px;
+            }
+
+            .standalone-section-title {
+                font-size: 18px;
+                font-weight: 600;
+                color: #4f46e5;
+            }
+
+            .standalone-section-count {
+                color: #6b7280;
+                font-size: 14px;
+            }
+
+            .standalone-group {
+                margin-bottom: 40px;
+            }
+
+            .standalone-group:last-child {
+                margin-bottom: 0;
+            }
+
+            .standalone-header {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                margin-bottom: 20px;
+                padding-bottom: 8px;
+                border-bottom: 1px solid #f0f0f0;
+            }
+
+            .standalone-name {
+                font-size: 17px;
+                font-weight: 600;
+                color: #1a1a1a;
+            }
+
+            .standalone-count {
+                color: #666;
+                font-size: 13px;
+                font-weight: 500;
+            }
         </style>
     </head>
     <body>
@@ -942,19 +999,247 @@ def render_html_content(
                 </div>"""
         return rss_html
 
+    # 生成独立展示区内容
+    def render_standalone_html(data: Optional[Dict]) -> str:
+        """渲染独立展示区 HTML(复用热点词汇统计区样式)
+
+        Args:
+            data: 独立展示数据,格式:
+                {
+                    "platforms": [
+                        {
+                            "id": "zhihu",
+                            "name": "知乎热榜",
+                            "items": [
+                                {
+                                    "title": "标题",
+                                    "url": "链接",
+                                    "rank": 1,
+                                    "ranks": [1, 2, 1],
+                                    "first_time": "08:00",
+                                    "last_time": "12:30",
+                                    "count": 3,
+                                }
+                            ]
+                        }
+                    ],
+                    "rss_feeds": [
+                        {
+                            "id": "hacker-news",
+                            "name": "Hacker News",
+                            "items": [
+                                {
+                                    "title": "标题",
+                                    "url": "链接",
+                                    "published_at": "2025-01-07T08:00:00",
+                                    "author": "作者",
+                                }
+                            ]
+                        }
+                    ]
+                }
+
+        Returns:
+            渲染后的 HTML 字符串
+        """
+        if not data:
+            return ""
+
+        platforms = data.get("platforms", [])
+        rss_feeds = data.get("rss_feeds", [])
+
+        if not platforms and not rss_feeds:
+            return ""
+
+        # 计算总条目数
+        total_platform_items = sum(len(p.get("items", [])) for p in platforms)
+        total_rss_items = sum(len(f.get("items", [])) for f in rss_feeds)
+        total_count = total_platform_items + total_rss_items
+
+        if total_count == 0:
+            return ""
+
+        standalone_html = f"""
+                <div class="standalone-section">
+                    <div class="standalone-section-header">
+                        <div class="standalone-section-title">📋 独立展示区</div>
+                        <div class="standalone-section-count">{total_count} 条</div>
+                    </div>"""
+
+        # 渲染热榜平台(复用 word-group 结构)
+        for platform in platforms:
+            platform_name = platform.get("name", platform.get("id", ""))
+            items = platform.get("items", [])
+            if not items:
+                continue
+
+            standalone_html += f"""
+                    <div class="standalone-group">
+                        <div class="standalone-header">
+                            <div class="standalone-name">{html_escape(platform_name)}</div>
+                            <div class="standalone-count">{len(items)} 条</div>
+                        </div>"""
+
+            # 渲染每个条目(复用 news-item 结构)
+            for j, item in enumerate(items, 1):
+                title = item.get("title", "")
+                url = item.get("url", "") or item.get("mobileUrl", "")
+                rank = item.get("rank", 0)
+                ranks = item.get("ranks", [])
+                first_time = item.get("first_time", "")
+                last_time = item.get("last_time", "")
+                count = item.get("count", 1)
+
+                standalone_html += f"""
+                        <div class="news-item">
+                            <div class="news-number">{j}</div>
+                            <div class="news-content">
+                                <div class="news-header">"""
+
+                # 排名显示(复用 rank-num 样式,无 # 前缀)
+                if ranks:
+                    min_rank = min(ranks)
+                    max_rank = max(ranks)
+
+                    # 确定排名等级
+                    if min_rank <= 3:
+                        rank_class = "top"
+                    elif min_rank <= 10:
+                        rank_class = "high"
+                    else:
+                        rank_class = ""
+
+                    if min_rank == max_rank:
+                        rank_text = str(min_rank)
+                    else:
+                        rank_text = f"{min_rank}-{max_rank}"
+
+                    standalone_html += f'<span class="rank-num {rank_class}">{rank_text}</span>'
+                elif rank > 0:
+                    if rank <= 3:
+                        rank_class = "top"
+                    elif rank <= 10:
+                        rank_class = "high"
+                    else:
+                        rank_class = ""
+                    standalone_html += f'<span class="rank-num {rank_class}">{rank}</span>'
+
+                # 时间显示(复用 time-info 样式,将 HH-MM 转换为 HH:MM)
+                if first_time and last_time and first_time != last_time:
+                    first_time_display = convert_time_for_display(first_time)
+                    last_time_display = convert_time_for_display(last_time)
+                    standalone_html += f'<span class="time-info">{html_escape(first_time_display)}~{html_escape(last_time_display)}</span>'
+                elif first_time:
+                    first_time_display = convert_time_for_display(first_time)
+                    standalone_html += f'<span class="time-info">{html_escape(first_time_display)}</span>'
+
+                # 出现次数(复用 count-info 样式)
+                if count > 1:
+                    standalone_html += f'<span class="count-info">{count}次</span>'
+
+                standalone_html += """
+                                </div>
+                                <div class="news-title">"""
+
+                # 标题和链接(复用 news-link 样式)
+                escaped_title = html_escape(title)
+                if url:
+                    escaped_url = html_escape(url)
+                    standalone_html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
+                else:
+                    standalone_html += escaped_title
+
+                standalone_html += """
+                                </div>
+                            </div>
+                        </div>"""
+
+            standalone_html += """
+                    </div>"""
+
+        # 渲染 RSS 源(复用相同结构)
+        for feed in rss_feeds:
+            feed_name = feed.get("name", feed.get("id", ""))
+            items = feed.get("items", [])
+            if not items:
+                continue
+
+            standalone_html += f"""
+                    <div class="standalone-group">
+                        <div class="standalone-header">
+                            <div class="standalone-name">{html_escape(feed_name)}</div>
+                            <div class="standalone-count">{len(items)} 条</div>
+                        </div>"""
+
+            for j, item in enumerate(items, 1):
+                title = item.get("title", "")
+                url = item.get("url", "")
+                published_at = item.get("published_at", "")
+                author = item.get("author", "")
+
+                standalone_html += f"""
+                        <div class="news-item">
+                            <div class="news-number">{j}</div>
+                            <div class="news-content">
+                                <div class="news-header">"""
+
+                # 时间显示(格式化 ISO 时间)
+                if published_at:
+                    try:
+                        from datetime import datetime as dt
+                        if "T" in published_at:
+                            dt_obj = dt.fromisoformat(published_at.replace("Z", "+00:00"))
+                            time_display = dt_obj.strftime("%m-%d %H:%M")
+                        else:
+                            time_display = published_at
+                    except:
+                        time_display = published_at
+
+                    standalone_html += f'<span class="time-info">{html_escape(time_display)}</span>'
+
+                # 作者显示
+                if author:
+                    standalone_html += f'<span class="source-name">{html_escape(author)}</span>'
+
+                standalone_html += """
+                                </div>
+                                <div class="news-title">"""
+
+                escaped_title = html_escape(title)
+                if url:
+                    escaped_url = html_escape(url)
+                    standalone_html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
+                else:
+                    standalone_html += escaped_title
+
+                standalone_html += """
+                                </div>
+                            </div>
+                        </div>"""
+
+            standalone_html += """
+                    </div>"""
+
+        standalone_html += """
+                </div>"""
+        return standalone_html
+
     # 生成 RSS 统计和新增 HTML
     rss_stats_html = render_rss_stats_html(rss_items, "RSS 订阅更新") if rss_items else ""
     rss_new_html = render_rss_stats_html(rss_new_items, "RSS 新增更新") if rss_new_items else ""
 
+    # 生成独立展示区 HTML
+    standalone_html = render_standalone_html(standalone_data)
+
     # 根据配置决定内容顺序(与推送逻辑一致)
     if reverse_content_order:
         # 新增在前,统计在后
-        # 顺序:热榜新增 → RSS新增 → 热榜统计 → RSS统计
-        html += new_titles_html + rss_new_html + stats_html + rss_stats_html
+        # 顺序:热榜新增 → RSS新增 → 热榜统计 → RSS统计 → 独立展示区
+        html += new_titles_html + rss_new_html + stats_html + rss_stats_html + standalone_html
     else:
         # 默认:统计在前,新增在后
-        # 顺序:热榜统计 → RSS统计 → 热榜新增 → RSS新增
-        html += stats_html + rss_stats_html + new_titles_html + rss_new_html
+        # 顺序:热榜统计 → RSS统计 → 热榜新增 → RSS新增 → 独立展示区
+        html += stats_html + rss_stats_html + new_titles_html + rss_new_html + standalone_html
 
     html += """
             </div>

+ 1 - 1
version

@@ -1 +1 @@
-4.7.0
+5.0.0

+ 1 - 0
version_mcp

@@ -0,0 +1 @@
+3.1.5