refactor: 收口 V3 分层架构与插件兼容边界

This commit is contained in:
jxxghp
2026-08-18 13:22:02 +08:00
parent cca99bd421
commit 8472bcff43
274 changed files with 10730 additions and 6130 deletions
+27 -21
View File
@@ -25,6 +25,16 @@ from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryI
from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo
from app.schemas.dashboard import ProcessInfo as _SchemaProcessInfo
from version import APP_VERSION
from app.foundation.environment import (
is_aarch,
is_aarch64,
is_docker,
is_frozen,
is_macos,
is_windows,
is_x86_32,
is_x86_64,
)
# Linux amd64/arm64 UAPI: _IOR(BTRFS_IOCTL_MAGIC, 31, struct btrfs_ioctl_fs_info_args)
@@ -142,7 +152,7 @@ class SystemUtils:
"""
判断是否为Docker环境
"""
return Path("/.dockerenv").exists()
return is_docker()
@staticmethod
def is_synology() -> bool:
@@ -158,50 +168,49 @@ class SystemUtils:
"""
判断是否为Windows系统
"""
return os.name == "nt"
return is_windows()
@staticmethod
def is_frozen() -> bool:
"""
判断是否为冻结的二进制文件
"""
return getattr(sys, 'frozen', False)
return is_frozen()
@staticmethod
def is_macos() -> bool:
"""
判断是否为MacOS系统
"""
return platform.system() == 'Darwin'
return is_macos()
@staticmethod
def is_aarch64() -> bool:
"""
判断是否为ARM64架构
"""
return platform.machine().lower() in ('aarch64', 'arm64')
return is_aarch64()
@staticmethod
def is_aarch() -> bool:
"""
判断是否为ARM32架构
"""
arch_name = platform.machine().lower()
return arch_name.startswith(('arm', 'aarch')) and arch_name not in ('aarch64', 'arm64')
return is_aarch()
@staticmethod
def is_x86_64() -> bool:
"""
判断是否为AMD64架构
"""
return platform.machine().lower() in ('amd64', 'x86_64')
return is_x86_64()
@staticmethod
def is_x86_32() -> bool:
"""
判断是否为AMD32架构
"""
return platform.machine().lower() in ('i386', 'i686', 'x86', '386', 'x86_32')
return is_x86_32()
@staticmethod
def platform() -> str:
@@ -224,14 +233,13 @@ class SystemUtils:
"""
if SystemUtils.is_x86_64():
return "x86_64"
elif SystemUtils.is_x86_32():
if SystemUtils.is_x86_32():
return "x86_32"
elif SystemUtils.is_aarch64():
if SystemUtils.is_aarch64():
return "Arm64"
elif SystemUtils.is_aarch():
if SystemUtils.is_aarch():
return "Arm32"
else:
return platform.machine()
return platform.machine()
@staticmethod
def copy(src: Path, dest: Path) -> Tuple[int, str]:
@@ -895,16 +903,14 @@ class SystemUtils:
"""
获取配置路径
"""
if not config_dir:
config_dir = os.getenv("CONFIG_DIR")
if config_dir:
return Path(config_dir)
configured = config_dir or os.getenv("CONFIG_DIR")
if configured:
return Path(configured)
if SystemUtils.is_docker():
return Path("/config")
elif SystemUtils.is_frozen():
if SystemUtils.is_frozen():
return Path(sys.executable).parent / "config"
else:
return Path(__file__).resolve().parents[3] / "config"
return Path(__file__).resolve().parents[3] / "config"
@staticmethod
def get_env_path() -> Path:
+1
View File
@@ -0,0 +1 @@
"""Web 传输层认证适配器。"""
+267
View File
@@ -0,0 +1,267 @@
"""把应用安全能力适配为 FastAPI 认证依赖和 Cookie 行为。"""
import datetime
from datetime import timedelta
from typing import Annotated, Any, Callable, Optional
import jwt
from fastapi import HTTPException, Request, Response, Security, status
from fastapi.security import (
APIKeyCookie,
APIKeyHeader,
APIKeyQuery,
HTTPBearer,
OAuth2PasswordBearer,
)
from app.runtime.cache import cached
from app.runtime.config import settings
from app.runtime.log import logger
from app.schemas.token import TokenPayload
SuperuserTokenPayloadProvider = Callable[[], TokenPayload]
TokenEncoder = Callable[..., str]
TokenDecoder = Callable[[str | None, str], TokenPayload]
_superuser_token_payload_provider: Optional[SuperuserTokenPayloadProvider] = None
_token_encoder: Optional[TokenEncoder] = None
_token_decoder: Optional[TokenDecoder] = None
JWT_ALGORITHM = "HS256"
oauth2_scheme_manual_error = OAuth2PasswordBearer(
auto_error=False,
tokenUrl=f"{settings.API_V1_STR}/login/access-token",
)
resource_token_cookie = APIKeyCookie(
name=settings.PROJECT_NAME,
auto_error=False,
scheme_name="resource_token_cookie",
)
api_token_query = APIKeyQuery(
name="token",
auto_error=False,
scheme_name="api_token_query",
)
api_key_header = APIKeyHeader(
name="X-API-KEY",
auto_error=False,
scheme_name="api_key_header",
)
api_key_query = APIKeyQuery(
name="apikey",
auto_error=False,
scheme_name="api_key_query",
)
openai_bearer_scheme = HTTPBearer(auto_error=False)
anthropic_api_key_header = APIKeyHeader(
name="x-api-key",
auto_error=False,
scheme_name="anthropic_api_key_header",
)
def set_superuser_token_payload_provider(
provider: SuperuserTokenPayloadProvider,
) -> None:
"""由启动组合根注入 API 密钥认证使用的超级用户载荷来源。"""
global _superuser_token_payload_provider
_superuser_token_payload_provider = provider
def configure_token_codec(
encoder: TokenEncoder,
decoder: TokenDecoder,
) -> None:
"""由组合根注入框架无关的令牌编码与解码能力。"""
global _token_encoder, _token_decoder
_token_encoder = encoder
_token_decoder = decoder
def _encode_token(**claims: Any) -> str:
"""使用已注入编码器创建令牌,未装配时给出明确错误。"""
if _token_encoder is None:
raise RuntimeError("Web 认证令牌编码器尚未配置")
return _token_encoder(**claims)
def _decode_token(token: str | None, purpose: str) -> TokenPayload:
"""使用已注入解码器验证令牌,未装配时给出明确错误。"""
if _token_decoder is None:
raise RuntimeError("Web 认证令牌解码器尚未配置")
return _token_decoder(token, purpose)
def _get_api_token(
token_query: Annotated[str | None, Security(api_token_query)] = None,
) -> str | None:
"""从 URL 查询参数读取兼容 API Token。"""
return token_query
def _get_api_key(
key_query: Annotated[str | None, Security(api_key_query)] = None,
key_header: Annotated[str | None, Security(api_key_header)] = None,
) -> str | None:
"""优先从请求头、其次从查询参数读取兼容 API Key。"""
return key_header or key_query
@cached(maxsize=1, ttl=600)
def _create_superuser_token_payload() -> TokenPayload:
"""使用组合根提供器创建 API 密钥调用的超级用户载荷。"""
if not _superuser_token_payload_provider:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="认证服务尚未初始化",
)
try:
return _superuser_token_payload_provider()
except PermissionError as error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(error) or "用户权限不足",
) from error
def set_or_refresh_resource_token_cookie(
request: Request,
response: Response,
payload: TokenPayload,
) -> None:
"""复用匹配的资源令牌,或为当前身份写入新的安全 Cookie。"""
resource_token = request.cookies.get(settings.PROJECT_NAME)
if resource_token:
try:
decoded = jwt.decode(
resource_token,
settings.RESOURCE_SECRET_KEY,
algorithms=[JWT_ALGORITHM],
)
exp = decoded.get("exp")
if exp:
remaining_time = datetime.datetime.fromtimestamp(
exp,
tz=datetime.UTC,
) - datetime.datetime.now(datetime.UTC)
if remaining_time < timedelta(
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS / 3
):
raise jwt.ExpiredSignatureError
expected_claims = {
"sub": str(payload.sub),
"username": payload.username,
"super_user": payload.super_user,
"level": payload.level,
"purpose": "resource",
}
if any(
decoded.get(claim) != value
for claim, value in expected_claims.items()
):
raise jwt.InvalidTokenError("资源令牌身份或权限上下文不匹配")
except jwt.PyJWTError:
logger.debug("Token error occurred. refreshing token")
except Exception as error:
logger.debug(
f"Unexpected error occurred while decoding token: {error}"
)
else:
return
resource_token = _encode_token(
userid=payload.sub,
username=payload.username or "",
super_user=payload.super_user,
expires_delta=timedelta(
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS
),
level=payload.level,
purpose="resource",
)
is_https = (
request.url.scheme == "https"
or request.headers.get("x-forwarded-proto", "").lower() == "https"
)
response.set_cookie(
key=settings.PROJECT_NAME,
value=resource_token,
httponly=True,
secure=is_https,
samesite="lax",
)
def _decode_or_http_error(
token: str | None,
purpose: str,
) -> TokenPayload:
"""把应用层令牌校验错误转换为 HTTP 403。"""
try:
return _decode_token(token, purpose)
except ValueError as error:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(error),
) from error
def verify_token(
request: Request,
response: Response,
jwt_token: Annotated[
str | None,
Security(oauth2_scheme_manual_error),
],
api_key: Annotated[str | None, Security(_get_api_key)],
api_token: Annotated[str | None, Security(_get_api_token)],
) -> TokenPayload:
"""验证 JWT、API Key 或 API Token,并维护资源 Cookie。"""
if jwt_token:
payload = _decode_or_http_error(jwt_token, "authentication")
set_or_refresh_resource_token_cookie(request, response, payload)
return payload
if api_key:
verify_apikey(api_key)
return _create_superuser_token_payload()
if api_token:
verify_apitoken(api_token)
return _create_superuser_token_payload()
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
def verify_resource_token(
resource_token: Annotated[
str | None,
Security(resource_token_cookie),
],
) -> TokenPayload:
"""验证 Cookie 中携带的资源访问令牌。"""
return _decode_or_http_error(resource_token, "resource")
def _verify_key(key: str | None, expected_key: str, key_type: str) -> str:
"""校验受信第三方集成使用的固定 API 凭据。"""
if not key or key != expected_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"{key_type} 校验不通过",
)
return key
def verify_apitoken(
token: Annotated[str | None, Security(_get_api_token)],
) -> str:
"""校验 URL 查询参数中的兼容 API Token。"""
return _verify_key(token, settings.API_TOKEN, "token")
def verify_apikey(
apikey: Annotated[str | None, Security(_get_api_key)],
) -> str:
"""校验请求头或查询参数中的兼容 API Key。"""
return _verify_key(apikey, settings.API_TOKEN, "apikey")