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:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -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",
]

View File

@@ -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 regionIdC 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"]

View File

@@ -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",
]