Files
taiji-AI-PAD/services/mcp-server/app/routes/platform_agent_quota.py
T
2026-03-10 06:40:38 +00:00

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)