commit e6047e36d5ded83fed5ffaee97e722223f7cbcb6 Author: gouki Date: Sat Aug 1 13:18:04 2026 +0000 feat: 初始化 vps-manager 资产管理系统(Asset/VPSDetail/DomainDetail/AIAccount 模型 + FastAPI 骨架) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2b74fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +.venv/ +data/ +.env +.DS_Store diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..69ea842 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""vps-manager 资产管理系统""" diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..8da6310 --- /dev/null +++ b/app/database.py @@ -0,0 +1,21 @@ +"""SQLite 数据库连接与初始化""" + +from pathlib import Path + +from sqlmodel import SQLModel, create_engine + +BASE_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = BASE_DIR / "data" +DATA_DIR.mkdir(parents=True, exist_ok=True) + +DB_URL = f"sqlite:///{DATA_DIR / 'vps_manager.db'}" + +engine = create_engine(DB_URL, echo=False, connect_args={"check_same_thread": False}) + + +def init_db() -> None: + """创建所有表(幂等,可重复调用)""" + # 显式导入模型,确保其注册到 SQLModel.metadata + from app.models.asset import AIAccount, Asset, DomainDetail, VPSDetail # noqa: F401 + + SQLModel.metadata.create_all(engine) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..b838045 --- /dev/null +++ b/app/main.py @@ -0,0 +1,23 @@ +"""vps-manager 后端入口""" + +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from app.database import init_db + + +@asynccontextmanager +async def lifespan(_: FastAPI): + """应用启动时自动建表""" + init_db() + yield + + +app = FastAPI(title="VPS 资产管理系统", version="0.1.0", lifespan=lifespan) + + +@app.get("/health") +def health_check() -> dict: + """健康检查""" + return {"status": "ok", "app": "vps-manager", "version": "0.1.0"} diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..82b9e0c --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +"""数据库模型包""" diff --git a/app/models/asset.py b/app/models/asset.py new file mode 100644 index 0000000..2ded2aa --- /dev/null +++ b/app/models/asset.py @@ -0,0 +1,136 @@ +"""资产数据库模型 + +包含四种核心资产模型: +- Asset: 资产主表(统一登记 VPS/域名/AI 账号/Cloudflare 等一切资产) +- VPSDetail: VPS 资产详情(与 Asset 一对一关联) +- DomainDetail: 域名资产详情(与 Asset 一对一关联) +- AIAccount: AI Agent 账号详情(与 Asset 一对一关联) +""" + +from datetime import date, datetime +from enum import Enum +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class AssetType(str, Enum): + """资产类型""" + + VPS = "vps" # 云服务器 + DOMAIN = "domain" # 域名 + AI_AGENT = "ai_agent" # AI Agent 账号(OpenAI/Kimi/DeepSeek 等) + CLOUDFLARE = "cloudflare" # Cloudflare 账号/子资产(DNS/Workers/R2/Tunnel) + OTHER = "other" # 其他 + + +class AssetStatus(str, Enum): + """资产运行状态""" + + ACTIVE = "active" # 正常使用中 + EXPIRED = "expired" # 已过期 + STOPPED = "stopped" # 已停止(不再使用) + CANCELLED = "cancelled" # 已注销 + UNKNOWN = "unknown" # 未知(待确认) + + +class Asset(SQLModel, table=True): + """资产主表:所有资产在此统一登记,用于续费提醒与状态总览""" + + __tablename__ = "assets" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True, description="资产名称,如:博客主站 VPS") + asset_type: AssetType = Field(index=True, description="资产类型") + provider: str = Field(index=True, description="服务商,如 aliyun/vultr/namesilo/openai") + account: Optional[str] = Field( + default=None, description="所属账号标识(如 Cloudflare 账号邮箱)" + ) + expiry_date: Optional[date] = Field( + default=None, index=True, description="到期/续费日期,用于续费提醒" + ) + auto_renew: bool = Field(default=False, description="是否自动续费") + cost: float = Field(default=0, description="单次续费费用") + currency: str = Field(default="USD", description="币种(USD/CNY/JPY)") + status: AssetStatus = Field(default=AssetStatus.ACTIVE, index=True, description="运行状态") + is_archived: bool = Field( + default=False, description="已归档:不再使用但可能仍在续费,需重点排查" + ) + remark: Optional[str] = Field(default=None, description="备注") + created_at: datetime = Field(default_factory=datetime.utcnow, description="创建时间") + updated_at: datetime = Field( + default_factory=datetime.utcnow, + sa_column_kwargs={"onupdate": datetime.utcnow}, + description="更新时间", + ) + + +class VPSDetail(SQLModel, table=True): + """VPS 资产详情(一对一关联 Asset)""" + + __tablename__ = "vps_details" + + id: Optional[int] = Field(default=None, primary_key=True) + asset_id: int = Field( + foreign_key="assets.id", unique=True, index=True, description="关联资产 ID" + ) + ip_address: str = Field(index=True, description="公网 IP") + tailscale_ip: Optional[str] = Field(default=None, description="Tailscale 内网 IP") + region: Optional[str] = Field(default=None, description="机房区域,如 Japan/US West") + os: Optional[str] = Field(default=None, description="操作系统,如 Debian 12") + cpu_cores: int = Field(default=1, description="CPU 核数") + memory_gb: float = Field(default=1, description="内存大小(GB)") + disk_gb: int = Field(default=20, description="磁盘大小(GB)") + bandwidth_gb: Optional[int] = Field(default=None, description="月流量配额(GB)") + ssh_port: int = Field(default=22, description="SSH 端口") + panel_url: Optional[str] = Field(default=None, description="服务商管理面板 URL") + + +class DomainDetail(SQLModel, table=True): + """域名资产详情(一对一关联 Asset)""" + + __tablename__ = "domain_details" + + id: Optional[int] = Field(default=None, primary_key=True) + asset_id: int = Field( + foreign_key="assets.id", unique=True, index=True, description="关联资产 ID" + ) + domain_name: str = Field(index=True, description="域名,如 example.com") + registrar: Optional[str] = Field( + default=None, description="注册商:namesilo/新网/腾讯/阿里/dynadot/cloudflare" + ) + dns_provider: Optional[str] = Field( + default=None, description="DNS 托管商:cloudflare/阿里云/注册商默认" + ) + cloudflare_account: Optional[str] = Field( + default=None, description="归属的 Cloudflare 账号标识" + ) + is_using: bool = Field(default=True, description="是否仍在实际使用") + redirect_target: Optional[str] = Field( + default=None, description="若为跳转用途域名,填写目标地址" + ) + + +class AIAccount(SQLModel, table=True): + """AI Agent 账号资产详情(一对一关联 Asset)""" + + __tablename__ = "ai_accounts" + + id: Optional[int] = Field(default=None, primary_key=True) + asset_id: int = Field( + foreign_key="assets.id", unique=True, index=True, description="关联资产 ID" + ) + provider: str = Field( + index=True, description="服务商:openai/minimax/kimi/agnes/deepseek" + ) + api_key: Optional[str] = Field( + default=None, description="API Key(后续版本将改为加密存储)" + ) + plan: Optional[str] = Field(default=None, description="订阅计划,如 ChatGPT Plus") + balance: Optional[float] = Field(default=None, description="当前余额") + currency: str = Field(default="USD", description="币种") + monthly_usage: Optional[float] = Field(default=None, description="本月用量") + monthly_limit: Optional[float] = Field(default=None, description="月度限额") + last_synced_at: Optional[datetime] = Field( + default=None, description="最近一次自动同步余额/用量的时间" + ) diff --git a/deploy/vps-manager.service b/deploy/vps-manager.service new file mode 100644 index 0000000..68d284f --- /dev/null +++ b/deploy/vps-manager.service @@ -0,0 +1,18 @@ +# vps-manager systemd 服务配置 +# 部署位置:/etc/systemd/system/vps-manager.service +# 使用方法:systemctl daemon-reload && systemctl enable --now vps-manager + +[Unit] +Description=VPS Asset Manager (vps-manager) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/opt/vps-manager +ExecStart=/opt/vps-manager/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f9e334 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.115 +uvicorn[standard]>=0.30 +sqlmodel>=0.0.22 +pydantic>=2.7 +python-dotenv>=1.0 diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..763d4fa --- /dev/null +++ b/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# vps-manager 启动脚本(部署于 Tailscale cc1 节点 100.89.0.11) +# 用法:./run.sh # 前台启动 +# ./run.sh --reload # 开发模式(热重载) + +cd "$(dirname "$0")" || exit 1 + +if [ ! -d ".venv" ]; then + echo "[init] 创建虚拟环境并安装依赖..." + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt +fi + +exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 "$@"