Files
taiji-AI-PAD/services/mcp-server/app/routes/frontend_integration.py
T
2025-12-28 13:28:16 +00:00

1262 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Frontend integration endpoints required by BACKEND_INTEGRATION_CHECKLIST.
All handlers return lightweight, mostly in-memory data so the frontend can
render flows before the full business services are ready. Where possible, we
hydrate responses from existing tables (Agents, Tools) to keep values realistic.
"""
from __future__ import annotations
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import (
Agent,
Balance,
Channel,
ChannelAgentQuota,
DataTemplate,
Execution,
Billing,
GatewayAPI,
ProviderModel,
Tenant,
Tool,
User,
)
from monitoring import system_monitor
from ..auth import create_access_token, ensure_user, get_password_hash, verify_password, require_auth
router = APIRouter(prefix="/api", tags=["frontend-integration"])
class _Store:
"""Simple in-memory store backing the checklist endpoints."""
def __init__(self) -> None:
self.gateway_type: str | None = None
self.gateway_apis: List[Dict[str, Any]] = []
self.data_templates: List[Dict[str, Any]] = []
self.deployments: List[Dict[str, Any]] = []
self.workflows: Dict[str, Dict[str, Any]] = {}
self.billing_balance: float = 1200.0
self.billing_history: List[Dict[str, Any]] = []
self.tenants: Dict[str, Dict[str, Any]] = {}
self.channels: Dict[str, Dict[str, Any]] = {}
self.channel_admins: Dict[str, Dict[str, Any]] = {}
self.resource_applications: Dict[str, Dict[str, Any]] = {}
self.provider_models: Dict[str, Dict[str, Any]] = {}
self.provider_data: List[Dict[str, Any]] = []
store = _Store()
def _now() -> str:
return datetime.utcnow().isoformat()
async def _get_principal_user_id(request: Request, db: AsyncSession) -> uuid.UUID:
principal = getattr(request.state, "principal", None) or {}
email = principal.get("email") or "dev@taiji-ai.com"
user = await ensure_user(email, "temp-pass", db)
return user.id
async def _get_balance(db: AsyncSession, user_id: uuid.UUID) -> Balance:
result = await db.execute(select(Balance).where(Balance.user_id == user_id))
balance = result.scalar_one_or_none()
if balance is None:
balance = Balance(user_id=user_id, eu_balance=0.0)
db.add(balance)
await db.commit()
await db.refresh(balance)
return balance
# ----- User Dashboard -----
@router.get("/user/dashboard/stats")
async def user_dashboard_stats(request: Request, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agent_count = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
total_requests = (
await db.execute(select(func.coalesce(func.sum(Agent.total_executions), 0)))
).scalar() or 0
health = await system_monitor.get_system_health()
balance = await _get_balance(db, await _get_principal_user_id(request, db))
return {
"activeAgents": agent_count,
"totalRequests": total_requests,
"euBalance": round(balance.eu_balance, 2),
"systemHealth": health.get("score", 100),
}
@router.get("/user/agents/activity")
async def user_agents_activity(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
query = (
select(Execution)
.order_by(Execution.started_at.desc())
.limit(20)
)
rows = (await db.execute(query)).scalars().all()
activity = [
{
"executionId": row.execution_id,
"agentId": str(row.agent_id),
"status": row.status,
"startedAt": row.started_at.isoformat(),
"duration": row.execution_time or 0.0,
"eu": row.eu_consumed or 0.0,
}
for row in rows
]
return {"items": activity, "count": len(activity)}
@router.get("/user/resources/usage")
async def user_resources_usage(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agent_count = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
execution_stats = (
await db.execute(
select(
func.coalesce(func.sum(Execution.eu_consumed), 0),
func.coalesce(func.sum(Execution.cpu_usage), 0),
func.coalesce(func.sum(Execution.memory_usage), 0),
)
)
).one()
return {
"agents": agent_count,
"euConsumed": float(execution_stats[0] or 0),
"cpuSeconds": float(execution_stats[1] or 0),
"memoryMb": float(execution_stats[2] or 0),
}
# ----- Service Gateway -----
@router.post("/gateway/select")
async def select_gateway(payload: Dict[str, str]) -> Dict[str, Any]:
gateway_type = payload.get("gatewayType")
if gateway_type not in {"MCP", "A2A", "API"}:
raise HTTPException(status_code=400, detail="gatewayType must be MCP | A2A | API")
store.gateway_type = gateway_type
return {"selected": gateway_type, "updatedAt": _now()}
@router.post("/gateway/api/create")
async def create_gateway_api(
request: Request, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)
) -> Dict[str, Any]:
if not payload.get("name") or not payload.get("content"):
raise HTTPException(status_code=400, detail="name and content are required")
owner_id = await _get_principal_user_id(request, db)
gateway_api = GatewayAPI(
name=payload["name"],
method=payload.get("method", "json"),
content=payload.get("content", ""),
owner_id=owner_id,
)
db.add(gateway_api)
await db.commit()
await db.refresh(gateway_api)
item = {
"id": str(gateway_api.id),
"name": gateway_api.name,
"method": gateway_api.method,
"content": gateway_api.content,
"createdAt": gateway_api.created_at.isoformat(),
}
return item
@router.get("/gateway/apis")
async def list_gateway_apis(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
apis = (await db.execute(select(GatewayAPI).order_by(GatewayAPI.created_at.desc()))).scalars().all()
items = [
{
"id": str(api.id),
"name": api.name,
"method": api.method,
"content": api.content,
"createdAt": api.created_at.isoformat() if api.created_at else None,
}
for api in apis
]
return {"items": items, "count": len(items)}
@router.get("/gateway/monitoring")
async def gateway_monitoring(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
total = (await db.execute(select(func.count(GatewayAPI.id)))).scalar() or 0
return {
"selected": store.gateway_type,
"totalApis": total,
"lastUpdated": _now(),
"throughput": {
"rpm": 120,
"errorRate": 0.01,
},
}
# ----- Data & Tools -----
@router.post("/tools/generate")
async def generate_tool(payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
name = payload.get("name")
description = payload.get("description", "")
framework_template = payload.get("frameworkTemplate", "API")
if not name:
raise HTTPException(status_code=400, detail="name is required")
tool = Tool(
name=name,
description=description,
category=framework_template.lower(),
schema={
"type": "object",
"properties": payload.get("config", {}),
},
method="POST",
endpoint=payload.get("gateway", ""),
rate_limit=payload.get("maxScale", 100),
cost_per_call=0.0,
is_public=True,
)
db.add(tool)
await db.commit()
await db.refresh(tool)
return {
"id": str(tool.id),
"name": tool.name,
"description": tool.description,
"category": tool.category,
"endpoint": tool.endpoint,
"createdAt": _now(),
}
@router.get("/tools/list")
async def list_tools(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
tools = (await db.execute(select(Tool).order_by(Tool.created_at.desc()).limit(100))).scalars().all()
items = [
{
"id": str(tool.id),
"name": tool.name,
"description": tool.description,
"category": tool.category,
"endpoint": tool.endpoint,
"method": tool.method,
"isActive": tool.is_active,
}
for tool in tools
]
return {"items": items, "count": len(items)}
@router.post("/data-templates/create")
async def create_data_template(request: Request, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
if not payload.get("name") or not payload.get("type"):
raise HTTPException(status_code=400, detail="name and type are required")
template = DataTemplate(
name=payload["name"],
type=payload.get("type", "json_api"),
config=payload.get("config", {}),
owner_id=await _get_principal_user_id(request, db),
)
db.add(template)
await db.commit()
await db.refresh(template)
return {"id": str(template.id), "status": "created", "createdAt": template.created_at.isoformat()}
# ----- Agent Factory -----
@router.get("/agents/platform")
async def platform_agents(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agents = (await db.execute(select(Agent).order_by(Agent.created_at.desc()).limit(50))).scalars().all()
items = [
{
"id": str(agent.id),
"name": agent.name,
"description": agent.description,
"status": agent.status,
"version": agent.version,
}
for agent in agents
]
return {"items": items, "count": len(items)}
@router.post("/agents/deploy")
async def deploy_agent(payload: Dict[str, Any]) -> Dict[str, Any]:
if not payload.get("agentId"):
raise HTTPException(status_code=400, detail="agentId is required")
deployment = {
"deploymentId": str(uuid.uuid4()),
"agentId": payload["agentId"],
"instances": payload.get("instances", 1),
"model": payload.get("model", ""),
"gateway": payload.get("gateway", "MCP"),
"createdAt": _now(),
}
store.deployments.append(deployment)
return deployment
@router.get("/agents/deployed")
async def deployed_agents() -> Dict[str, Any]:
return {"items": store.deployments, "count": len(store.deployments)}
# ----- Workflows -----
@router.post("/workflows/create")
async def create_workflow(payload: Dict[str, Any]) -> Dict[str, Any]:
if not payload.get("name"):
raise HTTPException(status_code=400, detail="name is required")
workflow_id = str(uuid.uuid4())
workflow = {"id": workflow_id, **payload, "createdAt": _now()}
store.workflows[workflow_id] = workflow
return workflow
@router.get("/workflows/list")
async def list_workflows() -> Dict[str, Any]:
items = list(store.workflows.values())
return {"items": items, "count": len(items)}
@router.put("/workflows/{workflow_id}")
async def update_workflow(workflow_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if workflow_id not in store.workflows:
raise HTTPException(status_code=404, detail="workflow not found")
store.workflows[workflow_id].update(payload)
store.workflows[workflow_id]["updatedAt"] = _now()
return store.workflows[workflow_id]
@router.delete("/workflows/{workflow_id}")
async def delete_workflow(workflow_id: str) -> Dict[str, Any]:
if workflow_id not in store.workflows:
raise HTTPException(status_code=404, detail="workflow not found")
store.workflows.pop(workflow_id)
return {"deleted": True, "id": workflow_id}
# ----- Billing & Resources -----
@router.get("/billing/balance")
async def billing_balance(request: Request, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
balance = await _get_balance(db, await _get_principal_user_id(request, db))
return {"balance": round(balance.eu_balance, 2), "currency": balance.currency}
@router.get("/billing/history")
async def billing_history(request: Request, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
user_id = await _get_principal_user_id(request, db)
result = await db.execute(
select(Execution.execution_id, Execution.agent_id, Billing.eu_consumed, Billing.cost, Billing.created_at)
.select_from(Billing)
.join(Execution, Billing.execution_id == Execution.id, isouter=True)
.where(Billing.user_id == user_id)
.order_by(Billing.created_at.desc())
.limit(100)
)
records = [
{
"executionId": row.execution_id,
"agentId": str(row.agent_id) if row.agent_id else None,
"eu": float(row.eu_consumed or 0),
"cost": float(row.cost or 0),
"timestamp": row.created_at.isoformat() if row.created_at else _now(),
}
for row in result.all()
]
return {"records": records, "count": len(records)}
@router.post("/billing/recharge")
async def billing_recharge(request: Request, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
amount = float(payload.get("amount", 0))
if amount <= 0:
raise HTTPException(status_code=400, detail="amount must be positive")
user_id = await _get_principal_user_id(request, db)
balance = await _get_balance(db, user_id)
balance.eu_balance += amount
db.add(balance)
# 记录充值交易
billing = Billing(
execution_id=None,
eu_consumed=0.0,
cost=amount,
currency="EU",
cpu_time=0.0,
memory_max=0.0,
network_io=0.0,
storage_io=0.0,
user_id=user_id,
)
db.add(billing)
await db.commit()
return {"balance": round(balance.eu_balance, 2)}
# ----- Channel Partner -----
@router.post("/channel/auth/login")
async def channel_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
email = payload.get("email")
password = payload.get("password") or "temp-pass"
if not email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email is required")
# 查找或创建渠道用户
user = await ensure_user(email, password, db)
if user.hashed_password and not verify_password(password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
# 如果用户没有channel_id,尝试查找或创建对应的渠道
channel_id = user.channel_id
if not channel_id:
# 查找是否有对应的渠道
from models import Channel
result = await db.execute(select(Channel).where(Channel.email == email))
channel = result.scalar_one_or_none()
if not channel:
# 创建一个默认渠道
channel = Channel(
name=f"渠道-{email.split('@')[0]}",
email=email,
password_hash=user.password_hash,
commission_rate=10.0,
channel_credit=0.0,
custom_agent_cpu=2.0,
custom_agent_memory=4.0,
status="active"
)
db.add(channel)
await db.flush()
# 更新用户的channel_id
user.channel_id = channel.id
await db.commit()
await db.refresh(user)
channel_id = channel.id
token = create_access_token({
"sub": str(user.id),
"email": email,
"role": "channel_admin",
"channelId": str(channel_id)
})
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
@router.get("/channel/dashboard/stats")
async def channel_dashboard_stats(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
tenants_count = (await db.execute(select(func.count(Tenant.id)))).scalar() or 0
deployments = len(store.deployments)
balances = (await db.execute(select(func.coalesce(func.sum(Balance.eu_balance), 0)))).scalar() or 0
return {
"tenants": tenants_count,
"agents": deployments,
"eu": round(float(balances), 2),
}
@router.get("/channel/agents/available")
async def channel_agents_available(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agents = (await db.execute(select(Agent).limit(50))).scalars().all()
return {
"items": [
{"agentId": str(a.id), "name": a.name, "quantity": 10, "status": a.status}
for a in agents
],
"count": len(agents),
}
@router.get("/channel/tenants")
async def channel_tenants(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
) -> Dict[str, Any]:
"""获取渠道下的租户列表(需要认证)"""
# 从token中获取渠道ID
role = principal.get("claims", {}).get("role", "")
channel_id_str = principal.get("claims", {}).get("channelId")
if not channel_id_str:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
try:
channel_id = uuid.UUID(channel_id_str)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的渠道ID"
)
# 查询该渠道下的所有租户(role="user"的用户)
result = await db.execute(
select(User).where(
User.channel_id == channel_id,
User.role == "user"
)
)
tenants = result.scalars().all()
items = [
{
"id": str(t.id),
"name": t.name or t.full_name,
"email": t.email,
"subscriptionTier": getattr(t, "subscription_tier", "free"),
"balance": float(getattr(t, "balance", 0)),
"creditLimit": float(getattr(t, "credit_limit", 0)),
"status": getattr(t, "status", "active"),
"channelId": str(t.channel_id) if t.channel_id else None,
}
for t in tenants
]
return {"items": items, "count": len(items)}
@router.post("/channel/tenants/create")
async def channel_create_tenant(
payload: Dict[str, Any],
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
) -> Dict[str, Any]:
"""创建租户(需要认证,自动关联到当前渠道)"""
# 从token中获取渠道ID
role = principal.get("claims", {}).get("role", "")
channel_id_str = principal.get("claims", {}).get("channelId")
if not channel_id_str:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
try:
channel_id = uuid.UUID(channel_id_str)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的渠道ID"
)
# 验证必需字段
if not payload.get("name"):
raise HTTPException(status_code=400, detail="name is required")
if not payload.get("email"):
raise HTTPException(status_code=400, detail="email is required")
if not payload.get("password"):
raise HTTPException(status_code=400, detail="password is required")
# 检查邮箱是否已存在
result = await db.execute(
select(User).where(User.email == payload["email"])
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被使用"
)
# 创建租户(User with role="user")
password_hash = get_password_hash(payload["password"])
tenant = User(
name=payload["name"],
email=payload["email"],
password_hash=password_hash,
hashed_password=password_hash,
username=payload["email"].split("@")[0],
full_name=payload["name"],
role="user",
channel_id=channel_id,
subscription_tier=payload.get("subscriptionTier", "free"),
status="active",
balance=0,
credit_limit=0,
)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return {
"id": str(tenant.id),
"name": tenant.name or tenant.full_name,
"email": tenant.email,
"subscriptionTier": getattr(tenant, "subscription_tier", "free"),
}
@router.put("/channel/tenants/{tenant_id}/resources")
async def channel_update_tenant_resources(tenant_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
tenant = await db.get(Tenant, uuid.UUID(tenant_id)) if tenant_id else None
if not tenant:
raise HTTPException(status_code=404, detail="tenant not found")
tenant.subscription_tier = payload.get("subscriptionTier", tenant.subscription_tier)
tenant.discount = payload.get("discount", tenant.discount)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return {
"id": str(tenant.id),
"name": tenant.name,
"subscriptionTier": tenant.subscription_tier,
"discount": tenant.discount,
}
@router.put("/channel/tenants/{tenant_id}/billing")
async def channel_update_tenant_billing(tenant_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
tenant = await db.get(Tenant, uuid.UUID(tenant_id)) if tenant_id else None
if not tenant:
raise HTTPException(status_code=404, detail="tenant not found")
tenant.subscription_tier = payload.get("subscriptionTier", tenant.subscription_tier)
tenant.discount = payload.get("discount", tenant.discount)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return {
"id": str(tenant.id),
"subscriptionTier": tenant.subscription_tier,
"discount": tenant.discount,
}
@router.delete("/channel/tenants/{tenant_id}")
async def channel_delete_tenant(tenant_id: str, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
"""删除租户(软删除)"""
tenant = await db.get(Tenant, uuid.UUID(tenant_id)) if tenant_id else None
if not tenant:
raise HTTPException(status_code=404, detail="tenant not found")
# 软删除:标记为不活跃
tenant.status = "inactive"
db.add(tenant)
await db.commit()
return {
"id": str(tenant.id),
"name": tenant.name,
"deleted": True,
"message": "租户已删除"
}
@router.put("/channel/tenants/{tenant_id}/status")
async def channel_update_tenant_status(tenant_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
"""更新租户状态"""
tenant = await db.get(Tenant, uuid.UUID(tenant_id)) if tenant_id else None
if not tenant:
raise HTTPException(status_code=404, detail="tenant not found")
new_status = payload.get("status")
if new_status not in ["active", "inactive", "suspended"]:
raise HTTPException(status_code=400, detail="status must be active, inactive, or suspended")
old_status = tenant.status
tenant.status = new_status
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return {
"tenantId": str(tenant.id),
"name": tenant.name,
"oldStatus": old_status,
"newStatus": new_status,
}
@router.put("/channel/tenants/{tenant_id}/permissions")
async def channel_update_tenant_permissions(tenant_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
"""更新租户权限"""
tenant = await db.get(Tenant, uuid.UUID(tenant_id)) if tenant_id else None
if not tenant:
raise HTTPException(status_code=404, detail="tenant not found")
permissions = payload.get("permissions", [])
# 有效权限列表
valid_permissions = {
"use:platform_agents",
"use:custom_agents",
"create:agents",
"read:billing",
"export:data",
}
# 验证权限
invalid_permissions = set(permissions) - valid_permissions
if invalid_permissions:
raise HTTPException(
status_code=400,
detail=f"无效的权限: {', '.join(invalid_permissions)}"
)
# 更新权限(存储在metadata中如果没有专门的permissions字段)
if hasattr(tenant, 'permissions'):
tenant.permissions = permissions
db.add(tenant)
await db.commit()
return {
"tenantId": str(tenant.id),
"name": tenant.name,
"permissions": permissions,
}
@router.get("/channel/resources/agents")
async def channel_resources_agents(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
quotas = (await db.execute(select(ChannelAgentQuota))).scalars().all()
if quotas:
items = [
{
"agentId": str(q.agent_id),
"channelId": str(q.channel_id),
"quota": q.quantity,
"cpu": q.cpu,
"memory": q.memory,
}
for q in quotas
]
else:
agents = (await db.execute(select(Agent))).scalars().all()
items = [{"agentId": str(a.id), "quota": 10} for a in agents]
return {"items": items}
@router.get("/channel/resources/models")
async def channel_resources_models(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
models = (await db.execute(select(ProviderModel))).scalars().all()
return {
"items": [
{
"modelId": str(m.id),
"name": m.name,
"supportedModels": m.supported_models,
"rpm": m.rpm,
"tpm": m.tpm,
}
for m in models
]
}
@router.post("/channel/resources/apply")
async def channel_resources_apply(payload: Dict[str, Any]) -> Dict[str, Any]:
request_id = str(uuid.uuid4())
store.resource_applications[request_id] = {"id": request_id, **payload, "status": "pending"}
return store.resource_applications[request_id]
@router.get("/channel/billing/stats")
async def channel_billing_stats() -> Dict[str, Any]:
return {
"totalEU": sum(rec.get("eu", 0) for rec in store.billing_history),
"totalCost": sum(rec.get("cost", 0) for rec in store.billing_history),
"records": store.billing_history,
}
@router.get("/channel/admins")
async def channel_admins() -> Dict[str, Any]:
return {"items": list(store.channel_admins.values())}
@router.post("/channel/admins/create")
async def channel_admins_create(payload: Dict[str, Any]) -> Dict[str, Any]:
admin_id = str(uuid.uuid4())
admin = {"id": admin_id, **payload, "createdAt": _now()}
store.channel_admins[admin_id] = admin
return admin
@router.put("/channel/admins/{admin_id}/permissions")
async def channel_admins_permissions(admin_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if admin_id not in store.channel_admins:
raise HTTPException(status_code=404, detail="admin not found")
store.channel_admins[admin_id]["permissions"] = payload.get("permissions", [])
return store.channel_admins[admin_id]
# ----- Super Admin -----
@router.post("/admin/auth/login")
async def admin_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
email = payload.get("email")
password = payload.get("password") or "temp-pass"
if not email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email is required")
user = await ensure_user(email, password, db)
if user.hashed_password and not verify_password(password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
user.is_admin = True
db.add(user)
await db.commit()
# 使用用户的实际角色,并在token中包含channelId(如果存在)
token_data = {
"sub": str(user.id),
"email": email,
"role": user.role
}
if user.channel_id:
token_data["channelId"] = str(user.channel_id)
token = create_access_token(token_data)
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
@router.get("/admin/dashboard/stats")
async def admin_dashboard_stats(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agent_count = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
channels_count = (await db.execute(select(func.count(Channel.id)))).scalar() or 0
balances = (await db.execute(select(func.coalesce(func.sum(Balance.eu_balance), 0)))).scalar() or 0
return {
"channels": channels_count,
"agents": agent_count,
"eu": round(float(balances), 2),
}
@router.get("/admin/channels")
async def admin_channels(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
channels = (await db.execute(select(Channel))).scalars().all()
items = [
{
"id": str(ch.id),
"name": ch.name,
"email": ch.email,
"commissionRate": ch.commission_rate,
"monthlyQuota": ch.monthly_quota,
"monthlyBudget": ch.monthly_budget,
}
for ch in channels
]
return {"items": items, "count": len(items)}
@router.post("/admin/channels/create")
async def admin_create_channel(payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
required = {"name", "email"}
missing = [k for k in required if not payload.get(k)]
if missing:
raise HTTPException(status_code=400, detail=f"Missing fields: {', '.join(missing)}")
channel = Channel(
name=payload["name"],
email=payload["email"],
commission_rate=payload.get("commissionRate", 0.0),
monthly_quota=payload.get("monthlyQuota", 0),
monthly_budget=payload.get("monthlyBudget", 0),
)
db.add(channel)
await db.commit()
await db.refresh(channel)
return {
"id": str(channel.id),
"name": channel.name,
"email": channel.email,
"commissionRate": channel.commission_rate,
"monthlyQuota": channel.monthly_quota,
"monthlyBudget": channel.monthly_budget,
}
@router.put("/admin/channels/{channel_id}/commission")
async def admin_update_channel_commission(channel_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
channel = await db.get(Channel, uuid.UUID(channel_id)) if channel_id else None
if not channel:
raise HTTPException(status_code=404, detail="channel not found")
channel.commission_rate = payload.get("commissionRate", channel.commission_rate)
db.add(channel)
await db.commit()
await db.refresh(channel)
return {
"id": str(channel.id),
"name": channel.name,
"commissionRate": channel.commission_rate,
}
@router.get("/admin/channels/{channel_id}/resources")
async def admin_get_channel_resources(channel_id: str, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
"""获取渠道的资源分配信息"""
channel = await db.get(Channel, uuid.UUID(channel_id)) if channel_id else None
if not channel:
raise HTTPException(status_code=404, detail="channel not found")
# 获取渠道的Agent配额
quotas = (await db.execute(
select(ChannelAgentQuota).where(ChannelAgentQuota.channel_id == channel.id)
)).scalars().all()
agents = []
for q in quotas:
agent = await db.get(Agent, q.agent_id)
if agent:
agents.append({
"agentId": str(q.agent_id),
"agentName": agent.name,
"quantity": q.quantity,
})
# 获取所有可用的模型供应商
models = (await db.execute(select(ProviderModel).where(ProviderModel.status == "active"))).scalars().all()
model_ids = [str(m.id) for m in models]
return {
"id": str(channel.id),
"channelName": channel.name,
"models": model_ids,
"agents": agents,
"customAgentResources": {
"cpu": float(channel.custom_agent_cpu) if channel.custom_agent_cpu else 2.0,
"memory": float(channel.custom_agent_memory) if channel.custom_agent_memory else 4.0,
},
"channelCredit": float(channel.channel_credit) if channel.channel_credit else 0.0,
}
@router.put("/admin/channels/{channel_id}/resources")
async def admin_update_channel_resources(channel_id: str, payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
channel = await db.get(Channel, uuid.UUID(channel_id)) if channel_id else None
if not channel:
raise HTTPException(status_code=404, detail="channel not found")
# 简化为保存配额到 ChannelAgentQuota
if payload.get("agents"):
for agent in payload["agents"]:
agent_id = uuid.UUID(agent.get("agentId"))
quota = ChannelAgentQuota(channel_id=channel.id, agent_id=agent_id, quantity=agent.get("quantity", 0))
db.add(quota)
db.add(channel)
await db.commit()
return {"id": str(channel.id), "updatedAt": _now()}
@router.get("/admin/channels/applications")
async def admin_channel_applications() -> Dict[str, Any]:
return {"items": list(store.resource_applications.values())}
@router.put("/admin/channels/applications/{request_id}/approve")
async def admin_channel_applications_approve(request_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if request_id not in store.resource_applications:
raise HTTPException(status_code=404, detail="application not found")
store.resource_applications[request_id]["status"] = "approved" if payload.get("approved") else "rejected"
store.resource_applications[request_id]["reason"] = payload.get("reason", "")
return store.resource_applications[request_id]
@router.get("/admin/resources/models")
async def admin_resources_models(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
models = (await db.execute(select(ProviderModel).order_by(ProviderModel.created_at.desc()))).scalars().all()
items = [
{
"id": str(m.id),
"name": m.name,
"apiUrl": m.api_url,
"supportedModels": m.supported_models,
"rpm": m.rpm,
"tpm": m.tpm,
"isActive": m.is_active,
}
for m in models
]
return {"items": items}
@router.post("/admin/resources/models/add")
async def admin_resources_models_add(payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
required = {"name", "apiUrl", "apiKey", "supportedModels"}
missing = [k for k in required if not payload.get(k)]
if missing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Missing fields: {', '.join(missing)}")
provider_model = ProviderModel(
name=payload["name"],
api_url=payload["apiUrl"],
api_key=payload["apiKey"],
supported_models=payload.get("supportedModels", []),
rpm=payload.get("rpm", 0),
tpm=payload.get("tpm", 0),
)
db.add(provider_model)
await db.commit()
await db.refresh(provider_model)
return {
"id": str(provider_model.id),
"name": provider_model.name,
"apiUrl": provider_model.api_url,
"supportedModels": provider_model.supported_models,
"rpm": provider_model.rpm,
"tpm": provider_model.tpm,
}
@router.get("/admin/resources/agents")
async def admin_resources_agents(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
agents = (await db.execute(select(Agent))).scalars().all()
return {"items": [{"id": str(a.id), "cpu": 1, "memory": 512, "maxInstances": 5} for a in agents]}
@router.put("/admin/resources/agents/{agent_id}")
async def admin_resources_agents_update(agent_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
return {"id": agent_id, **payload, "updatedAt": _now()}
@router.get("/admin/monitoring/agents")
async def admin_monitoring_agents() -> Dict[str, Any]:
metrics = await system_monitor.get_system_metrics()
return {"health": metrics, "updatedAt": _now()}
@router.get("/admin/billing/overview")
async def admin_billing_overview(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
billing_rows = (
await db.execute(
select(Billing.cost, Billing.eu_consumed, Billing.created_at, Billing.channel_id, Billing.tenant_id)
.order_by(Billing.created_at.desc())
.limit(200)
)
).all()
total_eu = sum(float(row.eu_consumed or 0) for row in billing_rows)
total_cost = sum(float(row.cost or 0) for row in billing_rows)
channels = [
{
"channelName": str(row.channel_id) if row.channel_id else "",
"calls": 1,
"totalEU": float(row.eu_consumed or 0),
"totalCost": float(row.cost or 0),
}
for row in billing_rows
]
tenants = [
{
"tenantName": str(row.tenant_id) if row.tenant_id else "",
"channelName": str(row.channel_id) if row.channel_id else "",
"calls": 1,
"totalEU": float(row.eu_consumed or 0),
"totalCost": float(row.cost or 0),
}
for row in billing_rows
]
call_records = [
{
"timestamp": row.created_at.isoformat() if row.created_at else _now(),
"channelName": str(row.channel_id) if row.channel_id else "",
"tenantName": str(row.tenant_id) if row.tenant_id else "",
"agentName": "",
"duration": 0,
"eu": float(row.eu_consumed or 0),
"cost": float(row.cost or 0),
}
for row in billing_rows
]
return {"channels": channels, "tenants": tenants, "callRecords": call_records, "totalEU": total_eu, "totalCost": total_cost}
@router.get("/admin/roles")
async def admin_roles() -> Dict[str, Any]:
"""获取可用角色列表"""
return {
"items": [
{
"id": "super_admin",
"name": "超级管理员",
"description": "拥有系统所有权限",
"permissions": ["*"]
},
{
"id": "billing_admin",
"name": "计费管理员",
"description": "完整写入权限,可创建渠道、管理租户、计费操作",
"permissions": ["read:*", "write:channels", "write:tenants", "write:billing"]
},
{
"id": "operations_admin",
"name": "运维管理员",
"description": "只读权限,仅查看和监控",
"permissions": ["read:*"]
},
{
"id": "channel_admin",
"name": "渠道管理员",
"description": "渠道内部管理权限",
"permissions": ["read:channel", "write:tenants", "read:billing"]
},
{
"id": "user",
"name": "普通用户",
"description": "标准用户权限",
"permissions": ["read:self", "use:agents"]
}
]
}
@router.get("/admin/channels/{channel_id}/admins")
async def admin_get_channel_admins(channel_id: str, db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
"""获取渠道管理员列表"""
from models import User
channel = await db.get(Channel, uuid.UUID(channel_id)) if channel_id else None
if not channel:
raise HTTPException(status_code=404, detail="channel not found")
# 查询渠道下的管理员
admins = (await db.execute(
select(User).where(
User.channel_id == channel.id,
User.role == "channel_admin",
User.status == "active"
)
)).scalars().all()
items = [
{
"id": str(admin.id),
"name": admin.name or admin.full_name,
"email": admin.email,
"role": admin.role,
"status": admin.status,
"createdAt": admin.created_at.isoformat() if admin.created_at else None,
}
for admin in admins
]
return {
"channelId": str(channel.id),
"channelName": channel.name,
"admins": items
}
@router.post("/admin/admins/create")
async def admin_admins_create(payload: Dict[str, Any]) -> Dict[str, Any]:
admin_id = str(uuid.uuid4())
admin = {"id": admin_id, **payload, "createdAt": _now()}
store.channel_admins[admin_id] = admin
return admin
@router.get("/admin/providers/stats")
async def admin_providers_stats(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
models = (await db.execute(select(ProviderModel))).scalars().all()
return {
"providers": len(models),
"models": [
{
"id": str(m.id),
"name": m.name,
"apiUrl": m.api_url,
"supportedModels": m.supported_models,
}
for m in models
],
}
@router.get("/admin/channels/backend/stats")
async def admin_channels_backend_stats() -> Dict[str, Any]:
return {"channels": len(store.channels), "applications": len(store.resource_applications)}
# ----- Provider Management -----
@router.post("/providers/auth/login")
async def providers_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
email = payload.get("email")
password = payload.get("password") or "temp-pass"
if not email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email is required")
user = await ensure_user(email, password, db)
if user.hashed_password and not verify_password(password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
token = create_access_token({"sub": str(user.id), "email": email, "role": "provider_admin"})
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
@router.get("/providers/models")
async def providers_models(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
models = (await db.execute(select(ProviderModel))).scalars().all()
return {
"items": [
{
"id": str(m.id),
"name": m.name,
"apiUrl": m.api_url,
"supportedModels": m.supported_models,
"rpm": m.rpm,
"tpm": m.tpm,
}
for m in models
]
}
@router.post("/providers/models/add")
async def providers_models_add(payload: Dict[str, Any], db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
required = {"name", "apiUrl", "apiKey"}
missing = [k for k in required if not payload.get(k)]
if missing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Missing fields: {', '.join(missing)}")
provider_model = ProviderModel(
name=payload["name"],
api_url=payload["apiUrl"],
api_key=payload["apiKey"],
supported_models=payload.get("supportedModels", []),
rpm=payload.get("rpm", 0),
tpm=payload.get("tpm", 0),
)
db.add(provider_model)
await db.commit()
await db.refresh(provider_model)
return {
"id": str(provider_model.id),
"name": provider_model.name,
"apiUrl": provider_model.api_url,
"supportedModels": provider_model.supported_models,
}
@router.get("/providers/data")
async def providers_data() -> Dict[str, Any]:
if not store.provider_data:
store.provider_data.append(
{
"id": str(uuid.uuid4()),
"name": "RapidAPI",
"category": "api",
"endpoints": len(store.gateway_apis),
}
)
return {"items": store.provider_data}