Files
taiji-AI-PAD/docs/dns-mapping-implementation-plan.md
T
2026-01-14 06:28:45 +00:00

36 KiB

Agent DNS 域名映射功能实现方案

1. 功能概述

1.1 目标

实现 Agent Pod 的 IP 地址与 GoDaddy 域名子域名的自动映射,让用户可以通过域名访问 Agent 服务。

1.2 业务场景

用户创建 Agent → Agent Manager 返回 Pod IP → 自动创建 DNS A 记录 → 用户通过子域名访问 Agent
     ↓
用户删除 Agent → 删除 K8s Pod → 同步删除 DNS A 记录

1.3 核心信息

配置项 值
主域名 taiji.ai.com
GoDaddy API Key gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4
GoDaddy API Secret AMs5RxZeaW3mFZDwXvU3Bs
生产 API 地址 https://api.godaddy.com
测试 API 地址 https://api.ote-godaddy.com

2. 技术架构

2.1 系统架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                           MCP Server                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐  │
│   │   agents.py     │────▶│ godaddy_client   │────▶│  GoDaddy API    │  │
│   │ (Agent CRUD)    │     │   (DNS管理)       │     │  (外部服务)      │  │
│   └────────┬────────┘     └──────────────────┘     └─────────────────┘  │
│            │                                                             │
│            │              ┌──────────────────┐                           │
│            └─────────────▶│ Agent Manager    │                           │
│                           │  (K8s Pod管理)    │                           │
│                           │  返回 Pod IP      │                           │
│                           └──────────────────┘                           │
│                                                                          │
│   ┌─────────────────┐     ┌──────────────────┐                           │
│   │   config.py     │     │   models.py      │                           │
│   │ (GoDaddy配置)    │     │ (subdomain字段)  │                           │
│   └─────────────────┘     └──────────────────┘                           │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

2.2 数据流程图

创建 Agent 流程:
┌──────────┐    ┌───────────────┐    ┌─────────────────┐    ┌──────────────┐
│  用户请求  │───▶│ create_agent  │───▶│ Agent Manager   │───▶│ 获取 Pod IP   │
└──────────┘    └───────────────┘    │ 创建 K8s Pod     │    └──────┬───────┘
                                     └─────────────────┘           │
                                                                   ▼
┌──────────┐    ┌───────────────┐    ┌─────────────────┐    ┌──────────────┐
│ 返回结果  │◀───│ 更新数据库     │◀───│ 创建 DNS A记录  │◀───│GoDaddy Client│
│(含子域名) │    │(存储subdomain)│    │                  │    └──────────────┘
└──────────┘    └───────────────┘    └─────────────────┘

删除 Agent 流程:
┌──────────┐    ┌───────────────┐    ┌─────────────────┐    ┌──────────────┐
│  用户请求  │───▶│ delete_agent  │───▶│ Agent Manager   │───▶│ 删除 K8s Pod │
└──────────┘    └───────────────┘    │ 删除 Pod         │    └──────┬───────┘
                                     └─────────────────┘           │
                                                                   ▼
┌──────────┐    ┌───────────────┐    ┌─────────────────┐    ┌──────────────┐
│ 返回结果  │◀───│ 删除数据库记录 │◀───│ 删除 DNS A记录  │◀───│GoDaddy Client│
└──────────┘    └───────────────┘    └─────────────────┘    └──────────────┘

3. 详细设计

3.1 子域名命名规则

格式: {agent-name}-{agent-id-short}.taiji.ai.com

示例:
- Agent 名称: my-chatbot
- Agent ID: 550e8400-e29b-41d4-a716-446655440000
- 生成子域名: my-chatbot-550e8400.taiji.ai.com

规则说明:
1. agent-name 转为小写,特殊字符替换为 '-'
2. agent-id 取前8位作为唯一标识
3. 子域名长度限制: 最大63字符(DNS标准)
4. 仅允许: a-z, 0-9, '-'(不能以'-'开头或结尾)

3.2 GoDaddy API 调用规范

3.2.1 认证方式

Authorization: sso-key {API_KEY}:{API_SECRET}
Content-Type: application/json

3.2.2 核心接口

操作 方法 端点 说明
创建/更新记录 PUT /v1/domains/{domain}/records/A/{name} 创建或替换 A 记录
删除记录 DELETE /v1/domains/{domain}/records/A/{name} 删除指定 A 记录
查询记录 GET /v1/domains/{domain}/records/A/{name} 获取指定 A 记录
批量添加 PATCH /v1/domains/{domain}/records 批量添加记录

3.2.3 请求/响应示例

创建 A 记录请求:

PUT https://api.godaddy.com/v1/domains/taiji.ai.com/records/A/my-chatbot-550e8400
Authorization: sso-key gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4:AMs5RxZeaW3mFZDwXvU3Bs
Content-Type: application/json

[
  {
    "data": "203.0.113.50",
    "ttl": 600
  }
]

成功响应: 200 OK (无 body)

查询记录响应:

[
  {
    "data": "203.0.113.50",
    "name": "my-chatbot-550e8400",
    "ttl": 600,
    "type": "A"
  }
]

4. 代码实现方案

4.1 文件结构

services/mcp-server/
├── app/
│   ├── godaddy_client.py    # [新增] GoDaddy DNS 客户端
│   ├── dns_service.py       # [新增] DNS 服务层(业务逻辑封装)
│   └── routes/
│       └── agents.py        # [修改] 集成 DNS 创建/删除
├── config.py                # [修改] 添加 GoDaddy 配置
├── models.py                # [修改] Agent 模型添加 subdomain 字段
├── schemas.py               # [修改] 响应模型添加 subdomain
└── migrations/
    └── versions/
        └── xxx_add_subdomain_to_agent.py  # [新增] 数据库迁移

4.2 配置项设计 (config.py)

class Settings(BaseSettings):
    # ... 现有配置 ...
    
    # GoDaddy DNS 配置
    godaddy_api_key: str = os.getenv("GODADDY_API_KEY", "")
    godaddy_api_secret: str = os.getenv("GODADDY_API_SECRET", "")
    godaddy_base_url: str = os.getenv("GODADDY_BASE_URL", "https://api.godaddy.com")
    godaddy_domain: str = os.getenv("GODADDY_DOMAIN", "taiji.ai.com")
    godaddy_dns_ttl: int = int(os.getenv("GODADDY_DNS_TTL", "600"))  # 10分钟
    godaddy_enabled: bool = os.getenv("GODADDY_ENABLED", "true").lower() == "true"

环境变量配置:

# .env 文件
GODADDY_API_KEY=gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4
GODADDY_API_SECRET=AMs5RxZeaW3mFZDwXvU3Bs
GODADDY_BASE_URL=https://api.godaddy.com
GODADDY_DOMAIN=taiji.ai.com
GODADDY_DNS_TTL=600
GODADDY_ENABLED=true

4.3 数据库模型变更 (models.py)

class Agent(Base):
    __tablename__ = "agents"
    
    # ... 现有字段 ...
    
    # DNS 相关字段(新增)
    subdomain = Column(String(255), nullable=True, index=True, comment="子域名")
    full_domain = Column(String(512), nullable=True, comment="完整域名")
    dns_record_id = Column(String(255), nullable=True, comment="DNS记录标识")
    dns_status = Column(String(50), default="pending", comment="DNS状态: pending/active/failed/deleted")
    dns_created_at = Column(DateTime, nullable=True, comment="DNS记录创建时间")
    dns_updated_at = Column(DateTime, nullable=True, comment="DNS记录更新时间")

数据库迁移脚本:

# migrations/versions/xxx_add_subdomain_to_agent.py
"""add subdomain to agent

Revision ID: xxx
"""

def upgrade():
    op.add_column('agents', sa.Column('subdomain', sa.String(255), nullable=True))
    op.add_column('agents', sa.Column('full_domain', sa.String(512), nullable=True))
    op.add_column('agents', sa.Column('dns_record_id', sa.String(255), nullable=True))
    op.add_column('agents', sa.Column('dns_status', sa.String(50), server_default='pending'))
    op.add_column('agents', sa.Column('dns_created_at', sa.DateTime, nullable=True))
    op.add_column('agents', sa.Column('dns_updated_at', sa.DateTime, nullable=True))
    
    op.create_index('ix_agents_subdomain', 'agents', ['subdomain'])

def downgrade():
    op.drop_index('ix_agents_subdomain', 'agents')
    op.drop_column('agents', 'dns_updated_at')
    op.drop_column('agents', 'dns_created_at')
    op.drop_column('agents', 'dns_status')
    op.drop_column('agents', 'dns_record_id')
    op.drop_column('agents', 'full_domain')
    op.drop_column('agents', 'subdomain')

4.4 GoDaddy 客户端设计 (godaddy_client.py)

"""
GoDaddy DNS API 客户端

功能:
- 创建/更新/删除 DNS A 记录
- 查询 DNS 记录状态
- 自动重试和错误处理
"""

import httpx
import structlog
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from config import settings

logger = structlog.get_logger(__name__)


@dataclass
class DNSRecord:
    """DNS 记录数据结构"""
    name: str           # 子域名名称
    data: str           # IP 地址
    ttl: int = 600      # TTL(秒)
    type: str = "A"     # 记录类型


class GoDaddyError(Exception):
    """GoDaddy API 错误"""
    def __init__(self, status_code: int, message: str, detail: Any = None):
        self.status_code = status_code
        self.message = message
        self.detail = detail
        super().__init__(f"GoDaddy API Error [{status_code}]: {message}")


class GoDaddyClient:
    """GoDaddy DNS API 客户端"""
    
    def __init__(
        self,
        api_key: str = None,
        api_secret: str = None,
        base_url: str = None,
        domain: str = None,
        timeout: float = 30.0
    ):
        self.api_key = api_key or settings.godaddy_api_key
        self.api_secret = api_secret or settings.godaddy_api_secret
        self.base_url = base_url or settings.godaddy_base_url
        self.domain = domain or settings.godaddy_domain
        self.timeout = timeout
        self._client: Optional[httpx.AsyncClient] = None
    
    @property
    def _headers(self) -> Dict[str, str]:
        """构建请求头"""
        return {
            "Authorization": f"sso-key {self.api_key}:{self.api_secret}",
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
    
    async def _get_client(self) -> httpx.AsyncClient:
        """获取或创建 HTTP 客户端"""
        if self._client is None:
            self._client = httpx.AsyncClient(
                base_url=self.base_url,
                headers=self._headers,
                timeout=self.timeout
            )
        return self._client
    
    async def close(self):
        """关闭客户端"""
        if self._client:
            await self._client.aclose()
            self._client = None
    
    async def create_a_record(self, name: str, ip: str, ttl: int = None) -> bool:
        """
        创建或更新 A 记录
        
        Args:
            name: 子域名名称(不含主域名)
            ip: IP 地址
            ttl: TTL 秒数,默认使用配置值
        
        Returns:
            bool: 是否成功
        """
        client = await self._get_client()
        ttl = ttl or settings.godaddy_dns_ttl
        
        url = f"/v1/domains/{self.domain}/records/A/{name}"
        payload = [{"data": ip, "ttl": ttl}]
        
        try:
            response = await client.put(url, json=payload)
            
            if response.status_code == 200:
                logger.info("DNS A 记录创建成功", name=name, ip=ip, domain=self.domain)
                return True
            else:
                error_detail = response.json() if response.content else None
                raise GoDaddyError(
                    status_code=response.status_code,
                    message=f"创建 DNS 记录失败",
                    detail=error_detail
                )
        except httpx.HTTPError as e:
            logger.error("GoDaddy API 请求失败", error=str(e), name=name)
            raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}")
    
    async def delete_a_record(self, name: str) -> bool:
        """
        删除 A 记录
        
        Args:
            name: 子域名名称
        
        Returns:
            bool: 是否成功
        """
        client = await self._get_client()
        url = f"/v1/domains/{self.domain}/records/A/{name}"
        
        try:
            response = await client.delete(url)
            
            if response.status_code in (200, 204, 404):
                # 404 也视为成功(记录不存在)
                logger.info("DNS A 记录删除成功", name=name, domain=self.domain)
                return True
            else:
                error_detail = response.json() if response.content else None
                raise GoDaddyError(
                    status_code=response.status_code,
                    message=f"删除 DNS 记录失败",
                    detail=error_detail
                )
        except httpx.HTTPError as e:
            logger.error("GoDaddy API 请求失败", error=str(e), name=name)
            raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}")
    
    async def get_a_record(self, name: str) -> Optional[DNSRecord]:
        """
        查询 A 记录
        
        Args:
            name: 子域名名称
        
        Returns:
            DNSRecord 或 None(不存在时)
        """
        client = await self._get_client()
        url = f"/v1/domains/{self.domain}/records/A/{name}"
        
        try:
            response = await client.get(url)
            
            if response.status_code == 200:
                records = response.json()
                if records and len(records) > 0:
                    r = records[0]
                    return DNSRecord(
                        name=r.get("name", name),
                        data=r.get("data"),
                        ttl=r.get("ttl", 600),
                        type="A"
                    )
                return None
            elif response.status_code == 404:
                return None
            else:
                error_detail = response.json() if response.content else None
                raise GoDaddyError(
                    status_code=response.status_code,
                    message=f"查询 DNS 记录失败",
                    detail=error_detail
                )
        except httpx.HTTPError as e:
            logger.error("GoDaddy API 请求失败", error=str(e), name=name)
            raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}")
    
    async def update_a_record(self, name: str, ip: str, ttl: int = None) -> bool:
        """
        更新 A 记录(实际上与 create 相同,PUT 会覆盖)
        """
        return await self.create_a_record(name, ip, ttl)
    
    async def list_a_records(self) -> List[DNSRecord]:
        """
        列出所有 A 记录
        
        Returns:
            DNSRecord 列表
        """
        client = await self._get_client()
        url = f"/v1/domains/{self.domain}/records/A"
        
        try:
            response = await client.get(url)
            
            if response.status_code == 200:
                records = response.json()
                return [
                    DNSRecord(
                        name=r.get("name"),
                        data=r.get("data"),
                        ttl=r.get("ttl", 600),
                        type="A"
                    )
                    for r in records
                ]
            else:
                error_detail = response.json() if response.content else None
                raise GoDaddyError(
                    status_code=response.status_code,
                    message=f"列出 DNS 记录失败",
                    detail=error_detail
                )
        except httpx.HTTPError as e:
            logger.error("GoDaddy API 请求失败", error=str(e))
            raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}")


# 全局客户端实例(单例模式)
_godaddy_client: Optional[GoDaddyClient] = None


def get_godaddy_client() -> GoDaddyClient:
    """获取 GoDaddy 客户端实例"""
    global _godaddy_client
    if _godaddy_client is None:
        _godaddy_client = GoDaddyClient()
    return _godaddy_client

4.5 DNS 服务层设计 (dns_service.py)

"""
DNS 服务层

封装 DNS 相关业务逻辑:
- 子域名生成规则
- DNS 记录生命周期管理
- 与数据库同步
"""

import re
import structlog
from datetime import datetime
from typing import Optional, Tuple

from config import settings
from .godaddy_client import get_godaddy_client, GoDaddyError

logger = structlog.get_logger(__name__)


class DNSService:
    """DNS 服务"""
    
    def __init__(self):
        self.domain = settings.godaddy_domain
        self.enabled = settings.godaddy_enabled
    
    def generate_subdomain(self, agent_name: str, agent_id: str) -> str:
        """
        生成子域名
        
        规则:
        1. agent_name 转小写
        2. 特殊字符替换为 '-'
        3. 移除连续的 '-'
        4. 添加 agent_id 前8位
        5. 确保不以 '-' 开头或结尾
        6. 总长度限制 63 字符
        
        Args:
            agent_name: Agent 名称
            agent_id: Agent UUID
        
        Returns:
            子域名(不含主域名)
        """
        # 清理名称
        clean_name = agent_name.lower()
        clean_name = re.sub(r'[^a-z0-9-]', '-', clean_name)
        clean_name = re.sub(r'-+', '-', clean_name)
        clean_name = clean_name.strip('-')
        
        # 截取 ID 前8位
        short_id = agent_id.replace('-', '')[:8].lower()
        
        # 组合
        subdomain = f"{clean_name}-{short_id}"
        
        # 长度限制(DNS 标准最大63字符)
        if len(subdomain) > 63:
            max_name_len = 63 - len(short_id) - 1
            clean_name = clean_name[:max_name_len].rstrip('-')
            subdomain = f"{clean_name}-{short_id}"
        
        return subdomain
    
    def get_full_domain(self, subdomain: str) -> str:
        """获取完整域名"""
        return f"{subdomain}.{self.domain}"
    
    async def create_dns_record(
        self, 
        agent_name: str, 
        agent_id: str, 
        ip: str
    ) -> Tuple[str, str]:
        """
        为 Agent 创建 DNS 记录
        
        Args:
            agent_name: Agent 名称
            agent_id: Agent UUID
            ip: Pod IP 地址
        
        Returns:
            (subdomain, full_domain)
        
        Raises:
            GoDaddyError: DNS 创建失败
        """
        if not self.enabled:
            logger.warning("DNS 功能未启用,跳过创建")
            return ("", "")
        
        if not ip:
            logger.warning("IP 地址为空,跳过 DNS 创建", agent_id=agent_id)
            return ("", "")
        
        subdomain = self.generate_subdomain(agent_name, agent_id)
        full_domain = self.get_full_domain(subdomain)
        
        client = get_godaddy_client()
        
        try:
            await client.create_a_record(subdomain, ip)
            logger.info(
                "DNS 记录创建成功",
                agent_id=agent_id,
                subdomain=subdomain,
                full_domain=full_domain,
                ip=ip
            )
            return (subdomain, full_domain)
        except GoDaddyError as e:
            logger.error(
                "DNS 记录创建失败",
                agent_id=agent_id,
                subdomain=subdomain,
                ip=ip,
                error=str(e)
            )
            raise
    
    async def delete_dns_record(self, subdomain: str) -> bool:
        """
        删除 DNS 记录
        
        Args:
            subdomain: 子域名
        
        Returns:
            是否成功
        """
        if not self.enabled:
            logger.warning("DNS 功能未启用,跳过删除")
            return True
        
        if not subdomain:
            logger.warning("子域名为空,跳过 DNS 删除")
            return True
        
        client = get_godaddy_client()
        
        try:
            await client.delete_a_record(subdomain)
            logger.info("DNS 记录删除成功", subdomain=subdomain)
            return True
        except GoDaddyError as e:
            logger.error("DNS 记录删除失败", subdomain=subdomain, error=str(e))
            # 删除失败不阻塞 Agent 删除流程
            return False
    
    async def update_dns_record(
        self, 
        subdomain: str, 
        new_ip: str
    ) -> bool:
        """
        更新 DNS 记录的 IP 地址
        
        Args:
            subdomain: 子域名
            new_ip: 新的 IP 地址
        
        Returns:
            是否成功
        """
        if not self.enabled:
            return False
        
        client = get_godaddy_client()
        
        try:
            await client.update_a_record(subdomain, new_ip)
            logger.info("DNS 记录更新成功", subdomain=subdomain, new_ip=new_ip)
            return True
        except GoDaddyError as e:
            logger.error("DNS 记录更新失败", subdomain=subdomain, error=str(e))
            return False
    
    async def check_dns_record(self, subdomain: str) -> Optional[str]:
        """
        检查 DNS 记录状态
        
        Args:
            subdomain: 子域名
        
        Returns:
            IP 地址,或 None(不存在)
        """
        if not self.enabled:
            return None
        
        client = get_godaddy_client()
        
        try:
            record = await client.get_a_record(subdomain)
            return record.data if record else None
        except GoDaddyError:
            return None


# 全局服务实例
_dns_service: Optional[DNSService] = None


def get_dns_service() -> DNSService:
    """获取 DNS 服务实例"""
    global _dns_service
    if _dns_service is None:
        _dns_service = DNSService()
    return _dns_service

4.6 Agent 路由修改 (agents.py)

# 在 create_agent 函数中集成 DNS 创建

from ..dns_service import get_dns_service, GoDaddyError as DNSError

@router.post("", response_model=AgentCard)
async def create_agent(
    request: AgentCreateRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
) -> AgentCard:
    """创建新的 Agent"""
    
    # ... 现有代码:创建 Agent 和 K8s Pod ...
    
    # ========== 新增: DNS 记录创建 ==========
    if agent.pod_ip and settings.godaddy_enabled:
        try:
            dns_service = get_dns_service()
            subdomain, full_domain = await dns_service.create_dns_record(
                agent_name=agent.name,
                agent_id=str(agent.id),
                ip=agent.pod_ip
            )
            
            # 更新数据库
            agent.subdomain = subdomain
            agent.full_domain = full_domain
            agent.dns_status = "active"
            agent.dns_created_at = datetime.utcnow()
            
            # 更新 access_url
            if full_domain:
                agent.access_url = f"http://{full_domain}:{agent.service_port or 8000}"
            
            logger.info(
                "DNS 记录创建成功",
                agent_id=str(agent.id),
                subdomain=subdomain,
                full_domain=full_domain
            )
            
        except DNSError as e:
            # DNS 创建失败不阻塞 Agent 创建
            logger.warning(
                "DNS 记录创建失败,Agent 仍可通过 IP 访问",
                agent_id=str(agent.id),
                error=str(e)
            )
            agent.dns_status = "failed"
    # ========================================
    
    await db.commit()
    await db.refresh(agent)
    
    return _build_agent_card(agent)


@router.delete("/{agent_id}")
async def delete_agent(
    agent_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
) -> Dict[str, Any]:
    """删除 Agent"""
    
    # ... 现有代码:权限检查 ...
    
    # ========== 新增: DNS 记录删除 ==========
    if agent.subdomain and settings.godaddy_enabled:
        try:
            dns_service = get_dns_service()
            await dns_service.delete_dns_record(agent.subdomain)
            logger.info(
                "DNS 记录删除成功",
                agent_id=agent_id,
                subdomain=agent.subdomain
            )
        except Exception as e:
            # DNS 删除失败不阻塞 Agent 删除
            logger.warning(
                "DNS 记录删除失败",
                agent_id=agent_id,
                subdomain=agent.subdomain,
                error=str(e)
            )
    # ========================================
    
    # ... 现有代码:删除 K8s Pod 和数据库记录 ...

4.7 响应模型更新 (schemas.py)

class AgentCard(BaseModel):
    """Agent 卡片信息"""
    
    # ... 现有字段 ...
    
    # DNS 相关字段(新增)
    subdomain: Optional[str] = None
    full_domain: Optional[str] = None
    dns_status: Optional[str] = None  # pending/active/failed/deleted

5. API 接口设计

5.1 新增 DNS 管理接口

5.1.1 获取 Agent DNS 状态

GET /api/agents/{agent_id}/dns

Response 200:
{
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "subdomain": "my-chatbot-550e8400",
  "full_domain": "my-chatbot-550e8400.taiji.ai.com",
  "ip": "203.0.113.50",
  "ttl": 600,
  "dns_status": "active",
  "created_at": "2026-01-14T12:00:00Z"
}

5.1.2 刷新 DNS 记录

POST /api/agents/{agent_id}/dns/refresh

Response 200:
{
  "status": "success",
  "message": "DNS 记录已刷新",
  "old_ip": "10.0.0.5",
  "new_ip": "203.0.113.50"
}

5.1.3 手动删除 DNS 记录

DELETE /api/agents/{agent_id}/dns

Response 200:
{
  "status": "success",
  "message": "DNS 记录已删除"
}

6. 错误处理策略

6.1 错误码定义

错误码 场景 处理方式
DNS_001 GoDaddy API 认证失败 检查 API Key/Secret,告警
DNS_002 域名不存在或无权限 检查域名配置,告警
DNS_003 记录创建失败 重试3次,失败后设置 dns_status=failed
DNS_004 记录删除失败 记录日志,不阻塞主流程
DNS_005 IP 地址无效 跳过 DNS 创建,设置 dns_status=pending

6.2 失败恢复机制

# 定时任务:修复失败的 DNS 记录
async def repair_failed_dns_records():
    """
    定时检查并修复 dns_status='failed' 的记录
    执行频率:每5分钟
    """
    async with get_db() as db:
        # 查询失败的记录
        result = await db.execute(
            select(Agent).where(
                Agent.dns_status == "failed",
                Agent.pod_ip.isnot(None)
            )
        )
        failed_agents = result.scalars().all()
        
        dns_service = get_dns_service()
        
        for agent in failed_agents:
            try:
                subdomain, full_domain = await dns_service.create_dns_record(
                    agent.name, str(agent.id), agent.pod_ip
                )
                agent.subdomain = subdomain
                agent.full_domain = full_domain
                agent.dns_status = "active"
                agent.dns_updated_at = datetime.utcnow()
                logger.info("DNS 记录修复成功", agent_id=str(agent.id))
            except Exception as e:
                logger.warning("DNS 记录修复失败", agent_id=str(agent.id), error=str(e))
        
        await db.commit()

7. 测试方案

7.1 单元测试

# tests/test_godaddy_client.py

import pytest
from app.godaddy_client import GoDaddyClient, GoDaddyError

@pytest.fixture
def client():
    return GoDaddyClient(
        api_key="test_key",
        api_secret="test_secret",
        base_url="https://api.ote-godaddy.com",  # 使用测试环境
        domain="taiji.ai.com"
    )

@pytest.mark.asyncio
async def test_create_a_record(client):
    """测试创建 A 记录"""
    result = await client.create_a_record("test-agent", "203.0.113.50")
    assert result is True

@pytest.mark.asyncio
async def test_delete_a_record(client):
    """测试删除 A 记录"""
    result = await client.delete_a_record("test-agent")
    assert result is True

@pytest.mark.asyncio
async def test_get_a_record(client):
    """测试查询 A 记录"""
    await client.create_a_record("test-agent", "203.0.113.50")
    record = await client.get_a_record("test-agent")
    assert record is not None
    assert record.data == "203.0.113.50"

7.2 集成测试

# tests/test_dns_integration.py

@pytest.mark.asyncio
async def test_agent_creation_with_dns():
    """测试 Agent 创建时自动创建 DNS 记录"""
    # 1. 创建 Agent
    response = await client.post("/api/agents", json={
        "name": "test-chatbot",
        "template": "base-agent",
        "description": "Test agent"
    })
    assert response.status_code == 200
    
    agent = response.json()
    
    # 2. 验证 DNS 字段
    assert agent["subdomain"] is not None
    assert agent["full_domain"].endswith("taiji.ai.com")
    assert agent["dns_status"] == "active"
    
    # 3. 验证 GoDaddy 记录存在
    dns_response = await client.get(f"/api/agents/{agent['id']}/dns")
    assert dns_response.status_code == 200
    
    # 4. 清理
    await client.delete(f"/api/agents/{agent['id']}")

@pytest.mark.asyncio
async def test_agent_deletion_removes_dns():
    """测试 Agent 删除时同步删除 DNS 记录"""
    # 1. 创建 Agent
    response = await client.post("/api/agents", json={...})
    agent = response.json()
    subdomain = agent["subdomain"]
    
    # 2. 删除 Agent
    await client.delete(f"/api/agents/{agent['id']}")
    
    # 3. 验证 DNS 记录已删除
    dns_client = get_godaddy_client()
    record = await dns_client.get_a_record(subdomain)
    assert record is None

7.3 手动测试脚本

# scripts/test_godaddy_api.py
"""
手动测试 GoDaddy API 连通性

使用方法:
python scripts/test_godaddy_api.py
"""

import asyncio
import sys
sys.path.append(".")

from app.godaddy_client import GoDaddyClient

async def main():
    client = GoDaddyClient()
    
    print("=" * 50)
    print("GoDaddy API 连通性测试")
    print("=" * 50)
    
    # 1. 测试创建记录
    print("\n1. 创建测试 A 记录...")
    try:
        await client.create_a_record("test-dns-integration", "1.2.3.4")
        print("   ✓ 创建成功")
    except Exception as e:
        print(f"   ✗ 创建失败: {e}")
        return
    
    # 2. 测试查询记录
    print("\n2. 查询 A 记录...")
    try:
        record = await client.get_a_record("test-dns-integration")
        if record:
            print(f"   ✓ 查询成功: {record.name} -> {record.data}")
        else:
            print("   ✗ 记录不存在")
    except Exception as e:
        print(f"   ✗ 查询失败: {e}")
    
    # 3. 测试删除记录
    print("\n3. 删除测试 A 记录...")
    try:
        await client.delete_a_record("test-dns-integration")
        print("   ✓ 删除成功")
    except Exception as e:
        print(f"   ✗ 删除失败: {e}")
    
    # 4. 列出所有 A 记录
    print("\n4. 列出所有 A 记录...")
    try:
        records = await client.list_a_records()
        print(f"   共 {len(records)} 条记录:")
        for r in records[:5]:
            print(f"   - {r.name} -> {r.data} (TTL: {r.ttl})")
        if len(records) > 5:
            print(f"   ... 还有 {len(records) - 5} 条")
    except Exception as e:
        print(f"   ✗ 列出失败: {e}")
    
    await client.close()
    print("\n" + "=" * 50)
    print("测试完成")

if __name__ == "__main__":
    asyncio.run(main())

8. 部署检查清单

8.1 环境变量配置

# 生产环境必须配置
GODADDY_API_KEY=gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4
GODADDY_API_SECRET=AMs5RxZeaW3mFZDwXvU3Bs
GODADDY_DOMAIN=taiji.ai.com
GODADDY_BASE_URL=https://api.godaddy.com
GODADDY_ENABLED=true
GODADDY_DNS_TTL=600

8.2 数据库迁移

# 执行迁移
alembic upgrade head

# 验证字段
psql -c "SELECT column_name FROM information_schema.columns WHERE table_name='agents' AND column_name LIKE 'dns%';"

8.3 功能验证

  • GoDaddy API 认证成功
  • 创建 Agent 后 subdomain 字段有值
  • full_domain 格式正确 (xxx.taiji.ai.com)
  • 通过域名可以访问 Agent 服务
  • 删除 Agent 后 DNS 记录被清理
  • 失败情况下有正确的日志和 dns_status

9. 后续优化建议

9.1 短期优化

  1. DNS 缓存: 本地缓存 DNS 记录状态,减少 API 调用
  2. 批量操作: 支持批量创建/删除 DNS 记录
  3. 监控告警: DNS 操作失败时发送告警

9.2 中期优化

  1. HTTPS 支持: 集成 Let's Encrypt 自动申请 SSL 证书
  2. 自定义域名: 支持用户绑定自己的域名
  3. DNS 健康检查: 定期检查 DNS 解析是否正常

9.3 长期规划

  1. 多 DNS 提供商: 支持 Cloudflare、AWS Route53 等
  2. 智能 DNS: 基于地理位置的 DNS 解析
  3. 域名市场: 提供子域名购买/租赁服务

10. 参考资料