Files
taiji-AI-PAD/services/mcp-server/app/routes/frontend_integration.py
T
2026-05-05 14:13:59 +08:00

761 lines
26 KiB
Python

"""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,
Execution,
Billing,
GatewayAPI,
ProviderModel,
Tenant,
Tool,
User,
ModelBillingRecord,
)
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.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]:
from datetime import datetime, timedelta
user_id = await _get_principal_user_id(request, db)
# 活跃Agent数量(属于该用户的)
agent_count = (
await db.execute(
select(func.count(Agent.id))
.where(Agent.owner_id == user_id)
.where(Agent.status == "active")
)
).scalar() or 0
# 总请求数(统计该用户的模型调用次数)
total_requests = (
await db.execute(
select(func.count(ModelBillingRecord.id))
.where(ModelBillingRecord.tenant_id == user_id)
)
).scalar() or 0
# 计算24小时内的请求数(统计该用户的模型调用次数)
time_24h_ago = datetime.utcnow() - timedelta(hours=24)
requests_24h = (
await db.execute(
select(func.count(ModelBillingRecord.id))
.where(ModelBillingRecord.tenant_id == user_id)
.where(ModelBillingRecord.created_at >= time_24h_ago)
)
).scalar() or 0
health = await system_monitor.get_system_health()
balance = await _get_balance(db, user_id)
return {
"activeAgents": agent_count,
"totalRequests": total_requests,
"requests24h": requests_24h,
"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)}
# ----- 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}
@router.post("/workflows/{workflow_id}/run")
async def run_workflow(workflow_id: str, payload: Dict[str, Any] = None) -> Dict[str, Any]:
"""
运行工作流
执行指定的工作流,按顺序调用工作流中的各个Agent节点
"""
if workflow_id not in store.workflows:
raise HTTPException(status_code=404, detail="workflow not found")
workflow = store.workflows[workflow_id]
# 创建执行记录
execution_id = str(uuid.uuid4())
started_at = _now()
# 模拟执行工作流中的节点
nodes = workflow.get("nodes", [])
node_results = []
for node in nodes:
node_result = {
"nodeId": node.get("agentId"),
"agentName": node.get("agentName"),
"order": node.get("order"),
"status": "completed",
"startedAt": _now(),
"completedAt": _now(),
"output": {"message": f"Node {node.get('order')} executed successfully"}
}
node_results.append(node_result)
# 更新工作流状态
workflow["lastRunAt"] = started_at
workflow["lastRunStatus"] = "completed"
return {
"success": True,
"data": {
"executionId": execution_id,
"workflowId": workflow_id,
"workflowName": workflow.get("name"),
"status": "completed",
"startedAt": started_at,
"completedAt": _now(),
"nodeResults": node_results,
"totalNodes": len(nodes),
"completedNodes": len(nodes),
},
"message": f"工作流 {workflow.get('name')} 执行完成"
}
# ----- 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/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
]
}
# ----- 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}
# =====================================================
# 注意: 以下 /admin/* 接口已移至 admin.py,避免重复定义
# 已删除的接口包括:
# - /admin/dashboard/stats (从 AgentBillingRecord 和 ModelBillingRecord 统计)
# - /admin/channels (使用 admin.py 中的完整实现)
# - /admin/channels/create (使用 admin.py 中带 LiteLLM 集成的实现)
# - /admin/channels/{channel_id}/commission (使用 admin.py 中的实现)
# - /admin/channels/{channel_id}/resources GET/PUT (使用 admin.py 中的实现)
# - /admin/channels/applications (使用 admin.py 中的数据库实现)
# - /admin/resources/models (使用 admin.py 中的 ModelProvider 实现)
# - /admin/resources/agents (使用 admin.py 中的完整实现)
# - /admin/monitoring/agents (使用 admin.py 中的 K8s+DB 实现)
# - /admin/billing/overview (使用 admin.py 中的正确计费表实现)
# - /admin/roles (使用 admin.py 中的实现)
# - /admin/channels/{channel_id}/admins (使用 admin.py 中的实现)
# - /admin/admins/create (使用 admin.py 中的实现)
# =====================================================
# 保留: /admin/resources/models/add - 独立功能,admin.py 中没有对应实现
@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,
}
# 保留: /admin/providers/stats - 独立功能,admin.py 中没有对应实现
@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
],
}
# ----- 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}