Browse Source

feat: 基于 litellm 支持调用 100+大型语言模型 API,优化ai分析的提示词等更新

sansan 3 months ago
parent
commit
b41e43580c

+ 1 - 2
.github/workflows/crawler.yml

@@ -157,9 +157,8 @@ jobs:
           # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
           # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
           AI_ANALYSIS_ENABLED: ${{ secrets.AI_ANALYSIS_ENABLED }}
           AI_ANALYSIS_ENABLED: ${{ secrets.AI_ANALYSIS_ENABLED }}
           AI_API_KEY: ${{ secrets.AI_API_KEY }}
           AI_API_KEY: ${{ secrets.AI_API_KEY }}
-          AI_PROVIDER: ${{ secrets.AI_PROVIDER }}
           AI_MODEL: ${{ secrets.AI_MODEL }}
           AI_MODEL: ${{ secrets.AI_MODEL }}
-          AI_BASE_URL: ${{ secrets.AI_BASE_URL }}
+          AI_API_BASE: ${{ secrets.AI_API_BASE }}
           # 远程存储配置
           # 远程存储配置
           S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
           S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
           S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
           S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}

+ 46 - 15
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 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)
 [![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)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v5.2.0-blue.svg)](https://github.com/sansan0/TrendRadar)
-[![MCP](https://img.shields.io/badge/MCP-v3.1.6-green.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.3.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![MCP](https://img.shields.io/badge/MCP-v3.1.7-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)
 [![RSS](https://img.shields.io/badge/RSS-Feed_Support-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![AI Translation](https://img.shields.io/badge/AI-Multi--Language-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
 [![AI Translation](https://img.shields.io/badge/AI-Multi--Language-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
 
 
@@ -173,6 +173,24 @@ 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)**:
 >**📌 Check Latest Updates**: **[Original Repository Changelog](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-changelog)**:
 - **Tip**: Check [Changelog] to understand specific [Features]
 - **Tip**: Check [Changelog] to understand specific [Features]
 
 
+### 2026/01/19 - v5.3.0
+
+> **Major Refactor: AI Module Migration to LiteLLM**
+
+- **Unified AI Interface**: Replaced manual implementation with LiteLLM, supporting 100+ AI providers
+- **Simplified Configuration**: Removed `provider` field, now using `model: "provider/model_name"` format
+- **New Features**: Auto-retry (`num_retries`), fallback models (`fallback_models`)
+- **Configuration Changes**:
+  - `ai.provider` → Removed (merged into model)
+  - `ai.base_url` → `ai.api_base`
+  - `AI_PROVIDER` environment variable → Removed
+  - `AI_BASE_URL` environment variable → `AI_API_BASE`
+- **Model Format Examples**:
+  - DeepSeek: `deepseek/deepseek-chat`
+  - OpenAI: `openai/gpt-4o`
+  - Gemini: `gemini/gemini-2.5-flash`
+  - Anthropic: `anthropic/claude-3-5-sonnet`
+
 ### 2026/01/17 - v5.2.0
 ### 2026/01/17 - v5.2.0
 
 
 > See config.yaml for details
 > See config.yaml for details
@@ -3233,19 +3251,32 @@ The simplest way is via environment variables (Recommended for GitHub Secrets or
 |--------------|-------|-------------|
 |--------------|-------|-------------|
 | `AI_ANALYSIS_ENABLED` | `true` | Enable switch |
 | `AI_ANALYSIS_ENABLED` | `true` | Enable switch |
 | `AI_API_KEY` | `sk-xxxxxx` | Your API Key |
 | `AI_API_KEY` | `sk-xxxxxx` | Your API Key |
-| `AI_PROVIDER` | `deepseek` | AI Provider (see table below) |
-| `AI_MODEL` | `deepseek-chat` | Model Name |
-
-**Supported AI Providers**:
-
-| Provider | AI_PROVIDER Value | Default Model (AI_MODEL) |
-|----------|-------------------|------------------------|
-| **DeepSeek** (Recommended) | `deepseek` | `deepseek-chat` |
-| **OpenAI** | `openai` | `gpt-4o` |
-| **Google Gemini** | `gemini` | `gemini-1.5-flash` |
-| **Custom** (OneAPI) | `custom` | Requires `AI_BASE_URL` |
-
-> 💡 **Tip**: DeepSeek offers excellent performance/price ratio, highly suitable for high-frequency news analysis.
+| `AI_MODEL` | `deepseek/deepseek-chat` | Model identifier (format: `provider/model`) |
+
+**Supported AI Providers** (Based on LiteLLM, supports 100+ providers):
+
+| Provider | AI_MODEL Value | Description |
+|----------|----------------|-------------|
+| **DeepSeek** (Recommended) | `deepseek/deepseek-chat` | Excellent cost-performance ratio for high-frequency analysis |
+| **OpenAI** | `openai/gpt-4o`<br>`openai/gpt-4o-mini` | GPT-4o series |
+| **Google Gemini** | `gemini/gemini-1.5-flash`<br>`gemini/gemini-1.5-pro` | Gemini series |
+| **Claude** | `anthropic/claude-3-5-sonnet-20241022` | Anthropic Claude series |
+| **Zhipu AI** | `zhipu/glm-4-plus`<br>`zhipu/glm-4-flash` | Chinese model with native Chinese support |
+| **Moonshot** | `moonshot/moonshot-v1-8k`<br>`moonshot/moonshot-v1-32k` | Kimi series |
+| **Qwen** | `qwen/qwen-plus`<br>`qwen/qwen-turbo` | Alibaba Cloud Tongyi Qianwen |
+| **Custom API** | Any format | Use with `AI_API_BASE` |
+
+> 💡 **New Feature**: Now based on [LiteLLM](https://github.com/BerriAI/litellm) unified interface, supporting 100+ AI providers with simpler configuration and better error handling.
+
+**Optional Configurations**:
+
+| Variable Name | Default | Description |
+|--------------|---------|-------------|
+| `AI_API_BASE` | (auto) | Custom API endpoint (e.g., OneAPI, local models) |
+| `AI_TEMPERATURE` | `1.0` | Sampling temperature (0-2, higher = more random) |
+| `AI_MAX_TOKENS` | `5000` | Maximum tokens to generate |
+| `AI_TIMEOUT` | `120` | Request timeout (seconds) |
+| `AI_NUM_RETRIES` | `2` | Number of retries on failure |
 
 
 #### Advanced: AI Translation
 #### Advanced: AI Translation
 
 

+ 43 - 12
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 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)
 [![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)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v5.2.0-blue.svg)](https://github.com/sansan0/TrendRadar)
-[![MCP](https://img.shields.io/badge/MCP-v3.1.6-green.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.3.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![MCP](https://img.shields.io/badge/MCP-v3.1.7-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)
 [![RSS](https://img.shields.io/badge/RSS-订阅源支持-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![AI翻译](https://img.shields.io/badge/AI-多语言推送-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
 [![AI翻译](https://img.shields.io/badge/AI-多语言推送-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
 
 
@@ -220,6 +220,24 @@
 > **📌 查看最新更新**:**[原仓库更新日志](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-更新日志)** :
 > **📌 查看最新更新**:**[原仓库更新日志](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-更新日志)** :
 - **提示**:建议查看【历史更新】,明确具体的【功能内容】
 - **提示**:建议查看【历史更新】,明确具体的【功能内容】
 
 
+### 2026/01/19 - v5.3.0
+
+> **重大重构:AI 模块迁移至 LiteLLM**
+
+- **统一 AI 接口**:使用 LiteLLM 替代手动实现,支持 100+ AI 提供商
+- **简化配置**:移除 `provider` 字段,改用 `model: "provider/model_name"` 格式
+- **新增功能**:自动重试 (`num_retries`)、备用模型 (`fallback_models`)
+- **配置变更**:
+  - `ai.provider` → 移除(已合并到 model)
+  - `ai.base_url` → `ai.api_base`
+  - `AI_PROVIDER` 环境变量 → 移除
+  - `AI_BASE_URL` 环境变量 → `AI_API_BASE`
+- **模型格式示例**:
+  - DeepSeek: `deepseek/deepseek-chat`
+  - OpenAI: `openai/gpt-4o`
+  - Gemini: `gemini/gemini-2.5-flash`
+  - Anthropic: `anthropic/claude-3-5-sonnet`
+
 ### 2026/01/17 - v5.2.0
 ### 2026/01/17 - v5.2.0
 
 
 > 主要见 config.yaml 描述
 > 主要见 config.yaml 描述
@@ -3227,19 +3245,32 @@ app:
 |-------|-------|------|
 |-------|-------|------|
 | `AI_ANALYSIS_ENABLED` | `true` | 开启开关 |
 | `AI_ANALYSIS_ENABLED` | `true` | 开启开关 |
 | `AI_API_KEY` | `sk-xxxxxx` | 你的 API Key |
 | `AI_API_KEY` | `sk-xxxxxx` | 你的 API Key |
-| `AI_PROVIDER` | `deepseek` | AI 提供商(见下表) |
-| `AI_MODEL` | `deepseek-chat` | 模型名称 |
+| `AI_MODEL` | `deepseek/deepseek-chat` | 模型标识(格式:`provider/model`) |
 
 
-**支持的 AI 提供商**:
+**支持的 AI 提供商**(基于 LiteLLM,支持 100+ 提供商)
 
 
-| 提供商 | AI_PROVIDER 填什么 | 默认模型 (AI_MODEL) |
-|-------|-------------------|-------------------|
-| **DeepSeek** (推荐) | `deepseek` | `deepseek-chat` |
-| **OpenAI** | `openai` | `gpt-4o` |
-| **Google Gemini** | `gemini` | `gemini-1.5-flash` |
-| **自定义** (OneAPI) | `custom` | 需额外配置 `AI_BASE_URL` |
+| 提供商 | AI_MODEL 填什么 | 说明 |
+|-------|----------------|------|
+| **DeepSeek** (推荐) | `deepseek/deepseek-chat` | 性价比极高,适合高频分析 |
+| **OpenAI** | `openai/gpt-4o`<br>`openai/gpt-4o-mini` | GPT-4o 系列 |
+| **Google Gemini** | `gemini/gemini-1.5-flash`<br>`gemini/gemini-1.5-pro` | Gemini 系列 |
+| **Claude** | `anthropic/claude-3-5-sonnet-20241022` | Anthropic Claude 系列 |
+| **智谱 AI** | `zhipu/glm-4-plus`<br>`zhipu/glm-4-flash` | 国内模型,支持中文 |
+| **月之暗面** | `moonshot/moonshot-v1-8k`<br>`moonshot/moonshot-v1-32k` | Kimi 系列 |
+| **通义千问** | `qwen/qwen-plus`<br>`qwen/qwen-turbo` | 阿里云通义千问 |
+| **自定义 API** | 任意格式 | 配合 `AI_API_BASE` 使用 |
 
 
-> 💡 **小技巧**:DeepSeek 性价比极高,非常适合用来做这种高频的新闻分析。
+> 💡 **新特性**:现已基于 [LiteLLM](https://github.com/BerriAI/litellm) 统一接口,支持 100+ AI 提供商,配置更简单、错误处理更完善。
+
+**可选配置项**:
+
+| 变量名 | 默认值 | 说明 |
+|-------|-------|------|
+| `AI_API_BASE` | (自动) | 自定义 API 地址(如 OneAPI、本地模型) |
+| `AI_TEMPERATURE` | `1.0` | 采样温度(0-2,越高越随机) |
+| `AI_MAX_TOKENS` | `5000` | 最大生成 token 数 |
+| `AI_TIMEOUT` | `120` | 请求超时时间(秒) |
+| `AI_NUM_RETRIES` | `2` | 失败重试次数 |
 
 
 #### 进阶玩法:AI 翻译
 #### 进阶玩法:AI 翻译
 
 

+ 62 - 33
config/ai_analysis_prompt.txt

@@ -19,60 +19,89 @@
 # ═══════════════════════════════════════════════════════════════
 # ═══════════════════════════════════════════════════════════════
 
 
 [system]
 [system]
-你是一位专业的新闻分析师和趋势观察者。你的任务是分析热点新闻数据,提供有价值的洞察。
+你是一名**高级情报分析师**。你的核心能力是从海量、碎片化的公开来源情报(OSINT)中提炼核心逻辑,并识别被大众忽略的**弱信号**。
+
+## 核心思维模型 (Mental Models)
+
+1. **见微知著 (Signal Detection)**:不要只盯着榜首的大新闻。要善于从"排名第50的冷门技术贴"与"排名第1的热门事件"中找到潜在的因果联系。
+2. **交叉验证 (Triangulation)**:利用"热榜"(大众情绪)与"RSS"(专家视角)的差异。当两者观点冲突时,通常隐藏着认知套利的机会。
+3. **反直觉思考 (Counter-Intuitive)**:当全网都在叫好时,寻找风险;当全网都在恐慌时,寻找机会。拒绝平庸的共识。
+4. **结构化输出 (MECE)**:确保分析维度相互独立且完全穷尽,避免逻辑混乱。
 
 
 ## 核心原则
 ## 核心原则
 
 
-1. 直击要害:避免废话,直接说"是什么"、"有多火"、"要注意什么"。
-2. 逻辑闭环:将"现象"、"原因"与"建议"打通,告诉读者信息背后的行动指南。
-3. 观点鲜明:明确指出是"泡沫"还是"机遇",是"争议"还是"共识"。
-4. 通俗易懂:使用大众能理解的词汇(如"过热"、"降温"、"反转"、"出圈"),避免生造复杂概念。
-5. 辩证思维:运用矛盾论视角,识别热点背后的"主要矛盾"与"次要矛盾",抓住事物发展的关键内因。
+1. **直击要害**:拒绝"综上所述"、"众所周知"等废话。直接输出结论。
+2. **逻辑闭环**:不仅描述"发生了什么",必须解释"为什么发生"以及"未来会怎样"。
+3. **去情绪化**:可以分析舆论的情绪,但你自己的分析必须冷静、客观、冷血。
+4. **辩证思维**:识别热点背后的"主要矛盾"(如技术变革vs既得利益),抓住事物发展的关键内因。
 
 
 ## 数据字段深度解读指南
 ## 数据字段深度解读指南
 
 
 为了做出精准判断,请充分利用以下数据维度:
 为了做出精准判断,请充分利用以下数据维度:
 
 
 ### 1. 基础维度
 ### 1. 基础维度
-- 排名:"1"为榜首,数字越小越热。"3-8"表示排名在第3到第8之间波动。
-- 出现次数:次数越多,说明在热榜由于停留时间越长,热度越持久。
-- 时间范围:如"09:30~12:45",跨度越大说明话题生命力越强。
+- **来源平台**:每一行新闻开头的 `[平台名称]`(如 `[微博]`、`[知乎]`)明确指出了数据来源。**请务必注意:后续的排名和轨迹数据仅针对该特定平台的榜单**。
+- **排名**:"1"为该平台榜首,数字越小越热。"3-8"表示在该平台排名在第3到第8之间波动。
+- **出现次数**:次数越多,说明在热榜停留时间越长,热度越持久。
+- **时间范围**:如"09:30~12:45",跨度越大说明话题生命力越强。
 
 
 ### 2. 轨迹量化分析 (重要)
 ### 2. 轨迹量化分析 (重要)
-当数据包含轨迹信息(如 `1(09:30)→0(10:00)→2(10:30)`)时,请关注:
-- 急升/爆发:排名在短时间内大幅上升(如从20名升至3名),往往意味着重大突发事件。
-- 僵尸热搜:排名持续阴跌且无反弹(如 10→15→20),说明热度正在衰退。
-- 回榜/反转:脱榜(显示为0)后又重回高位,通常意味着有新爆料或反转剧情。
+数据格式为 `排名(时间)→排名(时间)...`,例如 `1(09:30)→0(10:00)→2(10:30)`。
+
+**关键定义**:
+- **数值含义**:数字代表排名(1为榜首,数字越小越靠前)。**`0` 特指"未上榜"或"脱榜"**(即该时间点不在榜单中)。
+- **符号含义**:`→` 代表时间推移。
+
+**防幻觉警示(关键)**:
+- **高位横盘 ≠ 急升**:如果轨迹是 `2(10:00)→2(10:30)→2(11:00)`,说明热度**持续稳定**,绝对**不是**"急升"或"爆发"。只有排名数值**显著减小**(如 10→5)才是急升。请务必区分"热度高"和"热度升"。
+
+**请重点分析以下模式**:
+- **急升/爆发**:排名数值在短时间内大幅减小(如 20→3),代表热度飙升,往往意味着突发重大事件。
+- **衰退/僵尸**:排名数值持续变大且无反弹(如 10→15→20),代表热度正在自然衰退。
+- **回榜/反转**:序列中出现 `0` 后又变为高排名(如 5→0→2),代表话题曾脱榜但因新进展"复活",通常暗示有新爆料或剧情反转。
 
 
 ### 3. 跨平台特征 (分级标准)
 ### 3. 跨平台特征 (分级标准)
-- 全网霸屏:5 个及以上平台同时上榜。真正的“国民级”话题,无死角覆盖。
-- 破圈扩散:3-4 个平台同时上榜。话题已突破单一社区壁垒,正在向外蔓延。
-- 圈层热点:仅在 1-2 个平台火爆。属于特定人群的狂欢(如仅在技术社区或娱乐榜)。
+- **全网霸屏**:5 个及以上平台同时上榜。真正的“国民级”话题,无死角覆盖。
+- **破圈扩散**:3-4 个平台同时上榜。话题已突破单一社区壁垒,正在向外蔓延。
+- **圈层热点**:仅在 1-2 个平台火爆。属于特定人群的狂欢。
+
+**平台调性参考 (Platform DNA)**:
+- **舆论/情绪场**:微博(情绪/吃瓜)、抖音/快手(视觉/传播快)、B站(年轻/玩梗)。
+- **理性/专业场**:知乎(深度/批判)、雪球(投资/财经)、IT之家/36氪(科技/商业)。
+- **资讯/分发场**:今日头条(社会/民生)、百度热搜(综合/搜索量)。
+*分析"平台温差"时,请结合平台调性。例如:某话题在微博火但在知乎冷,可能说明该话题"情绪价值大于逻辑价值"或"缺乏深度讨论点"。*
 
 
 ## 分析板块说明 (5个核心板块)
 ## 分析板块说明 (5个核心板块)
 
 
 1. 核心热点态势 (Core Trends & Momentum)
 1. 核心热点态势 (Core Trends & Momentum)
    - 整合:"趋势概述"、"热度走势"、"跨平台关联"。
    - 整合:"趋势概述"、"热度走势"、"跨平台关联"。
-   - 任务:直接定性当前最火的话题。结合排名和跨平台数据,判断是"全网刷屏"还是"圈层热议"。
-   - 写法:避免简单罗列数据,而是总结态势。例如:"某话题霸榜多平台,热度持续超6小时,呈现极速爆发态势。"
+   - 任务:**提炼共性与定性**。不仅要识别最火话题,更要尝试寻找不同新闻背后的**底层逻辑或共性叙事**(如:多条看似无关的新闻共同指向"经济复苏乏力"或"AI应用落地"的大趋势)。
+   - 重点:判断热度性质(全网霸屏vs圈层自嗨)以及话题间的潜在关联。
+   - 写法:拒绝流水账。用"宏观主线+微观佐证"的结构,将散点信息串联成逻辑链条。
 
 
 2. 舆论风向争议 (Sentiment & Controversy)
 2. 舆论风向争议 (Sentiment & Controversy)
-   - 任务:运用矛盾分析法挖掘公众情绪内核。识别舆论场中的"根本对立"(主要矛盾)与"转化趋势",分析主流与非主流观点的博弈
-   - 重点:是否存在观点对立?(如技术乐观派 vs 隐私担忧派)。情绪是正面(期待、兴奋)、负面(愤怒、担忧)还是复杂(调侃、质疑)?
+   - 任务:**绘制情绪光谱**。拒绝简单的"褒/贬"二元对立。要识别"舆论断层"(如:专家担忧风险而大众狂欢,或媒体冷处理而民间热议)
+   - 核心:寻找**观点冲突点**。哪里有争吵,哪里就有价值。识别是"利益之争"(钱包问题)还是"认知之争"(观念问题)。
 
 
 3. 异动与弱信号 (Signals)
 3. 异动与弱信号 (Signals)
-   - 任务:通过"轨迹"和"排名变化"捕捉异常。
-   - 关注:排名骤升的突发事件、首次出现的新鲜话题、或者反直觉的热度波动(如深夜突然高热)。
+   - 任务:捕捉**时间轴(轨迹)**和**空间轴(跨平台)**上的异常波动。拒绝平铺直叙的单点罗列。
+   - 关注:
+     - **跨平台共振**:某话题在A平台爆发后,是否迅速引发B平台关注?(对应"破圈扩散")。
+     - **平台温差**:某话题在微博霸榜但在知乎无人问津(对应"圈层热点")。
+     - **轨迹突变**:排名骤升(急升)、死而不僵(僵尸)、反转复活(回榜)。
 
 
 4. RSS 深度洞察 (RSS Insights)
 4. RSS 深度洞察 (RSS Insights)
-   - 任务:分析 RSS 订阅源中的专业内容,提炼行业动态和深度信息。
-   - 关注:技术博客的前沿观点、行业媒体的独家报道、与热榜话题的关联或差异。
-   - 写法:突出 RSS 内容的"信息增量"——热榜没有但 RSS 有的独特视角或深度分析。
+   - 任务:**寻找信息增量**。RSS 源通常比大众热榜更垂直、更专业。
+   - 策略:
+     - **去重**:果断忽略与热榜大众新闻高度雷同的内容。
+     - **互补**:挖掘热榜未覆盖的**硬核细节**(如技术参数、深度行研)或**长尾话题**。
+     - **前瞻**:识别可能尚未引爆但极具价值的早期行业信号。
 
 
 5. 研判策略建议 (Outlook & Strategy)
 5. 研判策略建议 (Outlook & Strategy)
-   - 整合:"潜在影响"与"建议"。
-   - 任务:形成闭环。基于上述分析,预测后续走向(如"可能会引起监管注意"),并给出具体建议。
-   - 对象:建议可面向投资者、品牌方或普通大众,力求落地。
+   - 任务:**预测与推演**。不仅总结过去,更要预测未来。
+   - 核心:
+     - **后续推演**:预测事件的下一阶段(如:是否会反转?监管是否介入?热度是否可持续?)。
+     - **行动指南**:给出具体、有针对性的建议。**严禁使用"建议持续关注"等无意义的正确的废话**。
 
 
 [user]
 [user]
 请分析以下热点新闻数据:
 请分析以下热点新闻数据:
@@ -98,11 +127,11 @@
 
 
 ```json
 ```json
 {
 {
-  "core_trends": "核心热点态势(200字以内)。语言要像"大白话"一样通俗,但要像"手术刀"一样精准。拒绝学术词汇。严格按以下格式分段(注意换行):\n(一句话直击本质的开场白)\n\n【宏观主线】:\n(用通俗的话概括大势,如:国外巨头忙基建,国内市场炒应用...)\n\n【微观领域】:\n1. (细分点1):(描述)\n2. (细分点2):(描述)",
-  "sentiment_controversy": "舆论风向争议(100字以内)。先定性【整体】是褒是贬,再看【局部】有啥吵头。格式:\n【整体定性】:\n(如:全网都在骂,但也有人在这波流量里赚钱...)\n\n【争议焦点】:\n1. (焦点1):...\n2. (焦点2):...",
-  "signals": "异动与弱信号(100字以内)。按信号类型分点:\n1. 急升信号:...\n2. 异动信号:...\n3. 弱信号:...",
-  "rss_insights": "RSS 深度洞察(100字以内,无RSS数据时填"暂无RSS数据")。突出RSS的信息增量:\n【独家视角】:\n(热榜没有但RSS有的独特观点或深度分析)\n\n【行业动态】:\n(技术博客、行业媒体的前沿信息)",
-  "outlook_strategy": "研判策略建议。分受众群体给出建议:\n1. 投资者:...\n2. 品牌方:...\n3. 公众:..."
+  "core_trends": "核心热点态势(200字以内)。**任务:提炼共性叙事而非简单罗列**。语言要像'大白话'一样通俗,但要像'手术刀'一样精准。格式:\n(一句话开场白,必须使用'全网霸屏'/'破圈扩散'/'圈层热点'等词汇对整体热度定性)\n\n【宏观主线】:\n(挖掘多条新闻背后的底层逻辑或共性,如:'AI应用落地引发的资本狂欢')\n\n【微观领域】:\n1. (细分点1):...\n2. (细分点2):...",
+  "sentiment_controversy": "舆论风向争议(100字以内)。**拒绝和稀泥,直击情绪的'温差'与'断层'**。格式:\n【情绪光谱】:\n(识别'主流声音'与'潜流暗涌'的反差,如:'媒体高歌猛进,民间担忧焦虑')\n\n【核心矛盾】:\n1. (利益/观念冲突):(如:'打工人与资本家的利益对立')\n2. (事实/认知分歧):...",
+  "signals": "异动与弱信号(150字以内)。**必须结合跨平台特征分析,拒绝只列举单个平台的涨跌**。**请勿使用 1. 2. 3. 序号,直接使用【标签】分段**。按以下维度分析(至少覆盖2点):\n【跨平台共振/温差】:(如:'某话题实现全网霸屏,但在技术社区遇冷')\n【轨迹突变】:(如:'某话题在下午16点呈现急升/爆发态势,排名从20直冲第1')\n【弱信号捕捉】:(如:'某小众话题在多平台低位隐现,有起势征兆')",
+  "rss_insights": "RSS 深度洞察(100字以内,无RSS数据时填'暂无RSS数据')。**核心任务:寻找'热榜盲区'**。格式:\n【认知纠偏】:\n(专业视角如何修正大众热搜的误区或盲目)\n\n【硬核增量】:\n(热榜缺失的关键技术参数、行业内幕或深度数据)",
+  "outlook_strategy": "研判策略建议。**拒绝'建议持续关注'等废话,基于'推演'给出行动指南**。格式:\n1. 投资者:(风口捕捉或风险预警)\n2. 品牌方:(流量借势或公关避坑)\n3. 公众:(认知纠偏或生活决策)"
 }
 }
 ```
 ```
 
 

+ 44 - 33
config/config.yaml

@@ -285,7 +285,7 @@ storage:
   formats:
   formats:
     sqlite: true                      # 主存储(必须启用)
     sqlite: true                      # 主存储(必须启用)
     txt: false                        # 是否生成 TXT 快照
     txt: false                        # 是否生成 TXT 快照
-    html: true                       # 是否生成 HTML 报告(⚠️ 邮件推送必须设为 true)
+    html: true                       # 是否生成 HTML 报告(⚠️ 邮件推送或者需要看网页版报告必须设为 true)
 
 
   # 本地存储配置
   # 本地存储配置
   local:
   local:
@@ -319,29 +319,38 @@ storage:
 # 8. AI 模型配置(共享)
 # 8. AI 模型配置(共享)
 #
 #
 # ai_analysis 和 ai_translation 共用此模型配置
 # ai_analysis 和 ai_translation 共用此模型配置
-# 支持 OpenAI、DeepSeek、Google Gemini 等兼容接口
+# 基于 LiteLLM 统一接口,支持 100+ AI 提供商
 # ===============================================================
 # ===============================================================
 ai:
 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
+  # LiteLLM 模型格式: provider/model_name
+  # 示例:
+  #   - deepseek/deepseek-chat (DeepSeek)
+  #   - openai/gpt-4o (OpenAI)
+  #   - gemini/gemini-2.5-flash (Google Gemini)
+  #   - anthropic/claude-3-5-sonnet (Anthropic)
+  #   - ollama/llama3 (本地 Ollama)
+  # 完整列表: https://docs.litellm.ai/docs/providers
+  # 如果你对于看英文文档比较头疼,那么可以点击页面右下角的 【Ask AI】 ,用中文询问怎么配置 
+  
+  model: "deepseek/deepseek-chat"
 
 
-  timeout: 90                       # 请求超时(秒)
+  api_key: ""                       # API Key(建议使用环境变量 AI_API_KEY)
+  
+  api_base: ""                      # 自定义 API 端点(可选,大多数情况留空)
+                                    # 示例: https://api.openai.com/v1(自建代理或兼容接口)
+                                    #
+                                    # 💡 高级用法:连接任意兼容 OpenAI 协议的模型商
+                                    # 如果你使用的模型商不在上述支持列表中,但提供了兼容 OpenAI 的接口:
+                                    #
+                                    # 1. api_base 填写: 服务商提供的接口地址
+                                    #    例如: https://api.example.com/v1
+                                    #
+                                    # 2. model 填写: "openai/" + 实际模型名称
+                                    #    例如: openai/deepseek-ai/DeepSeek-V3
+                                    #    (原理:前缀 openai/ 强制 LiteLLM 使用 OpenAI 协议格式进行通信)
+
+
+  timeout: 120                      # 请求超时(秒)
 
 
   # AI 参数配置
   # AI 参数配置
   temperature: 1.0                  # 采样温度 (0.0-2.0)
   temperature: 1.0                  # 采样温度 (0.0-2.0)
@@ -350,23 +359,25 @@ ai:
   max_tokens: 5000                  # 最大生成 token 数
   max_tokens: 5000                  # 最大生成 token 数
                                     # 注意:如果 API 不支持此参数(报 HTTP 400),请设为 0 以禁用发送
                                     # 注意:如果 API 不支持此参数(报 HTTP 400),请设为 0 以禁用发送
 
 
+  # 高级选项
+  num_retries: 1                    # 失败重试次数
+  fallback_models: []               # 备用模型列表(可选)
+                                    # 示例: ["openai/gpt-4o-mini", "openai/deepseek-ai/DeepSeek-V3"]
+
   # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-  # 额外自定义参数 (高级选项)
+  # 额外参数 (高级选项,一般无需修改)
   # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-  # 说明:用于向 AI 传递模型特定的高级生成参数。
-  # ⚠️ 警告:如果你不了解这些参数的含义,强烈建议【不要改动】,保持当前的注释状态。
-  #          填写了不符合模型要求的参数会导致 AI 分析报错并停止工作。
-  #
-  # 提示:不仅限于下方的示例,你可以根据模型 API 文档自行添加任何支持的字段。
+  # LiteLLM 会自动将通用参数转换为各提供商格式,无需手动适配。
+  # 仅在需要传递特殊参数时启用此项。
   #
   #
-  # 操作:如果你确定需要修改,请删掉该行最前方的 "# " (井号和空格)。
-  # 注意:如果这几行都带着井号,则代表不使用额外参数(最推荐做法)。
+  # 提示:你可以根据模型 API 文档自行添加任何支持的字段。
+  # 操作:如需启用,请删掉该行最前方的 "# "(井号和空格)。
+  # 注意:如果这几行都带着井号,则代表不使用额外参数(推荐做法)。
   # -------------------------------------------------------------
   # -------------------------------------------------------------
   # extra_params:
   # extra_params:
-  #   top_p: 1.0            # [通用] 核采样:值越小生成结果越集中
-  #   topK: 40              # [Gemini 专用] 限制候选词数量
-  #   presence_penalty: 0.0 # [OpenAI 专用] 鼓励模型谈论新话题
-  #   # 你也可以在此继续添加模型支持的其他新字段,例如 stop, logit_bias 等
+  #   top_p: 1.0              # 核采样(通用)
+  #   presence_penalty: 0.0   # 话题多样性(OpenAI/DeepSeek)
+  #   stop: ["END"]           # 停止词列表(通用)
 
 
 
 
 # ===============================================================
 # ===============================================================

+ 5 - 6
docker/.env

@@ -64,12 +64,11 @@ GENERIC_WEBHOOK_TEMPLATE=
 AI_ANALYSIS_ENABLED=false
 AI_ANALYSIS_ENABLED=false
 # AI API Key(必填,启用 AI 功能时需要)
 # AI API Key(必填,启用 AI 功能时需要)
 AI_API_KEY=
 AI_API_KEY=
-# AI 提供商 (deepseek|openai|gemini|custom)
-AI_PROVIDER=deepseek
-# 模型名称
-AI_MODEL=deepseek-chat
-# 自定义 API 端点(使用 custom 提供商时必填)
-AI_BASE_URL=
+# 模型名称(LiteLLM 格式: provider/model_name)
+# 示例: deepseek/deepseek-chat, openai/gpt-4o, gemini/gemini-2.5-flash
+AI_MODEL=deepseek/deepseek-chat
+# 自定义 API 端点(可选,大多数情况留空)
+AI_API_BASE=
 
 
 # ============================================
 # ============================================
 # 远程存储配置(S3 兼容协议,支持 R2/OSS/COS/S3 等)
 # 远程存储配置(S3 兼容协议,支持 R2/OSS/COS/S3 等)

+ 1 - 2
docker/docker-compose-build.yml

@@ -45,9 +45,8 @@ services:
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
       - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
       - AI_API_KEY=${AI_API_KEY:-}
       - AI_API_KEY=${AI_API_KEY:-}
-      - AI_PROVIDER=${AI_PROVIDER:-}
       - AI_MODEL=${AI_MODEL:-}
       - AI_MODEL=${AI_MODEL:-}
-      - AI_BASE_URL=${AI_BASE_URL:-}
+      - AI_API_BASE=${AI_API_BASE:-}
       # 远程存储配置(S3 兼容协议)
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 1 - 2
docker/docker-compose.yml

@@ -43,9 +43,8 @@ services:
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
       - AI_ANALYSIS_ENABLED=${AI_ANALYSIS_ENABLED:-false}
       - AI_API_KEY=${AI_API_KEY:-}
       - AI_API_KEY=${AI_API_KEY:-}
-      - AI_PROVIDER=${AI_PROVIDER:-}
       - AI_MODEL=${AI_MODEL:-}
       - AI_MODEL=${AI_MODEL:-}
-      - AI_BASE_URL=${AI_BASE_URL:-}
+      - AI_API_BASE=${AI_API_BASE:-}
       # 远程存储配置(S3 兼容协议)
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 2 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 [project]
 name = "trendradar"
 name = "trendradar"
-version = "5.2.0"
+version = "5.3.0"
 description = "TrendRadar - 热点新闻聚合与分析工具"
 description = "TrendRadar - 热点新闻聚合与分析工具"
 requires-python = ">=3.10"
 requires-python = ">=3.10"
 dependencies = [
 dependencies = [
@@ -11,6 +11,7 @@ dependencies = [
     "websockets>=13.0,<14.0",
     "websockets>=13.0,<14.0",
     "feedparser>=6.0.0,<7.0.0",
     "feedparser>=6.0.0,<7.0.0",
     "boto3>=1.35.0,<2.0.0",
     "boto3>=1.35.0,<2.0.0",
+    "litellm>=1.57.0,<2.0.0",
 ]
 ]
 
 
 [project.scripts]
 [project.scripts]

+ 1 - 0
requirements.txt

@@ -5,3 +5,4 @@ fastmcp>=2.12.0,<2.14.0
 websockets>=13.0,<14.0
 websockets>=13.0,<14.0
 boto3>=1.35.0,<2.0.0
 boto3>=1.35.0,<2.0.0
 feedparser>=6.0.0,<7.0.0
 feedparser>=6.0.0,<7.0.0
+litellm>=1.57.0,<2.0.0

+ 1 - 1
trendradar/__init__.py

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

+ 27 - 157
trendradar/ai/analyzer.py

@@ -3,15 +3,16 @@
 AI 分析器模块
 AI 分析器模块
 
 
 调用 AI 大模型对热点新闻进行深度分析
 调用 AI 大模型对热点新闻进行深度分析
-支持 OpenAI、Google Gemini、Azure OpenAI 等兼容接口
+基于 LiteLLM 统一接口,支持 100+ AI 提供商
 """
 """
 
 
 import json
 import json
-import os
 from dataclasses import dataclass
 from dataclasses import dataclass
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional
 from typing import Any, Callable, Dict, List, Optional
 
 
+from trendradar.ai.client import AIClient
+
 
 
 @dataclass
 @dataclass
 class AIAnalysisResult:
 class AIAnalysisResult:
@@ -50,7 +51,7 @@ class AIAnalyzer:
         初始化 AI 分析器
         初始化 AI 分析器
 
 
         Args:
         Args:
-            ai_config: AI 模型共享配置(provider, api_key, model 等
+            ai_config: AI 模型配置(LiteLLM 格式
             analysis_config: AI 分析功能配置(language, prompt_file 等)
             analysis_config: AI 分析功能配置(language, prompt_file 等)
             get_time_func: 获取当前时间的函数
             get_time_func: 获取当前时间的函数
             debug: 是否开启调试模式
             debug: 是否开启调试模式
@@ -60,14 +61,13 @@ class AIAnalyzer:
         self.get_time_func = get_time_func
         self.get_time_func = get_time_func
         self.debug = debug
         self.debug = debug
 
 
-        # 从共享配置获取模型参数
-        self.api_key = ai_config.get("API_KEY") or os.environ.get("AI_API_KEY", "")
-        self.provider = ai_config.get("PROVIDER", "deepseek")
-        self.model = ai_config.get("MODEL", "deepseek-chat")
-        self.base_url = ai_config.get("BASE_URL", "")
-        self.timeout = ai_config.get("TIMEOUT", 90)
-        self.temperature = ai_config.get("TEMPERATURE", 1.0)
-        self.max_tokens = ai_config.get("MAX_TOKENS", 5000)
+        # 创建 AI 客户端(基于 LiteLLM)
+        self.client = AIClient(ai_config)
+
+        # 验证配置
+        valid, error = self.client.validate_config()
+        if not valid:
+            print(f"[AI] 配置警告: {error}")
 
 
         # 从分析配置获取功能参数
         # 从分析配置获取功能参数
         self.max_news = analysis_config.get("MAX_NEWS_FOR_ANALYSIS", 50)
         self.max_news = analysis_config.get("MAX_NEWS_FOR_ANALYSIS", 50)
@@ -75,18 +75,6 @@ class AIAnalyzer:
         self.include_rank_timeline = analysis_config.get("INCLUDE_RANK_TIMELINE", False)
         self.include_rank_timeline = analysis_config.get("INCLUDE_RANK_TIMELINE", False)
         self.language = analysis_config.get("LANGUAGE", "Chinese")
         self.language = analysis_config.get("LANGUAGE", "Chinese")
 
 
-        # 额外的自定义参数(支持字典或 JSON 字符串)
-        self.extra_params = ai_config.get("EXTRA_PARAMS", {})
-        if isinstance(self.extra_params, str) and self.extra_params.strip():
-            try:
-                self.extra_params = json.loads(self.extra_params)
-            except json.JSONDecodeError:
-                print(f"[AI] 解析 extra_params 失败,将忽略: {self.extra_params}")
-                self.extra_params = {}
-
-        if not isinstance(self.extra_params, dict):
-             self.extra_params = {}
-
         # 加载提示词模板
         # 加载提示词模板
         self.system_prompt, self.user_prompt_template = self._load_prompt_template(
         self.system_prompt, self.user_prompt_template = self._load_prompt_template(
             analysis_config.get("PROMPT_FILE", "ai_analysis_prompt.txt")
             analysis_config.get("PROMPT_FILE", "ai_analysis_prompt.txt")
@@ -146,7 +134,7 @@ class AIAnalyzer:
         Returns:
         Returns:
             AIAnalysisResult: 分析结果
             AIAnalysisResult: 分析结果
         """
         """
-        if not self.api_key:
+        if not self.client.api_key:
             return AIAnalysisResult(
             return AIAnalysisResult(
                 success=False,
                 success=False,
                 error="未配置 AI API Key,请在 config.yaml 或环境变量 AI_API_KEY 中设置"
                 error="未配置 AI API Key,请在 config.yaml 或环境变量 AI_API_KEY 中设置"
@@ -198,9 +186,9 @@ class AIAnalyzer:
             print(user_prompt)
             print(user_prompt)
             print("=" * 80 + "\n")
             print("=" * 80 + "\n")
 
 
-        # 调用 AI API
+        # 调用 AI API(使用 LiteLLM)
         try:
         try:
-            response = self._call_ai_api(user_prompt)
+            response = self._call_ai(user_prompt)
             result = self._parse_response(response)
             result = self._parse_response(response)
 
 
             # 如果配置未启用 RSS 分析,强制清空 AI 返回的 RSS 洞察
             # 如果配置未启用 RSS 分析,强制清空 AI 返回的 RSS 洞察
@@ -215,30 +203,13 @@ class AIAnalyzer:
             result.max_news_limit = self.max_news
             result.max_news_limit = self.max_news
             return result
             return result
         except Exception as e:
         except Exception as e:
-            import requests
             error_type = type(e).__name__
             error_type = type(e).__name__
             error_msg = str(e)
             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}"
+            # 截断过长的错误消息
+            if len(error_msg) > 200:
+                error_msg = error_msg[:200] + "..."
+            friendly_msg = f"AI 分析失败 ({error_type}): {error_msg}"
 
 
             return AIAnalysisResult(
             return AIAnalysisResult(
                 success=False,
                 success=False,
@@ -364,6 +335,15 @@ class AIAnalyzer:
 
 
         return news_content, rss_content, hotlist_total, rss_total, total_count
         return news_content, rss_content, hotlist_total, rss_total, total_count
 
 
+    def _call_ai(self, user_prompt: str) -> str:
+        """调用 AI API(使用 LiteLLM)"""
+        messages = []
+        if self.system_prompt:
+            messages.append({"role": "system", "content": self.system_prompt})
+        messages.append({"role": "user", "content": user_prompt})
+
+        return self.client.chat(messages)
+
     def _format_time_range(self, first_time: str, last_time: str) -> str:
     def _format_time_range(self, first_time: str, last_time: str) -> str:
         """格式化时间范围(简化显示,只保留时分)"""
         """格式化时间范围(简化显示,只保留时分)"""
         def extract_time(time_str: str) -> str:
         def extract_time(time_str: str) -> str:
@@ -409,116 +389,6 @@ class AIAnalyzer:
 
 
         return "→".join(parts)
         return "→".join(parts)
 
 
-    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": self.temperature,
-        }
-
-        # 某些 API 不支持 max_tokens
-        if self.max_tokens:
-            payload["max_tokens"] = self.max_tokens
-
-        if self.extra_params:
-            payload.update(self.extra_params)
-
-        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
-
-        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",
-        }
-
-        payload = {
-            "contents": [{
-                "role": "user",
-                "parts": [{"text": user_prompt}]
-            }],
-            "generationConfig": {
-                "temperature": self.temperature,
-            },
-            "safetySettings": [
-                {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
-            ]
-        }
-
-        if self.system_prompt:
-            payload["system_instruction"] = {
-                "parts": [{"text": self.system_prompt}]
-            }
-
-        if self.max_tokens:
-            payload["generationConfig"]["maxOutputTokens"] = self.max_tokens
-
-        if self.extra_params:
-            payload["generationConfig"].update(self.extra_params)
-
-        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:
     def _parse_response(self, response: str) -> AIAnalysisResult:
         """解析 AI 响应"""
         """解析 AI 响应"""
         result = AIAnalysisResult(raw_response=response)
         result = AIAnalysisResult(raw_response=response)

+ 114 - 0
trendradar/ai/client.py

@@ -0,0 +1,114 @@
+# coding=utf-8
+"""
+AI 客户端模块
+
+基于 LiteLLM 的统一 AI 模型接口
+支持 100+ AI 提供商(OpenAI、DeepSeek、Gemini、Claude、国内模型等)
+"""
+
+import os
+from typing import Any, Dict, List, Optional
+
+from litellm import completion
+
+
+class AIClient:
+    """统一的 AI 客户端(基于 LiteLLM)"""
+
+    def __init__(self, config: Dict[str, Any]):
+        """
+        初始化 AI 客户端
+
+        Args:
+            config: AI 配置字典
+                - MODEL: 模型标识(格式: provider/model_name)
+                - API_KEY: API 密钥
+                - API_BASE: API 基础 URL(可选)
+                - TEMPERATURE: 采样温度
+                - MAX_TOKENS: 最大生成 token 数
+                - TIMEOUT: 请求超时时间(秒)
+                - NUM_RETRIES: 重试次数(可选)
+                - FALLBACK_MODELS: 备用模型列表(可选)
+        """
+        self.model = config.get("MODEL", "deepseek/deepseek-chat")
+        self.api_key = config.get("API_KEY") or os.environ.get("AI_API_KEY", "")
+        self.api_base = config.get("API_BASE", "")
+        self.temperature = config.get("TEMPERATURE", 1.0)
+        self.max_tokens = config.get("MAX_TOKENS", 5000)
+        self.timeout = config.get("TIMEOUT", 120)
+        self.num_retries = config.get("NUM_RETRIES", 2)
+        self.fallback_models = config.get("FALLBACK_MODELS", [])
+
+    def chat(
+        self,
+        messages: List[Dict[str, str]],
+        **kwargs
+    ) -> str:
+        """
+        调用 AI 模型进行对话
+
+        Args:
+            messages: 消息列表,格式: [{"role": "system/user/assistant", "content": "..."}]
+            **kwargs: 额外参数,会覆盖默认配置
+
+        Returns:
+            str: AI 响应内容
+
+        Raises:
+            Exception: API 调用失败时抛出异常
+        """
+        # 构建请求参数
+        params = {
+            "model": self.model,
+            "messages": messages,
+            "temperature": kwargs.get("temperature", self.temperature),
+            "timeout": kwargs.get("timeout", self.timeout),
+            "num_retries": kwargs.get("num_retries", self.num_retries),
+        }
+
+        # 添加 API Key
+        if self.api_key:
+            params["api_key"] = self.api_key
+
+        # 添加 API Base(如果配置了)
+        if self.api_base:
+            params["api_base"] = self.api_base
+
+        # 添加 max_tokens(如果配置了且不为 0)
+        max_tokens = kwargs.get("max_tokens", self.max_tokens)
+        if max_tokens and max_tokens > 0:
+            params["max_tokens"] = max_tokens
+
+        # 添加 fallback 模型(如果配置了)
+        if self.fallback_models:
+            params["fallbacks"] = self.fallback_models
+
+        # 合并其他额外参数
+        for key, value in kwargs.items():
+            if key not in params:
+                params[key] = value
+
+        # 调用 LiteLLM
+        response = completion(**params)
+
+        # 提取响应内容
+        return response.choices[0].message.content
+
+    def validate_config(self) -> tuple[bool, str]:
+        """
+        验证配置是否有效
+
+        Returns:
+            tuple: (是否有效, 错误信息)
+        """
+        if not self.model:
+            return False, "未配置 AI 模型(model)"
+
+        if not self.api_key:
+            return False, "未配置 AI API Key,请在 config.yaml 或环境变量 AI_API_KEY 中设置"
+
+        # 验证模型格式(应该包含 provider/model)
+        if "/" not in self.model:
+            return False, f"模型格式错误: {self.model},应为 'provider/model' 格式(如 'deepseek/deepseek-chat')"
+
+        return True, ""

+ 16 - 149
trendradar/ai/translator.py

@@ -3,15 +3,16 @@
 AI 翻译器模块
 AI 翻译器模块
 
 
 对推送内容进行多语言翻译
 对推送内容进行多语言翻译
-使用共享的 AI 模型配置
+基于 LiteLLM 统一接口,支持 100+ AI 提供商
 """
 """
 
 
 import json
 import json
-import os
 from dataclasses import dataclass, field
 from dataclasses import dataclass, field
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 from typing import Any, Dict, List, Optional
 
 
+from trendradar.ai.client import AIClient
+
 
 
 @dataclass
 @dataclass
 class TranslationResult:
 class TranslationResult:
@@ -40,7 +41,7 @@ class AITranslator:
 
 
         Args:
         Args:
             translation_config: AI 翻译配置 (AI_TRANSLATION)
             translation_config: AI 翻译配置 (AI_TRANSLATION)
-            ai_config: AI 模型共享配置 (AI)
+            ai_config: AI 模型配置(LiteLLM 格式)
         """
         """
         self.translation_config = translation_config
         self.translation_config = translation_config
         self.ai_config = ai_config
         self.ai_config = ai_config
@@ -49,28 +50,8 @@ class AITranslator:
         self.enabled = translation_config.get("ENABLED", False)
         self.enabled = translation_config.get("ENABLED", False)
         self.target_language = translation_config.get("LANGUAGE", "English")
         self.target_language = translation_config.get("LANGUAGE", "English")
 
 
-        # 从共享配置获取模型参数
-        self.api_key = ai_config.get("API_KEY") or os.environ.get("AI_API_KEY", "")
-        self.provider = ai_config.get("PROVIDER", "deepseek")
-        self.model = ai_config.get("MODEL", "deepseek-chat")
-        self.base_url = ai_config.get("BASE_URL", "")
-        self.timeout = ai_config.get("TIMEOUT", 90)
-
-        # AI 参数配置
-        self.temperature = ai_config.get("TEMPERATURE", 1.0)
-        self.max_tokens = ai_config.get("MAX_TOKENS", 5000)
-
-        # 额外参数
-        self.extra_params = ai_config.get("EXTRA_PARAMS", {})
-        if isinstance(self.extra_params, str) and self.extra_params.strip():
-            try:
-                self.extra_params = json.loads(self.extra_params)
-            except json.JSONDecodeError:
-                print(f"[翻译] 解析 extra_params 失败,将忽略: {self.extra_params}")
-                self.extra_params = {}
-
-        if not isinstance(self.extra_params, dict):
-            self.extra_params = {}
+        # 创建 AI 客户端(基于 LiteLLM)
+        self.client = AIClient(ai_config)
 
 
         # 加载提示词模板
         # 加载提示词模板
         self.system_prompt, self.user_prompt_template = self._load_prompt_template(
         self.system_prompt, self.user_prompt_template = self._load_prompt_template(
@@ -122,7 +103,7 @@ class AITranslator:
             result.error = "翻译功能未启用"
             result.error = "翻译功能未启用"
             return result
             return result
 
 
-        if not self.api_key:
+        if not self.client.api_key:
             result.error = "未配置 AI API Key"
             result.error = "未配置 AI API Key"
             return result
             return result
 
 
@@ -138,31 +119,16 @@ class AITranslator:
             user_prompt = user_prompt.replace("{content}", text)
             user_prompt = user_prompt.replace("{content}", text)
 
 
             # 调用 AI API
             # 调用 AI API
-            response = self._call_ai_api(user_prompt)
+            response = self._call_ai(user_prompt)
             result.translated_text = response.strip()
             result.translated_text = response.strip()
             result.success = True
             result.success = True
 
 
         except Exception as e:
         except Exception as e:
-            import requests
             error_type = type(e).__name__
             error_type = type(e).__name__
             error_msg = str(e)
             error_msg = str(e)
-
-            if isinstance(e, requests.exceptions.Timeout):
-                result.error = f"翻译请求超时({self.timeout}秒)"
-            elif isinstance(e, requests.exceptions.ConnectionError):
-                result.error = f"无法连接到 AI 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:
-                    result.error = "API 认证失败"
-                elif status_code == 429:
-                    result.error = "API 请求频率过高"
-                else:
-                    result.error = f"API 错误 (HTTP {status_code})"
-            else:
-                if len(error_msg) > 100:
-                    error_msg = error_msg[:100] + "..."
-                result.error = f"翻译失败 ({error_type}): {error_msg}"
+            if len(error_msg) > 100:
+                error_msg = error_msg[:100] + "..."
+            result.error = f"翻译失败 ({error_type}): {error_msg}"
 
 
         return result
         return result
 
 
@@ -187,7 +153,7 @@ class AITranslator:
             batch_result.fail_count = len(texts)
             batch_result.fail_count = len(texts)
             return batch_result
             return batch_result
 
 
-        if not self.api_key:
+        if not self.client.api_key:
             for text in texts:
             for text in texts:
                 batch_result.results.append(TranslationResult(
                 batch_result.results.append(TranslationResult(
                     original_text=text,
                     original_text=text,
@@ -231,7 +197,7 @@ class AITranslator:
             user_prompt = user_prompt.replace("{content}", batch_content)
             user_prompt = user_prompt.replace("{content}", batch_content)
 
 
             # 调用 AI API
             # 调用 AI API
-            response = self._call_ai_api(user_prompt)
+            response = self._call_ai(user_prompt)
 
 
             # 解析批量翻译结果
             # 解析批量翻译结果
             translated_texts = self._parse_batch_response(response, len(non_empty_texts))
             translated_texts = self._parse_batch_response(response, len(non_empty_texts))
@@ -319,110 +285,11 @@ class AITranslator:
 
 
         return translated[:expected_count]
         return translated[:expected_count]
 
 
-    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")
-        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",
-        }
-
+    def _call_ai(self, user_prompt: str) -> str:
+        """调用 AI API(使用 LiteLLM)"""
         messages = []
         messages = []
         if self.system_prompt:
         if self.system_prompt:
             messages.append({"role": "system", "content": self.system_prompt})
             messages.append({"role": "system", "content": self.system_prompt})
         messages.append({"role": "user", "content": user_prompt})
         messages.append({"role": "user", "content": user_prompt})
 
 
-        payload = {
-            "model": self.model,
-            "messages": messages,
-            "temperature": self.temperature,
-        }
-
-        if self.max_tokens:
-            payload["max_tokens"] = self.max_tokens
-
-        if self.extra_params:
-            payload.update(self.extra_params)
-
-        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
-
-        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",
-        }
-
-        payload = {
-            "contents": [{
-                "role": "user",
-                "parts": [{"text": user_prompt}]
-            }],
-            "generationConfig": {
-                "temperature": self.temperature,
-            },
-            "safetySettings": [
-                {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
-                {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
-            ]
-        }
-
-        if self.system_prompt:
-            payload["system_instruction"] = {
-                "parts": [{"text": self.system_prompt}]
-            }
-
-        if self.max_tokens:
-            payload["generationConfig"]["maxOutputTokens"] = self.max_tokens
-
-        if self.extra_params:
-            payload["generationConfig"].update(self.extra_params)
-
-        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"]
+        return self.client.chat(messages)

+ 11 - 5
trendradar/core/loader.py

@@ -217,19 +217,25 @@ def _load_display_config(config_data: Dict) -> Dict:
 
 
 
 
 def _load_ai_config(config_data: Dict) -> Dict:
 def _load_ai_config(config_data: Dict) -> Dict:
-    """加载 AI 模型共享配置"""
+    """加载 AI 模型配置(LiteLLM 格式)"""
     ai_config = config_data.get("ai", {})
     ai_config = config_data.get("ai", {})
 
 
     timeout_env = _get_env_int_or_none("AI_TIMEOUT")
     timeout_env = _get_env_int_or_none("AI_TIMEOUT")
 
 
     return {
     return {
-        "PROVIDER": _get_env_str("AI_PROVIDER") or ai_config.get("provider", "deepseek"),
+        # LiteLLM 核心配置
+        "MODEL": _get_env_str("AI_MODEL") or ai_config.get("model", "deepseek/deepseek-chat"),
         "API_KEY": _get_env_str("AI_API_KEY") or ai_config.get("api_key", ""),
         "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),
+        "API_BASE": _get_env_str("AI_API_BASE") or ai_config.get("api_base", ""),
+
+        # 生成参数
+        "TIMEOUT": timeout_env if timeout_env is not None else ai_config.get("timeout", 120),
         "TEMPERATURE": ai_config.get("temperature", 1.0),
         "TEMPERATURE": ai_config.get("temperature", 1.0),
         "MAX_TOKENS": ai_config.get("max_tokens", 5000),
         "MAX_TOKENS": ai_config.get("max_tokens", 5000),
+
+        # LiteLLM 高级选项
+        "NUM_RETRIES": ai_config.get("num_retries", 2),
+        "FALLBACK_MODELS": ai_config.get("fallback_models", []),
         "EXTRA_PARAMS": ai_config.get("extra_params", {}),
         "EXTRA_PARAMS": ai_config.get("extra_params", {}),
     }
     }
 
 

+ 1 - 1
version

@@ -1 +1 @@
-5.2.0
+5.3.0