""" 计费与资源管理逻辑 EU(执行单元)计算说明: ======================= 系统中有两种EU计算方式: 1. Agent 运行时长计费(本模块) - 计算公式:EU = ceil(duration_seconds / 10) - 即:1 EU = 10秒运行时间,不足10秒按1 EU计算 - 适用于:平台Agent、自定义Agent的Pod运行时长 - 周期性统计:每小时更新一次计费记录(periodic_billing.py) 2. 模型 Token 计费(billing_webhook.py) - 计算公式:EU = total_tokens * MODEL_EU_RATE[model_name] - 不同模型有不同的转换率: - gpt-4: 0.0001 EU/token (1000 tokens = 0.1 EU) - gpt-3.5-turbo: 0.00005 EU/token (1000 tokens = 0.05 EU) - claude: 0.0001 EU/token - 默认: 0.0001 EU/token - 适用于:LiteLLM模型调用的Token消耗 数据存储: - Agent计费记录:agent_billing_records 表 - 模型计费记录:model_billing_records 表 - 用户余额:balances 表(Balance模型) 注意:User.eu_balance 和 User.balance 字段已废弃,请使用 Balance 表 """ import math from datetime import datetime from decimal import Decimal from typing import Optional, Tuple from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from models import User, Channel, BillingRecord, Agent, Balance, AgentBillingRecord # ============= EU定价配置 ============= # EU单价配置(按订阅等级) EU_PRICING = { "free": Decimal("0.015"), # 入门级(Starter):$0.015 / EU "starter": Decimal("0.015"), # 入门级别名 "pro": Decimal("0.02"), # 专业级(Pro):$0.02 / EU "enterprise": Decimal("0.03"), # 企业级(Enterprise):$0.03 / EU } # 默认单价(未知等级时使用专业级价格) DEFAULT_EU_PRICE = Decimal("0.02") def get_eu_price_by_tier(subscription_tier: str) -> Decimal: """ 根据订阅等级获取EU单价 Args: subscription_tier: 订阅等级(free/starter/pro/enterprise) Returns: EU单价(USD) 定价说明: - 入门级(Starter/free):$0.015 / EU - 拉新、试用、轻 Agent - 专业级(Pro):$0.02 / EU - 主力商业用户 - 企业级(Enterprise):$0.03 / EU - 高复杂度 / 高 SLA """ tier = subscription_tier.lower() if subscription_tier else "pro" return EU_PRICING.get(tier, DEFAULT_EU_PRICE) # ============= EU计算规则 ============= def calculate_eu(duration_seconds: int) -> int: """ 计算EU:1 EU = 10秒,不足10秒按1 EU计算 Args: duration_seconds: 调用时长(秒) Returns: EU数量 """ return math.ceil(duration_seconds / 10) def calculate_cost(eu: int, unit_price: Decimal = None, subscription_tier: str = None) -> Decimal: """ 计算成本 Args: eu: EU数量 unit_price: 单价(可直接指定,优先级最高) subscription_tier: 订阅等级(用于自动获取单价) Returns: 成本金额(USD) 定价说明: - 入门级(Starter/free):$0.015 / EU - 专业级(Pro):$0.02 / EU - 企业级(Enterprise):$0.03 / EU """ if unit_price is None: unit_price = get_eu_price_by_tier(subscription_tier) if subscription_tier else DEFAULT_EU_PRICE return Decimal(eu) * unit_price # ============= 余额与授信管理 ============= async def get_available_balance(user_id: str, db: AsyncSession) -> Tuple[Decimal, Decimal, Decimal]: """ 获取可用额度:账户余额 + 授信额度 Returns: (账户余额, 授信额度, 可用额度) """ # 从 Balance 表获取余额 balance_result = await db.execute( select(Balance).where(Balance.user_id == user_id) ) balance_obj = balance_result.scalar_one_or_none() # 从 User 表获取授信额度 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return Decimal(0), Decimal(0), Decimal(0) balance = Decimal(str(balance_obj.eu_balance)) if balance_obj else Decimal(0) credit_limit = Decimal(str(user.credit_limit)) available = balance + credit_limit return balance, credit_limit, available async def check_balance_sufficient( user_id: str, required_amount: Decimal, db: AsyncSession ) -> bool: """ 检查余额是否充足 Args: user_id: 用户ID required_amount: 所需金额 db: 数据库会话 Returns: 是否充足 """ balance, credit_limit, available = await get_available_balance(user_id, db) return available >= required_amount async def deduct_balance( user_id: str, amount: Decimal, db: AsyncSession, description: str = "消费", auto_commit: bool = False ) -> Tuple[bool, str]: """ 扣除余额(优先扣除账户余额,不足时使用授信额度) 使用行锁保护并发扣款操作,防止超扣。 Args: user_id: 用户ID amount: 扣除金额 db: 数据库会话 description: 描述 auto_commit: 是否自动提交(默认False,由调用者管理事务) Returns: (是否成功, 消息) Note: 默认不会 commit,由调用者统一管理事务。 如需独立提交,请设置 auto_commit=True。 """ # 使用 FOR UPDATE 锁定余额行,防止并发扣款 balance_result = await db.execute( select(Balance) .where(Balance.user_id == user_id) .with_for_update() ) balance_obj = balance_result.scalar_one_or_none() # 从 User 表获取授信额度 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return False, "用户不存在" if balance_obj is None: # 如果余额记录不存在,创建一个新的(初始余额为0) balance_obj = Balance(user_id=user_id, eu_balance=0.0) db.add(balance_obj) await db.flush() # 确保记录创建后再继续 balance = Decimal(str(balance_obj.eu_balance)) credit_limit = Decimal(str(user.credit_limit)) available = balance + credit_limit if available < amount: return False, f"余额不足,当前可用额度: {available}, 需要: {amount}" # 优先扣除账户余额,允许透支到授信额度 new_balance = balance - amount balance_obj.eu_balance = float(new_balance) # 可选:自动提交 if auto_commit: await db.commit() return True, f"成功扣除 {amount} 元" async def add_balance( user_id: str, amount: Decimal, db: AsyncSession, description: str = "充值", auto_commit: bool = False ) -> Tuple[bool, str]: """ 增加余额 使用行锁保护并发充值操作,确保余额准确。 Args: user_id: 用户ID amount: 充值金额 db: 数据库会话 description: 描述 auto_commit: 是否自动提交(默认False,由调用者管理事务) Returns: (是否成功, 消息) Note: 默认不会 commit,由调用者统一管理事务。 如需独立提交,请设置 auto_commit=True。 """ # 检查用户是否存在 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return False, "用户不存在" # 使用 FOR UPDATE 锁定余额行,防止并发更新 balance_result = await db.execute( select(Balance) .where(Balance.user_id == user_id) .with_for_update() ) balance_obj = balance_result.scalar_one_or_none() if balance_obj is None: # 如果余额记录不存在,创建一个新的 balance_obj = Balance(user_id=user_id, eu_balance=0.0) db.add(balance_obj) await db.flush() # 确保记录创建后再继续 old_balance = Decimal(str(balance_obj.eu_balance)) new_balance = old_balance + amount balance_obj.eu_balance = float(new_balance) # 可选:自动提交 if auto_commit: await db.commit() return True, f"成功充值 {amount} 元,当前余额: {new_balance}" # ============= 计费记录创建(旧版,已废弃) ============= async def create_billing_record( tenant_id: str, agent_id: str, agent_name: str, duration_seconds: int, db: AsyncSession, channel_id: Optional[str] = None, ) -> BillingRecord: """ 创建计费记录 ⚠️ 已废弃 (DEPRECATED) 此函数使用旧的 BillingRecord 表,已废弃。 请使用 create_agent_billing_record() 函数替代。 Args: tenant_id: 租户ID agent_id: Agent ID agent_name: Agent名称 duration_seconds: 调用时长(秒) db: 数据库会话 channel_id: 渠道ID(可选) Returns: 计费记录 计费说明: 根据租户订阅等级自动计算EU单价: - 入门级(Starter/free):$0.015 / EU - 专业级(Pro):$0.02 / EU - 企业级(Enterprise):$0.03 / EU """ import warnings warnings.warn( "create_billing_record() 已废弃,请使用 create_agent_billing_record() 替代", DeprecationWarning, stacklevel=2 ) # 获取用户信息(包括订阅等级和channel_id) result = await db.execute( select(User.channel_id, User.subscription_tier).where(User.id == tenant_id) ) row = result.first() subscription_tier = "pro" # 默认专业级 if row: if not channel_id and row[0]: channel_id = str(row[0]) if row[1]: subscription_tier = row[1] # 计算EU和成本(根据订阅等级) eu = calculate_eu(duration_seconds) cost = calculate_cost(eu, subscription_tier=subscription_tier) # 创建计费记录 record = BillingRecord( timestamp=datetime.utcnow(), channel_id=channel_id, tenant_id=tenant_id, agent_id=agent_id, agent_name=agent_name, duration=duration_seconds, eu=eu, cost=cost, ) db.add(record) # 扣除用户余额 success, message = await deduct_balance(tenant_id, cost, db, f"Agent调用: {agent_name}") if not success: # 如果余额不足,记录但不扣除(实际应该阻止调用) # 这里简化处理,只记录 pass await db.commit() await db.refresh(record) return record # ============= 资源配额检查 ============= async def check_agent_quota( user_id: str, agent_id: str, db: AsyncSession ) -> Tuple[bool, str]: """ 检查Agent配额是否足够 Args: user_id: 用户ID agent_id: Agent ID db: 数据库会话 Returns: (是否有配额, 消息) """ # 简化实现:实际应该查询ResourceAllocation表 # 这里只是返回True,实际应该检查配额 return True, "配额充足" async def check_model_quota( user_id: str, model_name: str, db: AsyncSession ) -> Tuple[bool, int, int]: """ 检查模型配额(RPM/TPM) Args: user_id: 用户ID model_name: 模型名称 db: 数据库会话 Returns: (是否有配额, 当前RPM, 当前TPM) """ # 简化实现:实际应该查询ResourceAllocation和使用情况 # 这里只是返回默认值 return True, 60, 60000 # ============= 资源分配层级验证 ============= async def validate_resource_allocation( parent_type: str, # channel | tenant parent_id: str, resource_type: str, # agent | model resource_id: str, quantity: int, db: AsyncSession ) -> Tuple[bool, str]: """ 验证资源分配是否超过上级分配的资源 Args: parent_type: 父级类型 parent_id: 父级ID resource_type: 资源类型 resource_id: 资源ID quantity: 数量 db: 数据库会话 Returns: (是否有效, 消息) """ # 简化实现:实际应该递归检查上级的配额 # 这里只是返回True return True, "资源分配有效" # ============= 工作流限制验证 ============= def validate_workflow_nodes(nodes: list) -> Tuple[bool, str]: """ 验证工作流节点数量限制 Args: nodes: 节点列表 Returns: (是否有效, 消息) """ if len(nodes) > 3: return False, "工作流最多支持3个Agent节点" return True, "节点数量有效" # ============= 平台Agent资源固定配置 ============= PLATFORM_AGENT_CONFIG = { "cpu": 2.0, # 2核 "memory": 4.0, # 4GB } def get_platform_agent_resources() -> dict: """ 获取平台Agent的固定资源配置 Returns: 资源配置字典 """ return PLATFORM_AGENT_CONFIG.copy() # ============= 统计辅助函数 ============= async def calculate_monthly_cost(user_id: str, db: AsyncSession) -> Decimal: """ 计算用户本月消费(从 AgentBillingRecord 和 ModelBillingRecord 合并统计) Args: user_id: 用户ID db: 数据库会话 Returns: 本月消费金额 """ from sqlalchemy import func from models import ModelBillingRecord # 获取本月第一天 now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) # Agent 计费 agent_result = await db.execute( select(func.sum(AgentBillingRecord.cost)) .where(AgentBillingRecord.user_id == user_id) .where(AgentBillingRecord.start_time >= month_start) ) agent_cost = agent_result.scalar() or 0 # 模型调用计费 model_result = await db.execute( select(func.sum(ModelBillingRecord.total_cost)) .where(ModelBillingRecord.tenant_id == user_id) .where(ModelBillingRecord.created_at >= month_start) ) model_cost = model_result.scalar() or 0 total = Decimal(str(agent_cost)) + Decimal(str(model_cost)) return total async def calculate_channel_commission( channel_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> Tuple[Decimal, Decimal]: """ 计算渠道佣金(从 AgentBillingRecord 和 ModelBillingRecord 合并统计) Args: channel_id: 渠道ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: (总收入, 佣金金额) """ from sqlalchemy import func from models import ModelBillingRecord # 查询渠道佣金率 result = await db.execute( select(Channel.commission_rate).where(Channel.id == channel_id) ) row = result.first() commission_rate = Decimal(str(row[0])) / 100 if row else Decimal(0) # Agent 计费收入 agent_result = await db.execute( select(func.sum(AgentBillingRecord.cost)) .where(AgentBillingRecord.channel_id == channel_id) .where(AgentBillingRecord.start_time >= start_date) .where(AgentBillingRecord.start_time <= end_date) ) agent_revenue = agent_result.scalar() or 0 # 模型调用计费收入 model_result = await db.execute( select(func.sum(ModelBillingRecord.total_cost)) .where(ModelBillingRecord.channel_id == channel_id) .where(ModelBillingRecord.created_at >= start_date) .where(ModelBillingRecord.created_at <= end_date) ) model_revenue = model_result.scalar() or 0 total_revenue = Decimal(str(agent_revenue)) + Decimal(str(model_revenue)) commission = total_revenue * commission_rate return total_revenue, commission # ============= Agent 计费配置 ============= # Agent 资源单价配置(按资源类型) AGENT_RESOURCE_PRICING = { "cpu_per_hour": Decimal("0.05"), # CPU 每核每小时 $0.05 "memory_per_gb_hour": Decimal("0.01"), # 内存每 GB 每小时 $0.01 } # 平台 Agent 固定价格(按模板) PLATFORM_AGENT_PRICING = { "gpt-assistant": Decimal("0.10"), # GPT 助手每小时 $0.10 "code-reviewer": Decimal("0.15"), # 代码审查每小时 $0.15 "data-analyst": Decimal("0.12"), # 数据分析每小时 $0.12 "default": Decimal("0.10"), # 默认每小时 $0.10 } def get_platform_agent_hourly_price(template_name: str) -> Decimal: """ 获取平台 Agent 每小时价格 Args: template_name: 模板名称 Returns: 每小时价格(USD) """ return PLATFORM_AGENT_PRICING.get(template_name, PLATFORM_AGENT_PRICING["default"]) def calculate_agent_cost_by_resources( cpu_cores: float, memory_gb: float, duration_seconds: int ) -> Decimal: """ 根据资源使用量计算成本(用于自定义 Agent) Args: cpu_cores: CPU 核心数 memory_gb: 内存 GB 数 duration_seconds: 运行时长(秒) Returns: 成本金额(USD) """ hours = Decimal(str(duration_seconds)) / Decimal("3600") cpu_cost = Decimal(str(cpu_cores)) * hours * AGENT_RESOURCE_PRICING["cpu_per_hour"] memory_cost = Decimal(str(memory_gb)) * hours * AGENT_RESOURCE_PRICING["memory_per_gb_hour"] return cpu_cost + memory_cost def calculate_platform_agent_cost( template_name: str, duration_seconds: int ) -> Decimal: """ 计算平台 Agent 成本(固定价格) Args: template_name: 模板名称 duration_seconds: 运行时长(秒) Returns: 成本金额(USD) """ hours = Decimal(str(duration_seconds)) / Decimal("3600") hourly_price = get_platform_agent_hourly_price(template_name) return hours * hourly_price # ============= Agent 计费记录创建 ============= async def create_agent_billing_record( user_id: str, agent_name: str, agent_type: str, is_platform_agent: bool, duration_seconds: int, db: AsyncSession, channel_id: Optional[str] = None, cpu_used: Optional[str] = None, memory_used: Optional[str] = None, tools_used: Optional[list] = None, request_id: Optional[str] = None, ) -> "AgentBillingRecord": """ 创建 Agent 计费记录 Args: user_id: 用户 ID agent_name: Agent 名称 agent_type: Agent 类型/模板名称 is_platform_agent: 是否为平台 Agent duration_seconds: 运行时长(秒) db: 数据库会话 channel_id: 渠道 ID(可选) cpu_used: CPU 使用量(K8s 格式,如 "100m") memory_used: 内存使用量(K8s 格式,如 "128Mi") tools_used: 使用的工具列表(可选) request_id: 请求 ID(可选) Returns: 计费记录 """ from models import AgentBillingRecord # 计算成本 if is_platform_agent: cost = calculate_platform_agent_cost(agent_type, duration_seconds) else: # 解析资源量 cpu_cores = _parse_cpu_to_cores(cpu_used) if cpu_used else 0.1 memory_gb = _parse_memory_to_gb(memory_used) if memory_used else 0.125 cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, duration_seconds) # 计算 EU eu = calculate_eu(duration_seconds) # 创建记录 record = AgentBillingRecord( user_id=user_id, channel_id=channel_id, agent_name=agent_name, agent_type=agent_type, is_platform_agent=is_platform_agent, period_start=datetime.utcnow(), duration_seconds=duration_seconds, eu_consumed=eu, cpu_used=cpu_used, memory_used=memory_used, cost=cost, tools_used=tools_used, request_id=request_id, ) db.add(record) # 扣除用户余额 success, message = await deduct_balance(user_id, cost, db, f"Agent 使用: {agent_name}") await db.commit() await db.refresh(record) return record async def finalize_agent_billing( agent_name: str, user_id: str, db: AsyncSession ) -> Optional["AgentBillingRecord"]: """ 结算 Agent 计费(Agent 停止时调用) Args: agent_name: Agent 名称 user_id: 用户 ID db: 数据库会话 Returns: 更新后的计费记录 """ from models import AgentBillingRecord from sqlalchemy import and_ # 查找未结束的计费记录 result = await db.execute( select(AgentBillingRecord).where( and_( AgentBillingRecord.agent_name == agent_name, AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_end == None ) ) ) record = result.scalar_one_or_none() if not record: return None # 计算实际运行时长 record.period_end = datetime.utcnow() duration = (record.period_end - record.period_start).total_seconds() record.duration_seconds = int(duration) record.eu_consumed = calculate_eu(int(duration)) # 重新计算成本 if record.is_platform_agent: record.cost = calculate_platform_agent_cost(record.agent_type, int(duration)) else: cpu_cores = _parse_cpu_to_cores(record.cpu_used) if record.cpu_used else 0.1 memory_gb = _parse_memory_to_gb(record.memory_used) if record.memory_used else 0.125 record.cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, int(duration)) await db.commit() await db.refresh(record) return record # ============= Agent 计费统计 ============= async def get_agent_billing_stats( user_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> dict: """ 获取 Agent 计费统计 Args: user_id: 用户 ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: 统计数据 """ from models import AgentBillingRecord from sqlalchemy import func, and_ # 总体统计 result = await db.execute( select( func.sum(AgentBillingRecord.cost).label("total_cost"), func.sum(AgentBillingRecord.duration_seconds).label("total_duration"), func.count(AgentBillingRecord.id).label("total_count") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ) ) row = result.first() total_cost = float(row.total_cost) if row.total_cost else 0 total_duration = int(row.total_duration) if row.total_duration else 0 total_count = int(row.total_count) if row.total_count else 0 # 按 Agent 类型统计 result = await db.execute( select( AgentBillingRecord.is_platform_agent, func.sum(AgentBillingRecord.cost).label("cost") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.is_platform_agent) ) by_agent_type = {} for row in result.all(): agent_type = "platform" if row.is_platform_agent else "custom" by_agent_type[agent_type] = float(row.cost) if row.cost else 0 # 按模板统计 result = await db.execute( select( AgentBillingRecord.agent_type, func.sum(AgentBillingRecord.cost).label("cost") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.agent_type) ) by_template = {} for row in result.all(): by_template[row.agent_type] = float(row.cost) if row.cost else 0 return { "totalCost": total_cost, "totalDurationSeconds": total_duration, "totalRequests": total_count, "byAgentType": by_agent_type, "byTemplate": by_template, } async def get_channel_agent_billing_stats( channel_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> dict: """ 获取渠道 Agent 计费统计 Args: channel_id: 渠道 ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: 统计数据 """ from models import AgentBillingRecord from sqlalchemy import func, and_ # 总体统计 result = await db.execute( select( func.sum(AgentBillingRecord.cost).label("total_cost"), func.sum(AgentBillingRecord.duration_seconds).label("total_duration"), func.count(AgentBillingRecord.id).label("total_count") ).where( and_( AgentBillingRecord.channel_id == channel_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ) ) row = result.first() total_cost = float(row.total_cost) if row.total_cost else 0 total_duration = int(row.total_duration) if row.total_duration else 0 total_count = int(row.total_count) if row.total_count else 0 # 按用户统计 result = await db.execute( select( AgentBillingRecord.user_id, func.sum(AgentBillingRecord.cost).label("cost"), func.count(AgentBillingRecord.id).label("count") ).where( and_( AgentBillingRecord.channel_id == channel_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.user_id) ) by_user = [] for row in result.all(): by_user.append({ "userId": row.user_id, "cost": float(row.cost) if row.cost else 0, "count": int(row.count) if row.count else 0, }) return { "totalCost": total_cost, "totalDurationSeconds": total_duration, "totalRequests": total_count, "byUser": by_user, } # ============= 资源解析辅助函数 ============= def _parse_cpu_to_cores(cpu_str: str) -> float: """ 解析 CPU 字符串为核心数 支持格式: - "100m" -> 0.1 核 - "1" -> 1 核 - "1.5" -> 1.5 核 """ if not cpu_str: return 0.0 cpu_str = cpu_str.strip().lower() if cpu_str.endswith("m"): return float(cpu_str[:-1]) / 1000 else: return float(cpu_str) def _parse_memory_to_gb(memory_str: str) -> float: """ 解析内存字符串为 GB 支持格式: - "128Mi" -> 0.125 GB - "1Gi" -> 1 GB - "512M" -> 0.5 GB - "2G" -> 2 GB """ if not memory_str: return 0.0 memory_str = memory_str.strip() # 处理 Kubernetes 格式 if memory_str.endswith("Gi"): return float(memory_str[:-2]) elif memory_str.endswith("Mi"): return float(memory_str[:-2]) / 1024 elif memory_str.endswith("Ki"): return float(memory_str[:-2]) / (1024 * 1024) # 处理简化格式 elif memory_str.endswith("G"): return float(memory_str[:-1]) elif memory_str.endswith("M"): return float(memory_str[:-1]) / 1024 elif memory_str.endswith("K"): return float(memory_str[:-1]) / (1024 * 1024) else: # 假设是字节 return float(memory_str) / (1024 * 1024 * 1024)