feat: 平台预设字典+凭证加密+分库备份+Agent采集+移动端优先前端重做

This commit is contained in:
gouki
2026-08-01 23:10:31 +00:00
parent 1dac5d8735
commit e672086b67
31 changed files with 1792 additions and 421 deletions
+5
View File
@@ -17,7 +17,12 @@ class Settings:
"""应用配置项"""
APP_NAME: str = os.getenv("APP_NAME", "VPS 资产管理系统")
# 写操作(POST/PUT/DELETE)鉴权密钥;留空则不校验
API_KEY: str = os.getenv("API_KEY", "")
# Agent 上报鉴权密钥;配置后 Agent 上报需携带 X-Agent-Key
AGENT_KEY: str = os.getenv("AGENT_KEY", "")
# 凭证加密主密钥(Fernet);生成:python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
MASTER_KEY: str = os.getenv("MASTER_KEY", "")
settings = Settings()
+40
View File
@@ -0,0 +1,40 @@
"""凭证加密模块(Fernet 对称加密)
用于加密存储 SSH 密钥、密码、API Key、平台 API 配置等敏感信息。
MASTER_KEY 从 .env 读取,不入库。
"""
from typing import Optional
from cryptography.fernet import Fernet, InvalidToken
from app.core.config import settings
def _get_fernet() -> Fernet:
"""获取 Fernet 实例(MASTER_KEY 未配置时抛错)"""
key = settings.MASTER_KEY
if not key:
raise RuntimeError(
"MASTER_KEY 未配置,无法加解密凭证。请在 .env 设置 MASTER_KEY"
"生成命令:python -c 'from cryptography.fernet import Fernet; "
"print(Fernet.generate_key().decode())'"
)
return Fernet(key.encode() if isinstance(key, str) else key)
def encrypt(plain: Optional[str]) -> Optional[str]:
"""加密明文,返回 token 字符串;空值原样返回 None"""
if plain is None or plain == "":
return None
return _get_fernet().encrypt(plain.encode()).decode()
def decrypt(token: Optional[str]) -> Optional[str]:
"""解密 token,返回明文;空值或解密失败返回 None"""
if not token:
return None
try:
return _get_fernet().decrypt(token.encode()).decode()
except (InvalidToken, ValueError):
return None
+13
View File
@@ -23,3 +23,16 @@ async def require_api_key(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API Key 无效或缺失",
)
async def require_agent_key(
x_agent_key: Optional[str] = Header(default=None, alias="X-Agent-Key"),
) -> None:
"""校验 Agent 上报 Key(可选启用)"""
if not settings.AGENT_KEY:
return
if x_agent_key != settings.AGENT_KEY:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Agent Key 无效或缺失",
)
+66
View File
@@ -0,0 +1,66 @@
"""平台预设种子数据
首次部署或调用 POST /api/providers/seed 时初始化常见平台。
每个平台有唯一 slug,供未来按 sdk_type 对接自动同步扩展。
"""
from sqlmodel import Session, select
from app.models.provider import Provider
PRESET_PROVIDERS = [
# ---- VPS / 云服务商 ----
{"slug": "aliyun", "name": "阿里云", "name_en": "Aliyun", "category": "vps",
"website": "https://aliyun.com", "console_url": "https://ecs.console.aliyun.com", "sdk_type": "aliyun-sdk"},
{"slug": "tencent", "name": "腾讯云", "name_en": "Tencent Cloud", "category": "vps",
"website": "https://cloud.tencent.com", "console_url": "https://console.cloud.tencent.com/cvm", "sdk_type": "tencent-sdk"},
{"slug": "aliyun-intl", "name": "阿里国际", "name_en": "Alibaba Cloud", "category": "vps",
"website": "https://alibabacloud.com", "console_url": "https://ecs.console.alibabacloud.com", "sdk_type": "alibabacloud-sdk"},
{"slug": "tencent-intl", "name": "腾讯国际", "name_en": "Tencent Cloud Intl", "category": "vps",
"website": "https://intl.cloud.tencent.com", "console_url": "https://console.intl.cloud.tencent.com", "sdk_type": "tencent-intl-sdk"},
{"slug": "vultr", "name": "Vultr", "name_en": "Vultr", "category": "vps",
"website": "https://vultr.com", "console_url": "https://my.vultr.com", "sdk_type": "vultr-api"},
{"slug": "digitalocean", "name": "DigitalOcean", "name_en": "DigitalOcean", "category": "vps",
"website": "https://digitalocean.com", "console_url": "https://cloud.digitalocean.com", "sdk_type": "do-api"},
{"slug": "linode", "name": "Linode", "name_en": "Akamai Linode", "category": "vps",
"website": "https://linode.com", "console_url": "https://cloud.linode.com", "sdk_type": "linode-api"},
{"slug": "cloudcone", "name": "CloudCone", "name_en": "CloudCone", "category": "vps",
"website": "https://cloudcone.com", "console_url": "https://app.cloudcone.com", "sdk_type": "cloudcone-api"},
{"slug": "zeabur", "name": "Zeabur", "name_en": "Zeabur", "category": "vps",
"website": "https://zeabur.com", "console_url": "https://dash.zeabur.com", "sdk_type": "zeabur-api"},
# ---- 域名注册商 ----
{"slug": "namesilo", "name": "Namesilo", "name_en": "Namesilo", "category": "domain",
"website": "https://namesilo.com", "console_url": "https://www.namesilo.com/account_domains.php", "sdk_type": "namesilo-api"},
{"slug": "xinwang", "name": "新网", "name_en": "Xinnet", "category": "domain",
"website": "https://xinnet.com", "console_url": "https://www.xinnet.com", "sdk_type": None},
{"slug": "dynadot", "name": "Dynadot", "name_en": "Dynadot", "category": "domain",
"website": "https://dynadot.com", "console_url": "https://www.dynadot.com/account/domains", "sdk_type": "dynadot-api"},
# ---- AI 服务商 ----
{"slug": "openai", "name": "OpenAI", "name_en": "OpenAI", "category": "ai_agent",
"website": "https://openai.com", "console_url": "https://platform.openai.com", "sdk_type": "openai-api"},
{"slug": "minimax", "name": "Minimax", "name_en": "Minimax", "category": "ai_agent",
"website": "https://minimax.io", "console_url": "https://platform.minimaxi.com", "sdk_type": "minimax-api"},
{"slug": "kimi", "name": "Kimi", "name_en": "Moonshot", "category": "ai_agent",
"website": "https://moonshot.cn", "console_url": "https://platform.moonshot.cn", "sdk_type": "moonshot-api"},
{"slug": "agnes", "name": "Agnes", "name_en": "Agnes", "category": "ai_agent",
"website": None, "console_url": None, "sdk_type": None},
{"slug": "deepseek", "name": "DeepSeek", "name_en": "DeepSeek", "category": "ai_agent",
"website": "https://deepseek.com", "console_url": "https://platform.deepseek.com", "sdk_type": "deepseek-api"},
# ---- Cloudflare ----
{"slug": "cloudflare", "name": "Cloudflare", "name_en": "Cloudflare", "category": "cloudflare",
"website": "https://cloudflare.com", "console_url": "https://dash.cloudflare.com", "sdk_type": "cloudflare-api"},
]
def seed_providers(session: Session) -> int:
"""初始化预设平台(已存在的 slug 跳过),返回新增数量"""
added = 0
for data in PRESET_PROVIDERS:
existing = session.exec(
select(Provider).where(Provider.slug == data["slug"])
).first()
if not existing:
session.add(Provider(**data))
added += 1
session.commit()
return added