Files
AstrBot/tests/unit/test_webchat_upload_image_format.py
T
这只猫猫加了糖andRuochenPan 6ddcb2fc08 fix(provider): Dsable OpenAI SDK built-in retries to prevent request multiplication from stacked retry layers (#9669)
* fix(provider): disable OpenAI SDK built-in retries under AstrBot retry layer (#9663)

AsyncOpenAI/AsyncAzureOpenAI inherit the SDK default max_retries=2, which
nests underneath AstrBot's tenacity-based retry_provider_request(). As a
result provider_settings.request_max_retries no longer reflects the true
attempt count (e.g. request_max_retries=1 still sends up to 3 HTTP
requests) and delays fallback-provider switchover. Pass max_retries=0 so
AstrBot's retry layer is the single source of truth for retries.

* test(provider): add retry-count regression test, parameterize client retry tests

Address review feedback on #9669: merge the near-identical official/Azure
client construction tests into a parametrized test, and add a regression
test asserting _query() performs exactly request_max_retries attempts
through retry_provider_request() when the call keeps failing, guarding
against multiplicative retries being reintroduced.

* fix(media): guard oversized image inputs and attachment uploads

为模型图片输入与聊天附件上传增加大小护栏 / Add size guards for model
image inputs and chat attachment uploads:

- _convert_image_bytes_sync 复用原始编码字节的分支增加 2MB 上限;更大的
  输入重编码,输出始终受像素尺寸与质量约束
- prepare_model_image 在读入前检查文件大小,超过 32MB 跳过并记录日志
- 聊天附件上传上限 100MB:路由先检查 Content-Length,服务层落盘后复核
  实际大小

- Reuse the original encoded bytes in _convert_image_bytes_sync only for
  stills up to 2MB; larger inputs are re-encoded so the output stays
  bounded by pixel size and quality.
- prepare_model_image checks the file size before reading and skips
  inputs above 32MB with a warning log.
- Chat attachment uploads are capped at 100MB: the route checks
  Content-Length first, and save_uploaded_file re-checks the on-disk
  size after saving.

Tests: tests/test_media_utils.py, tests/test_chat_route.py

* fix(media): sniff image headers instead of decoding for MIME detection

MIME 探测改为读取文件头魔数,不再解码整个图片 / Detect image MIME
types by reading header magic bytes instead of decoding the whole file:

- detect_image_mime_type 仅读取前 32 字节识别 PNG/JPEG/GIF/WebP/BMP/
  TIFF/AVIF,内存占用与文件大小无关
- 聊天附件上传上限从 100MB 放宽到 512MB

- detect_image_mime_type reads only the first 32 bytes and recognizes
  PNG/JPEG/GIF/WebP/BMP/TIFF/AVIF, so memory usage no longer scales
  with file size
- Chat attachment upload limit raised from 100MB to 512MB

Tests: tests/test_media_utils.py

* test: remove obsolete image assertion and fix upload fixtures

* test: mock plugin marketplace requests in dashboard tests

---------

Co-authored-by: RuochenPan <badbatch0x01@gmail.com>
2026-09-17 21:15:01 +08:00

83 lines
2.4 KiB
Python

from io import BytesIO
from types import SimpleNamespace
import pytest
from PIL import Image as PILImage
from astrbot.dashboard.services.chat_service import (
WEBCHAT_IMAGE_MIME_TYPES,
ChatService,
)
@pytest.mark.asyncio
async def test_webchat_upload_uses_detected_image_type(tmp_path):
image_buffer = BytesIO()
PILImage.new("RGB", (2, 2), (255, 0, 0)).save(image_buffer, format="JPEG")
class FakeUploadFile:
filename = "pasted.png"
content_type = "image/png"
content_length = None
async def save(self, destination):
with open(destination, "wb") as output:
output.write(image_buffer.getvalue())
class FakeDatabase:
def __init__(self):
self.inserted = None
async def insert_attachment(self, path, type, mime_type):
self.inserted = {
"path": path,
"type": type,
"mime_type": mime_type,
}
return SimpleNamespace(attachment_id="attachment-1", path=path)
fake_db = FakeDatabase()
service = ChatService.__new__(ChatService)
service.db = fake_db
service.attachments_dir = str(tmp_path)
result = await service.save_uploaded_file(FakeUploadFile())
assert result["filename"].endswith(".jpg")
assert fake_db.inserted["mime_type"] == "image/jpeg"
assert fake_db.inserted["type"] == "image"
assert (tmp_path / result["filename"]).exists()
assert not (tmp_path / "pasted.png").exists()
@pytest.mark.parametrize(
("filename", "expected_mime_type"),
[(f"photo{ext}", mime_type) for ext, mime_type in WEBCHAT_IMAGE_MIME_TYPES.items()],
)
@pytest.mark.asyncio
async def test_resolve_webchat_file_uses_image_extension_mime_type(
tmp_path,
monkeypatch,
filename,
expected_mime_type,
):
monkeypatch.setattr(
"astrbot.dashboard.services.chat_service.get_astrbot_data_path",
lambda: str(tmp_path),
)
service = ChatService(
SimpleNamespace(),
SimpleNamespace(
conversation_manager=None,
platform_message_history_manager=None,
umop_config_router=None,
),
)
file_path = tmp_path / "attachments" / filename
file_path.write_bytes(b"image-bytes")
resolved_path, mime_type = await service.resolve_webchat_file(filename)
assert resolved_path == str(file_path.resolve(strict=False))
assert mime_type == expected_mime_type