mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-17 04:41:01 +08:00
130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""公共 HTTP 能力和联网反向图片 callback 装配边界。
|
|
|
|
本模块承载 operation availability 以及联网反向图片 callback 的薄 HTTP glue。task、
|
|
claim、scope、provider/service 逻辑位于各自领域模块;入口通过显式 callback 注入依赖。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from datetime import datetime
|
|
from typing import Any, Awaitable
|
|
|
|
from fastapi import HTTPException, Request
|
|
|
|
from backend.callbacks import DEFAULT_CALLBACK_REGISTRY
|
|
from backend.operation_policy import Operations, OperationPolicyError
|
|
from backend.reverse_image_http import internal_reverse_image_search as _reverse_image_search_http
|
|
|
|
|
|
ErrorFactory = Callable[[int, str, str], HTTPException]
|
|
|
|
|
|
async def operation_availability(
|
|
request: Request,
|
|
operation: str | None = None,
|
|
*,
|
|
request_scope: Callable[[Request], Any],
|
|
operation_gateway: Callable[[Request], Any],
|
|
error: ErrorFactory,
|
|
) -> dict[str, object]:
|
|
"""返回当前 scope 的 operation 能力提示且不建立 reservation。
|
|
|
|
输入是请求、可选 operation 名称和入口注入的 scope/policy/error callback;输出是稳定
|
|
`items` 列表。调用场景是公开能力查询,始终只调用 gateway probe,不调用 acquire、
|
|
commit 或 release。
|
|
"""
|
|
names = [operation] if operation else sorted(Operations.ALL)
|
|
if any(name not in Operations.ALL for name in names):
|
|
raise error(400, "operation_unknown", "操作类型无效")
|
|
gateway = operation_gateway(request)
|
|
values: list[dict[str, object]] = []
|
|
for name in names:
|
|
try:
|
|
decision = gateway.probe(gateway.request(request_scope(request), name, f"probe:{name}"))
|
|
except OperationPolicyError as exc:
|
|
item = {"operation": name, "available": False, "reason": exc.code}
|
|
if exc.retry_at is not None:
|
|
item["retry_at"] = exc.retry_at.isoformat() if isinstance(exc.retry_at, datetime) else str(exc.retry_at)
|
|
values.append(item)
|
|
continue
|
|
item = {"operation": name, "available": bool(decision.allowed)}
|
|
if not decision.allowed:
|
|
item["reason"] = decision.reason if decision.reason in {"operation_forbidden", "operation_limit_exceeded", "operation_daily_limit_exceeded", "operation_policy_unavailable"} else "operation_policy_unavailable"
|
|
if decision.retry_at is not None:
|
|
item["retry_at"] = decision.retry_at.isoformat() if isinstance(decision.retry_at, datetime) else str(decision.retry_at)
|
|
values.append(item)
|
|
return {"items": values}
|
|
|
|
|
|
async def internal_reverse_image_search(
|
|
request: Request,
|
|
*,
|
|
task_id: str,
|
|
image: Any,
|
|
request_id: str | None,
|
|
input_digest: str | None,
|
|
search_type: str,
|
|
language: str,
|
|
country: str | None,
|
|
query: str | None,
|
|
auto_crop: bool,
|
|
refresh: bool,
|
|
binding: Callable[[Request], Any],
|
|
registration: Callable[[Request], Any],
|
|
database: Callable[[Request], Any],
|
|
scope_services: Callable[[Request, Any], Any],
|
|
error: ErrorFactory,
|
|
delegate: Callable[..., Awaitable[dict[str, object]]] = _reverse_image_search_http,
|
|
) -> dict[str, object]:
|
|
"""在 callback binding 初步可信后读取图片并委托领域反向检索 handler。
|
|
|
|
输入是 FastAPI UploadFile、callback 字段和显式宿主依赖;输出是领域 handler 的脱敏
|
|
结果。调用场景是内部 multipart callback:缺少 registration/binding 或 task 不匹配时
|
|
先拒绝并不调用 `image.read()`,随后由已有领域模块完成完整 target SHA、claim、scope
|
|
和 request digest 复核。
|
|
"""
|
|
callback_binding = binding(request)
|
|
callback_registration = registration(request)
|
|
if callback_registration is None:
|
|
raise error(413, "agent_callback_body_too_large", "内部请求体超过限制")
|
|
if callback_binding is None or getattr(callback_binding, "task_id", None) != task_id:
|
|
raise error(401, "agent_callback_unauthorized", "内部执行凭据无效")
|
|
content = await image.read()
|
|
return await delegate(
|
|
request,
|
|
task_id=task_id,
|
|
content=content,
|
|
filename=getattr(image, "filename", None),
|
|
request_id=request_id,
|
|
input_digest=input_digest,
|
|
search_type=search_type,
|
|
language=language,
|
|
country=country,
|
|
query=query,
|
|
auto_crop=auto_crop,
|
|
refresh=refresh,
|
|
binding=binding,
|
|
registration=registration,
|
|
database=database,
|
|
scope_services=scope_services,
|
|
error=error,
|
|
)
|
|
|
|
|
|
def callback_registration(request: Request) -> Any:
|
|
"""读取当前 callback path 的 registry 注册项。
|
|
|
|
输入是 request;输出是注册描述或 None。调用场景是入口注入 callback delegate,缺失
|
|
registry 时明确返回 None,由内部 handler 映射为稳定拒绝而不安装默认跨宿主状态。
|
|
"""
|
|
registry = getattr(request.app.state, "callback_registry", DEFAULT_CALLBACK_REGISTRY)
|
|
return registry.get(request.url.path) if registry is not None else None
|
|
|
|
|
|
__all__ = [
|
|
"callback_registration",
|
|
"internal_reverse_image_search",
|
|
"operation_availability",
|
|
]
|