Explorar el Código

Merge branch 'feature/luojunhui/20260625-test-ark-deepseek' of Server/LongArticleTaskServer into master

luojunhui hace 1 mes
padre
commit
3c5f587c69

+ 3 - 0
app/core/config/global_settings.py

@@ -32,6 +32,9 @@ class GlobalConfigSettings(BaseSettings):
 
     # ============ 外部服务配置 ============
     deepseek: DeepSeekConfig = Field(default_factory=DeepSeekConfig)
+    deepseek_volcengine: DeepSeekVolcengineConfig = Field(
+        default_factory=DeepSeekVolcengineConfig
+    )
 
     aliyun_log: AliyunLogConfig = Field(default_factory=AliyunLogConfig)
     aliyun_oss: AliyunOssConfig = Field(default_factory=AliyunOssConfig)

+ 2 - 0
app/core/config/settings/__init__.py

@@ -4,6 +4,7 @@ from .aliyun import AliyunOssConfig
 from .category import CategoryConfig
 from .cold_start import ColdStartConfig
 from .deepseek import DeepSeekConfig
+from .deepseek_volcengine import DeepSeekVolcengineConfig
 from .elasticsearch import ElasticsearchConfig
 from .mysql import AigcDatabaseConfig
 from .mysql import GrowthDatabaseConfig
@@ -22,6 +23,7 @@ __ALL__ = [
     "CategoryConfig",
     "ColdStartConfig",
     "DeepSeekConfig",
+    "DeepSeekVolcengineConfig",
     "ElasticsearchConfig",
     "AigcDatabaseConfig",
     "GrowthDatabaseConfig",

+ 1 - 1
app/core/config/settings/deepseek.py

@@ -6,7 +6,7 @@ class DeepSeekConfig(BaseSettings):
     """DeepSeek API 配置"""
 
     api_key: str = Field(
-        default="sk-cfd2df92c8864ab999d66a615ee812c5", description="DeepSeek API Key"
+        default="", description="DeepSeek API Key"
     )
     reasoner_model: str = Field(
         default="deepseek-reasoner", description="DeepSeek 推理模型"

+ 38 - 0
app/core/config/settings/deepseek_volcengine.py

@@ -0,0 +1,38 @@
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class DeepSeekVolcengineConfig(BaseSettings):
+    """DeepSeek 火山引擎 Ark API 配置"""
+
+    api_key: str = Field(
+        default="ark-0d5dd17a-35a1-48dc-817a-f2f69c743acb-701ce",
+        description="火山引擎 Ark API Key",
+    )
+    base_url: str = Field(
+        default="https://ark.cn-beijing.volces.com/api/v3",
+        description="火山引擎 Ark API 地址",
+    )
+    pro_model: str = Field(
+        default="ep-20260625202225-w4vkd", description="DeepSeek V4-Pro (火山引擎)"
+    )
+    reasoner_model: str = Field(
+        default="ep-20260625202225-w4vkd", description="DeepSeek 推理模型 (火山引擎)"
+    )
+    chat_model: str = Field(
+        default="ep-20260625202225-w4vkd", description="DeepSeek 对话模型 (火山引擎)"
+    )
+
+    model_config = SettingsConfigDict(
+        env_prefix="DEEPSEEK_VOLCENGINE_",
+        env_file=".env",
+        case_sensitive=False,
+        extra="ignore",
+    )
+
+    def get_model_map(self) -> dict:
+        """获取模型映射字典,兼容旧代码"""
+        return {
+            "DeepSeek-R1": self.reasoner_model,
+            "DeepSeek-V3": self.chat_model,
+        }

+ 7 - 3
app/domains/monitor_tasks/auto_reply_cards_monitor/_mapper.py

@@ -174,9 +174,13 @@ class AutoReplyCardsMonitorMapper(AutoReplyCardsMonitorConst):
         return await self.pool.async_save(
             query=query,
             params=(
-                cover_id, oss_key, cover_status,
-                cover_status, self.CoverStatus.FAILED,
-                task_id, position,
+                cover_id,
+                oss_key,
+                cover_status,
+                cover_status,
+                self.CoverStatus.FAILED,
+                task_id,
+                position,
             ),
         )
 

+ 3 - 1
app/domains/monitor_tasks/auto_reply_cards_monitor/entrance.py

@@ -616,7 +616,9 @@ class AutoReplyCardsMonitor(AutoReplyCardsMonitorConst):
                     if blogger_code == self.BLOGGER_ACCOUNT_BANNED_CODE:
                         alert_title = f"自动回复卡片-账号违规: {account_name}"
                     else:
-                        alert_title = f"自动回复卡片-账号异常(可能已迁移): {account_name}"
+                        alert_title = (
+                            f"自动回复卡片-账号异常(可能已迁移): {account_name}"
+                        )
 
                     await self.mapper.set_account_as_invalid(gh_id)
                     try:

+ 1 - 1
app/infra/external/__init__.py

@@ -1,5 +1,5 @@
 from .aliyun import log
-from .deepseek_official import fetch_deepseek_completion
+from .deepseek_volcengine import fetch_deepseek_completion
 from .apollo import AsyncApolloApi
 from .feishu import FeishuBotApi
 from .feishu import FeishuSheetApi

+ 2 - 2
app/infra/external/deepseek_official.py

@@ -5,7 +5,7 @@
 
 import json
 
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
 from openai import OpenAI
 
 from app.core.config import GlobalConfigSettings
@@ -26,7 +26,7 @@ def fetch_deepseek_completion(
     tools: List[Dict] = None,
 ) -> Optional[Dict | List]:
     messages = [{"role": "user", "content": prompt}]
-    kwargs = {
+    kwargs: dict[str, Any] = {
         "model": model_map.get(model, chat_model),
         "messages": messages,
     }

+ 47 - 0
app/infra/external/deepseek_volcengine.py

@@ -0,0 +1,47 @@
+"""
+@author: luojunhui
+@description: deepseek 火山引擎 Ark API 版本 (Responses API)
+"""
+
+from typing import Any, Dict, List, Optional
+from volcenginesdkarkruntime import Ark
+
+from app.core.config import GlobalConfigSettings
+from app.infra.shared.tools import safe_json_parse
+
+config = GlobalConfigSettings()
+
+model_map = config.deepseek_volcengine.get_model_map()
+api_key = config.deepseek_volcengine.api_key
+base_url = config.deepseek_volcengine.base_url
+chat_model = config.deepseek_volcengine.chat_model
+reasoner_model = config.deepseek_volcengine.reasoner_model
+
+
+def fetch_deepseek_completion(
+    model: str,
+    prompt: str,
+    output_type: str = "text",
+) -> Optional[Dict | List]:
+    input_messages = [{"role": "user", "content": prompt}]
+    kwargs: dict[str, Any] = {
+        "model": model_map.get(model, chat_model),
+        "messages": input_messages,
+    }
+
+    client = Ark(base_url=base_url, api_key=api_key)
+
+    try:
+        response = client.chat.completions.create(**kwargs)
+        output_text = response.choices[0].message.content
+
+        if output_type == "text":
+            return output_text
+        elif output_type == "json":
+            return safe_json_parse(output_text)
+        else:
+            raise ValueError(f"Invalid output_type: {output_type}")
+
+    except Exception as e:
+        print(f"[ERROR] fetch_deepseek_completion (volcengine) failed: {e}")
+        return None

+ 3 - 1
app/infra/internal/aigc_decode_server.py

@@ -12,7 +12,9 @@ class AigcDecodeServer:
             images = post.get("images")
             if images:
                 post["images"] = [
-                    img for img in images if img and img.startswith(("http://", "https://"))
+                    img
+                    for img in images
+                    if img and img.startswith(("http://", "https://"))
                 ]
         return posts
 

+ 80 - 5
app/infra/shared/tools.py

@@ -2,6 +2,7 @@
 @author: luojunhui
 """
 
+import json
 import re
 import oss2
 import random
@@ -13,7 +14,7 @@ from scipy.stats import t
 from odps import ODPS
 
 from datetime import datetime, timezone, date, timedelta
-from typing import List
+from typing import Dict, List, Optional
 
 from requests import RequestException
 from urllib.parse import urlparse, parse_qs
@@ -24,6 +25,80 @@ from tenacity import (
 )
 
 
+def safe_json_parse(text: str) -> Optional[Dict | List]:
+    """多层降级解析 JSON:直接解析 → 提取代码块 → 提取 JSON 对象/数组
+
+    模型有时返回 ```json ... ``` 包裹的文本,或文本中夹杂 markdown 前缀/后缀。
+    先尝试直接解析(最常见路径),失败后逐层降级提取。
+    """
+    if not text:
+        return None
+
+    # 降级 1:直接解析
+    try:
+        return json.loads(text)
+    except (json.JSONDecodeError, TypeError):
+        pass
+
+    clean = text.strip()
+
+    # 降级 2:提取最外层 json 代码块 ```json ... ```
+    # 优先匹配带语言标注的,再退到任意 code fence
+    m = re.search(r"```json\s*(.*?)\s*```", clean, re.DOTALL)
+    if m:
+        try:
+            return json.loads(m.group(1))
+        except (json.JSONDecodeError, TypeError):
+            pass
+    else:
+        m = re.search(r"```\s*(.*?)\s*```", clean, re.DOTALL)
+        if m:
+            try:
+                return json.loads(m.group(1))
+            except (json.JSONDecodeError, TypeError):
+                pass
+
+    # 降级 3:在文本中查找第一个完整 JSON 对象 { ... } 或数组 [ ... ]
+    # 逐字符扫描,维护字符串状态机,正确处理内嵌括号和转义引号
+    for bracket_pair in [("{}", "{", "}"), ("[]", "[", "]")]:
+        opener, closer = bracket_pair[1], bracket_pair[2]
+        start = clean.find(opener)
+        if start == -1:
+            continue
+        depth = 0
+        in_string = False
+        escape_next = False
+        for i in range(start, len(clean)):
+            ch = clean[i]
+            if escape_next:
+                escape_next = False
+                continue
+            if ch == "\\":
+                escape_next = True
+                continue
+            if ch == '"' and not escape_next:
+                in_string = not in_string
+                continue
+            if in_string:
+                continue
+            if ch == opener:
+                depth += 1
+            elif ch == closer:
+                depth -= 1
+                if depth == 0:
+                    try:
+                        return json.loads(clean[start : i + 1])
+                    except (json.JSONDecodeError, TypeError):
+                        return None
+        # 数组或对象未闭合时也尝试下
+        try:
+            return json.loads(clean[start:])
+        except (json.JSONDecodeError, TypeError):
+            pass
+
+    return None
+
+
 def str_to_md5(strings):
     """
     字符串转化为 md5 值
@@ -132,7 +207,7 @@ def show_desc_to_sta(show_desc: str):
         show_v = show_v.replace(",", ".")
 
         # 提取 数字 + 单位
-        match = re.search(r"(\d+(?:\.\d+)?)([a-z\u4e00-\u9fa5]*)", show_v)
+        match = re.search(r"(\d+(?:\.\d+)?)([a-z一-龥]*)", show_v)
         if not match:
             return 0
 
@@ -202,8 +277,8 @@ def show_desc_to_sta(show_desc: str):
 
     sta = {}
 
-    # 按“组”切分(兼容各种奇怪空格)
-    groups = re.split(r"[\u2004\u2005]+", show_desc)
+    # 按"组"切分(兼容各种奇怪空格)
+    groups = re.split(r"[  ]+", show_desc)
 
     for group in groups:
         group = group.strip()
@@ -211,7 +286,7 @@ def show_desc_to_sta(show_desc: str):
             continue
 
         # 按 key-value 分隔符拆
-        parts = group.split("\u2006")
+        parts = group.split("")
         if len(parts) != 2:
             continue
 

+ 1 - 0
requirements.txt

@@ -17,6 +17,7 @@ pyapollos~=0.1.5
 pyotp~=2.9.0
 elasticsearch~=8.17.2
 openai~=1.98.0
+volcengine-python-sdk
 tenacity~=9.0.0
 fake-useragent~=2.1.0
 pydantic~=2.10.6