Compare commits

..

4 Commits

Author SHA1 Message Date
xiaoxia 58d78c3184 Merge pull request 'feat(ditto): 支持后台配置豆包API Key + 修复单句兜底bug' (#2268) from fix/ditto-apikey-config into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m29s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m9s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7m12s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 9m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 8m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Integration Tests (push) Successful in 30m3s
CI/CD Pipeline / Validate - Style (push) Successful in 31m48s
CI/CD Pipeline / Unit Tests (push) Successful in 39m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 40m14s
CI/CD Pipeline / Validate - Security (push) Successful in 1h43m4s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-10-11 20:30:54 +08:00
xiaoxia 0c1dbfc5a7 Merge pull request 'fix(viral-video): worker侧OSS签名URL重签2h,修复Wan/Seedance下载过期URL失败' (#2270) from fix/viral-video-step-error-isolation into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m14s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m47s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m1s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 5m19s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Validate - Style (push) Successful in 25m4s
CI/CD Pipeline / Integration Tests (push) Successful in 22m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 27m4s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-10-11 19:57:10 +08:00
自动化测试Agent 222c41bd55 test(ditto): 补充API Key配置/脱敏/client重建/单句兜底单测
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 42s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 6m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 7m14s
AI Code Review / AI Code Review (pull_request) Successful in 7m56s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 16m46s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 17m33s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m51s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 18m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 22m41s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 1h9m54s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 55s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 2m11s
- 路由层:_validate_value 密钥校验、白名单包含 doubao_api_key、
  GET 脱敏(DB配置/env配置/未配置/不明文泄露)、PUT 响应脱敏
- service 层:_get_llm_client 用 DB key/key变化重建/key不变复用/
  空key走默认、单句文案中性兜底(>=1)
2026-10-11 19:14:23 +08:00
自动化测试Agent eceb454111 feat(ditto): 支持后台配置豆包API Key + 修复单句兜底bug
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m29s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 5m50s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m19s
AI Code Review / AI Code Review (pull_request) Successful in 6m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 11m14s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
- admin/ditto_emotion 白名单新增 doubao_api_key(STRING)
- GET /config 对密钥脱敏:已配置返回 ***已配置***,不暴露明文和 env 默认值
- PUT 响应中密钥值同样脱敏
- DittoEmotionService._get_llm_client 优先读 DB 配置 doubao_api_key,
  key 变化时自动重建 client;DB/env 均未配置时降级关键词匹配
- 修复 _cached_analyze 单句兜底:len(pre_splits) > 1 → >= 1,
  单句文案也走中性兜底而非直接返回空
2026-10-11 19:02:23 +08:00
4 changed files with 182 additions and 8 deletions
+24 -4
View File
@@ -1,9 +1,10 @@
"""Ditto 数字人表情后台配置 — #2246.
路由前缀 /api/v1/admin/ditto-emotion,全部使用 _verify_internal_api_key 鉴权
(X-API-Key header)。仅开放 5 项白名单配置:
(X-API-Key header)。仅开放 6 项白名单配置:
- ditto_emotion_enabled / ditto_emotion_model / ditto_emotion_temperature
- ditto_emotion_prompt / ditto_blend_frames
- ditto_emotion_prompt / ditto_blend_frames / doubao_api_key
注意:doubao_api_key 属于密钥,GET 只返回脱敏占位,绝不返回明文。
"""
from __future__ import annotations
@@ -43,6 +44,7 @@ _WHITELIST: dict[str, str] = {
"ditto_emotion_temperature": SETTING_TYPE_FLOAT,
"ditto_emotion_prompt": SETTING_TYPE_STRING,
"ditto_blend_frames": SETTING_TYPE_INT,
"doubao_api_key": SETTING_TYPE_STRING,
}
_DESCRIPTIONS: dict[str, str] = {
@@ -51,8 +53,13 @@ _DESCRIPTIONS: dict[str, str] = {
"ditto_emotion_temperature": "模型温度,0-1,越低越稳定保守。",
"ditto_emotion_prompt": "情绪分析提示词,核心调优入口,必须包含 {文案} 占位符。",
"ditto_blend_frames": "表情切换过渡帧数(6-30),越大越柔和。",
"doubao_api_key": "豆包/火山方舟 API Key(用于情绪分析LLM调用)",
}
# 密钥类配置:GET 脱敏、PUT 响应不回显明文
_SECRET_KEYS = {"doubao_api_key"}
_SECRET_PLACEHOLDER = "***已配置***"
def _settings():
return get_api_settings()
@@ -63,13 +70,24 @@ def _default_value(key: str) -> Any:
def _build_config_item(key: str) -> dict[str, Any]:
service = get_system_config_service()
if key in _SECRET_KEYS:
# 密钥不回明文、不暴露 env 默认值:DB 或 env 任一侧已配置即视为已配置
db_value = service.get_config(key, None)
configured = bool((db_value or "").strip()) or bool((getattr(_settings(), key, "") or "").strip())
return {
"key": key,
"type": _WHITELIST[key],
"description": _DESCRIPTIONS.get(key, ""),
"default": "",
"value": _SECRET_PLACEHOLDER if configured else "",
}
item: dict[str, Any] = {
"key": key,
"type": _WHITELIST[key],
"description": _DESCRIPTIONS.get(key, ""),
"default": _default_value(key),
}
service = get_system_config_service()
item["value"] = service.get_config(key, _default_value(key))
if key == "ditto_emotion_model":
item["model_options"] = list(MODEL_OPTIONS)
@@ -110,6 +128,8 @@ def _validate_value(key: str, value: Any) -> Any:
raise ValueError("提示词必须包含 {文案} 占位符")
if key == "ditto_emotion_model" and value not in MODEL_OPTIONS:
raise ValueError(f"模型必须是以下之一:{', '.join(MODEL_OPTIONS)}")
if key == "doubao_api_key" and not value.strip():
raise ValueError(f"{key} 不能为空字符串")
return value
@@ -143,7 +163,7 @@ def update_config(
setting_type=_WHITELIST[key],
updated_by=str(x_api_key)[:8] if x_api_key and x_api_key is not True else None,
)
updated[key] = value
updated[key] = _SECRET_PLACEHOLDER if key in _SECRET_KEYS else value
return {"ok": True, "updated": updated}
+11 -4
View File
@@ -211,6 +211,7 @@ class DittoEmotionService:
self.settings = settings or get_api_settings()
self._client = None
self._client_api_key: str | None = None
def _cfg(self, key: str) -> Any:
"""优先读后台 system_config,未配置则回退到 settings(env 默认)."""
@@ -236,10 +237,16 @@ class DittoEmotionService:
return hashlib.md5(text.strip().encode("utf-8")).hexdigest()
def _get_llm_client(self):
if self._client is None:
from packages.shared.ai_client import get_doubao_client
"""创建/复用 DoubaoClient;API Key 优先读后台配置(DB),fallback 环境变量。
self._client = get_doubao_client()
后台可能随时改 doubao_api_key,每次调用前对比 key,变化则重建 client。
"""
api_key = (self._cfg("doubao_api_key") or "").strip()
if self._client is None or self._client_api_key != api_key:
from packages.shared.ai_client import DoubaoClient
self._client = DoubaoClient(api_key=api_key)
self._client_api_key = api_key
return self._client
def _call_llm(self, text: str) -> list[EmotionSegment]:
@@ -309,7 +316,7 @@ def _cached_analyze(service: DittoEmotionService, cache_key: str, text: str) ->
# 如果 LLM 返回空(比如分句数量不匹配),尝试直接对预分句结果分析
if not segments:
pre_splits = split_sentences(text)
if len(pre_splits) > 1:
if len(pre_splits) >= 1:
# 用预分句结果兜底:全中性低强度
segments = [EmotionSegment(text=s, emo=EMO_NEUTRAL, intensity=0.1) for s in pre_splits]
return segments
+57
View File
@@ -223,3 +223,60 @@ class TestPromptLoading:
custom = "分析情绪: {文案}"
svc = _make_service(enabled=True, prompt=custom)
assert svc._get_prompt_template() == custom
# ── _get_llm_client:DB 配置 api_key 优先 + 变化重建 ─────────────
class TestGetLLMClient:
def test_uses_config_api_key(self):
svc = _make_service(enabled=True)
with patch.object(svc, "_cfg", return_value="sk-from-db") as mock_cfg:
with patch("packages.shared.ai_client.DoubaoClient") as MockClient:
svc._get_llm_client()
mock_cfg.assert_called_with("doubao_api_key")
assert MockClient.call_args.kwargs["api_key"] == "sk-from-db"
def test_rebuilds_client_when_key_changes(self):
svc = _make_service(enabled=True)
keys = iter(["sk-key-v1", "sk-key-v2"])
with patch.object(svc, "_cfg", side_effect=lambda k: next(keys)):
with patch("packages.shared.ai_client.DoubaoClient") as MockClient:
MockClient.side_effect = lambda **kw: MagicMock(api_key=kw["api_key"])
c1 = svc._get_llm_client()
c2 = svc._get_llm_client()
assert c1 is not c2
assert MockClient.call_count == 2
def test_reuses_client_when_key_unchanged(self):
svc = _make_service(enabled=True)
with patch.object(svc, "_cfg", return_value="sk-same"):
with patch("packages.shared.ai_client.DoubaoClient") as MockClient:
MockClient.side_effect = lambda **kw: MagicMock(api_key=kw["api_key"])
c1 = svc._get_llm_client()
c2 = svc._get_llm_client()
assert c1 is c2
assert MockClient.call_count == 1
def test_empty_key_falls_back_to_doubao_client_default(self):
"""DB/env 都没配时传空串,由 DoubaoClient 内部 fallback env / 标记不可用."""
svc = _make_service(enabled=True)
with patch.object(svc, "_cfg", return_value=""):
with patch("packages.shared.ai_client.DoubaoClient") as MockClient:
svc._get_llm_client()
assert MockClient.call_args.kwargs["api_key"] == ""
# ── 单句兜底:LLM 返回空时单句也走中性兜底(>= 1)────────────────
class TestSingleSentenceFallback:
def test_single_sentence_gets_neutral_fallback(self):
import packages.application.ditto_emotion_service as m
svc = _make_service(enabled=True)
m._cached_analyze.cache_clear()
try:
with patch.object(svc, "_call_llm", return_value=[]):
segs = svc.analyze("独一无二单句兜底测试文案abc123")
assert len(segs) == 1
assert segs[0].emo == EMO_NEUTRAL
assert segs[0].intensity == 0.1
finally:
m._cached_analyze.cache_clear()
+90
View File
@@ -6,6 +6,8 @@ Ditto 单例 reset 联动、PUT 校验。
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
@@ -285,3 +287,91 @@ class TestAdminRouteValidation:
with pytest.raises(ValueError):
route._validate_value("ditto_emotion_enabled", "yes")
assert route._validate_value("ditto_emotion_enabled", True) is True
def test_validate_api_key(self):
route = self._route()
with pytest.raises(ValueError):
route._validate_value("doubao_api_key", " ")
with pytest.raises(ValueError):
route._validate_value("doubao_api_key", 123)
assert route._validate_value("doubao_api_key", "sk-abc123") == "sk-abc123"
def test_whitelist_contains_api_key(self):
route = self._route()
assert "doubao_api_key" in route._WHITELIST
assert "doubao_api_key" in route._DESCRIPTIONS
class TestAdminRouteSecretMasking:
"""doubao_api_key GET 脱敏 + PUT 响应脱敏."""
def _route(self):
from apps.api.app.api.routes.admin import ditto_emotion as route
return route
def _mock_service(self, values):
svc = MagicMock()
svc.get_config = lambda key, default=None: values.get(key, default)
return svc
def test_get_config_masks_db_configured_key(self, monkeypatch):
route = self._route()
monkeypatch.setattr(
route,
"get_system_config_service",
lambda: self._mock_service({"doubao_api_key": "sk-real-secret-key"}),
)
monkeypatch.setattr(route, "_settings", lambda: MagicMock(doubao_api_key=""))
item = route._build_config_item("doubao_api_key")
assert item["value"] == route._SECRET_PLACEHOLDER
assert item["default"] == ""
assert "sk-real-secret-key" not in str(item)
def test_get_config_masks_env_configured_key(self, monkeypatch):
route = self._route()
monkeypatch.setattr(
route,
"get_system_config_service",
lambda: self._mock_service({}),
)
monkeypatch.setattr(route, "_settings", lambda: MagicMock(doubao_api_key="sk-env-key"))
item = route._build_config_item("doubao_api_key")
assert item["value"] == route._SECRET_PLACEHOLDER
assert "sk-env-key" not in str(item)
def test_get_config_empty_when_unconfigured(self, monkeypatch):
route = self._route()
monkeypatch.setattr(
route,
"get_system_config_service",
lambda: self._mock_service({}),
)
monkeypatch.setattr(route, "_settings", lambda: MagicMock(doubao_api_key=""))
item = route._build_config_item("doubao_api_key")
assert item["value"] == ""
def test_get_config_never_leaks_plaintext(self, monkeypatch):
route = self._route()
secret = "sk-top-secret-12345"
monkeypatch.setattr(
route,
"get_system_config_service",
lambda: self._mock_service({"doubao_api_key": secret}),
)
monkeypatch.setattr(route, "_settings", lambda: MagicMock(doubao_api_key=secret))
result = route.get_config()
assert secret not in str(result)
def test_put_response_masks_secret(self, monkeypatch):
route = self._route()
svc = self._mock_service({})
svc.set_config = MagicMock(return_value="sk-new-key")
monkeypatch.setattr(route, "get_system_config_service", lambda: svc)
payload = route.ConfigUpdatePayload(configs={"doubao_api_key": "sk-new-key"})
result = route.update_config(payload, x_api_key="test-key")
assert result["ok"] is True
assert result["updated"]["doubao_api_key"] == route._SECRET_PLACEHOLDER
assert "sk-new-key" not in str(result)
# 落库值必须是明文(服务要用),只有响应脱敏
assert svc.set_config.call_args[0][1] == "sk-new-key"