feat(product-suite): add cmhub vision AI writing

This commit is contained in:
chengma
2026-07-17 09:09:09 +08:00
parent 56d6b59a00
commit 40a9f5fbf2
15 changed files with 575 additions and 40 deletions
+139
View File
@@ -97,6 +97,7 @@ class AITests(TempDirMixin, unittest.TestCase):
"base_url": "https://cmhub.example.com",
"title_alias": "title-standard",
"image_alias": "image-hd",
"vision_alias": "vision-standard",
"connect_timeout": 3,
"download_with_curl": "false",
"check_balance_before_batch": False,
@@ -454,6 +455,144 @@ class AITests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_cmhub_analyze_product_images_uses_vision_alias_and_safe_metadata(self):
with self.make_temp_dir() as temp_dir:
cfg, key_path = self._cmhub_config(temp_dir)
first = os.path.join(temp_dir, "first.png")
second = os.path.join(temp_dir, "second.jpg")
with open(first, "wb") as fh:
fh.write(b"first-image")
with open(second, "wb") as fh:
fh.write(b"second-image")
calls = []
def fake_request(method, url, **kwargs):
calls.append((method, url, kwargs))
return _RequestsResponse(
{
"text": "浅绿色连帽上衣,突出宽松版型与日常穿搭场景。",
"alias": "vision-standard",
"model_used": "vision-provider",
"points_cost": 1,
"points_balance": 231,
"call_id": "vision-call-1",
}
)
with mock.patch.object(ai._cmhub_session(), "request", side_effect=fake_request):
result = ai.analyze_product_images(
"补充卖点:避免夸大",
"输出语言:繁体中文",
[first, second],
config=cfg,
cmhub_config_path=key_path,
)
self.assertEqual("浅绿色连帽上衣,突出宽松版型与日常穿搭场景。", result["text"])
self.assertEqual(2, result["image_count"])
self.assertEqual(1, result["metadata"]["points_cost"])
self.assertEqual(231, result["metadata"]["points_balance"])
self.assertNotIn("image_base64", result)
self.assertEqual("POST", calls[0][0])
self.assertEqual(
"https://cmhub.example.com/api/v1/analyze/images",
calls[0][1],
)
payload = calls[0][2]["json"]
self.assertEqual("vision-standard", payload["model"])
self.assertEqual(2, len(payload["images"]))
self.assertTrue(payload["images"][0]["image_base64"].startswith("data:image/png;base64,"))
self.assertTrue(payload["images"][1]["image_base64"].startswith("data:image/jpeg;base64,"))
self.assertEqual({"temperature": 0.2}, payload["parameters"])
self.assertEqual((3, ai.CMHUB_VISION_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"])
self.assert_removed(temp_dir)
def test_cmhub_analyze_product_images_requires_vision_alias_and_local_limits(self):
with self.make_temp_dir() as temp_dir:
cfg, key_path = self._cmhub_config(temp_dir)
source = os.path.join(temp_dir, "source.jpg")
with open(source, "wb") as fh:
fh.write(b"source")
cfg["ai"]["cmhub"]["vision_alias"] = ""
with self.assertRaises(ai.CMHubError) as raised:
ai.analyze_product_images(
"提示",
"输出语言:繁体中文",
[source],
config=cfg,
cmhub_config_path=key_path,
)
self.assertEqual("cmhub_not_configured", raised.exception.code)
self.assertIn("图片理解别名", str(raised.exception))
cfg["ai"]["cmhub"]["vision_alias"] = "vision-standard"
paths = []
for index in range(ai.CMHUB_VISION_MAX_IMAGES + 1):
path = os.path.join(temp_dir, "source-%d.jpg" % index)
with open(path, "wb") as fh:
fh.write(b"image")
paths.append(path)
with mock.patch.object(ai._cmhub_session(), "request") as request:
with self.assertRaises(ai.AIError) as too_many:
ai.analyze_product_images(
"提示",
"输出语言:繁体中文",
paths,
config=cfg,
cmhub_config_path=key_path,
)
self.assertIn("最多支持", str(too_many.exception))
request.assert_not_called()
oversized = os.path.join(temp_dir, "oversized.jpg")
with open(oversized, "wb") as fh:
fh.truncate(ai.CMHUB_VISION_MAX_IMAGE_BYTES + 1)
with mock.patch.object(ai._cmhub_session(), "request") as request:
with self.assertRaises(ai.AIError) as too_large:
ai.analyze_product_images(
"提示",
"输出语言:繁体中文",
[oversized],
config=cfg,
cmhub_config_path=key_path,
)
self.assertIn("超过10MiB", str(too_large.exception))
request.assert_not_called()
self.assert_removed(temp_dir)
def test_cmhub_analyze_product_images_read_timeout_is_not_retried(self):
with self.make_temp_dir() as temp_dir:
cfg, key_path = self._cmhub_config(temp_dir)
source = os.path.join(temp_dir, "source.jpg")
with open(source, "wb") as fh:
fh.write(b"source")
calls = []
def fake_request(method, url, **kwargs):
calls.append((method, url, kwargs))
raise ai.requests.exceptions.ReadTimeout("slow")
with mock.patch.object(ai._cmhub_session(), "request", side_effect=fake_request):
with self.assertRaises(ai.CMHubError) as raised:
ai.analyze_product_images(
"提示",
"输出语言:繁体中文",
[source],
config=cfg,
cmhub_config_path=key_path,
)
self.assertEqual("read_timeout", raised.exception.code)
self.assertIn("结果未确认", str(raised.exception))
self.assertEqual(1, len(calls))
self.assertEqual((3, ai.CMHUB_VISION_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"])
self.assert_removed(temp_dir)
def test_cmhub_gen_cover_downloads_image_url_safely(self):
try:
from PIL import Image