forked from xiaohei/taiji-AI-PAD
1208 lines
42 KiB
Python
1208 lines
42 KiB
Python
"""
|
|
平台 Agent 配额管理路由
|
|
|
|
实现平台 Agent 的申请、审批和配额管理功能。
|
|
|
|
业务流程:
|
|
1. 渠道查看可用的平台 Agent 模板
|
|
2. 渠道申请平台 Agent 配额
|
|
3. 管理员审批申请
|
|
4. 渠道分配配额给租户(立即启动 Pod)
|
|
5. 租户使用平台 Agent
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
import structlog
|
|
from datetime import datetime
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import select, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from pydantic import BaseModel, Field
|
|
|
|
from database import get_db
|
|
from models import (
|
|
User, Channel, ResourceApplication, PlatformAgentQuota,
|
|
Agent, AgentBillingRecord, PlatformAgentTemplateConfig
|
|
)
|
|
from ..auth import get_current_user
|
|
from ..agent_manager_client import (
|
|
get_agent_manager_client,
|
|
AgentConfig,
|
|
AgentManagerError,
|
|
)
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
# ==================== 辅助函数 ====================
|
|
|
|
def _get_role(current_user: dict) -> str:
|
|
"""从 current_user 获取角色"""
|
|
# JWT 认证时,role 在 claims 中
|
|
if "claims" in current_user:
|
|
return current_user.get("claims", {}).get("role", "")
|
|
# 直接返回 role(兼容其他认证方式)
|
|
return current_user.get("role", "")
|
|
|
|
|
|
def _get_user_id(current_user: dict) -> str:
|
|
"""从 current_user 获取用户 ID"""
|
|
return current_user.get("user_id", "")
|
|
|
|
|
|
def _get_channel_id(current_user: dict) -> Optional[str]:
|
|
"""从 current_user 获取渠道 ID"""
|
|
# JWT 认证时,channelId 在 claims 中
|
|
if "claims" in current_user:
|
|
return current_user.get("claims", {}).get("channelId")
|
|
# 直接从 current_user 获取(兼容其他认证方式)
|
|
return current_user.get("channelId")
|
|
|
|
|
|
# ==================== 请求/响应模型 ====================
|
|
|
|
class PlatformAgentTemplateInfo(BaseModel):
|
|
"""平台 Agent 模板信息"""
|
|
name: str
|
|
display_name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
category: Optional[str] = None
|
|
port: Optional[int] = None
|
|
has_access: bool = False
|
|
current_quota: int = 0
|
|
|
|
|
|
class AvailablePlatformAgentsResponse(BaseModel):
|
|
"""可用平台 Agent 列表响应"""
|
|
templates: List[PlatformAgentTemplateInfo]
|
|
count: int
|
|
|
|
|
|
class PlatformAgentApplicationCreate(BaseModel):
|
|
"""创建平台 Agent 申请"""
|
|
templateName: str = Field(..., description="平台 Agent 模板名称")
|
|
requestedPodQuota: int = Field(..., ge=1, le=100, description="申请的 Pod 配额")
|
|
reason: str = Field(..., min_length=10, max_length=500, description="申请理由")
|
|
|
|
|
|
class PlatformAgentApplicationResponse(BaseModel):
|
|
"""平台 Agent 申请响应"""
|
|
id: uuid.UUID
|
|
channelId: uuid.UUID
|
|
templateName: str
|
|
requestedPodQuota: int
|
|
reason: str
|
|
status: str
|
|
approvedPodQuota: Optional[int] = None
|
|
reviewReason: Optional[str] = None
|
|
reviewedAt: Optional[datetime] = None
|
|
createdAt: datetime
|
|
|
|
|
|
class ApplicationReviewRequest(BaseModel):
|
|
"""审批申请请求"""
|
|
action: str = Field(..., pattern="^(approve|reject)$", description="审批动作")
|
|
podQuota: Optional[int] = Field(None, ge=0, description="批准的 Pod 配额(approve 时必填)")
|
|
reviewReason: Optional[str] = Field(None, max_length=500, description="审批意见")
|
|
|
|
|
|
class TenantPlatformAgentAllocation(BaseModel):
|
|
"""分配平台 Agent 给租户"""
|
|
templateName: str = Field(..., description="平台 Agent 模板名称")
|
|
podQuota: int = Field(..., ge=1, le=50, description="分配的 Pod 配额")
|
|
description: Optional[str] = None
|
|
|
|
|
|
class PlatformAgentQuotaResponse(BaseModel):
|
|
"""平台 Agent 配额响应"""
|
|
id: uuid.UUID
|
|
targetId: uuid.UUID
|
|
targetType: str
|
|
templateName: str
|
|
podQuota: int
|
|
podUsed: int
|
|
allocatedAt: datetime
|
|
|
|
|
|
class PlatformAgentTemplateConfigRequest(BaseModel):
|
|
"""平台 Agent 模板配置请求(管理员配置)"""
|
|
cpuRequest: Optional[str] = Field(None, description="CPU 请求量,如 100m")
|
|
cpuLimit: Optional[str] = Field(None, description="CPU 限制量,如 500m")
|
|
memoryRequest: Optional[str] = Field(None, description="内存请求量,如 128Mi")
|
|
memoryLimit: Optional[str] = Field(None, description="内存限制量,如 512Mi")
|
|
maxPods: Optional[int] = Field(None, ge=0, description="最大 Pod 数量")
|
|
isEnabled: Optional[bool] = Field(True, description="是否启用")
|
|
displayName: Optional[str] = Field(None, max_length=200, description="显示名称")
|
|
description: Optional[str] = Field(None, description="描述")
|
|
|
|
|
|
class PlatformAgentTemplateConfigResponse(BaseModel):
|
|
"""平台 Agent 模板配置响应"""
|
|
templateName: str
|
|
cpuRequest: Optional[str] = None
|
|
cpuLimit: Optional[str] = None
|
|
memoryRequest: Optional[str] = None
|
|
memoryLimit: Optional[str] = None
|
|
maxPods: int = 0
|
|
isEnabled: bool = True
|
|
displayName: Optional[str] = None
|
|
description: Optional[str] = None
|
|
configuredAt: Optional[datetime] = None
|
|
configuredBy: Optional[uuid.UUID] = None
|
|
|
|
|
|
class UsePlatformAgentRequest(BaseModel):
|
|
"""使用平台 Agent 请求"""
|
|
name: str = Field(..., min_length=1, max_length=63, description="Agent 实例名称")
|
|
|
|
|
|
class PlatformAgentInstanceResponse(BaseModel):
|
|
"""平台 Agent 实例响应"""
|
|
name: str
|
|
template: str
|
|
status: str
|
|
podIp: Optional[str] = None
|
|
createdAt: datetime
|
|
|
|
|
|
# ==================== 渠道路由 ====================
|
|
|
|
channel_router = APIRouter(prefix="/api/channel", tags=["channel-platform-agents"])
|
|
|
|
|
|
@channel_router.get("/available-platform-agents", response_model=AvailablePlatformAgentsResponse)
|
|
async def get_available_platform_agents(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> AvailablePlatformAgentsResponse:
|
|
"""
|
|
获取所有可用的平台 Agent 模板
|
|
|
|
渠道管理员可以查看平台上所有可用的平台 Agent,
|
|
以及自己是否已有权限和当前配额。
|
|
"""
|
|
# 验证权限
|
|
if _get_role(current_user) not in ["channel_admin", "admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要渠道管理员权限")
|
|
|
|
try:
|
|
# 从 Agent Manager 获取平台模板
|
|
client = get_agent_manager_client()
|
|
templates = await client.list_platform_templates()
|
|
|
|
# 获取渠道 ID
|
|
channel_id = _get_channel_id(current_user)
|
|
if not channel_id:
|
|
# 如果是管理员,可能没有 channel_id
|
|
channel_id = None
|
|
|
|
# 查询渠道已有的配额
|
|
quotas = {}
|
|
if channel_id:
|
|
quota_result = await db.execute(
|
|
select(PlatformAgentQuota).where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == uuid.UUID(channel_id),
|
|
PlatformAgentQuota.target_type == "channel"
|
|
)
|
|
)
|
|
)
|
|
for quota in quota_result.scalars().all():
|
|
quotas[quota.template_name] = quota.pod_quota
|
|
|
|
# 构建响应
|
|
template_list = []
|
|
for t in templates:
|
|
template_name = t.template
|
|
# 优先使用 Agent Manager 返回的 displayName
|
|
display_name = t.display_name or template_name.replace("_", " ").title()
|
|
template_list.append(PlatformAgentTemplateInfo(
|
|
name=template_name,
|
|
display_name=display_name,
|
|
description=t.description,
|
|
category=t.category,
|
|
port=t.port,
|
|
has_access=template_name in quotas,
|
|
current_quota=quotas.get(template_name, 0)
|
|
))
|
|
|
|
return AvailablePlatformAgentsResponse(
|
|
templates=template_list,
|
|
count=len(template_list)
|
|
)
|
|
|
|
except AgentManagerError as e:
|
|
logger.error("获取平台模板失败", error=str(e))
|
|
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
|
|
|
|
|
@channel_router.post("/applications/platform-agents", response_model=PlatformAgentApplicationResponse)
|
|
async def create_platform_agent_application(
|
|
request: PlatformAgentApplicationCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> PlatformAgentApplicationResponse:
|
|
"""
|
|
申请平台 Agent 配额
|
|
|
|
渠道管理员发起申请,请求使用某个平台 Agent 模板。
|
|
"""
|
|
# 验证权限
|
|
if _get_role(current_user) not in ["channel_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要渠道管理员权限")
|
|
|
|
channel_id = _get_channel_id(current_user)
|
|
if not channel_id:
|
|
raise HTTPException(status_code=400, detail="用户未关联渠道")
|
|
|
|
try:
|
|
# 验证模板是否存在
|
|
client = get_agent_manager_client()
|
|
templates = await client.list_platform_templates()
|
|
template_names = [t.template for t in templates]
|
|
|
|
if request.templateName not in template_names:
|
|
raise HTTPException(status_code=400, detail=f"模板 {request.templateName} 不存在")
|
|
|
|
# 检查是否已有待审批的申请
|
|
existing = await db.execute(
|
|
select(ResourceApplication).where(
|
|
and_(
|
|
ResourceApplication.channel_id == uuid.UUID(channel_id),
|
|
ResourceApplication.resource_type == "platform_agent",
|
|
ResourceApplication.template_name == request.templateName,
|
|
ResourceApplication.status == "pending"
|
|
)
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="已有待审批的申请")
|
|
|
|
# 创建申请
|
|
application = ResourceApplication(
|
|
channel_id=uuid.UUID(channel_id),
|
|
resource_type="platform_agent",
|
|
template_name=request.templateName,
|
|
requested_pod_quota=request.requestedPodQuota,
|
|
reason=request.reason,
|
|
status="pending"
|
|
)
|
|
db.add(application)
|
|
await db.commit()
|
|
await db.refresh(application)
|
|
|
|
logger.info(
|
|
"创建平台 Agent 申请",
|
|
application_id=str(application.id),
|
|
channel_id=channel_id,
|
|
template=request.templateName
|
|
)
|
|
|
|
return PlatformAgentApplicationResponse(
|
|
id=application.id,
|
|
channelId=application.channel_id,
|
|
templateName=application.template_name,
|
|
requestedPodQuota=application.requested_pod_quota,
|
|
reason=application.reason,
|
|
status=application.status,
|
|
createdAt=application.created_at
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
await db.rollback()
|
|
logger.error("创建申请失败", error=str(e))
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@channel_router.get("/applications/platform-agents", response_model=List[PlatformAgentApplicationResponse])
|
|
async def list_channel_platform_agent_applications(
|
|
status: Optional[str] = Query(None, description="按状态过滤"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> List[PlatformAgentApplicationResponse]:
|
|
"""
|
|
查看渠道的平台 Agent 申请列表
|
|
"""
|
|
if _get_role(current_user) not in ["channel_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要渠道管理员权限")
|
|
|
|
channel_id = _get_channel_id(current_user)
|
|
if not channel_id:
|
|
raise HTTPException(status_code=400, detail="用户未关联渠道")
|
|
|
|
query = select(ResourceApplication).where(
|
|
and_(
|
|
ResourceApplication.channel_id == uuid.UUID(channel_id),
|
|
ResourceApplication.resource_type == "platform_agent"
|
|
)
|
|
).order_by(ResourceApplication.created_at.desc())
|
|
|
|
if status:
|
|
query = query.where(ResourceApplication.status == status)
|
|
|
|
result = await db.execute(query)
|
|
applications = result.scalars().all()
|
|
|
|
return [
|
|
PlatformAgentApplicationResponse(
|
|
id=app.id,
|
|
channelId=app.channel_id,
|
|
templateName=app.template_name,
|
|
requestedPodQuota=app.requested_pod_quota,
|
|
reason=app.reason,
|
|
status=app.status,
|
|
approvedPodQuota=app.approved_pod_quota,
|
|
reviewReason=app.review_reason,
|
|
reviewedAt=app.reviewed_at,
|
|
createdAt=app.created_at
|
|
)
|
|
for app in applications
|
|
]
|
|
|
|
|
|
@channel_router.get("/platform-agents", response_model=List[PlatformAgentQuotaResponse])
|
|
async def list_channel_platform_agent_quotas(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> List[PlatformAgentQuotaResponse]:
|
|
"""
|
|
查看渠道已分配的平台 Agent 配额
|
|
"""
|
|
if _get_role(current_user) not in ["channel_admin", "admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要渠道管理员权限")
|
|
|
|
channel_id = _get_channel_id(current_user)
|
|
if not channel_id:
|
|
raise HTTPException(status_code=400, detail="用户未关联渠道")
|
|
|
|
result = await db.execute(
|
|
select(PlatformAgentQuota).where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == uuid.UUID(channel_id),
|
|
PlatformAgentQuota.target_type == "channel"
|
|
)
|
|
)
|
|
)
|
|
quotas = result.scalars().all()
|
|
|
|
return [
|
|
PlatformAgentQuotaResponse(
|
|
id=q.id,
|
|
targetId=q.target_id,
|
|
targetType=q.target_type,
|
|
templateName=q.template_name,
|
|
podQuota=q.pod_quota,
|
|
podUsed=q.pod_used,
|
|
allocatedAt=q.allocated_at
|
|
)
|
|
for q in quotas
|
|
]
|
|
|
|
|
|
@channel_router.post("/tenants/{tenant_id}/platform-agents", response_model=PlatformAgentQuotaResponse)
|
|
async def allocate_platform_agent_to_tenant(
|
|
tenant_id: str,
|
|
request: TenantPlatformAgentAllocation,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> PlatformAgentQuotaResponse:
|
|
"""
|
|
分配平台 Agent 给租户
|
|
|
|
渠道分配配额给租户时,会立即调用 Agent Manager 启动 Pod。
|
|
"""
|
|
if _get_role(current_user) not in ["channel_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要渠道管理员权限")
|
|
|
|
channel_id = _get_channel_id(current_user)
|
|
if not channel_id:
|
|
raise HTTPException(status_code=400, detail="用户未关联渠道")
|
|
|
|
try:
|
|
tenant_uuid = uuid.UUID(tenant_id)
|
|
channel_uuid = uuid.UUID(channel_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="无效的 ID 格式")
|
|
|
|
# 验证租户属于该渠道
|
|
tenant = await db.get(User, tenant_uuid)
|
|
if not tenant or tenant.channel_id != channel_uuid:
|
|
raise HTTPException(status_code=404, detail="租户不存在或不属于该渠道")
|
|
|
|
# 检查渠道配额(使用行锁防止并发更新)
|
|
channel_quota_result = await db.execute(
|
|
select(PlatformAgentQuota)
|
|
.where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == channel_uuid,
|
|
PlatformAgentQuota.target_type == "channel",
|
|
PlatformAgentQuota.template_name == request.templateName
|
|
)
|
|
)
|
|
.with_for_update() # 行锁
|
|
)
|
|
channel_quota = channel_quota_result.scalar_one_or_none()
|
|
|
|
if not channel_quota:
|
|
raise HTTPException(status_code=400, detail=f"渠道没有 {request.templateName} 的配额")
|
|
|
|
# 检查租户是否已有该模板的配额(使用行锁防止并发更新)
|
|
existing_quota_result = await db.execute(
|
|
select(PlatformAgentQuota)
|
|
.where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == tenant_uuid,
|
|
PlatformAgentQuota.target_type == "tenant",
|
|
PlatformAgentQuota.template_name == request.templateName
|
|
)
|
|
)
|
|
.with_for_update() # 行锁
|
|
)
|
|
existing_quota = existing_quota_result.scalar_one_or_none()
|
|
|
|
# 追加模式:request.podQuota 表示要追加的配额量
|
|
current_quota = existing_quota.pod_quota if existing_quota else 0
|
|
quota_delta = request.podQuota # 追加量
|
|
new_quota = current_quota + quota_delta # 新的总配额
|
|
|
|
# 使用渠道的 pod_used 检查剩余配额(pod_used 表示已分配给租户的配额)
|
|
if channel_quota.pod_used + quota_delta > channel_quota.pod_quota:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"配额不足,渠道配额: {channel_quota.pod_quota},已分配: {channel_quota.pod_used},请求追加: {quota_delta}"
|
|
)
|
|
|
|
# 更新或创建租户配额
|
|
if existing_quota:
|
|
# 追加模式:在现有配额基础上增加
|
|
existing_quota.pod_quota = new_quota
|
|
existing_quota.allocated_at = datetime.utcnow()
|
|
quota = existing_quota
|
|
logger.info(
|
|
"追加平台Agent配额",
|
|
tenant_id=tenant_id,
|
|
template=request.templateName,
|
|
quota_delta=quota_delta,
|
|
new_quota=new_quota
|
|
)
|
|
else:
|
|
# 创建新配额
|
|
quota = PlatformAgentQuota(
|
|
target_id=tenant_uuid,
|
|
target_type="tenant",
|
|
template_name=request.templateName,
|
|
pod_quota=new_quota,
|
|
pod_used=0,
|
|
allocated_by=uuid.UUID(_get_user_id(current_user))
|
|
)
|
|
db.add(quota)
|
|
logger.info(
|
|
"创建平台Agent配额",
|
|
tenant_id=tenant_id,
|
|
template=request.templateName,
|
|
quota=new_quota
|
|
)
|
|
|
|
# Bug 修复:更新渠道的 pod_used(分配给租户的配额视为渠道已使用的配额)
|
|
channel_quota.pod_used = (channel_quota.pod_used or 0) + quota_delta
|
|
|
|
# 立即启动 Pod(平台 Agent 分配时立即启动)
|
|
try:
|
|
client = get_agent_manager_client()
|
|
|
|
# 构建 Pod 名称
|
|
pod_name = f"{request.templateName}-{tenant_id[:8]}"
|
|
|
|
# 创建 Agent 配置
|
|
agent_config = AgentConfig(
|
|
user_id=tenant_id,
|
|
cpu_request="100m",
|
|
memory_request="128Mi",
|
|
replicas=1 # 平台 Agent 默认单副本
|
|
)
|
|
|
|
# 调用 Agent Manager 创建 Pod
|
|
result = await client.create_agent(
|
|
name=pod_name,
|
|
template=request.templateName,
|
|
config=agent_config
|
|
)
|
|
|
|
# 更新已使用配额(租户)
|
|
# 注意:这里强制设置为1是因为这是立即启动逻辑
|
|
# 在正常使用流程中,租户应该通过 /api/user/platform-agents/use 接口启动
|
|
quota.pod_used = 1
|
|
|
|
logger.info(
|
|
"平台Agent分配并启动",
|
|
tenant_id=tenant_id,
|
|
template=request.templateName,
|
|
tenant_pod_used=quota.pod_used
|
|
)
|
|
|
|
# 创建 Agent 记录
|
|
agent = Agent(
|
|
name=pod_name,
|
|
type="platform",
|
|
template=request.templateName,
|
|
pod_name=result.name,
|
|
k8s_namespace=result.namespace,
|
|
k8s_status=result.status,
|
|
service_port=result.service_port,
|
|
owner_id=tenant_uuid,
|
|
status="active"
|
|
)
|
|
db.add(agent)
|
|
|
|
# ✅ 创建计费记录
|
|
billing_record = AgentBillingRecord(
|
|
user_id=tenant_uuid,
|
|
channel_id=channel_uuid,
|
|
agent_type=request.templateName,
|
|
agent_name=pod_name,
|
|
is_platform_agent=True,
|
|
start_time=datetime.utcnow(),
|
|
cpu_used=agent_config.cpu_request,
|
|
memory_used=agent_config.memory_request,
|
|
duration_seconds=0,
|
|
eu_consumed=0,
|
|
cost=0,
|
|
)
|
|
db.add(billing_record)
|
|
|
|
logger.info(
|
|
"平台 Agent 分配成功",
|
|
tenant_id=tenant_id,
|
|
template=request.templateName,
|
|
pod_name=result.name
|
|
)
|
|
|
|
except AgentManagerError as e:
|
|
await db.rollback()
|
|
logger.error("创建 Pod 失败", error=str(e))
|
|
raise HTTPException(
|
|
status_code=e.status_code,
|
|
detail={"error": "pod_creation_failed", "message": e.message}
|
|
)
|
|
|
|
await db.commit()
|
|
await db.refresh(quota)
|
|
|
|
return PlatformAgentQuotaResponse(
|
|
id=quota.id,
|
|
targetId=quota.target_id,
|
|
targetType=quota.target_type,
|
|
templateName=quota.template_name,
|
|
podQuota=quota.pod_quota,
|
|
podUsed=quota.pod_used,
|
|
allocatedAt=quota.allocated_at
|
|
)
|
|
|
|
|
|
# ==================== 管理员路由 ====================
|
|
|
|
admin_router = APIRouter(prefix="/api/admin", tags=["admin-platform-agents"])
|
|
|
|
|
|
@admin_router.get("/applications/platform-agents", response_model=List[PlatformAgentApplicationResponse])
|
|
async def list_all_platform_agent_applications(
|
|
status: Optional[str] = Query(None, description="按状态过滤"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> List[PlatformAgentApplicationResponse]:
|
|
"""
|
|
查看所有平台 Agent 申请(管理员)
|
|
"""
|
|
if _get_role(current_user) not in ["admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
query = select(ResourceApplication).where(
|
|
ResourceApplication.resource_type == "platform_agent"
|
|
).order_by(ResourceApplication.created_at.desc())
|
|
|
|
if status:
|
|
query = query.where(ResourceApplication.status == status)
|
|
|
|
result = await db.execute(query)
|
|
applications = result.scalars().all()
|
|
|
|
return [
|
|
PlatformAgentApplicationResponse(
|
|
id=app.id,
|
|
channelId=app.channel_id,
|
|
templateName=app.template_name,
|
|
requestedPodQuota=app.requested_pod_quota,
|
|
reason=app.reason,
|
|
status=app.status,
|
|
approvedPodQuota=app.approved_pod_quota,
|
|
reviewReason=app.review_reason,
|
|
reviewedAt=app.reviewed_at,
|
|
createdAt=app.created_at
|
|
)
|
|
for app in applications
|
|
]
|
|
|
|
|
|
@admin_router.put("/applications/platform-agents/{application_id}/review")
|
|
async def review_platform_agent_application(
|
|
application_id: str,
|
|
request: ApplicationReviewRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
审批平台 Agent 申请
|
|
|
|
管理员审批通过后,会自动创建渠道的配额记录。
|
|
"""
|
|
if _get_role(current_user) not in ["admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
try:
|
|
app_uuid = uuid.UUID(application_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="无效的申请 ID")
|
|
|
|
# 获取申请
|
|
application = await db.get(ResourceApplication, app_uuid)
|
|
if not application:
|
|
raise HTTPException(status_code=404, detail="申请不存在")
|
|
|
|
if application.status != "pending":
|
|
raise HTTPException(status_code=400, detail="申请已处理")
|
|
|
|
if application.resource_type != "platform_agent":
|
|
raise HTTPException(status_code=400, detail="申请类型不匹配")
|
|
|
|
# 处理审批
|
|
if request.action == "approve":
|
|
if request.podQuota is None or request.podQuota <= 0:
|
|
raise HTTPException(status_code=400, detail="批准时必须指定 Pod 配额")
|
|
|
|
application.status = "approved"
|
|
application.approved_pod_quota = request.podQuota
|
|
application.review_reason = request.reviewReason
|
|
application.reviewed_by = uuid.UUID(_get_user_id(current_user))
|
|
application.reviewed_at = datetime.utcnow()
|
|
|
|
# 创建或更新渠道配额
|
|
existing_quota_result = await db.execute(
|
|
select(PlatformAgentQuota).where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == application.channel_id,
|
|
PlatformAgentQuota.target_type == "channel",
|
|
PlatformAgentQuota.template_name == application.template_name
|
|
)
|
|
)
|
|
)
|
|
existing_quota = existing_quota_result.scalar_one_or_none()
|
|
|
|
if existing_quota:
|
|
existing_quota.pod_quota += request.podQuota
|
|
else:
|
|
quota = PlatformAgentQuota(
|
|
target_id=application.channel_id,
|
|
target_type="channel",
|
|
template_name=application.template_name,
|
|
pod_quota=request.podQuota,
|
|
pod_used=0,
|
|
allocated_by=uuid.UUID(_get_user_id(current_user))
|
|
)
|
|
db.add(quota)
|
|
|
|
logger.info(
|
|
"审批通过",
|
|
application_id=application_id,
|
|
channel_id=str(application.channel_id),
|
|
template=application.template_name,
|
|
pod_quota=request.podQuota
|
|
)
|
|
|
|
else: # reject
|
|
application.status = "rejected"
|
|
application.review_reason = request.reviewReason
|
|
application.reviewed_by = uuid.UUID(_get_user_id(current_user))
|
|
application.reviewed_at = datetime.utcnow()
|
|
|
|
logger.info(
|
|
"审批拒绝",
|
|
application_id=application_id,
|
|
channel_id=str(application.channel_id),
|
|
reason=request.reviewReason
|
|
)
|
|
|
|
await db.commit()
|
|
|
|
return {
|
|
"success": True,
|
|
"application_id": application_id,
|
|
"status": application.status,
|
|
"message": f"申请已{('通过' if request.action == 'approve' else '拒绝')}"
|
|
}
|
|
|
|
|
|
@admin_router.get("/platform-agents/templates")
|
|
async def list_platform_agent_templates_admin(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
获取平台 Agent 模板列表(管理员)
|
|
|
|
返回所有模板及其管理员配置信息。
|
|
"""
|
|
if _get_role(current_user) not in ["admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
try:
|
|
client = get_agent_manager_client()
|
|
templates = await client.list_platform_templates()
|
|
|
|
# 获取管理员配置
|
|
config_result = await db.execute(select(PlatformAgentTemplateConfig))
|
|
configs = {c.template_name: c for c in config_result.scalars().all()}
|
|
|
|
template_list = []
|
|
for t in templates:
|
|
config = configs.get(t.template)
|
|
template_list.append({
|
|
"name": t.template,
|
|
"port": t.port,
|
|
"envInfo": t.env_info,
|
|
"isConfigured": config is not None,
|
|
"config": {
|
|
"cpuRequest": config.cpu_request if config else None,
|
|
"cpuLimit": config.cpu_limit if config else None,
|
|
"memoryRequest": config.memory_request if config else None,
|
|
"memoryLimit": config.memory_limit if config else None,
|
|
"maxPods": config.max_pods if config else 0,
|
|
"isEnabled": config.is_enabled if config else False,
|
|
"displayName": config.display_name if config else None,
|
|
"description": config.description if config else None,
|
|
"configuredAt": config.configured_at.isoformat() if config and config.configured_at else None,
|
|
} if config else None
|
|
})
|
|
|
|
return {
|
|
"templates": template_list,
|
|
"count": len(template_list)
|
|
}
|
|
except AgentManagerError as e:
|
|
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
|
|
|
|
|
@admin_router.put("/platform-agents/templates/{template_name}/config", response_model=PlatformAgentTemplateConfigResponse)
|
|
async def configure_platform_agent_template(
|
|
template_name: str,
|
|
request: PlatformAgentTemplateConfigRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> PlatformAgentTemplateConfigResponse:
|
|
"""
|
|
配置平台 Agent 模板(管理员)
|
|
|
|
管理员可以配置模板的资源限制、最大 Pod 数量等参数。
|
|
只有配置过的模板才会在渠道的 available-platform-agents 接口中显示资源配置。
|
|
"""
|
|
if _get_role(current_user) not in ["admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
try:
|
|
# 验证模板是否存在
|
|
client = get_agent_manager_client()
|
|
templates = await client.list_platform_templates()
|
|
template_names = [t.template for t in templates]
|
|
|
|
if template_name not in template_names:
|
|
raise HTTPException(status_code=404, detail=f"模板 {template_name} 不存在")
|
|
|
|
# 查找或创建配置
|
|
config_result = await db.execute(
|
|
select(PlatformAgentTemplateConfig).where(
|
|
PlatformAgentTemplateConfig.template_name == template_name
|
|
)
|
|
)
|
|
config = config_result.scalar_one_or_none()
|
|
|
|
if config:
|
|
# 更新现有配置
|
|
if request.cpuRequest is not None:
|
|
config.cpu_request = request.cpuRequest
|
|
if request.cpuLimit is not None:
|
|
config.cpu_limit = request.cpuLimit
|
|
if request.memoryRequest is not None:
|
|
config.memory_request = request.memoryRequest
|
|
if request.memoryLimit is not None:
|
|
config.memory_limit = request.memoryLimit
|
|
if request.maxPods is not None:
|
|
config.max_pods = request.maxPods
|
|
if request.isEnabled is not None:
|
|
config.is_enabled = request.isEnabled
|
|
if request.displayName is not None:
|
|
config.display_name = request.displayName
|
|
if request.description is not None:
|
|
config.description = request.description
|
|
config.configured_by = uuid.UUID(_get_user_id(current_user))
|
|
config.configured_at = datetime.utcnow()
|
|
else:
|
|
# 创建新配置
|
|
config = PlatformAgentTemplateConfig(
|
|
template_name=template_name,
|
|
cpu_request=request.cpuRequest,
|
|
cpu_limit=request.cpuLimit,
|
|
memory_request=request.memoryRequest,
|
|
memory_limit=request.memoryLimit,
|
|
max_pods=request.maxPods or 0,
|
|
is_enabled=request.isEnabled if request.isEnabled is not None else True,
|
|
display_name=request.displayName,
|
|
description=request.description,
|
|
configured_by=uuid.UUID(_get_user_id(current_user)),
|
|
configured_at=datetime.utcnow()
|
|
)
|
|
db.add(config)
|
|
|
|
await db.commit()
|
|
await db.refresh(config)
|
|
|
|
logger.info(
|
|
"配置平台 Agent 模板",
|
|
template_name=template_name,
|
|
configured_by=_get_user_id(current_user)
|
|
)
|
|
|
|
return PlatformAgentTemplateConfigResponse(
|
|
templateName=config.template_name,
|
|
cpuRequest=config.cpu_request,
|
|
cpuLimit=config.cpu_limit,
|
|
memoryRequest=config.memory_request,
|
|
memoryLimit=config.memory_limit,
|
|
maxPods=config.max_pods or 0,
|
|
isEnabled=config.is_enabled,
|
|
displayName=config.display_name,
|
|
description=config.description,
|
|
configuredAt=config.configured_at,
|
|
configuredBy=config.configured_by
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except AgentManagerError as e:
|
|
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
|
except Exception as e:
|
|
await db.rollback()
|
|
logger.error("配置模板失败", error=str(e))
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@admin_router.get("/platform-agents/templates/{template_name}/config", response_model=PlatformAgentTemplateConfigResponse)
|
|
async def get_platform_agent_template_config(
|
|
template_name: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> PlatformAgentTemplateConfigResponse:
|
|
"""
|
|
获取平台 Agent 模板配置(管理员)
|
|
"""
|
|
if _get_role(current_user) not in ["admin", "super_admin"]:
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
config_result = await db.execute(
|
|
select(PlatformAgentTemplateConfig).where(
|
|
PlatformAgentTemplateConfig.template_name == template_name
|
|
)
|
|
)
|
|
config = config_result.scalar_one_or_none()
|
|
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail=f"模板 {template_name} 尚未配置")
|
|
|
|
return PlatformAgentTemplateConfigResponse(
|
|
templateName=config.template_name,
|
|
cpuRequest=config.cpu_request,
|
|
cpuLimit=config.cpu_limit,
|
|
memoryRequest=config.memory_request,
|
|
memoryLimit=config.memory_limit,
|
|
maxPods=config.max_pods or 0,
|
|
isEnabled=config.is_enabled,
|
|
displayName=config.display_name,
|
|
description=config.description,
|
|
configuredAt=config.configured_at,
|
|
configuredBy=config.configured_by
|
|
)
|
|
|
|
|
|
# ==================== 用户路由 ====================
|
|
|
|
user_router = APIRouter(prefix="/api/user", tags=["user-platform-agents"])
|
|
|
|
|
|
@user_router.get("/platform-agents", response_model=List[PlatformAgentQuotaResponse])
|
|
async def list_user_platform_agents(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> List[PlatformAgentQuotaResponse]:
|
|
"""
|
|
查看用户可用的平台 Agent 配额
|
|
"""
|
|
user_id = _get_user_id(current_user)
|
|
|
|
result = await db.execute(
|
|
select(PlatformAgentQuota).where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == uuid.UUID(user_id),
|
|
PlatformAgentQuota.target_type == "tenant"
|
|
)
|
|
)
|
|
)
|
|
quotas = result.scalars().all()
|
|
|
|
return [
|
|
PlatformAgentQuotaResponse(
|
|
id=q.id,
|
|
targetId=q.target_id,
|
|
targetType=q.target_type,
|
|
templateName=q.template_name,
|
|
podQuota=q.pod_quota,
|
|
podUsed=q.pod_used,
|
|
allocatedAt=q.allocated_at
|
|
)
|
|
for q in quotas
|
|
]
|
|
|
|
|
|
@user_router.get("/platform-agents/{template}/instances", response_model=List[PlatformAgentInstanceResponse])
|
|
async def list_user_platform_agent_instances(
|
|
template: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> List[PlatformAgentInstanceResponse]:
|
|
"""
|
|
查看用户的平台 Agent 实例
|
|
"""
|
|
user_id = _get_user_id(current_user)
|
|
|
|
result = await db.execute(
|
|
select(Agent).where(
|
|
and_(
|
|
Agent.owner_id == uuid.UUID(user_id),
|
|
Agent.type == "platform",
|
|
Agent.template == template
|
|
)
|
|
)
|
|
)
|
|
agents = result.scalars().all()
|
|
|
|
return [
|
|
PlatformAgentInstanceResponse(
|
|
name=a.pod_name or a.name,
|
|
template=a.template,
|
|
status=a.k8s_status or "Unknown",
|
|
podIp=a.pod_ip,
|
|
createdAt=a.created_at
|
|
)
|
|
for a in agents
|
|
]
|
|
|
|
|
|
@user_router.delete("/platform-agents/{agent_name}")
|
|
async def stop_platform_agent(
|
|
agent_name: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
停止平台 Agent 实例
|
|
|
|
停止 Agent 时会同时释放租户和渠道的配额。
|
|
"""
|
|
user_id = _get_user_id(current_user)
|
|
|
|
# 查找 Agent
|
|
result = await db.execute(
|
|
select(Agent).where(
|
|
and_(
|
|
Agent.owner_id == uuid.UUID(user_id),
|
|
Agent.type == "platform",
|
|
Agent.pod_name == agent_name
|
|
)
|
|
)
|
|
)
|
|
agent = result.scalar_one_or_none()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent 不存在")
|
|
|
|
# 获取租户信息以找到所属渠道
|
|
tenant = await db.get(User, uuid.UUID(user_id))
|
|
channel_id = tenant.channel_id if tenant else None
|
|
|
|
# 删除 Pod
|
|
try:
|
|
client = get_agent_manager_client()
|
|
await client.delete_agent(agent_name)
|
|
except AgentManagerError as e:
|
|
if e.status_code != 404:
|
|
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
|
|
|
# ✅ 结束计费记录
|
|
billing_result = await db.execute(
|
|
select(AgentBillingRecord).where(
|
|
and_(
|
|
AgentBillingRecord.agent_name == agent_name,
|
|
AgentBillingRecord.user_id == uuid.UUID(user_id),
|
|
AgentBillingRecord.is_platform_agent == True,
|
|
AgentBillingRecord.end_time == None
|
|
)
|
|
)
|
|
)
|
|
billing_record = billing_result.scalar_one_or_none()
|
|
|
|
if billing_record:
|
|
billing_record.end_time = datetime.utcnow()
|
|
duration = (billing_record.end_time - billing_record.start_time).total_seconds()
|
|
billing_record.duration_seconds = int(duration)
|
|
# 计算EU: 1 EU = 10秒,不足10秒按1 EU计算
|
|
import math
|
|
billing_record.eu_consumed = math.ceil(duration / 10)
|
|
# 计算成本: 平台Agent固定费率 $0.10/小时
|
|
billing_record.cost = 0.10 * (duration / 3600)
|
|
|
|
logger.info(
|
|
f"结束平台Agent计费: {agent_name}, "
|
|
f"运行时长={duration}秒, EU={billing_record.eu_consumed}"
|
|
)
|
|
else:
|
|
logger.warning(f"未找到Agent {agent_name} 的计费记录")
|
|
|
|
# 更新租户配额(使用行锁防止并发更新)
|
|
tenant_quota_result = await db.execute(
|
|
select(PlatformAgentQuota)
|
|
.where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == uuid.UUID(user_id),
|
|
PlatformAgentQuota.target_type == "tenant",
|
|
PlatformAgentQuota.template_name == agent.template
|
|
)
|
|
)
|
|
.with_for_update() # 行锁
|
|
)
|
|
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
|
if tenant_quota and tenant_quota.pod_used > 0:
|
|
tenant_quota.pod_used -= 1
|
|
logger.info(
|
|
"释放租户配额",
|
|
user_id=user_id,
|
|
template=agent.template,
|
|
new_pod_used=tenant_quota.pod_used
|
|
)
|
|
|
|
# 注意:渠道的 pod_used 表示"已分配给租户的配额",不会因租户停止 Pod 而减少
|
|
# 只有在撤销租户配额分配时才会更新渠道的 pod_used
|
|
|
|
# 删除 Agent 记录
|
|
await db.delete(agent)
|
|
await db.commit()
|
|
|
|
logger.info("停止平台 Agent", agent_name=agent_name, user_id=user_id)
|
|
|
|
return {"success": True, "message": f"Agent {agent_name} 已停止"}
|
|
|
|
|
|
@user_router.get("/platform-agents/quota")
|
|
async def get_user_platform_agent_quota(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
查看用户的平台 Agent 配额使用情况
|
|
"""
|
|
user_id = _get_user_id(current_user)
|
|
|
|
result = await db.execute(
|
|
select(PlatformAgentQuota).where(
|
|
and_(
|
|
PlatformAgentQuota.target_id == uuid.UUID(user_id),
|
|
PlatformAgentQuota.target_type == "tenant"
|
|
)
|
|
)
|
|
)
|
|
quotas = result.scalars().all()
|
|
|
|
# 构建配额使用情况
|
|
quota_usage = []
|
|
for q in quotas:
|
|
quota_usage.append({
|
|
"templateName": q.template_name,
|
|
"podQuota": q.pod_quota,
|
|
"podUsed": q.pod_used,
|
|
"podAvailable": q.pod_quota - q.pod_used,
|
|
"usagePercent": round((q.pod_used / q.pod_quota * 100) if q.pod_quota > 0 else 0, 2)
|
|
})
|
|
|
|
return {
|
|
"userId": user_id,
|
|
"quotas": quota_usage,
|
|
"totalQuota": sum(q.pod_quota for q in quotas),
|
|
"totalUsed": sum(q.pod_used for q in quotas)
|
|
}
|
|
|
|
|
|
@user_router.get("/platform-agents/{agent_name}/status")
|
|
async def get_platform_agent_status(
|
|
agent_name: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
查看平台 Agent 实例状态
|
|
"""
|
|
user_id = _get_user_id(current_user)
|
|
|
|
# 查找 Agent
|
|
result = await db.execute(
|
|
select(Agent).where(
|
|
and_(
|
|
Agent.owner_id == uuid.UUID(user_id),
|
|
Agent.type == "platform",
|
|
Agent.pod_name == agent_name
|
|
)
|
|
)
|
|
)
|
|
agent = result.scalar_one_or_none()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent 不存在")
|
|
|
|
# 从 Agent Manager 获取实时状态
|
|
try:
|
|
client = get_agent_manager_client()
|
|
status = await client.get_agent_status(agent_name)
|
|
|
|
return {
|
|
"name": agent_name,
|
|
"template": agent.template,
|
|
"status": status.status,
|
|
"podIp": status.pod_ip,
|
|
"nodeName": status.node_name,
|
|
"createdAt": status.created_at,
|
|
"labels": status.labels
|
|
}
|
|
except AgentManagerError as e:
|
|
# 如果 Agent Manager 返回 404,返回数据库中的状态
|
|
if e.status_code == 404:
|
|
return {
|
|
"name": agent_name,
|
|
"template": agent.template,
|
|
"status": agent.k8s_status or "Unknown",
|
|
"podIp": agent.pod_ip,
|
|
"nodeName": None,
|
|
"createdAt": agent.created_at.isoformat() if agent.created_at else None,
|
|
"labels": {}
|
|
}
|
|
raise HTTPException(status_code=e.status_code, detail=e.detail) |