refactor: 重构 AgentScope 运行时模块并优化前端附件展示
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
from typing import Any
|
||||
|
||||
from supabase import create_client
|
||||
from storage3.exceptions import StorageApiError
|
||||
|
||||
from core.config.settings import SupabaseSettings, config
|
||||
from core.config.settings import config as app_config
|
||||
@@ -139,6 +140,148 @@ class SupabaseService(BaseServiceProvider):
|
||||
|
||||
await asyncio.to_thread(_check_and_create)
|
||||
|
||||
def _get_storage(self) -> Any:
|
||||
"""Get the storage client from admin client."""
|
||||
client = self.get_admin_client()
|
||||
storage = getattr(client, "storage", None)
|
||||
if storage is None:
|
||||
raise RuntimeError("Supabase storage client unavailable")
|
||||
return storage
|
||||
|
||||
def _get_bucket_client(self, bucket: str) -> Any:
|
||||
"""Get a bucket client for the specified bucket."""
|
||||
storage = self._get_storage()
|
||||
from_bucket = getattr(storage, "from_", None)
|
||||
if not callable(from_bucket):
|
||||
raise RuntimeError("Supabase storage bucket accessor unavailable")
|
||||
return from_bucket(bucket)
|
||||
|
||||
def _validate_bucket(self, bucket: str) -> None:
|
||||
"""Validate that the bucket matches the configured bucket."""
|
||||
expected = app_config.storage.bucket
|
||||
if bucket != expected:
|
||||
raise RuntimeError("Invalid attachment bucket")
|
||||
|
||||
def _ensure_bucket_client(self, bucket: str) -> Any:
|
||||
"""Validate bucket and return authenticated bucket client."""
|
||||
self._validate_bucket(bucket)
|
||||
return self._get_bucket_client(bucket)
|
||||
|
||||
def _is_bucket_not_found_error(self, exc: Exception) -> bool:
|
||||
"""Check if the exception indicates a bucket was not found."""
|
||||
if isinstance(exc, StorageApiError):
|
||||
message = str(exc).lower()
|
||||
return "bucket" in message and "not found" in message
|
||||
message = str(exc).lower()
|
||||
return "bucket" in message and "not found" in message
|
||||
|
||||
async def upload_bytes(
|
||||
self,
|
||||
*,
|
||||
bucket: str,
|
||||
path: str,
|
||||
content: bytes,
|
||||
content_type: str,
|
||||
) -> str:
|
||||
def _upload() -> object:
|
||||
bucket_client = self._ensure_bucket_client(bucket)
|
||||
upload = getattr(bucket_client, "upload", None)
|
||||
if not callable(upload):
|
||||
raise RuntimeError("Supabase storage upload is unavailable")
|
||||
return upload(
|
||||
path,
|
||||
content,
|
||||
{
|
||||
"content-type": content_type,
|
||||
"upsert": "true",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(_upload)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
if not self._is_bucket_not_found_error(exc):
|
||||
raise
|
||||
await self._ensure_bucket_exists(bucket=bucket)
|
||||
await asyncio.to_thread(_upload)
|
||||
return path
|
||||
|
||||
async def _ensure_bucket_exists(self, *, bucket: str) -> None:
|
||||
def _ensure() -> None:
|
||||
storage = self._get_storage()
|
||||
get_bucket = getattr(storage, "get_bucket", None)
|
||||
if not callable(get_bucket):
|
||||
raise RuntimeError("Supabase storage get_bucket is unavailable")
|
||||
try:
|
||||
get_bucket(bucket)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
msg = str(exc).lower()
|
||||
if "bucket" in msg and "not found" in msg:
|
||||
raise RuntimeError(f"Storage bucket '{bucket}' does not exist")
|
||||
raise
|
||||
|
||||
await asyncio.to_thread(_ensure)
|
||||
|
||||
async def download_bytes(self, *, bucket: str, path: str) -> bytes:
|
||||
def _download() -> object:
|
||||
bucket_client = self._ensure_bucket_client(bucket)
|
||||
download = getattr(bucket_client, "download", None)
|
||||
if not callable(download):
|
||||
raise RuntimeError("Supabase storage download is unavailable")
|
||||
return download(path)
|
||||
|
||||
raw = await asyncio.to_thread(_download)
|
||||
if isinstance(raw, bytes):
|
||||
return raw
|
||||
if isinstance(raw, bytearray):
|
||||
return bytes(raw)
|
||||
if isinstance(raw, memoryview):
|
||||
return raw.tobytes()
|
||||
raise RuntimeError("Invalid attachment payload")
|
||||
|
||||
async def create_signed_url(
|
||||
self,
|
||||
*,
|
||||
bucket: str,
|
||||
path: str,
|
||||
expires_in_seconds: int,
|
||||
) -> str:
|
||||
def _create_signed_url() -> object:
|
||||
bucket_client = self._ensure_bucket_client(bucket)
|
||||
signer = getattr(bucket_client, "create_signed_url", None)
|
||||
if not callable(signer):
|
||||
raise RuntimeError("Supabase storage signed url is unavailable")
|
||||
return signer(path, expires_in_seconds)
|
||||
|
||||
raw = await asyncio.to_thread(_create_signed_url)
|
||||
if isinstance(raw, str):
|
||||
return raw
|
||||
if isinstance(raw, dict):
|
||||
signed_url = raw.get("signedURL") or raw.get("signedUrl") or raw.get("url")
|
||||
if isinstance(signed_url, str) and signed_url:
|
||||
return signed_url
|
||||
raise RuntimeError("Invalid signed url payload")
|
||||
|
||||
def parse_signed_url(self, url: str) -> tuple[str, str]:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(url)
|
||||
path_parts = parsed.path.strip("/").split("/")
|
||||
|
||||
if (
|
||||
len(path_parts) < 4
|
||||
or path_parts[0] != "storage"
|
||||
or path_parts[1] != "v1"
|
||||
or path_parts[2] != "object"
|
||||
or path_parts[3] != "sign"
|
||||
):
|
||||
raise RuntimeError("Invalid signed URL format")
|
||||
|
||||
bucket = path_parts[4]
|
||||
path = "/".join(path_parts[5:])
|
||||
|
||||
return bucket, path
|
||||
|
||||
|
||||
supabase_service: SupabaseService = register_service_instance(
|
||||
"supabase", SupabaseService()
|
||||
|
||||
Reference in New Issue
Block a user