From b155db260a0389466cae3ae56d7bc814395ef338 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 10 Aug 2026 15:57:07 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E5=B0=81=E9=9D=A2=E7=94=9F=E6=88=90?= =?UTF-8?q?=20stub=20=E8=BF=94=E5=9B=9E=20SVG=20data=20URI=20=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E5=9B=BE=E6=9B=BF=E4=BB=A3=E4=B8=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E7=9A=84=20placeholder=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:_call_ai_cover_service 降级到 stub 时,image_url 返回 /api/v1/assets/placeholder/cover?plan={plan_id},该端点不存在, 浏览器 404 加载失败,显示 broken image。 修复: - manual 模式和 AI stub 模式的 image_url 改为 SVG data URI 占位图(1080x1920,深色背景 + 提示文字),前端可直接渲染 - 不依赖任何后端端点,无需网络请求 --- packages/shared/ai_service.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index 32014d23d..c4a02a0fa 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -323,9 +323,16 @@ def _call_ai_cover_service( } if cover_type == "manual" and frame_time is not None: + svg_placeholder = ( + "data:image/svg+xml," + "" + "" + "手动选帧" + "" + ) return { "type": "manual", - "image_url": f"/api/v1/assets/placeholder/cover?time={frame_time}", + "image_url": svg_placeholder, "frame_time": frame_time, } @@ -368,12 +375,20 @@ def _call_ai_cover_service( except Exception as e: logger.exception("MediaKit 抽帧失败,降级到 stub: %s", str(e)) - # 降级:stub 行为 + # 降级:stub 行为 - 返回 SVG data URI 占位图 logger.info("使用 stub 封面: plan_id=%s", plan_id) time.sleep(0.3) + svg_placeholder = ( + "data:image/svg+xml," + "" + "" + "封面生成中" + "请配置 MediaKit API Key" + "" + ) return { "type": "ai_frame", - "image_url": f"/api/v1/assets/placeholder/cover?plan={plan_id}", + "image_url": svg_placeholder, "frame_time": round(random.uniform(1.0, 10.0), 1), "confidence": round(random.uniform(0.80, 0.98), 2), } -- 2.54.0 From ea953ed92ed64473b3cbc1887e6916eb3a75dc0f Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 10 Aug 2026 16:00:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=20stub=20=E5=8D=95=E6=B5=8B=EF=BC=8C=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=20SVG=20data=20URI=20=E5=8D=A0=E4=BD=8D=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 test_cover_type_manual_with_frame_time:断言 SVG data URI + 手动选帧文字 - 更新 test_cover_type_ai_frame:断言 SVG data URI + 封面生成中文字 - 新增 test_stub_returns_svg_data_uri:验证 stub 不含 /api/v1/ 路径 - 新增 test_manual_stub_returns_svg_data_uri:验证 manual 模式 SVG 占位图 --- tests/unit/test_shared_ai_service.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_shared_ai_service.py b/tests/unit/test_shared_ai_service.py index f46002dbf..7603a90bb 100755 --- a/tests/unit/test_shared_ai_service.py +++ b/tests/unit/test_shared_ai_service.py @@ -431,7 +431,8 @@ class TestAiCoverService: result = _call_ai_cover_service("plan1", ["a1"], "manual", frame_time=5.5) assert result["type"] == "manual" assert result["frame_time"] == 5.5 - assert "5.5" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") + assert "手动选帧" in result["image_url"] def test_cover_type_ai_frame(self): with patch("shared.ai_service.time.sleep"): @@ -440,7 +441,8 @@ class TestAiCoverService: assert result["type"] == "ai_frame" assert result["frame_time"] == 5.0 assert result["confidence"] == 0.9 - assert "plan1" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") + assert "封面生成中" in result["image_url"] def test_cover_type_ai_regenerate(self): with patch("shared.ai_service.time.sleep"): @@ -451,3 +453,20 @@ class TestAiCoverService: with patch("shared.ai_service.time.sleep"): result = _call_ai_cover_service("plan1", ["a1"], "ai_frame") assert 1.0 <= result["frame_time"] <= 10.0 + + def test_stub_returns_svg_data_uri(self): + """stub 降级返回 SVG data URI,不含任何后端 API 路径.""" + with patch("shared.ai_service.time.sleep"): + result = _call_ai_cover_service("plan1", ["a1"], "ai_frame") + assert result["image_url"].startswith("data:image/svg+xml,") + assert "/api/v1/" not in result["image_url"] + assert "1080" in result["image_url"] + assert "1920" in result["image_url"] + + def test_manual_stub_returns_svg_data_uri(self): + """manual 模式返回 SVG data URI 占位图.""" + with patch("shared.ai_service.time.sleep"): + result = _call_ai_cover_service("plan1", ["a1"], "manual", frame_time=3.0) + assert result["image_url"].startswith("data:image/svg+xml,") + assert "/api/v1/" not in result["image_url"] + assert "手动选帧" in result["image_url"] -- 2.54.0 From 644d2b9aa12321f43aae7e8ab151a638e1d9e2c6 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 10 Aug 2026 16:33:27 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=20test=5Fmediaki?= =?UTF-8?q?t=5Fcover=20=E4=B8=AD=20placeholder=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=EF=BC=8C=E9=80=82=E9=85=8D=20SVG=20data=20URI=20=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_mediakit_cover.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_mediakit_cover.py b/tests/unit/test_mediakit_cover.py index 49864e136..22193e67e 100755 --- a/tests/unit/test_mediakit_cover.py +++ b/tests/unit/test_mediakit_cover.py @@ -185,7 +185,7 @@ class TestAICoverService: # 应该降级到 stub assert result["type"] == "ai_frame" - assert "placeholder" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") def test_call_ai_cover_without_video_url_fallback(self): """没有视频 URL 时使用 stub.""" @@ -199,7 +199,7 @@ class TestAICoverService: ) assert result["type"] == "ai_frame" - assert "placeholder" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") def test_call_ai_cover_upload_type(self): """upload 类型直接返回.""" @@ -247,7 +247,7 @@ class TestAICoverService: ) assert result["type"] == "ai_frame" - assert "placeholder" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") @patch("packages.shared.mediakit_client.get_mediakit_client") def test_call_ai_cover_empty_frames_fallback(self, mock_get_client): @@ -267,7 +267,7 @@ class TestAICoverService: ) assert result["type"] == "ai_frame" - assert "placeholder" in result["image_url"] + assert result["image_url"].startswith("data:image/svg+xml,") class TestGenerateCover: -- 2.54.0