Explorar el Código

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

sansan hace 3 meses
padre
commit
b41e43580c

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

@@ -157,9 +157,8 @@ jobs:
           # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
           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 }}
+          AI_API_BASE: ${{ secrets.AI_API_BASE }}
           # 远程存储配置
           S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
           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 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-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)
 [![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)**:
 - **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
 
 > 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_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
 

+ 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 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-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)
 [![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#-更新日志)** :
 - **提示**:建议查看【历史更新】,明确具体的【功能内容】
 
+### 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
 
 > 主要见 config.yaml 描述
@@ -3227,19 +3245,32 @@ app:
 |-------|-------|------|
 | `AI_ANALYSIS_ENABLED` | `true` | 开启开关 |
 | `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 翻译
 

+ 62 - 33
config/ai_analysis_prompt.txt

@@ -19,60 +19,89 @@
 # ═══════════════════════════════════════════════════════════════
 
 [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"为榜首,数字越小越热。"3-8"表示排名在第3到第8之间波动。
-- 出现次数:次数越多,说明在热榜由于停留时间越长,热度越持久。
-- 时间范围:如"09:30~12:45",跨度越大说明话题生命力越强。
+- **来源平台**:每一行新闻开头的 `[平台名称]`(如 `[微博]`、`[知乎]`)明确指出了数据来源。**请务必注意:后续的排名和轨迹数据仅针对该特定平台的榜单**。
+- **排名**:"1"为该平台榜首,数字越小越热。"3-8"表示在该平台排名在第3到第8之间波动。
+- **出现次数**:次数越多,说明在热榜停留时间越长,热度越持久。
+- **时间范围**:如"09:30~12:45",跨度越大说明话题生命力越强。
 
 ### 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. 跨平台特征 (分级标准)
-- 全网霸屏:5 个及以上平台同时上榜。真正的“国民级”话题,无死角覆盖。
-- 破圈扩散:3-4 个平台同时上榜。话题已突破单一社区壁垒,正在向外蔓延。
-- 圈层热点:仅在 1-2 个平台火爆。属于特定人群的狂欢(如仅在技术社区或娱乐榜)。
+- **全网霸屏**:5 个及以上平台同时上榜。真正的“国民级”话题,无死角覆盖。
+- **破圈扩散**:3-4 个平台同时上榜。话题已突破单一社区壁垒,正在向外蔓延。
+- **圈层热点**:仅在 1-2 个平台火爆。属于特定人群的狂欢。
+
+**平台调性参考 (Platform DNA)**:
+- **舆论/情绪场**:微博(情绪/吃瓜)、抖音/快手(视觉/传播快)、B站(年轻/玩梗)。
+- **理性/专业场**:知乎(深度/批判)、雪球(投资/财经)、IT之家/36氪(科技/商业)。
+- **资讯/分发场**:今日头条(社会/民生)、百度热搜(综合/搜索量)。
+*分析"平台温差"时,请结合平台调性。例如:某话题在微博火但在知乎冷,可能说明该话题"情绪价值大于逻辑价值"或"缺乏深度讨论点"。*
 
 ## 分析板块说明 (5个核心板块)
 
 1. 核心热点态势 (Core Trends & Momentum)
    - 整合:"趋势概述"、"热度走势"、"跨平台关联"。
-   - 任务:直接定性当前最火的话题。结合排名和跨平台数据,判断是"全网刷屏"还是"圈层热议"。
-   - 写法:避免简单罗列数据,而是总结态势。例如:"某话题霸榜多平台,热度持续超6小时,呈现极速爆发态势。"
+   - 任务:**提炼共性与定性**。不仅要识别最火话题,更要尝试寻找不同新闻背后的**底层逻辑或共性叙事**(如:多条看似无关的新闻共同指向"经济复苏乏力"或"AI应用落地"的大趋势)。
+   - 重点:判断热度性质(全网霸屏vs圈层自嗨)以及话题间的潜在关联。
+   - 写法:拒绝流水账。用"宏观主线+微观佐证"的结构,将散点信息串联成逻辑链条。
 
 2. 舆论风向争议 (Sentiment & Controversy)
-   - 任务:运用矛盾分析法挖掘公众情绪内核。识别舆论场中的"根本对立"(主要矛盾)与"转化趋势",分析主流与非主流观点的博弈
-   - 重点:是否存在观点对立?(如技术乐观派 vs 隐私担忧派)。情绪是正面(期待、兴奋)、负面(愤怒、担忧)还是复杂(调侃、质疑)?
+   - 任务:**绘制情绪光谱**。拒绝简单的"褒/贬"二元对立。要识别"舆论断层"(如:专家担忧风险而大众狂欢,或媒体冷处理而民间热议)
+   - 核心:寻找**观点冲突点**。哪里有争吵,哪里就有价值。识别是"利益之争"(钱包问题)还是"认知之争"(观念问题)。
 
 3. 异动与弱信号 (Signals)
-   - 任务:通过"轨迹"和"排名变化"捕捉异常。
-   - 关注:排名骤升的突发事件、首次出现的新鲜话题、或者反直觉的热度波动(如深夜突然高热)。
+   - 任务:捕捉**时间轴(轨迹)**和**空间轴(跨平台)**上的异常波动。拒绝平铺直叙的单点罗列。
+   - 关注:
+     - **跨平台共振**:某话题在A平台爆发后,是否迅速引发B平台关注?(对应"破圈扩散")。
+     - **平台温差**:某话题在微博霸榜但在知乎无人问津(对应"圈层热点")。
+     - **轨迹突变**:排名骤升(急升)、死而不僵(僵尸)、反转复活(回榜)。
 
 4. RSS 深度洞察 (RSS Insights)
-   - 任务:分析 RSS 订阅源中的专业内容,提炼行业动态和深度信息。
-   - 关注:技术博客的前沿观点、行业媒体的独家报道、与热榜话题的关联或差异。
-   - 写法:突出 RSS 内容的"信息增量"——热榜没有但 RSS 有的独特视角或深度分析。
+   - 任务:**寻找信息增量**。RSS 源通常比大众热榜更垂直、更专业。
+   - 策略:
+     - **去重**:果断忽略与热榜大众新闻高度雷同的内容。
+     - **互补**:挖掘热榜未覆盖的**硬核细节**(如技术参数、深度行研)或**长尾话题**。
+     - **前瞻**:识别可能尚未引爆但极具价值的早期行业信号。
 
 5. 研判策略建议 (Outlook & Strategy)
-   - 整合:"潜在影响"与"建议"。
-   - 任务:形成闭环。基于上述分析,预测后续走向(如"可能会引起监管注意"),并给出具体建议。
-   - 对象:建议可面向投资者、品牌方或普通大众,力求落地。
+   - 任务:**预测与推演**。不仅总结过去,更要预测未来。
+   - 核心:
+     - **后续推演**:预测事件的下一阶段(如:是否会反转?监管是否介入?热度是否可持续?)。
+     - **行动指南**:给出具体、有针对性的建议。**严禁使用"建议持续关注"等无意义的正确的废话**。
 
 [user]
 请分析以下热点新闻数据:
@@ -98,11 +127,11 @@
 
 ```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:
     sqlite: true                      # 主存储(必须启用)
     txt: false                        # 是否生成 TXT 快照
-    html: true                       # 是否生成 HTML 报告(⚠️ 邮件推送必须设为 true)
+    html: true                       # 是否生成 HTML 报告(⚠️ 邮件推送或者需要看网页版报告必须设为 true)
 
   # 本地存储配置
   local:
@@ -319,29 +319,38 @@ storage:
 # 8. AI 模型配置(共享)
 #
 # ai_analysis 和 ai_translation 共用此模型配置
-# 支持 OpenAI、DeepSeek、Google Gemini 等兼容接口
+# 基于 LiteLLM 统一接口,支持 100+ 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 参数配置
   temperature: 1.0                  # 采样温度 (0.0-2.0)
@@ -350,23 +359,25 @@ ai:
   max_tokens: 5000                  # 最大生成 token 数
                                     # 注意:如果 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:
-  #   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 API Key(必填,启用 AI 功能时需要)
 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 等)

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

@@ -45,9 +45,8 @@ services:
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       - 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:-}
+      - AI_API_BASE=${AI_API_BASE:-}
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 1 - 2
docker/docker-compose.yml

@@ -43,9 +43,8 @@ services:
       # AI 配置(ai_analysis 和 ai_translation 共享模型配置)
       - 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:-}
+      - AI_API_BASE=${AI_API_BASE:-}
       # 远程存储配置(S3 兼容协议)
       - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
       - S3_BUCKET_NAME=${S3_BUCKET_NAME:-}

+ 2 - 1
pyproject.toml

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

+ 1 - 0
requirements.txt

@@ -5,3 +5,4 @@ fastmcp>=2.12.0,<2.14.0
 websockets>=13.0,<14.0
 boto3>=1.35.0,<2.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
 
-__version__ = "5.2.0"
+__version__ = "5.3.0"
 __all__ = ["AppContext", "__version__"]

+ 27 - 157
trendradar/ai/analyzer.py

@@ -3,15 +3,16 @@
 AI 分析器模块
 
 调用 AI 大模型对热点新闻进行深度分析
-支持 OpenAI、Google Gemini、Azure OpenAI 等兼容接口
+基于 LiteLLM 统一接口,支持 100+ AI 提供商
 """
 
 import json
-import os
 from dataclasses import dataclass
 from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional
 
+from trendradar.ai.client import AIClient
+
 
 @dataclass
 class AIAnalysisResult:
@@ -50,7 +51,7 @@ class AIAnalyzer:
         初始化 AI 分析器
 
         Args:
-            ai_config: AI 模型共享配置(provider, api_key, model 等
+            ai_config: AI 模型配置(LiteLLM 格式
             analysis_config: AI 分析功能配置(language, prompt_file 等)
             get_time_func: 获取当前时间的函数
             debug: 是否开启调试模式
@@ -60,14 +61,13 @@ class AIAnalyzer:
         self.get_time_func = get_time_func
         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)
@@ -75,18 +75,6 @@ class AIAnalyzer:
         self.include_rank_timeline = analysis_config.get("INCLUDE_RANK_TIMELINE", False)
         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(
             analysis_config.get("PROMPT_FILE", "ai_analysis_prompt.txt")
@@ -146,7 +134,7 @@ class AIAnalyzer:
         Returns:
             AIAnalysisResult: 分析结果
         """
-        if not self.api_key:
+        if not self.client.api_key:
             return AIAnalysisResult(
                 success=False,
                 error="未配置 AI API Key,请在 config.yaml 或环境变量 AI_API_KEY 中设置"
@@ -198,9 +186,9 @@ class AIAnalyzer:
             print(user_prompt)
             print("=" * 80 + "\n")
 
-        # 调用 AI API
+        # 调用 AI API(使用 LiteLLM)
         try:
-            response = self._call_ai_api(user_prompt)
+            response = self._call_ai(user_prompt)
             result = self._parse_response(response)
 
             # 如果配置未启用 RSS 分析,强制清空 AI 返回的 RSS 洞察
@@ -215,30 +203,13 @@ class AIAnalyzer:
             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}"
+            # 截断过长的错误消息
+            if len(error_msg) > 200:
+                error_msg = error_msg[:200] + "..."
+            friendly_msg = f"AI 分析失败 ({error_type}): {error_msg}"
 
             return AIAnalysisResult(
                 success=False,
@@ -364,6 +335,15 @@ class AIAnalyzer:
 
         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 extract_time(time_str: str) -> str:
@@ -409,116 +389,6 @@ class AIAnalyzer:
 
         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:
         """解析 AI 响应"""
         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 模型配置
+基于 LiteLLM 统一接口,支持 100+ AI 提供商
 """
 
 import json
-import os
 from dataclasses import dataclass, field
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
+from trendradar.ai.client import AIClient
+
 
 @dataclass
 class TranslationResult:
@@ -40,7 +41,7 @@ class AITranslator:
 
         Args:
             translation_config: AI 翻译配置 (AI_TRANSLATION)
-            ai_config: AI 模型共享配置 (AI)
+            ai_config: AI 模型配置(LiteLLM 格式)
         """
         self.translation_config = translation_config
         self.ai_config = ai_config
@@ -49,28 +50,8 @@ class AITranslator:
         self.enabled = translation_config.get("ENABLED", False)
         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(
@@ -122,7 +103,7 @@ class AITranslator:
             result.error = "翻译功能未启用"
             return result
 
-        if not self.api_key:
+        if not self.client.api_key:
             result.error = "未配置 AI API Key"
             return result
 
@@ -138,31 +119,16 @@ class AITranslator:
             user_prompt = user_prompt.replace("{content}", text)
 
             # 调用 AI API
-            response = self._call_ai_api(user_prompt)
+            response = self._call_ai(user_prompt)
             result.translated_text = response.strip()
             result.success = True
 
         except Exception as e:
-            import requests
             error_type = type(e).__name__
             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
 
@@ -187,7 +153,7 @@ class AITranslator:
             batch_result.fail_count = len(texts)
             return batch_result
 
-        if not self.api_key:
+        if not self.client.api_key:
             for text in texts:
                 batch_result.results.append(TranslationResult(
                     original_text=text,
@@ -231,7 +197,7 @@ class AITranslator:
             user_prompt = user_prompt.replace("{content}", batch_content)
 
             # 调用 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))
@@ -319,110 +285,11 @@ class AITranslator:
 
         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 = []
         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,
-        }
-
-        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:
-    """加载 AI 模型共享配置"""
+    """加载 AI 模型配置(LiteLLM 格式)"""
     ai_config = config_data.get("ai", {})
 
     timeout_env = _get_env_int_or_none("AI_TIMEOUT")
 
     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", ""),
-        "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),
         "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", {}),
     }
 

+ 1 - 1
version

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