落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,5 +2,68 @@
|
||||
对象存储:只存字节;元数据登记走主账 attachment。
|
||||
|
||||
禁止:在本包维护工单状态。
|
||||
骨架:仅占位。
|
||||
骨架:S3 兼容客户端工厂与 put/get 占位(未配 endpoint 时不连网)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from agent.config import Settings, get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectStorageClient:
|
||||
"""
|
||||
对象存储骨架。
|
||||
|
||||
正式用 boto3/minio SDK;本版仅记录配置是否齐全,put_bytes 在未配置时返回 False。
|
||||
"""
|
||||
|
||||
endpoint_url: str
|
||||
bucket: str
|
||||
access_key: str = ""
|
||||
secret_key: str = ""
|
||||
path_style: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings: Optional[Settings] = None) -> "ObjectStorageClient":
|
||||
cfg = settings or get_settings()
|
||||
return cls(
|
||||
endpoint_url=getattr(cfg, "object_storage_endpoint_url", "") or "",
|
||||
bucket=getattr(cfg, "object_storage_bucket", "") or "",
|
||||
access_key=getattr(cfg, "object_storage_access_key", "") or "",
|
||||
secret_key=getattr(cfg, "object_storage_secret_key", "") or "",
|
||||
path_style=bool(getattr(cfg, "object_storage_path_style", True)),
|
||||
)
|
||||
|
||||
def configured(self) -> bool:
|
||||
"""endpoint + bucket 是否已配(不含密钥是否正确)。"""
|
||||
return bool(self.endpoint_url and self.bucket)
|
||||
|
||||
def put_bytes_stub(self, *, key: str, data: bytes, content_type: str = "") -> bool:
|
||||
"""
|
||||
上传占位:未配置则 False;已配置也仅打日志不真正上传(骨架防误写桶)。
|
||||
|
||||
正式实现再打开真实 PUT。
|
||||
"""
|
||||
if not self.configured():
|
||||
logger.warning("object_storage 未配置,跳过 put key=%s", key)
|
||||
return False
|
||||
logger.info(
|
||||
"object_storage.put_bytes_stub bucket=%s key=%s bytes=%s(骨架不上传)",
|
||||
self.bucket,
|
||||
key,
|
||||
len(data),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
# SDK 壳:见 s3_client(默认 dry_run)
|
||||
from agent.storage.s3_client import S3ObjectStorage # noqa: E402
|
||||
|
||||
__all__ = ["ObjectStorageClient", "S3ObjectStorage"]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
对象存储:S3 兼容客户端壳(boto3)。
|
||||
|
||||
本文件职责:组装 client、put_bytes / get_bytes;缺依赖或未配齐时明确失败。
|
||||
禁止:在本包写工单状态;正式登记附件元数据走主账。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from agent.config import Settings, get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class S3ObjectStorage:
|
||||
"""
|
||||
基于 boto3 的 S3/MinIO 壳。
|
||||
|
||||
allow_network=False(默认):不真正 PUT,只校验配置与 SDK 可导入。
|
||||
"""
|
||||
|
||||
settings: Settings
|
||||
allow_network: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_settings(
|
||||
cls,
|
||||
settings: Optional[Settings] = None,
|
||||
*,
|
||||
allow_network: bool = False,
|
||||
) -> "S3ObjectStorage":
|
||||
return cls(settings=settings or get_settings(), allow_network=allow_network)
|
||||
|
||||
def _cfg(self) -> tuple[str, str, str, str, bool]:
|
||||
s = self.settings
|
||||
return (
|
||||
(s.object_storage_endpoint_url or "").rstrip("/"),
|
||||
s.object_storage_bucket or "",
|
||||
s.object_storage_access_key or "",
|
||||
s.object_storage_secret_key or "",
|
||||
bool(s.object_storage_path_style),
|
||||
)
|
||||
|
||||
def configured(self) -> bool:
|
||||
endpoint, bucket, *_rest = self._cfg()
|
||||
return bool(endpoint and bucket)
|
||||
|
||||
def sdk_available(self) -> bool:
|
||||
try:
|
||||
import boto3 # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def _client(self):
|
||||
"""构造 boto3 client;未配置抛 RuntimeError。"""
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
|
||||
endpoint, bucket, ak, sk, path_style = self._cfg()
|
||||
if not endpoint or not bucket or not ak or not sk:
|
||||
raise RuntimeError("object_storage_not_configured")
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=ak,
|
||||
aws_secret_access_key=sk,
|
||||
config=Config(s3={"addressing_style": "path" if path_style else "auto"}),
|
||||
)
|
||||
|
||||
def put_bytes(self, *, key: str, data: bytes, content_type: str = "") -> dict:
|
||||
"""
|
||||
上传字节。
|
||||
|
||||
allow_network=False:返回 dry_run,不访问网络。
|
||||
"""
|
||||
endpoint, bucket, ak, sk, _ps = self._cfg()
|
||||
if not endpoint or not bucket:
|
||||
return {"ok": False, "error": "not_configured"}
|
||||
if not self.sdk_available():
|
||||
return {"ok": False, "error": "boto3_not_installed"}
|
||||
if not self.allow_network:
|
||||
logger.info(
|
||||
"storage.s3.put dry_run bucket=%s key=%s bytes=%s",
|
||||
bucket,
|
||||
key,
|
||||
len(data),
|
||||
)
|
||||
return {"ok": True, "dry_run": True, "bucket": bucket, "key": key}
|
||||
extra = {}
|
||||
if content_type:
|
||||
extra["ContentType"] = content_type
|
||||
self._client().put_object(Bucket=bucket, Key=key, Body=data, **extra)
|
||||
return {"ok": True, "dry_run": False, "bucket": bucket, "key": key}
|
||||
|
||||
def get_bytes(self, *, key: str) -> dict:
|
||||
"""下载壳;默认 dry_run 不拉网。"""
|
||||
endpoint, bucket, *_r = self._cfg()
|
||||
if not endpoint or not bucket:
|
||||
return {"ok": False, "error": "not_configured"}
|
||||
if not self.allow_network:
|
||||
return {"ok": True, "dry_run": True, "key": key, "data": b""}
|
||||
if not self.sdk_available():
|
||||
return {"ok": False, "error": "boto3_not_installed"}
|
||||
obj = self._client().get_object(Bucket=bucket, Key=key)
|
||||
body = obj["Body"].read()
|
||||
return {"ok": True, "dry_run": False, "key": key, "data": body}
|
||||
Reference in New Issue
Block a user