forked from xiaohei/taiji-AI-PAD
更新模型计费
This commit is contained in:
@@ -16,7 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import settings
|
||||
from database import get_db
|
||||
from models import APIKey, User
|
||||
from models import APIKey, User, TokenBlacklist
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
bearer_scheme = HTTPBearer(auto_error=False)
|
||||
@@ -32,8 +32,12 @@ def get_password_hash(password: str) -> str:
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.jwt_expire_minutes))
|
||||
to_encode.update({"exp": expire})
|
||||
now = datetime.utcnow()
|
||||
expire = now + (expires_delta or timedelta(minutes=settings.jwt_expire_minutes))
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
"iat": now, # 添加签发时间,用于登出验证
|
||||
})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.jwt_algorithm)
|
||||
return encoded_jwt
|
||||
|
||||
@@ -89,6 +93,59 @@ async def _check_api_key(key: str, db: AsyncSession) -> Optional[APIKey]:
|
||||
return None
|
||||
|
||||
|
||||
async def _is_user_logged_out(user_id: str, token_iat: Optional[int], db: AsyncSession) -> bool:
|
||||
"""
|
||||
检查用户是否已登出
|
||||
|
||||
通过检查 token_blacklist 表中是否存在该用户的登出记录,
|
||||
且登出时间在 token 签发时间之后。
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
token_iat: Token 签发时间戳 (iat claim)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
True 如果用户已登出,False 否则
|
||||
"""
|
||||
try:
|
||||
# 查询该用户的登出记录
|
||||
result = await db.execute(
|
||||
select(TokenBlacklist).where(
|
||||
TokenBlacklist.user_id == user_id,
|
||||
TokenBlacklist.reason == "logout",
|
||||
TokenBlacklist.expires_at > datetime.utcnow() # 只查询未过期的记录
|
||||
).order_by(TokenBlacklist.created_at.desc()).limit(1)
|
||||
)
|
||||
blacklist_entry = result.scalar_one_or_none()
|
||||
|
||||
if not blacklist_entry:
|
||||
return False
|
||||
|
||||
# 如果有登出记录,检查 token 签发时间
|
||||
if token_iat:
|
||||
token_issued_at = datetime.utcfromtimestamp(token_iat)
|
||||
# 从 token_jti 中提取登出时间戳
|
||||
# 格式: logout_{user_id}_{timestamp}
|
||||
try:
|
||||
logout_timestamp = float(blacklist_entry.token_jti.split("_")[-1])
|
||||
logout_time = datetime.utcfromtimestamp(logout_timestamp)
|
||||
# 如果 token 是在登出之前签发的,则认为已登出
|
||||
if token_issued_at < logout_time:
|
||||
return True
|
||||
except (ValueError, IndexError):
|
||||
# 无法解析登出时间,保守起见认为已登出
|
||||
return True
|
||||
else:
|
||||
# 没有 iat claim,保守起见认为已登出
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception:
|
||||
# 查询失败时不阻止认证
|
||||
return False
|
||||
|
||||
|
||||
async def require_auth(
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
|
||||
@@ -106,6 +163,7 @@ async def require_auth(
|
||||
"/api/channel/auth/login",
|
||||
"/api/admin/auth/login",
|
||||
"/api/providers/auth/login",
|
||||
"/api/auth/login", # 添加统一登录接口
|
||||
"/agents/templates", # 模板列表公开访问
|
||||
}
|
||||
# 允许公开路径和非 API/agents 路径
|
||||
@@ -132,8 +190,15 @@ async def require_auth(
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
|
||||
user_id: str | None = payload.get("sub")
|
||||
email: str | None = payload.get("email")
|
||||
token_iat: int | None = payload.get("iat")
|
||||
|
||||
if user_id is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
|
||||
|
||||
# 检查用户是否已登出
|
||||
if await _is_user_logged_out(user_id, token_iat, db):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已失效,请重新登录")
|
||||
|
||||
request.state.principal = {"type": "jwt", "user_id": user_id, "email": email, "claims": payload}
|
||||
return request.state.principal
|
||||
except JWTError:
|
||||
@@ -155,6 +220,7 @@ async def authenticate_request(request: Request, db: AsyncSession) -> Optional[D
|
||||
"/api/channel/auth/login",
|
||||
"/api/admin/auth/login",
|
||||
"/api/providers/auth/login",
|
||||
"/api/auth/login", # 添加统一登录接口
|
||||
"/agents/templates", # 模板列表公开访问
|
||||
}
|
||||
# 允许公开路径
|
||||
@@ -181,8 +247,15 @@ async def authenticate_request(request: Request, db: AsyncSession) -> Optional[D
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
|
||||
user_id: str | None = payload.get("sub")
|
||||
email: str | None = payload.get("email")
|
||||
token_iat: int | None = payload.get("iat")
|
||||
|
||||
if user_id is None:
|
||||
return None
|
||||
|
||||
# 检查用户是否已登出
|
||||
if await _is_user_logged_out(user_id, token_iat, db):
|
||||
return None
|
||||
|
||||
return {"type": "jwt", "user_id": user_id, "email": email, "claims": payload}
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
@@ -325,8 +325,9 @@ async def allocate_tenant_resources(
|
||||
)
|
||||
)
|
||||
|
||||
# 分配Agent资源
|
||||
# 分配平台 Agent 资源(更新 PlatformAgentQuota)
|
||||
for agent_alloc in req.agents:
|
||||
# 1. 创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
@@ -335,6 +336,93 @@ async def allocate_tenant_resources(
|
||||
quantity=agent_alloc.quantity,
|
||||
)
|
||||
db.add(allocation)
|
||||
|
||||
# 2. 更新 PlatformAgentQuota(平台 Agent 配额管理)
|
||||
# agentId 在这里是模板名称(如 echo_agent)
|
||||
template_name = agent_alloc.agentId
|
||||
|
||||
# 获取渠道的平台 Agent 配额
|
||||
channel_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota).where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == channel_id,
|
||||
PlatformAgentQuota.target_type == "channel",
|
||||
PlatformAgentQuota.template_name == template_name
|
||||
)
|
||||
)
|
||||
)
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if channel_quota:
|
||||
# 计算已分配给该渠道其他租户的配额
|
||||
other_tenants_quota_result = await db.execute(
|
||||
select(func.sum(PlatformAgentQuota.pod_quota).label("total"))
|
||||
.select_from(PlatformAgentQuota)
|
||||
.join(User, PlatformAgentQuota.target_id == User.id)
|
||||
.where(
|
||||
and_(
|
||||
User.channel_id == channel_id,
|
||||
PlatformAgentQuota.target_type == "tenant",
|
||||
PlatformAgentQuota.template_name == template_name,
|
||||
PlatformAgentQuota.target_id != tenant_id
|
||||
)
|
||||
)
|
||||
)
|
||||
other_quota = other_tenants_quota_result.scalar() or 0
|
||||
|
||||
# 检查是否超过渠道配额
|
||||
remaining = channel_quota.pod_quota - other_quota
|
||||
if agent_alloc.quantity > remaining:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"平台 Agent '{template_name}' 配额超出渠道剩余配额。渠道剩余: {remaining},请求: {agent_alloc.quantity}"
|
||||
)
|
||||
|
||||
# 查找或创建租户的平台 Agent 配额记录
|
||||
tenant_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota).where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == tenant_id,
|
||||
PlatformAgentQuota.target_type == "tenant",
|
||||
PlatformAgentQuota.template_name == template_name
|
||||
)
|
||||
)
|
||||
)
|
||||
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
||||
|
||||
# 计算配额变化量(用于更新渠道的 pod_used)
|
||||
old_tenant_quota = tenant_quota.pod_quota if tenant_quota else 0
|
||||
quota_delta = agent_alloc.quantity - old_tenant_quota
|
||||
|
||||
if tenant_quota:
|
||||
# 更新现有配额
|
||||
tenant_quota.pod_quota = agent_alloc.quantity
|
||||
tenant_quota.allocated_at = datetime.utcnow()
|
||||
else:
|
||||
# 创建新配额记录
|
||||
tenant_quota = PlatformAgentQuota(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
template_name=template_name,
|
||||
pod_quota=agent_alloc.quantity,
|
||||
pod_used=0,
|
||||
allocated_by=None, # 渠道管理员分配
|
||||
allocated_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(tenant_quota)
|
||||
|
||||
# 更新渠道的 pod_used(分配给租户的配额视为渠道已使用的配额)
|
||||
channel_quota.pod_used = (channel_quota.pod_used or 0) + quota_delta
|
||||
|
||||
logger.info(
|
||||
f"分配平台 Agent 配额: template={template_name}, tenant={tenant_id}, "
|
||||
f"quota={agent_alloc.quantity}, channel_pod_used={channel_quota.pod_used}"
|
||||
)
|
||||
else:
|
||||
# 渠道没有该平台 Agent 的配额,记录警告但不阻止操作
|
||||
logger.warning(
|
||||
f"渠道没有平台 Agent '{template_name}' 的配额,仅创建 ResourceAllocation 记录"
|
||||
)
|
||||
|
||||
# 分配模型资源(集成 LiteLLM)
|
||||
# 获取渠道信息(用于 LiteLLM 集成)
|
||||
|
||||
@@ -1095,6 +1095,58 @@ async def recharge_balance(
|
||||
)
|
||||
|
||||
|
||||
def _parse_datetime(dt_str: str) -> datetime:
|
||||
"""
|
||||
解析日期时间字符串,支持多种格式:
|
||||
- YYYY-MM-DD
|
||||
- YYYY-MM-DDTHH:MM:SS
|
||||
- YYYY-MM-DDTHH:MM:SSZ
|
||||
- YYYY-MM-DDTHH:MM:SS+00:00
|
||||
|
||||
返回的 datetime 对象不包含时区信息(naive datetime),
|
||||
以便与数据库中的 TIMESTAMP WITHOUT TIME ZONE 兼容。
|
||||
"""
|
||||
if not dt_str:
|
||||
raise ValueError("日期时间字符串不能为空")
|
||||
|
||||
dt_str = dt_str.strip()
|
||||
|
||||
# 移除末尾的 Z 并替换为 +00:00
|
||||
if dt_str.endswith("Z"):
|
||||
dt_str = dt_str[:-1] + "+00:00"
|
||||
|
||||
result = None
|
||||
|
||||
try:
|
||||
# 尝试解析完整的 ISO 格式
|
||||
result = datetime.fromisoformat(dt_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if result is None:
|
||||
# 尝试解析简单日期格式 YYYY-MM-DD
|
||||
try:
|
||||
result = datetime.strptime(dt_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if result is None:
|
||||
# 尝试解析带时间的格式
|
||||
try:
|
||||
result = datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if result is None:
|
||||
raise ValueError(f"无法解析日期时间格式: {dt_str}")
|
||||
|
||||
# 移除时区信息,返回 naive datetime
|
||||
if result.tzinfo is not None:
|
||||
result = result.replace(tzinfo=None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/billing/history", response_model=SuccessResponse)
|
||||
async def get_billing_history(
|
||||
startTime: str = Query(...),
|
||||
@@ -1110,12 +1162,23 @@ async def get_billing_history(
|
||||
):
|
||||
"""
|
||||
获取计费历史记录
|
||||
|
||||
支持的时间格式:
|
||||
- YYYY-MM-DD (如: 2026-01-01)
|
||||
- YYYY-MM-DDTHH:MM:SS (如: 2026-01-01T00:00:00)
|
||||
- YYYY-MM-DDTHH:MM:SSZ (如: 2026-01-01T00:00:00Z)
|
||||
"""
|
||||
user_id = principal.get("user_id")
|
||||
|
||||
# 解析时间
|
||||
start_dt = datetime.fromisoformat(startTime.replace("Z", "+00:00"))
|
||||
end_dt = datetime.fromisoformat(endTime.replace("Z", "+00:00"))
|
||||
try:
|
||||
start_dt = _parse_datetime(startTime)
|
||||
end_dt = _parse_datetime(endTime)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"时间格式错误: {str(e)}。支持的格式: YYYY-MM-DD 或 YYYY-MM-DDTHH:MM:SSZ"
|
||||
)
|
||||
|
||||
# 构建查询
|
||||
query = select(BillingRecord).where(
|
||||
@@ -2060,8 +2123,14 @@ async def get_my_agent_billing_history(
|
||||
user_id = principal.get("user_id")
|
||||
|
||||
# 解析时间
|
||||
start_dt = datetime.fromisoformat(startTime.replace("Z", "+00:00"))
|
||||
end_dt = datetime.fromisoformat(endTime.replace("Z", "+00:00"))
|
||||
try:
|
||||
start_dt = _parse_datetime(startTime)
|
||||
end_dt = _parse_datetime(endTime)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"时间格式错误: {str(e)}。支持的格式: YYYY-MM-DD 或 YYYY-MM-DDTHH:MM:SSZ"
|
||||
)
|
||||
|
||||
# 构建查询
|
||||
conditions = [
|
||||
|
||||
@@ -31,8 +31,11 @@ class SystemMonitor:
|
||||
self._service_endpoints = {
|
||||
"mcp_server": "http://localhost:8000/health", # 本服务
|
||||
"data_ingestion": "http://data-ingestion:8000/health", # Docker 内部网络
|
||||
"api_gateway": "http://api-gateway:80/health", # Nginx
|
||||
"model_gateway": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/health", # LiteLLM 模型网关
|
||||
"agent_manager": "http://127.0.0.1:8000/health", # Agent Manager 服务(本地)
|
||||
}
|
||||
# LiteLLM API Key
|
||||
self._litellm_api_key = "sk-taiji-prod-2026"
|
||||
|
||||
async def get_system_health(self) -> Dict[str, Any]:
|
||||
"""获取系统健康状态,包括三个微服务的状态"""
|
||||
@@ -44,13 +47,13 @@ class SystemMonitor:
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# 检查数据库
|
||||
# 检查数据库(内部使用,不对外暴露)
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
health["services"]["database"] = {"status": "healthy", "latency": 0}
|
||||
# 数据库健康检查成功,但不添加到 services 中
|
||||
except Exception as e:
|
||||
health["services"]["database"] = {"status": "error", "latency": 0, "error": str(e)}
|
||||
# 数据库不健康会影响整体状态
|
||||
health["status"] = "degraded"
|
||||
health["score"] -= 25
|
||||
|
||||
@@ -72,13 +75,14 @@ class SystemMonitor:
|
||||
|
||||
async def _check_microservices_health(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
检查三个微服务的健康状态
|
||||
检查四个微服务的健康状态
|
||||
|
||||
返回格式:
|
||||
{
|
||||
"mcp_server": { "status": "healthy", "latency": 45 },
|
||||
"data_ingestion": { "status": "healthy", "latency": 32 },
|
||||
"api_gateway": { "status": "healthy", "latency": 28 }
|
||||
"model_gateway": { "status": "healthy", "latency": 28 },
|
||||
"agent_manager": { "status": "healthy", "latency": 15 }
|
||||
}
|
||||
|
||||
状态值:
|
||||
@@ -88,12 +92,12 @@ class SystemMonitor:
|
||||
"""
|
||||
results = {}
|
||||
|
||||
async def check_service(name: str, url: str) -> Dict[str, Any]:
|
||||
async def check_service(name: str, url: str, headers: Dict[str, str] = None) -> Dict[str, Any]:
|
||||
"""检查单个服务的健康状态"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(url)
|
||||
response = await client.get(url, headers=headers)
|
||||
latency = int((time.time() - start_time) * 1000)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -119,10 +123,17 @@ class SystemMonitor:
|
||||
self._service_endpoints["data_ingestion"]
|
||||
)
|
||||
|
||||
# 检查 api_gateway (Nginx)
|
||||
results["api_gateway"] = await check_service(
|
||||
"api_gateway",
|
||||
self._service_endpoints["api_gateway"]
|
||||
# 检查 model_gateway (LiteLLM) - 需要带上 API Key
|
||||
results["model_gateway"] = await check_service(
|
||||
"model_gateway",
|
||||
self._service_endpoints["model_gateway"],
|
||||
headers={"Authorization": f"Bearer {self._litellm_api_key}"}
|
||||
)
|
||||
|
||||
# 检查 agent_manager 服务
|
||||
results["agent_manager"] = await check_service(
|
||||
"agent_manager",
|
||||
self._service_endpoints["agent_manager"]
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user