forked from xiaohei/taiji-AI-PAD
更新超级管理员收入
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
邮箱验证码功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import Optional
|
||||
import structlog
|
||||
from app.state import get_state
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# 邮箱配置 - 从环境变量读取
|
||||
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.office365.com")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
||||
SMTP_EMAIL = os.getenv("SMTP_EMAIL", "supportagnet@taijiaicloud.com")
|
||||
# 注意:生产环境应该通过环境变量设置SMTP_PASSWORD,不要硬编码密码
|
||||
# 这里使用默认值仅用于开发测试,生产环境必须通过环境变量配置
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "l5YYL7TOK2WvRKtf")
|
||||
|
||||
# 验证码配置
|
||||
VERIFICATION_CODE_LENGTH = 6
|
||||
VERIFICATION_CODE_EXPIRE_SECONDS = 600 # 10分钟
|
||||
|
||||
|
||||
def _send_email_sync(msg: MIMEMultipart) -> None:
|
||||
"""同步发送邮件(在 executor 中运行)"""
|
||||
if not SMTP_PASSWORD:
|
||||
raise ValueError("SMTP_PASSWORD环境变量未设置,无法发送邮件")
|
||||
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
|
||||
try:
|
||||
server.starttls()
|
||||
server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
||||
server.send_message(msg)
|
||||
logger.info("邮件发送成功", to=msg['To'])
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error("SMTP认证失败", error=str(e), smtp_server=SMTP_SERVER, smtp_email=SMTP_EMAIL)
|
||||
raise
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error("SMTP发送失败", error=str(e))
|
||||
raise
|
||||
finally:
|
||||
server.quit()
|
||||
|
||||
|
||||
def generate_verification_code() -> str:
|
||||
"""生成6位数字验证码"""
|
||||
return ''.join(random.choices(string.digits, k=VERIFICATION_CODE_LENGTH))
|
||||
|
||||
|
||||
async def send_verification_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
发送验证码邮件
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if not SMTP_PASSWORD:
|
||||
logger.error("SMTP密码未配置,无法发送邮件", email=email)
|
||||
return False
|
||||
|
||||
try:
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = SMTP_EMAIL
|
||||
msg['To'] = email
|
||||
msg['Subject'] = "Taiji AI-PAD 注册验证码"
|
||||
|
||||
# 邮件正文
|
||||
body = f"""
|
||||
尊敬的用户:
|
||||
|
||||
您的注册验证码是:{code}
|
||||
|
||||
验证码有效期为10分钟,请勿泄露给他人。
|
||||
|
||||
如果您没有进行注册操作,请忽略此邮件。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
|
||||
---
|
||||
Taiji AI-PAD 团队
|
||||
"""
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件(使用同步方式,因为 smtplib 不支持异步)
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _send_email_sync, msg)
|
||||
|
||||
logger.info("验证码邮件发送成功", email=email)
|
||||
return True
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(
|
||||
"SMTP认证失败,请检查SMTP_PASSWORD是否正确",
|
||||
email=email,
|
||||
error=str(e),
|
||||
smtp_server=SMTP_SERVER,
|
||||
smtp_email=SMTP_EMAIL,
|
||||
hint="Office365可能需要使用应用专用密码(App Password)而不是普通密码"
|
||||
)
|
||||
return False
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error("SMTP发送失败", email=email, error=str(e), smtp_server=SMTP_SERVER)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("验证码邮件发送失败", email=email, error=str(e), error_type=type(e).__name__)
|
||||
return False
|
||||
|
||||
|
||||
async def store_verification_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
存储验证码到Redis
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否存储成功
|
||||
"""
|
||||
try:
|
||||
state = get_state()
|
||||
if not state.redis_client:
|
||||
logger.warning("Redis未连接,无法存储验证码")
|
||||
return False
|
||||
|
||||
key = f"verification_code:{email}"
|
||||
await state.redis_client.setex(
|
||||
key,
|
||||
VERIFICATION_CODE_EXPIRE_SECONDS,
|
||||
code
|
||||
)
|
||||
logger.info("验证码已存储", email=email)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("验证码存储失败", email=email, error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def verify_code(email: str, code: str) -> bool:
|
||||
"""
|
||||
验证验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
是否验证成功
|
||||
"""
|
||||
try:
|
||||
state = get_state()
|
||||
if not state.redis_client:
|
||||
logger.warning("Redis未连接,无法验证验证码")
|
||||
return False
|
||||
|
||||
key = f"verification_code:{email}"
|
||||
|
||||
# 处理Redis集群的MOVED重定向
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
stored_code = await state.redis_client.get(key)
|
||||
break
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if "MOVED" in error_str and attempt < max_retries - 1:
|
||||
# Redis集群重定向,等待后重试
|
||||
import asyncio
|
||||
await asyncio.sleep(0.1)
|
||||
logger.debug("Redis集群重定向,重试中", attempt=attempt+1, error=error_str)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if not stored_code:
|
||||
logger.warning("验证码不存在或已过期", email=email, provided_code=code)
|
||||
return False
|
||||
|
||||
# 确保都是字符串类型进行比较
|
||||
stored_code = str(stored_code).strip()
|
||||
code = str(code).strip()
|
||||
|
||||
if stored_code != code:
|
||||
logger.warning(
|
||||
"验证码错误",
|
||||
email=email,
|
||||
provided_code=code,
|
||||
stored_code=stored_code,
|
||||
provided_type=type(code).__name__,
|
||||
stored_type=type(stored_code).__name__
|
||||
)
|
||||
return False
|
||||
|
||||
# 验证成功后删除验证码(同样处理集群重定向)
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
await state.redis_client.delete(key)
|
||||
break
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if "MOVED" in error_str and attempt < max_retries - 1:
|
||||
import asyncio
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
else:
|
||||
logger.warning("删除验证码失败,但验证已成功", email=email, error=error_str)
|
||||
break
|
||||
|
||||
logger.info("验证码验证成功", email=email)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("验证码验证失败", email=email, code=code, error=str(e), error_type=type(e).__name__)
|
||||
return False
|
||||
|
||||
|
||||
async def send_and_store_verification_code(email: str) -> Optional[str]:
|
||||
"""
|
||||
生成、发送并存储验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
验证码(如果成功),None(如果失败)
|
||||
"""
|
||||
import os
|
||||
code = generate_verification_code()
|
||||
|
||||
# 先存储验证码(即使邮件发送失败,验证码也已存储,可以手动查看Redis)
|
||||
store_success = await store_verification_code(email, code)
|
||||
if not store_success:
|
||||
logger.error("验证码存储失败,无法继续", email=email)
|
||||
return None
|
||||
|
||||
# 发送邮件
|
||||
send_success = await send_verification_code(email, code)
|
||||
|
||||
# 测试模式:即使邮件发送失败也返回验证码(仅用于开发/测试环境)
|
||||
test_mode = os.getenv("ENABLE_TEST_MODE", "false").lower() == "true" or os.getenv("DEBUG", "false").lower() == "true"
|
||||
|
||||
if not send_success:
|
||||
if test_mode:
|
||||
# 测试模式:记录验证码到日志(仅测试环境)
|
||||
logger.warning(
|
||||
"测试模式:邮件发送失败,但验证码已存储到Redis",
|
||||
email=email,
|
||||
verification_code=code,
|
||||
hint="验证码已存储到Redis,可通过Redis获取或查看日志(仅测试环境)"
|
||||
)
|
||||
return code
|
||||
else:
|
||||
# 生产模式:邮件发送失败则不返回验证码
|
||||
logger.error("邮件发送失败,验证码已存储但未发送", email=email)
|
||||
return None
|
||||
|
||||
logger.info("验证码已发送并存储", email=email)
|
||||
return code
|
||||
|
||||
@@ -39,11 +39,37 @@ def register_lifecycle_events(app: FastAPI) -> None:
|
||||
# Redis是可选的,连接失败不影响服务启动
|
||||
try:
|
||||
if settings.redis_url:
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
# 检查是否是集群模式(通过URL或错误信息判断)
|
||||
# Azure Redis Cache集群模式需要使用集群客户端
|
||||
try:
|
||||
# 先尝试普通连接
|
||||
test_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
await test_client.ping()
|
||||
await test_client.aclose()
|
||||
# 普通连接成功,使用普通客户端
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
)
|
||||
except Exception as cluster_test:
|
||||
# 如果普通连接失败,可能是集群模式,尝试使用集群客户端
|
||||
# 注意:redis-py的集群支持需要额外配置
|
||||
# 这里先使用普通连接,但添加重定向处理
|
||||
logger.warning("Redis普通连接失败,尝试集群模式", error=str(cluster_test))
|
||||
# 对于Azure Redis Cache,通常使用普通连接但需要处理MOVED重定向
|
||||
# 使用skip_full_coverage_check=True来允许部分节点连接
|
||||
state.redis_client = redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
skip_full_coverage_check=True,
|
||||
)
|
||||
|
||||
await state.redis_client.ping()
|
||||
redis_connections.set(1)
|
||||
logger.info("Redis连接成功")
|
||||
|
||||
@@ -18,7 +18,7 @@ from models import (
|
||||
BillingRecord, Application, ModelProvider,
|
||||
ChannelProviderAccess, ProviderApplication,
|
||||
ChannelCustomAgentQuota, ResourceApplication, PlatformAgentQuota,
|
||||
PlatformAgentTemplateConfig
|
||||
PlatformAgentTemplateConfig, AgentBillingRecord, ModelBillingRecord
|
||||
)
|
||||
from app.auth import require_auth, get_password_hash
|
||||
from app.schemas import (
|
||||
@@ -483,13 +483,25 @@ async def get_admin_dashboard_stats(
|
||||
# 总 Agent 数
|
||||
total_agents = platform_agents_count + custom_agents_count
|
||||
|
||||
# 总调用次数
|
||||
calls_count = await db.execute(select(func.count(BillingRecord.id)))
|
||||
total_calls = calls_count.scalar() or 0
|
||||
# 总调用次数 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
|
||||
agent_calls_count = await db.execute(select(func.count(AgentBillingRecord.id)))
|
||||
agent_calls = agent_calls_count.scalar() or 0
|
||||
|
||||
# 总收入
|
||||
revenue = await db.execute(select(func.sum(BillingRecord.cost)))
|
||||
total_revenue = float(revenue.scalar() or 0)
|
||||
model_calls_count = await db.execute(select(func.count(ModelBillingRecord.id)))
|
||||
model_calls = model_calls_count.scalar() or 0
|
||||
|
||||
total_calls = agent_calls + model_calls
|
||||
|
||||
# 总收入 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
|
||||
# AgentBillingRecord.cost 是 Agent 使用费用
|
||||
agent_revenue = await db.execute(select(func.sum(AgentBillingRecord.cost)))
|
||||
agent_total = float(agent_revenue.scalar() or 0)
|
||||
|
||||
# ModelBillingRecord.total_cost 是模型调用费用
|
||||
model_revenue = await db.execute(select(func.sum(ModelBillingRecord.total_cost)))
|
||||
model_total = float(model_revenue.scalar() or 0)
|
||||
|
||||
total_revenue = agent_total + model_total
|
||||
|
||||
# 从 Agent Manager 获取 K8s 中实际运行的平台端 Agent 资源统计
|
||||
k8s_agents_count = 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import secrets
|
||||
@@ -11,7 +11,10 @@ import hashlib
|
||||
from typing import Optional
|
||||
|
||||
from database import get_db
|
||||
from models import User, Channel, APIKey
|
||||
from models import (
|
||||
User, Channel, APIKey, Balance, TenantCustomAgentQuota,
|
||||
PlatformAgentQuota, TenantModelKey, ResourceAllocation, ModelProvider
|
||||
)
|
||||
from app.auth import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
@@ -28,7 +31,16 @@ from app.schemas import (
|
||||
RegenerateAPIKeyResponse,
|
||||
UserCreate,
|
||||
)
|
||||
from app.email_verification import verify_code, send_and_store_verification_code
|
||||
from app.agent_manager_client import get_agent_manager_client, AgentManagerError
|
||||
from app.litellm_client import get_litellm_client, LiteLLMClientError
|
||||
from config import settings
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import and_
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
|
||||
@@ -410,13 +422,60 @@ async def regenerate_api_key(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register/send-code", response_model=SuccessResponse)
|
||||
async def send_verification_code_endpoint(
|
||||
email: str = Query(..., description="邮箱地址"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
发送邮箱验证码
|
||||
|
||||
在用户注册前,先调用此接口发送验证码到邮箱
|
||||
"""
|
||||
# 检查邮箱是否已存在
|
||||
result = await db.execute(select(User).where(User.email == email))
|
||||
existing_user = result.scalar_one_or_none()
|
||||
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该邮箱已被注册"
|
||||
)
|
||||
|
||||
# 发送验证码
|
||||
code = await send_and_store_verification_code(email)
|
||||
if not code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="验证码发送失败,请稍后重试"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
message="验证码已发送到您的邮箱,请查收"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register", response_model=SuccessResponse)
|
||||
async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
用户注册接口
|
||||
|
||||
允许用户自由注册,创建普通用户账户
|
||||
注册成功后自动分配默认资源:
|
||||
- 分配到 taiji 渠道 (channelId: b415e70b-8d37-481c-b229-bc3b7871607b)
|
||||
- 自定义 agent 配额:2 CPU, 2 GB 内存
|
||||
- 平台 agent 各1个
|
||||
- 供应商所有模型
|
||||
- 余额:20元
|
||||
"""
|
||||
# 验证邮箱验证码
|
||||
is_valid = await verify_code(req.email, req.verification_code)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="验证码错误或已过期"
|
||||
)
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
existing_user = result.scalar_one_or_none()
|
||||
@@ -427,7 +486,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
detail="该邮箱已被注册"
|
||||
)
|
||||
|
||||
# 检查用户名是否已存在(如果提供了username)
|
||||
# 检查用户名是否已存在
|
||||
if req.username:
|
||||
result = await db.execute(select(User).where(User.username == req.username))
|
||||
existing_username = result.scalar_one_or_none()
|
||||
@@ -437,6 +496,20 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
detail="该用户名已被使用"
|
||||
)
|
||||
|
||||
# taiji 渠道 ID
|
||||
TAIJI_CHANNEL_ID = uuid.UUID("b415e70b-8d37-481c-b229-bc3b7871607b")
|
||||
|
||||
# 验证渠道存在
|
||||
channel_result = await db.execute(
|
||||
select(Channel).where(Channel.id == TAIJI_CHANNEL_ID)
|
||||
)
|
||||
channel = channel_result.scalar_one_or_none()
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="渠道不存在,请联系管理员"
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
password_hash = get_password_hash(req.password)
|
||||
username = req.username or req.email.split("@")[0]
|
||||
@@ -450,6 +523,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
username=username,
|
||||
full_name=req.full_name or name,
|
||||
role="user", # 默认角色为普通用户
|
||||
channel_id=TAIJI_CHANNEL_ID, # 分配到 taiji 渠道
|
||||
status="active",
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
@@ -460,8 +534,143 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
await db.flush() # 获取 user.id
|
||||
|
||||
user_id = new_user.id
|
||||
|
||||
try:
|
||||
# 1. 创建余额记录,初始余额 20 元
|
||||
balance = Balance(
|
||||
user_id=user_id,
|
||||
eu_balance=20.0
|
||||
)
|
||||
db.add(balance)
|
||||
|
||||
# 2. 分配自定义 Agent 配额:2 CPU, 2 GB 内存
|
||||
custom_agent_quota = TenantCustomAgentQuota(
|
||||
tenant_id=user_id,
|
||||
cpu_quota=2.0,
|
||||
memory_quota=2.0,
|
||||
cpu_used=0.0,
|
||||
memory_used=0.0,
|
||||
agent_count=0
|
||||
)
|
||||
db.add(custom_agent_quota)
|
||||
|
||||
# 3. 获取所有平台 Agent 模板,为每个模板分配 1 个配额
|
||||
try:
|
||||
agent_manager_client = get_agent_manager_client()
|
||||
platform_templates = await agent_manager_client.list_platform_templates()
|
||||
|
||||
for template in platform_templates:
|
||||
template_name = template.template
|
||||
# 创建平台 Agent 配额记录
|
||||
platform_quota = PlatformAgentQuota(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
template_name=template_name,
|
||||
pod_quota=1, # 每个平台 agent 分配 1 个配额
|
||||
pod_used=0,
|
||||
allocated_by=None, # 系统自动分配
|
||||
allocated_at=datetime.utcnow()
|
||||
)
|
||||
db.add(platform_quota)
|
||||
|
||||
# 同时创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
resource_type="agent",
|
||||
resource_id=template_name,
|
||||
quantity=1
|
||||
)
|
||||
db.add(allocation)
|
||||
except (AgentManagerError, Exception) as e:
|
||||
logger.warning(f"获取平台 Agent 模板失败,跳过平台 Agent 分配: {e}")
|
||||
|
||||
# 4. 分配所有供应商模型
|
||||
providers_result = await db.execute(
|
||||
select(ModelProvider).where(ModelProvider.is_active == True)
|
||||
)
|
||||
providers = providers_result.scalars().all()
|
||||
|
||||
if channel.litellm_team_id:
|
||||
litellm_client = get_litellm_client()
|
||||
|
||||
for provider in providers:
|
||||
# 为每个供应商的每个模型创建 TenantModelKey
|
||||
for model_name in provider.supported_models:
|
||||
try:
|
||||
# 在 LiteLLM 中创建 Key
|
||||
key = await litellm_client.generate_key(
|
||||
team_id=channel.litellm_team_id,
|
||||
models=[model_name],
|
||||
rpm_limit=provider.rpm or 60,
|
||||
tpm_limit=provider.tpm or 10000,
|
||||
max_budget=500.0, # 默认预算
|
||||
budget_duration="monthly",
|
||||
key_name=f"tenant-{user_id}-{model_name}",
|
||||
metadata={
|
||||
"tenant_id": str(user_id),
|
||||
"tenant_name": name,
|
||||
"channel_id": str(TAIJI_CHANNEL_ID),
|
||||
"channel_name": channel.name,
|
||||
"model": model_name,
|
||||
}
|
||||
)
|
||||
|
||||
# 加密存储 Key
|
||||
encrypted_key = litellm_client.encrypt_key(key.key)
|
||||
|
||||
# 保存到数据库
|
||||
tenant_key = TenantModelKey(
|
||||
tenant_id=user_id,
|
||||
channel_id=TAIJI_CHANNEL_ID,
|
||||
model_name=model_name,
|
||||
litellm_key_id=key.key,
|
||||
litellm_key_hash=encrypted_key,
|
||||
rpm_limit=provider.rpm or 60,
|
||||
tpm_limit=provider.tpm or 10000,
|
||||
max_budget=500.0,
|
||||
budget_duration="monthly",
|
||||
status="active",
|
||||
)
|
||||
db.add(tenant_key)
|
||||
|
||||
# 同时记录到 ResourceAllocation
|
||||
model_allocation = ResourceAllocation(
|
||||
target_id=user_id,
|
||||
target_type="tenant",
|
||||
resource_type="model",
|
||||
resource_id=str(provider.id),
|
||||
rpm=provider.rpm or 60,
|
||||
tpm=provider.tpm or 10000,
|
||||
)
|
||||
db.add(model_allocation)
|
||||
except (LiteLLMClientError, Exception) as e:
|
||||
logger.warning(f"为模型 {model_name} 创建 LiteLLM Key 失败: {e}")
|
||||
# 继续处理其他模型
|
||||
else:
|
||||
logger.warning(f"渠道 {TAIJI_CHANNEL_ID} 未配置 LiteLLM team,跳过模型分配")
|
||||
|
||||
# 提交所有更改
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
logger.info(
|
||||
f"用户注册成功并分配默认资源",
|
||||
user_id=str(user_id),
|
||||
email=req.email,
|
||||
channel_id=str(TAIJI_CHANNEL_ID)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"用户注册失败: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"注册失败: {str(e)}"
|
||||
)
|
||||
|
||||
# 创建JWT token,自动登录
|
||||
token = create_access_token(
|
||||
@@ -470,6 +679,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"email": new_user.email,
|
||||
"role": new_user.role,
|
||||
"user_id": str(new_user.id),
|
||||
"channelId": str(TAIJI_CHANNEL_ID),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -483,6 +693,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"email": new_user.email,
|
||||
"username": new_user.username,
|
||||
"role": new_user.role,
|
||||
"channelId": str(TAIJI_CHANNEL_ID),
|
||||
}
|
||||
},
|
||||
message="注册成功"
|
||||
|
||||
@@ -295,11 +295,19 @@ async def allocate_tenant_resources(
|
||||
detail="无法获取渠道ID"
|
||||
)
|
||||
|
||||
# 验证租户存在且属于指定渠道
|
||||
# 验证 tenant_id 格式并确认租户属于指定渠道
|
||||
try:
|
||||
tenant_uuid = uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无效的租户ID格式"
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
and_(
|
||||
User.id == tenant_id,
|
||||
User.id == tenant_uuid,
|
||||
User.channel_id == channel_id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -897,102 +897,8 @@ async def admin_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db
|
||||
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]:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
tenants_count = (await db.execute(select(func.count(User.id)).where(User.role == "user"))).scalar() or 0
|
||||
balances = (await db.execute(select(func.coalesce(func.sum(Balance.eu_balance), 0)))).scalar() or 0
|
||||
|
||||
# 统计平台端 Agent (type='platform')
|
||||
platform_agents = (await db.execute(
|
||||
select(Agent).where(Agent.type == "platform")
|
||||
)).scalars().all()
|
||||
platform_agent_count = len(platform_agents)
|
||||
platform_cpu = sum(float(agent.cpu or 0) for agent in platform_agents)
|
||||
platform_memory = sum(float(agent.memory or 0) for agent in platform_agents)
|
||||
|
||||
# 统计自定义 Agent (type='custom')
|
||||
custom_agents = (await db.execute(
|
||||
select(Agent).where(Agent.type == "custom")
|
||||
)).scalars().all()
|
||||
custom_agent_count = len(custom_agents)
|
||||
custom_cpu = sum(float(agent.cpu or 0) for agent in custom_agents)
|
||||
custom_memory = sum(float(agent.memory or 0) for agent in custom_agents)
|
||||
|
||||
# 从 Agent Manager 获取 K8s 中实际运行的 Agent 资源统计
|
||||
k8s_agents_count = 0
|
||||
k8s_total_cpu = 0.0
|
||||
k8s_total_memory = 0.0
|
||||
|
||||
try:
|
||||
from app.agent_manager_client import get_agent_manager_client
|
||||
client = get_agent_manager_client()
|
||||
|
||||
# 获取所有运行中的 Agent
|
||||
k8s_agents_result = await client.list_agents()
|
||||
k8s_agents = k8s_agents_result.agents # 从 AgentListResult 中获取 agents 列表
|
||||
k8s_agents_count = len(k8s_agents)
|
||||
|
||||
# 获取每个 Agent 的资源配置
|
||||
for agent in k8s_agents:
|
||||
agent_name = agent.get("name")
|
||||
if agent_name:
|
||||
try:
|
||||
metrics = await client.get_agent_metrics(agent_name)
|
||||
# 解析 CPU (如 "500m" -> 0.5 核)
|
||||
cpu_limit = metrics.limits.get("cpu", "0")
|
||||
if cpu_limit.endswith("m"):
|
||||
k8s_total_cpu += float(cpu_limit[:-1]) / 1000
|
||||
else:
|
||||
k8s_total_cpu += float(cpu_limit)
|
||||
|
||||
# 解析内存 (如 "512Mi" -> 0.5 GB)
|
||||
memory_limit = metrics.limits.get("memory", "0")
|
||||
if memory_limit.endswith("Mi"):
|
||||
k8s_total_memory += float(memory_limit[:-2]) / 1024
|
||||
elif memory_limit.endswith("Gi"):
|
||||
k8s_total_memory += float(memory_limit[:-2])
|
||||
elif memory_limit.endswith("Ki"):
|
||||
k8s_total_memory += float(memory_limit[:-2]) / (1024 * 1024)
|
||||
except Exception as e:
|
||||
logger.warning(f"获取 Agent {agent_name} 资源指标失败: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"连接 Agent Manager 失败,使用数据库统计: {e}")
|
||||
# 如果 Agent Manager 不可用,使用数据库中的平台端 Agent 统计
|
||||
k8s_total_cpu = platform_cpu
|
||||
k8s_total_memory = platform_memory
|
||||
|
||||
# 总 Agent 数:平台端 + 自定义
|
||||
total_agents = platform_agent_count + custom_agent_count
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"totalChannels": channels_count,
|
||||
"totalTenants": tenants_count,
|
||||
"totalAgents": total_agents,
|
||||
"totalCalls": 0,
|
||||
"totalRevenue": round(float(balances), 2),
|
||||
"totalAllocatedCpu": round(k8s_total_cpu + custom_cpu, 2),
|
||||
"totalAllocatedMemory": round(k8s_total_memory + custom_memory, 2),
|
||||
"platformAgents": {
|
||||
"count": k8s_agents_count if k8s_agents_count > 0 else platform_agent_count,
|
||||
"cpu": round(k8s_total_cpu if k8s_total_cpu > 0 else platform_cpu, 2),
|
||||
"memory": round(k8s_total_memory if k8s_total_memory > 0 else platform_memory, 2),
|
||||
},
|
||||
"customAgents": {
|
||||
"count": custom_agent_count,
|
||||
"cpu": round(custom_cpu, 2),
|
||||
"memory": round(custom_memory, 2),
|
||||
},
|
||||
},
|
||||
"message": None,
|
||||
}
|
||||
# 注意: /admin/dashboard/stats 接口已移至 admin.py,避免重复定义
|
||||
# 该接口从 AgentBillingRecord 和 ModelBillingRecord 统计收入和调用次数
|
||||
|
||||
|
||||
@router.get("/admin/channels")
|
||||
|
||||
@@ -51,6 +51,15 @@ class PasswordChangeRequest(BaseModel):
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""创建用户请求(自由注册)"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名,3-50个字符")
|
||||
email: EmailStr = Field(..., description="邮箱地址")
|
||||
password: str = Field(..., min_length=8, description="密码,至少8个字符")
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码,6位数字")
|
||||
full_name: Optional[str] = Field(None, description="全名/显示名称")
|
||||
|
||||
|
||||
# ============= 密钥管理 =============
|
||||
|
||||
class APIKeyInfo(BaseModel):
|
||||
|
||||
@@ -398,6 +398,7 @@ class UserCreate(BaseModel):
|
||||
email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')
|
||||
password: str = Field(..., min_length=8)
|
||||
full_name: Optional[str] = None
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码")
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user