Download api/third_party_images.py from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/api/third_party_images.py
- Command line
-
hf download hf://spaces/zlcy/study/api/third_party_images.py
-
curl -L -o third_party_images.py https://huggingface.co/spaces/zlcy/study/resolve/main/api/third_party_images.py
13.2 kB
| from __future__ import annotations | |
| import json | |
| from typing import Any | |
| from fastapi import APIRouter, Header, HTTPException, Request | |
| from fastapi.concurrency import run_in_threadpool | |
| from pydantic import BaseModel, Field, ValidationError | |
| from starlette.datastructures import UploadFile | |
| from api.support import require_admin, require_identity | |
| from services.image_api_adapter import ( | |
| ADAPTER_MODEL, | |
| adapter_system_prompt, | |
| adapter_user_prompt, | |
| document_to_text, | |
| infer_document_auth_type, | |
| normalize_adapter_result, | |
| parse_model_json, | |
| redact_document_secrets, | |
| ) | |
| from services.log_service import LoggedCall | |
| from services.protocol import openai_v1_chat_complete | |
| from services.third_party_image_service import third_party_image_service | |
| class ThirdPartyImageGroupsRequest(BaseModel): | |
| groups: list[dict[str, Any]] = Field(default_factory=list) | |
| settings: dict[str, Any] | None = None | |
| class ThirdPartyImageTestRequest(BaseModel): | |
| connection_id: str = "" | |
| connection: dict[str, Any] | None = None | |
| class ThirdPartyImageAdaptDocumentRequest(BaseModel): | |
| document: str = Field(..., min_length=1, max_length=650_000) | |
| filename: str = Field(default="", max_length=300) | |
| base_url: str = Field(default="", max_length=2000) | |
| class ThirdPartyImageOnlineCallRequest(BaseModel): | |
| connection: dict[str, Any] | None = None | |
| adapter_template: dict[str, Any] | None = None | |
| request_body: dict[str, Any] = Field(default_factory=dict) | |
| operation: str = "generate" | |
| call_mode: str = "custom" | |
| class ThirdPartyImageOnlineModelsRequest(BaseModel): | |
| connection: dict[str, Any] | None = None | |
| adapter_template: dict[str, Any] | None = None | |
| MAX_ONLINE_CALL_IMAGES = 8 | |
| MAX_ONLINE_CALL_IMAGE_BYTES = 50 * 1024 * 1024 | |
| def _json_object_field(form: Any, name: str, required: bool = False): | |
| raw = form.get(name) | |
| if raw in (None, "") and not required: | |
| return None | |
| if not isinstance(raw, str): | |
| raise HTTPException(status_code=400, detail={"error": f"{name} 必须是 JSON 对象"}) | |
| try: | |
| value = json.loads(raw) | |
| except json.JSONDecodeError as exc: | |
| raise HTTPException(status_code=400, detail={"error": f"{name} 不是有效的 JSON"}) from exc | |
| if not isinstance(value, dict): | |
| raise HTTPException(status_code=400, detail={"error": f"{name} 必须是 JSON 对象"}) | |
| return value | |
| async def _parse_online_call_request( | |
| request: Request, | |
| ) -> tuple[ThirdPartyImageOnlineCallRequest, list[tuple[bytes, str, str]]]: | |
| content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().lower() | |
| if content_type != "multipart/form-data": | |
| try: | |
| return ThirdPartyImageOnlineCallRequest.model_validate(await request.json()), [] | |
| except (json.JSONDecodeError, ValidationError, TypeError) as exc: | |
| raise HTTPException(status_code=400, detail={"error": "在线调用参数格式无效"}) from exc | |
| form = await request.form() | |
| body = ThirdPartyImageOnlineCallRequest( | |
| connection=_json_object_field(form, "connection"), | |
| adapter_template=_json_object_field(form, "adapter_template"), | |
| request_body=_json_object_field(form, "request_body", required=True) or {}, | |
| operation=str(form.get("operation") or "generate"), | |
| call_mode=str(form.get("call_mode") or "custom"), | |
| ) | |
| uploads = [value for key, value in form.multi_items() if key == "images" and isinstance(value, UploadFile)] | |
| if len(uploads) > MAX_ONLINE_CALL_IMAGES: | |
| raise HTTPException(status_code=400, detail={"error": "参考图最多上传 8 张"}) | |
| images: list[tuple[bytes, str, str]] = [] | |
| total_bytes = 0 | |
| for index, upload in enumerate(uploads): | |
| mime_type = str(upload.content_type or "application/octet-stream").lower() | |
| if not mime_type.startswith("image/"): | |
| raise HTTPException(status_code=400, detail={"error": f"{upload.filename or '参考图'} 不是图片文件"}) | |
| data = await upload.read(MAX_ONLINE_CALL_IMAGE_BYTES + 1) | |
| total_bytes += len(data) | |
| if not data: | |
| raise HTTPException(status_code=400, detail={"error": f"{upload.filename or '参考图'} 内容为空"}) | |
| if len(data) > MAX_ONLINE_CALL_IMAGE_BYTES or total_bytes > MAX_ONLINE_CALL_IMAGE_BYTES: | |
| raise HTTPException(status_code=400, detail={"error": "参考图总大小不能超过 50 MB"}) | |
| images.append((data, upload.filename or f"reference_{index + 1}.png", mime_type)) | |
| return body, images | |
| def _chat_completion_text(value: object) -> str: | |
| if not isinstance(value, dict): | |
| return "" | |
| choices = value.get("choices") | |
| if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict): | |
| return "" | |
| message = choices[0].get("message") | |
| if not isinstance(message, dict): | |
| return "" | |
| content = message.get("content") | |
| if isinstance(content, str): | |
| return content.strip() | |
| if not isinstance(content, list): | |
| return "" | |
| return "".join( | |
| str(item.get("text") or "") | |
| for item in content | |
| if isinstance(item, dict) and str(item.get("type") or "") in {"text", "output_text"} | |
| ).strip() | |
| def create_router() -> APIRouter: | |
| router = APIRouter() | |
| async def list_groups(authorization: str | None = Header(default=None)): | |
| require_admin(authorization) | |
| return { | |
| "groups": third_party_image_service.list_groups(), | |
| "settings": third_party_image_service.get_settings(), | |
| } | |
| async def save_groups(body: ThirdPartyImageGroupsRequest, authorization: str | None = Header(default=None)): | |
| require_admin(authorization) | |
| try: | |
| groups = await run_in_threadpool(third_party_image_service.save_groups, body.groups, body.settings) | |
| return {"groups": groups, "settings": third_party_image_service.get_settings()} | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| async def adapt_document( | |
| body: ThirdPartyImageAdaptDocumentRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_admin(authorization) | |
| try: | |
| readable = document_to_text(body.document, body.filename) | |
| redacted = redact_document_secrets(readable) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| auth_hint = infer_document_auth_type(readable) | |
| system_prompt = adapter_system_prompt() | |
| user_prompt = adapter_user_prompt( | |
| redacted, | |
| filename=body.filename, | |
| base_url=body.base_url, | |
| auth_hint=auth_hint, | |
| ) | |
| payload = { | |
| "model": ADAPTER_MODEL, | |
| "stream": False, | |
| "messages": [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| } | |
| call = LoggedCall( | |
| identity, | |
| "/api/third-party-images/adapt-document", | |
| ADAPTER_MODEL, | |
| "生图 API 文档适配", | |
| request_text=f"导入文档:{body.filename or '未命名文档'}", | |
| ) | |
| result = await call.run(openai_v1_chat_complete.handle, payload) | |
| model_text = _chat_completion_text(result) | |
| if not model_text: | |
| raise HTTPException(status_code=502, detail={"error": "站内 AI 未返回调用模板"}) | |
| try: | |
| normalized = normalize_adapter_result( | |
| parse_model_json(model_text), | |
| base_url=body.base_url, | |
| auth_hint=auth_hint, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=502, detail={"error": f"AI 生成的调用模板未通过安全校验:{exc}"}) from exc | |
| return { | |
| **normalized, | |
| "model": ADAPTER_MODEL, | |
| "document_redacted": redacted != readable, | |
| "auth_hint": auth_hint, | |
| } | |
| async def online_call_adapter( | |
| group_id: str, | |
| connection_id: str, | |
| request: Request, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| require_admin(authorization) | |
| body, reference_images = await _parse_online_call_request(request) | |
| try: | |
| if body.call_mode == "openai": | |
| return await run_in_threadpool( | |
| third_party_image_service.online_test_openai, | |
| group_id, | |
| connection_id, | |
| connection_override=body.connection, | |
| request_body=body.request_body, | |
| reference_images=reference_images, | |
| operation=body.operation, | |
| ) | |
| return await run_in_threadpool( | |
| third_party_image_service.online_test_adapter, | |
| group_id, | |
| connection_id, | |
| connection_override=body.connection, | |
| adapter_template=body.adapter_template, | |
| request_body=body.request_body, | |
| reference_images=reference_images, | |
| operation=body.operation, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| async def online_adapter_models( | |
| group_id: str, | |
| connection_id: str, | |
| body: ThirdPartyImageOnlineModelsRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| require_admin(authorization) | |
| try: | |
| return await run_in_threadpool( | |
| third_party_image_service.online_adapter_models, | |
| group_id, | |
| connection_id, | |
| connection_override=body.connection, | |
| adapter_template=body.adapter_template, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| async def sync_models(group_id: str, authorization: str | None = Header(default=None)): | |
| require_admin(authorization) | |
| try: | |
| return await run_in_threadpool(third_party_image_service.sync_models, group_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail={"error": str(exc)}) from exc | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| async def sync_connection_models( | |
| group_id: str, | |
| connection_id: str, | |
| body: ThirdPartyImageTestRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| require_admin(authorization) | |
| try: | |
| return await run_in_threadpool( | |
| third_party_image_service.sync_connection_models, | |
| group_id, | |
| connection_id, | |
| body.connection, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail={"error": str(exc)}) from exc | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| async def test_connection( | |
| group_id: str, | |
| body: ThirdPartyImageTestRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| require_admin(authorization) | |
| try: | |
| return { | |
| "result": await run_in_threadpool( | |
| third_party_image_service.test_connection, | |
| group_id, | |
| body.connection_id, | |
| body.connection, | |
| ) | |
| } | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail={"error": str(exc)}) from exc | |
| async def list_user_options(authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| # 生图页的模型下拉:只要**能生图**的第三方模型。 | |
| # 第三方接入里可能挂着文本模型(capabilities 只有 chat),它们不能生图, | |
| # 放进来只会让用户在生图页选到一个必然失败的模型。 | |
| return { | |
| "models": third_party_image_service.public_models_for_identity(identity, image_only=True) | |
| } | |
| return router | |