mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from .credentials import KiroAuthConfig
|
||||
from .usage_limits import (
|
||||
Bonus,
|
||||
FreeTrialInfo,
|
||||
SubscriptionInfo,
|
||||
UsageBreakdown,
|
||||
UsageLimitsResponse,
|
||||
calculate_current_usage,
|
||||
calculate_total_usage_limit,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Bonus",
|
||||
"FreeTrialInfo",
|
||||
"KiroAuthConfig",
|
||||
"SubscriptionInfo",
|
||||
"UsageBreakdown",
|
||||
"UsageLimitsResponse",
|
||||
"calculate_current_usage",
|
||||
"calculate_total_usage_limit",
|
||||
]
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Internal Kiro credential schema (stored in ProviderAPIKey.auth_config)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _parse_epoch_seconds(value: object) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
if isinstance(value, str) and value.strip().isdigit():
|
||||
return int(value.strip())
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _get_str(raw: dict[str, Any], *keys: str) -> str | None:
|
||||
"""Return the first non-empty stripped string for *keys*, or ``None``."""
|
||||
for k in keys:
|
||||
v = raw.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
return v.strip()
|
||||
return None
|
||||
|
||||
|
||||
def _nonempty(s: str | None) -> str | None:
|
||||
"""Return *s* if it's a non-empty stripped string, else ``None``."""
|
||||
if isinstance(s, str) and s.strip():
|
||||
return s.strip()
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_auth_method(value: str | None) -> str:
|
||||
method = (value or "").strip().lower()
|
||||
if not method:
|
||||
return "social"
|
||||
# 历史/别名兼容:统一映射到 idc
|
||||
if method in {
|
||||
"idc",
|
||||
"builder-id",
|
||||
"builder_id",
|
||||
"builderid",
|
||||
"identity-center",
|
||||
"identity_center",
|
||||
"identitycenter",
|
||||
"iam",
|
||||
"device",
|
||||
"device_authorization",
|
||||
"device-auth",
|
||||
}:
|
||||
return "idc"
|
||||
return method
|
||||
|
||||
|
||||
def _parse_iso_to_epoch_seconds(value: object) -> int | None:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return None
|
||||
text = value.strip()
|
||||
# Support RFC3339 with Z suffix.
|
||||
if text.endswith("Z"):
|
||||
text = text[:-1] + "+00:00"
|
||||
try:
|
||||
dt = datetime.fromisoformat(text)
|
||||
except Exception:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return int(dt.timestamp())
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class KiroAuthConfig:
|
||||
provider_type: str = "kiro"
|
||||
|
||||
auth_method: str = "social" # social | idc
|
||||
refresh_token: str = ""
|
||||
expires_at: int = 0
|
||||
|
||||
profile_arn: str | None = None
|
||||
region: str | None = None # OIDC region(IdC token 刷新用)
|
||||
|
||||
# 独立的 auth / api region(与 kiro.rs 对齐)
|
||||
# auth_region: token 刷新端点,未设置时回退到 region
|
||||
# api_region: q.{region} 服务端点,未设置时回退到 DEFAULT_REGION
|
||||
auth_region: str | None = None
|
||||
api_region: str | None = None
|
||||
|
||||
client_id: str | None = None
|
||||
client_secret: str | None = None
|
||||
|
||||
machine_id: str | None = None
|
||||
kiro_version: str | None = None
|
||||
system_version: str | None = None
|
||||
node_version: str | None = None
|
||||
|
||||
email: str | None = None # 账号邮箱
|
||||
|
||||
# 缓存的 access_token(可选,用于避免频繁刷新)
|
||||
access_token: str | None = None
|
||||
|
||||
def effective_auth_region(self) -> str:
|
||||
"""Token 刷新用的 region。
|
||||
|
||||
优先级: auth_region > region > DEFAULT_REGION
|
||||
"""
|
||||
from src.services.provider.adapters.kiro.constants import DEFAULT_REGION
|
||||
|
||||
return _nonempty(self.auth_region) or _nonempty(self.region) or DEFAULT_REGION
|
||||
|
||||
def effective_api_region(self) -> str:
|
||||
"""API 服务端点(q.{region})用的 region。
|
||||
|
||||
优先级: api_region > DEFAULT_REGION
|
||||
注意: 不从 region 继承,因为 region 通常是 OIDC region(如 eu-north-1),
|
||||
而 q.{region} 端点目前仅 us-east-1 可用。
|
||||
"""
|
||||
from src.services.provider.adapters.kiro.constants import DEFAULT_REGION
|
||||
|
||||
return _nonempty(self.api_region) or DEFAULT_REGION
|
||||
|
||||
@staticmethod
|
||||
def infer_auth_method(raw: dict[str, Any]) -> str:
|
||||
"""
|
||||
根据凭据字段自动推断认证类型。
|
||||
|
||||
规则:
|
||||
- 包含 clientId + clientSecret -> IdC
|
||||
- 仅含 refreshToken -> Social
|
||||
"""
|
||||
explicit_method = _get_str(raw, "auth_method", "authMethod", "auth_type", "authType")
|
||||
normalized_explicit = _normalize_auth_method(explicit_method)
|
||||
if normalized_explicit != "social":
|
||||
return normalized_explicit
|
||||
|
||||
client_id = raw.get("client_id") or raw.get("clientId")
|
||||
client_secret = raw.get("client_secret") or raw.get("clientSecret")
|
||||
|
||||
if client_id and client_secret:
|
||||
return "idc"
|
||||
return "social"
|
||||
|
||||
@staticmethod
|
||||
def validate_required_fields(raw: dict[str, Any]) -> tuple[bool, str]:
|
||||
"""
|
||||
验证凭据是否包含必需字段。
|
||||
|
||||
返回: (is_valid, error_message)
|
||||
"""
|
||||
refresh_token = raw.get("refresh_token") or raw.get("refreshToken") or ""
|
||||
refresh_token = str(refresh_token).strip()
|
||||
|
||||
if not refresh_token:
|
||||
return False, "refreshToken 为必填字段"
|
||||
|
||||
# refreshToken 不能含有 ...(表示被截断)
|
||||
if "..." in refresh_token:
|
||||
return False, "refreshToken 不完整(含有 ...),请导出完整的 Token"
|
||||
|
||||
# IdC 类型需要 clientId 和 clientSecret
|
||||
explicit_method = _get_str(raw, "auth_method", "authMethod", "auth_type", "authType")
|
||||
auth_method = (
|
||||
_normalize_auth_method(explicit_method)
|
||||
if explicit_method
|
||||
else KiroAuthConfig.infer_auth_method(raw)
|
||||
)
|
||||
if auth_method == "idc":
|
||||
client_id = raw.get("client_id") or raw.get("clientId")
|
||||
client_secret = raw.get("client_secret") or raw.get("clientSecret")
|
||||
if not client_id:
|
||||
return False, "IdC 类型需要 clientId"
|
||||
if not client_secret:
|
||||
return False, "IdC 类型需要 clientSecret"
|
||||
|
||||
return True, ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "KiroAuthConfig":
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
|
||||
provider_type = _get_str(raw, "provider_type", "providerType") or "kiro"
|
||||
|
||||
# 自动推断 auth_method(如果未显式指定)
|
||||
explicit_method = _get_str(raw, "auth_method", "authMethod", "auth_type", "authType")
|
||||
auth_method = (
|
||||
_normalize_auth_method(explicit_method)
|
||||
if explicit_method
|
||||
else cls.infer_auth_method(raw)
|
||||
)
|
||||
|
||||
refresh_token = (_get_str(raw, "refresh_token", "refreshToken") or "").strip()
|
||||
|
||||
expires_at = _parse_epoch_seconds(raw.get("expires_at"))
|
||||
if expires_at is None:
|
||||
expires_at = _parse_iso_to_epoch_seconds(raw.get("expiresAt"))
|
||||
if expires_at is None:
|
||||
expires_at = 0
|
||||
|
||||
cfg = cls(
|
||||
provider_type=provider_type,
|
||||
auth_method=_normalize_auth_method(auth_method),
|
||||
refresh_token=refresh_token,
|
||||
expires_at=int(expires_at),
|
||||
profile_arn=_get_str(raw, "profile_arn", "profileArn"),
|
||||
region=_get_str(raw, "region"),
|
||||
auth_region=_get_str(raw, "auth_region", "authRegion"),
|
||||
api_region=_get_str(raw, "api_region", "apiRegion"),
|
||||
client_id=_get_str(raw, "client_id", "clientId"),
|
||||
client_secret=_get_str(raw, "client_secret", "clientSecret"),
|
||||
machine_id=_get_str(raw, "machine_id", "machineId"),
|
||||
kiro_version=_get_str(raw, "kiro_version", "kiroVersion"),
|
||||
system_version=_get_str(raw, "system_version", "systemVersion"),
|
||||
node_version=_get_str(raw, "node_version", "nodeVersion"),
|
||||
email=_get_str(raw, "email"),
|
||||
access_token=_get_str(raw, "access_token", "accessToken"),
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"provider_type": self.provider_type,
|
||||
"auth_method": self.auth_method,
|
||||
"refresh_token": self.refresh_token,
|
||||
"expires_at": self.expires_at,
|
||||
"profile_arn": self.profile_arn,
|
||||
"region": self.region,
|
||||
"auth_region": self.auth_region,
|
||||
"api_region": self.api_region,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"machine_id": self.machine_id,
|
||||
"kiro_version": self.kiro_version,
|
||||
"system_version": self.system_version,
|
||||
"node_version": self.node_version,
|
||||
"email": self.email,
|
||||
"access_token": self.access_token,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["KiroAuthConfig"]
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Kiro getUsageLimits response models (best-effort).
|
||||
|
||||
The AWS API uses camelCase fields; we parse defensively.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SubscriptionInfo:
|
||||
subscription_title: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> "SubscriptionInfo | None":
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
title = raw.get("subscriptionTitle")
|
||||
if isinstance(title, str) and title.strip():
|
||||
return cls(subscription_title=title.strip())
|
||||
return cls(subscription_title=None)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Bonus:
|
||||
current_usage: float = 0.0
|
||||
usage_limit: float = 0.0
|
||||
status: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> "Bonus | None":
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
status = raw.get("status")
|
||||
status_val = status.strip() if isinstance(status, str) and status.strip() else None
|
||||
cu = raw.get("currentUsage")
|
||||
ul = raw.get("usageLimit")
|
||||
try:
|
||||
cu_f = float(cu) if cu is not None else 0.0
|
||||
except Exception:
|
||||
cu_f = 0.0
|
||||
try:
|
||||
ul_f = float(ul) if ul is not None else 0.0
|
||||
except Exception:
|
||||
ul_f = 0.0
|
||||
return cls(current_usage=cu_f, usage_limit=ul_f, status=status_val)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FreeTrialInfo:
|
||||
current_usage: int = 0
|
||||
current_usage_with_precision: float = 0.0
|
||||
usage_limit: int = 0
|
||||
usage_limit_with_precision: float = 0.0
|
||||
free_trial_expiry: float | None = None
|
||||
free_trial_status: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> "FreeTrialInfo | None":
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
|
||||
def _int(v: Any) -> int:
|
||||
try:
|
||||
return int(v)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _float(v: Any) -> float:
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
expiry = raw.get("freeTrialExpiry")
|
||||
try:
|
||||
expiry_f = float(expiry) if expiry is not None else None
|
||||
except Exception:
|
||||
expiry_f = None
|
||||
|
||||
status = raw.get("freeTrialStatus")
|
||||
status_val = status.strip() if isinstance(status, str) and status.strip() else None
|
||||
|
||||
return cls(
|
||||
current_usage=_int(raw.get("currentUsage")),
|
||||
current_usage_with_precision=_float(raw.get("currentUsageWithPrecision")),
|
||||
usage_limit=_int(raw.get("usageLimit")),
|
||||
usage_limit_with_precision=_float(raw.get("usageLimitWithPrecision")),
|
||||
free_trial_expiry=expiry_f,
|
||||
free_trial_status=status_val,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class UsageBreakdown:
|
||||
current_usage: int = 0
|
||||
current_usage_with_precision: float = 0.0
|
||||
usage_limit: int = 0
|
||||
usage_limit_with_precision: float = 0.0
|
||||
next_date_reset: float | None = None
|
||||
bonuses: list[Bonus] = field(default_factory=list)
|
||||
free_trial_info: FreeTrialInfo | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> "UsageBreakdown | None":
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
|
||||
def _int(v: Any) -> int:
|
||||
try:
|
||||
return int(v)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _float(v: Any) -> float:
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
next_reset = raw.get("nextDateReset")
|
||||
try:
|
||||
next_reset_f = float(next_reset) if next_reset is not None else None
|
||||
except Exception:
|
||||
next_reset_f = None
|
||||
|
||||
bonuses_raw = raw.get("bonuses")
|
||||
bonuses: list[Bonus] = []
|
||||
if isinstance(bonuses_raw, list):
|
||||
for b in bonuses_raw:
|
||||
parsed = Bonus.from_dict(b)
|
||||
if parsed is not None:
|
||||
bonuses.append(parsed)
|
||||
|
||||
return cls(
|
||||
current_usage=_int(raw.get("currentUsage")),
|
||||
current_usage_with_precision=_float(raw.get("currentUsageWithPrecision")),
|
||||
usage_limit=_int(raw.get("usageLimit")),
|
||||
usage_limit_with_precision=_float(raw.get("usageLimitWithPrecision")),
|
||||
next_date_reset=next_reset_f,
|
||||
bonuses=bonuses,
|
||||
free_trial_info=FreeTrialInfo.from_dict(raw.get("freeTrialInfo")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class UsageLimitsResponse:
|
||||
next_date_reset: float | None = None
|
||||
subscription_info: SubscriptionInfo | None = None
|
||||
usage_breakdown_list: list[UsageBreakdown] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> "UsageLimitsResponse":
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
|
||||
next_reset = raw.get("nextDateReset")
|
||||
try:
|
||||
next_reset_f = float(next_reset) if next_reset is not None else None
|
||||
except Exception:
|
||||
next_reset_f = None
|
||||
|
||||
breakdown_raw = raw.get("usageBreakdownList")
|
||||
breakdowns: list[UsageBreakdown] = []
|
||||
if isinstance(breakdown_raw, list):
|
||||
for b in breakdown_raw:
|
||||
parsed = UsageBreakdown.from_dict(b)
|
||||
if parsed is not None:
|
||||
breakdowns.append(parsed)
|
||||
|
||||
return cls(
|
||||
next_date_reset=next_reset_f,
|
||||
subscription_info=SubscriptionInfo.from_dict(raw.get("subscriptionInfo")),
|
||||
usage_breakdown_list=breakdowns,
|
||||
)
|
||||
|
||||
|
||||
def calculate_total_usage_limit(response: UsageLimitsResponse) -> float:
|
||||
if not response.usage_breakdown_list:
|
||||
return 0.0
|
||||
|
||||
breakdown = response.usage_breakdown_list[0]
|
||||
total = breakdown.usage_limit_with_precision
|
||||
|
||||
if breakdown.free_trial_info and breakdown.free_trial_info.free_trial_status == "ACTIVE":
|
||||
total += breakdown.free_trial_info.usage_limit_with_precision
|
||||
|
||||
for bonus in breakdown.bonuses:
|
||||
if bonus.status == "ACTIVE":
|
||||
total += bonus.usage_limit
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def calculate_current_usage(response: UsageLimitsResponse) -> float:
|
||||
if not response.usage_breakdown_list:
|
||||
return 0.0
|
||||
|
||||
breakdown = response.usage_breakdown_list[0]
|
||||
total = breakdown.current_usage_with_precision
|
||||
|
||||
if breakdown.free_trial_info and breakdown.free_trial_info.free_trial_status == "ACTIVE":
|
||||
total += breakdown.free_trial_info.current_usage_with_precision
|
||||
|
||||
for bonus in breakdown.bonuses:
|
||||
if bonus.status == "ACTIVE":
|
||||
total += bonus.current_usage
|
||||
|
||||
return total
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Bonus",
|
||||
"FreeTrialInfo",
|
||||
"SubscriptionInfo",
|
||||
"UsageBreakdown",
|
||||
"UsageLimitsResponse",
|
||||
"calculate_current_usage",
|
||||
"calculate_total_usage_limit",
|
||||
]
|
||||
Reference in New Issue
Block a user