From 3f183d14dbc5b6a7d37e796480d67cca737b8047 Mon Sep 17 00:00:00 2001 From: Mavis Date: Wed, 10 Jun 2026 23:48:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(translate):=20service.py=20=E6=8E=A5=20zhi?= =?UTF-8?q?pu=20/=20config=20=E5=8A=A0=20zhipu=5F*=20/=20.env.example=20?= =?UTF-8?q?=E5=8A=A0=20ZHIPU=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配 zhipu.py(zhipu 模块)一起使用: - service.py: 新增 _zhipu_translator(),引擎选择链路 spark → zhipu → tencent → maas → agnes → local - config.py: 新增 zhipu_api_key / zhipu_base_url / zhipu_model / zhipu_interval_sec - .env.example: 补 ZHIPU_* 字段说明 留空 zhipu_api_key = spark 不可用时直接降级 tencent(向后兼容) --- .env.example | 9 +++++ backend/app/config.py | 7 ++++ backend/app/services/translation/service.py | 44 +++++++++++++++------ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index ed8fa40..b2b621f 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,15 @@ SPARK_MODEL=lite # 单次调用间隔(秒),避免被限速 SPARK_INTERVAL_SEC=1.0 +# ===== 智谱 GLM(第二序位翻译;glm-4-flash 免费)===== +# 留空 = 不启用智谱(spark 不可用时直接走 tencent) +# 控制台 https://open.bigmodel.cn/ → API Keys → 新建 +ZHIPU_API_KEY=your_zhipu_api_key +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +# 模型:glm-4-flash(默认,免费) / glm-4-air / glm-4-airx / glm-4 / glm-4-plus / glm-4.5 / glm-4.6 +ZHIPU_MODEL=glm-4-flash +ZHIPU_INTERVAL_SEC=1.0 + # ===== 本地翻译(降级) ===== # 不启用就留空:不会用本地模�?LOCAL_TRANSLATE_ENABLED=false LOCAL_TRANSLATE_MODEL=nllb-200-distilled-600M diff --git a/backend/app/config.py b/backend/app/config.py index fc5a2e1..c6a4a97 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -77,6 +77,13 @@ class Settings(BaseSettings): spark_model: str = "lite" spark_interval_sec: float = 1.0 + # ===== 智谱 GLM(第二序位翻译;GLM-4-Flash 免费)===== + # 留空 = 不启用智谱(spark 不可用时直接降级到 tencent) + zhipu_api_key: str = "" + zhipu_base_url: str = "https://open.bigmodel.cn/api/paas/v4" + zhipu_model: str = "glm-4-flash" + zhipu_interval_sec: float = 1.0 + @field_validator("tencent_tmt_quota_buffer") @classmethod def _check_buffer(cls, v: float) -> float: diff --git a/backend/app/services/translation/service.py b/backend/app/services/translation/service.py index 6cdfcd6..fcee088 100644 --- a/backend/app/services/translation/service.py +++ b/backend/app/services/translation/service.py @@ -2,16 +2,17 @@ 引擎链路(优先级降序): 1. spark(主,Lite 免费;spark_api_password 配了才用) -2. tencent TMT(第二级,按月配额;快满时主动切走) -3. tencent_maas(备用,OpenAI 兼容,无配额;主失败/TMT 配额耗尽时启用) -4. agnes(第三级,通用 LLM 做翻译;MaaS 不可用时启用 — 质量次之但够用) -5. local(最后兜底,需 settings.local_translate_enabled=true) +2. zhipu(第二序位,GLM-4-Flash 免费;zhipu_api_key 配了才用) +3. tencent TMT(第三级,按月配额;快满时主动切走) +4. tencent_maas(备用,OpenAI 兼容,无配额;主失败/TMT 配额耗尽时启用) +5. agnes(第四级,通用 LLM 做翻译;MaaS 不可用时启用 — 质量次之但够用) +6. local(最后兜底,需 settings.local_translate_enabled=true) 注: - TMT 是按月计费的(腾讯云后台可能计费口径是请求字节,我们 redis 累加的是字符数, 差异约 2-3x);用户从腾讯云后台看"已用 2M"时,我们 redis 显示约 80 万字符 - 用户决策:以腾讯云后台数字为准,快满时降级 -- spark 是 Lite 免费,默认走它;spark 不可用时降级到 tencent(继续吃配额)。 +- spark / zhipu 都是免费模型,默认优先;不可用时降级到 tencent(继续吃配额)。 想要完全绕开 tencent,把 TENCENTCLOUD_SECRET_ID 留空即可。 """ from __future__ import annotations @@ -30,6 +31,7 @@ from app.services.translation.local import LocalTranslator from app.services.translation.spark import SparkTranslator from app.services.translation.tencent import TencentTranslator from app.services.translation.tencent_maas import TencentMaaSTranslator +from app.services.translation.zhipu import ZhipuTranslator logger = logging.getLogger("news.translate.service") @@ -48,6 +50,7 @@ def _month_key() -> str: class TranslationService: def __init__(self): self._spark: BaseTranslator | None = None + self._zhipu: BaseTranslator | None = None self._tencent: BaseTranslator | None = None self._tencent_maas: BaseTranslator | None = None self._agnes: BaseTranslator | None = None @@ -65,8 +68,18 @@ class TranslationService: self._spark = None return self._spark + def _zhipu_translator(self) -> BaseTranslator | None: + """第二序位引擎:智谱 GLM(免费)。配了 zhipu_api_key 才启用。""" + if self._zhipu is None and settings.zhipu_api_key: + try: + self._zhipu = ZhipuTranslator() + except Exception as e: + logger.warning("zhipu init failed: %s", e) + self._zhipu = None + return self._zhipu + def _primary(self) -> BaseTranslator | None: - """第二级:腾讯 TMT(初始化失败返回 None 表示不可用)。""" + """第三级:腾讯 TMT(初始化失败返回 None 表示不可用)。""" if self._tencent is None: try: self._tencent = TencentTranslator() @@ -155,10 +168,12 @@ class TranslationService: return TranslationResult(text=cached, engine="cache", chars=chars, cached=True) # 2) 选引擎 - # 优先级:spark → tencent(配额)→ maas → agnes → local + # 优先级:spark → zhipu → tencent(配额)→ maas → agnes → local engine: BaseTranslator | None = None if self._spark_translator() is not None: engine = self._spark_translator() + elif self._zhipu_translator() is not None: + engine = self._zhipu_translator() elif await self.can_use_tencent(chars): engine = self._primary() if engine is None: @@ -168,7 +183,7 @@ class TranslationService: engine = None if engine is None: - # spark 不可用 + 配额耗尽 / TMT 不可用:走备用链(maas → agnes → local) + # spark / zhipu 不可用 + 配额耗尽 / TMT 不可用:走备用链(maas → agnes → local) engine = self._fallback() if engine is None: # 全无可用:返回原文 + 标记 @@ -189,10 +204,17 @@ class TranslationService: res = await engine.translate(text, source=source, target=target) except Exception as e: logger.exception("translate failed with %s: %s", engine.name, e) - # 失败时按 tencent_maas → local 顺序找一个不同的 fallback - # spark 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用) + # 失败时按 zhipu → tencent → maas → local 顺序找一个不同的 fallback + # spark / zhipu 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用) fb: BaseTranslator | None = None if engine.name == "spark": + if self._zhipu_translator() is not None: + fb = self._zhipu_translator() + if fb is None and await self.can_use_tencent(chars): + fb = self._primary() + if fb is None: + fb = self._maas() if engine.name != "tencent_maas" else None + elif engine.name == "zhipu": if await self.can_use_tencent(chars): fb = self._primary() if fb is None: @@ -212,7 +234,7 @@ class TranslationService: raise RuntimeError(f"translation failed for {chars} chars (engine={engine.name})") # 4) 写缓存 — 只缓存真实翻译结果;失败/降级文本不缓存(避免污染 30 天) - if res.engine in ("spark", "tencent", "tencent_maas", "agnes", "nllb") and not res.cached: + if res.engine in ("spark", "zhipu", "tencent", "tencent_maas", "agnes", "nllb") and not res.cached: if "[翻译失败" not in res.text and "[本条未翻译" not in res.text: try: await r.set(ck, res.text, ex=60 * 60 * 24 * 30) # 30 天