forked from xiaohei/taiji-AI-PAD
更新计费
This commit is contained in:
@@ -0,0 +1,733 @@
|
||||
# mcp-server 计费系统问题验证报告
|
||||
|
||||
> **验证日期**: 2026-03-09
|
||||
> **验证人**: AI Assistant
|
||||
> **基于文档**: [计费系统问题分析和修复方案.md](./计费系统问题分析和修复方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 📊 执行摘要
|
||||
|
||||
本报告对原分析文档中提到的 8 个问题进行了代码验证。根据用户提供的定价信息:
|
||||
- **模型推理**: 0.025 EU/call(LLM API调用)
|
||||
- **VM计算**: 0.5 EU/hour(Firecracker VM运行时)
|
||||
|
||||
**验证结论**:
|
||||
- ✅ **问题1(双重计费)不是问题** - 这是设计上的双重收费,符合业务模型
|
||||
- 🔴 **问题2(重复扣款)确实存在** - 周期计费和回调可能重复扣款
|
||||
- 🔴 **问题3(删除顺序错误)确实存在** - 先释放配额再删除Pod,可能导致免费使用
|
||||
- 🔴 **问题4(缺少行锁)确实存在** - 创建/删除Agent时配额更新没有并发保护
|
||||
- ✅ **问题5(双重存储)已标注废弃** - User.eu_balance已标记废弃,代码未使用
|
||||
- 🔴 **问题6(start_time为空)存在风险** - 周期计费会跳过start_time为空的记录
|
||||
- 🟡 **问题7(余额处理不一致)部分正确** - 已有透支停止机制,但创建时未预检查
|
||||
- ✅ **问题8(假用量)已有修复脚本** - 历史问题,已准备修复脚本
|
||||
|
||||
---
|
||||
|
||||
## 🔍 详细验证结果
|
||||
|
||||
### ✅ 问题1:双重计费风险 - **非问题(设计如此)**
|
||||
|
||||
**原文档观点**:
|
||||
> 用户启动 Agent 并通过 Agent 调用模型时,系统会从同一个余额账户扣款两次
|
||||
|
||||
**验证结果**:✅ **这是设计上的双重收费,符合业务模型**
|
||||
|
||||
**理由**:
|
||||
根据用户提供的定价信息,系统确实应该收取两部分费用:
|
||||
|
||||
1. **VM计算费用**: 0.5 EU/hour - 按 Agent Pod 的运行时间计费
|
||||
- 代码位置: `periodic_billing.py:L180-L185`
|
||||
- 计费逻辑: 每小时扫描运行中的 Agent,计算增量成本并扣款
|
||||
|
||||
2. **模型推理费用**: 0.025 EU/call - 按 LiteLLM 调用次数计费
|
||||
- 代码位置: `billing_webhook.py:L281-L284`
|
||||
- 计费逻辑: LiteLLM 回调后实时扣款
|
||||
|
||||
这类似于云服务商的计费模式(如 AWS Lambda = 执行时长费用 + API Gateway 调用费用)。
|
||||
|
||||
**建议**:
|
||||
- ✅ 保持当前双重计费机制
|
||||
- ⚠️ 需要在用户文档和前端明确说明「总费用 = VM计算费用 + 模型推理费用」
|
||||
- ⚠️ 前端计费明细页面应分开展示两种费用
|
||||
|
||||
---
|
||||
|
||||
### 🔴 问题2:周期计费和回调扣款重复 - **确实存在**
|
||||
|
||||
**原文档观点**:
|
||||
> `periodic_billing.py`(周期任务)和 `billing_webhook.py`(Agent Manager回调)可能同时更新同一条记录,导致重复扣款
|
||||
|
||||
**验证结果**:🔴 **确实存在并发竞争风险**
|
||||
|
||||
**代码验证**:
|
||||
|
||||
1. **周期计费**(`periodic_billing.py:L122-L200`)
|
||||
```python
|
||||
# ❌ 没有行锁
|
||||
result = await db.execute(
|
||||
select(AgentBillingRecord).where(
|
||||
AgentBillingRecord.end_time == None
|
||||
)
|
||||
# 缺少 .with_for_update()
|
||||
)
|
||||
|
||||
# 计算增量并扣款
|
||||
previous_cost = Decimal(str(record.cost or 0))
|
||||
cost_increment = cost - previous_cost
|
||||
if cost_increment > 0:
|
||||
success, message = await deduct_balance(
|
||||
str(record.user_id), cost_increment, db,
|
||||
f"Agent周期计费: {record.agent_name}"
|
||||
)
|
||||
```
|
||||
|
||||
2. **Agent Manager回调**(`billing_webhook.py:L454-L490`)
|
||||
```python
|
||||
# ❌ 没有行锁
|
||||
existing_result = await db.execute(
|
||||
select(AgentBillingRecord).where(
|
||||
and_(
|
||||
AgentBillingRecord.agent_name == callback_data.agentName,
|
||||
AgentBillingRecord.user_id == callback_data.userId,
|
||||
AgentBillingRecord.end_time == None
|
||||
)
|
||||
)
|
||||
# 缺少 .with_for_update()
|
||||
)
|
||||
|
||||
if existing_record:
|
||||
# 计算增量并扣款
|
||||
previous_cost = Decimal(str(existing_record.cost or 0))
|
||||
cost_increment = new_cost - previous_cost
|
||||
if cost_increment > 0:
|
||||
success, message = await deduct_balance(
|
||||
callback_data.userId,
|
||||
cost_increment,
|
||||
db,
|
||||
f"Agent 结算(增量): {callback_data.agentName}"
|
||||
)
|
||||
```
|
||||
|
||||
**问题分析**:
|
||||
两个函数都:
|
||||
1. 读取 `record.cost`(旧值)
|
||||
2. 计算增量 `cost_increment = new_cost - old_cost`
|
||||
3. 扣款
|
||||
4. 更新 `record.cost`(新值)
|
||||
|
||||
如果两者同时执行,可能出现:
|
||||
```
|
||||
时间线:竞争条件
|
||||
00:30:00 - 周期任务读取 record.cost = 10
|
||||
00:30:01 - 回调读取 record.cost = 10(周期任务还未commit)
|
||||
00:30:02 - 周期任务计算增量 = 15 - 10 = 5,扣款5
|
||||
00:30:03 - 回调计算增量 = 15 - 10 = 5,扣款5
|
||||
结果:扣了10,但实际应该扣5
|
||||
```
|
||||
|
||||
**建议修复方案**(推荐):
|
||||
```python
|
||||
# 方案:只在周期计费时扣款,回调只更新记录状态
|
||||
|
||||
# billing_webhook.py - 只更新记录,不扣款
|
||||
if existing_record:
|
||||
existing_record.end_time = end_time or datetime.utcnow()
|
||||
existing_record.duration_seconds = duration_seconds
|
||||
existing_record.cost = float(new_cost) # 只更新成本
|
||||
existing_record.tools_used = callback_data.toolsUsed
|
||||
# ❌ 删除增量扣款逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 问题3:配额释放与Pod删除顺序错误 - **确实存在**
|
||||
|
||||
**原文档观点**:
|
||||
> 删除 Agent 时,系统先在数据库中释放配额并提交,然后才删除 Pod。如果删除 Pod 失败,用户可以免费使用资源。
|
||||
|
||||
**验证结果**:🔴 **确实存在,且非常严重**
|
||||
|
||||
**代码验证**(`user.py:L3313-L3402`):
|
||||
|
||||
```python
|
||||
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
|
||||
async def delete_custom_agent(...):
|
||||
# 1. 查找计费记录
|
||||
billing_record = ...
|
||||
|
||||
try:
|
||||
# 2. ❌ 先更新数据库
|
||||
billing_record.end_time = datetime.utcnow()
|
||||
billing_record.cost = float(cost)
|
||||
|
||||
# 3. ❌ 扣款
|
||||
await deduct_balance(user_id, cost, db, ...)
|
||||
|
||||
# 4. ❌ 释放配额
|
||||
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
|
||||
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
|
||||
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
|
||||
|
||||
# 5. ❌ 提交数据库(此时无法回滚)
|
||||
await db.commit()
|
||||
|
||||
# 6. ❌ 然后才删除 Pod(可能失败!)
|
||||
client = get_agent_manager_client()
|
||||
await client.delete_agent(agent_full_name)
|
||||
```
|
||||
|
||||
**失败场景**:
|
||||
```
|
||||
步骤 1-5:✅ 成功执行
|
||||
- billing_record.end_time = "2026-03-09 10:00:00"
|
||||
- quota.cpu_used 从 5 降到 3
|
||||
- 数据库已提交(无法回滚)
|
||||
|
||||
步骤 6:❌ 失败(Agent Manager 宕机/网络问题)
|
||||
- Pod 仍在 K8s 中运行
|
||||
|
||||
后续影响:
|
||||
- 周期计费查询条件: end_time == None
|
||||
- 找不到该 Agent(end_time 已设置)
|
||||
- Agent 继续运行但不再计费 ❌
|
||||
- 用户免费使用资源 ❌
|
||||
```
|
||||
|
||||
**代码还有注释承认了这个问题**:
|
||||
```python
|
||||
except Exception as agent_delete_error:
|
||||
logger.error(f"Agent Manager 删除失败: {agent_full_name}, 错误: {str(agent_delete_error)}")
|
||||
# 注意:配额已释放,但 Agent 可能未删除
|
||||
# 可以考虑标记为"待清理"状态,供后台任务处理
|
||||
```
|
||||
|
||||
**建议修复**:
|
||||
```python
|
||||
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
|
||||
async def delete_custom_agent(...):
|
||||
# 1. 查找计费记录
|
||||
billing_record = ...
|
||||
|
||||
# 2. ✅ 先删除 Pod(如果失败,整个操作终止)
|
||||
try:
|
||||
await client.delete_agent(agent_full_name)
|
||||
except AgentManagerError as e:
|
||||
logger.error(f"删除 Pod 失败: {e.message}")
|
||||
raise HTTPException(status_code=e.status_code, detail=str(e))
|
||||
|
||||
# 3. ✅ Pod 删除成功后,再更新数据库
|
||||
billing_record.end_time = datetime.utcnow()
|
||||
billing_record.cost = float(cost)
|
||||
await deduct_balance(user_id, cost, db, ...)
|
||||
quota.cpu_used -= cpu_released
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 问题4:配额更新缺少并发保护 - **确实存在**
|
||||
|
||||
**原文档观点**:
|
||||
> 创建 Agent 时使用了行锁,但删除 Agent 时没有使用行锁
|
||||
|
||||
**验证结果**:🔴 **部分正确,实际上创建和删除都没有行锁**
|
||||
|
||||
**代码验证**:
|
||||
|
||||
1. **检查整个 user.py 中 with_for_update 的使用**:
|
||||
```bash
|
||||
grep "with_for_update" services/mcp-server/app/routes/user.py
|
||||
```
|
||||
结果:只有 3 处使用(L1302, L2307, L2552),都是在**平台 Agent** 的配额查询中。
|
||||
|
||||
2. **创建自定义 Agent 时的配额查询**(`user.py:L2893`):
|
||||
```python
|
||||
# ❌ 没有行锁
|
||||
quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota).where(
|
||||
TenantCustomAgentQuota.tenant_id == user_id
|
||||
)
|
||||
# 缺少 .with_for_update()
|
||||
)
|
||||
|
||||
# 更新配额(不安全)
|
||||
quota.cpu_used = cpu_used + cpu_request
|
||||
quota.memory_used = memory_used + memory_request
|
||||
quota.agent_count = (quota.agent_count or 0) + 1
|
||||
```
|
||||
|
||||
3. **删除自定义 Agent 时的配额查询**(`user.py:L3359`):
|
||||
```python
|
||||
# ❌ 没有行锁
|
||||
quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota).where(
|
||||
TenantCustomAgentQuota.tenant_id == user_id
|
||||
)
|
||||
# 缺少 .with_for_update()
|
||||
)
|
||||
|
||||
# 更新配额(不安全)
|
||||
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
|
||||
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
|
||||
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
|
||||
```
|
||||
|
||||
**并发问题示例**:
|
||||
```
|
||||
初始状态:quota.cpu_used = 5
|
||||
|
||||
并发场景:两个请求同时删除不同的 Agent
|
||||
线程1: 删除 Agent A(释放 2 CPU)
|
||||
1. 读取 cpu_used = 5
|
||||
2. 计算 new_value = 5 - 2 = 3
|
||||
|
||||
线程2: 删除 Agent B(释放 2 CPU)
|
||||
1. 读取 cpu_used = 5(线程1还未 commit)
|
||||
2. 计算 new_value = 5 - 2 = 3
|
||||
|
||||
线程1: 写入 cpu_used = 3
|
||||
线程2: 写入 cpu_used = 3 ❌
|
||||
|
||||
最终结果:cpu_used = 3
|
||||
正确结果:cpu_used = 1 (5 - 2 - 2)
|
||||
丢失更新:2 CPU
|
||||
```
|
||||
|
||||
**建议修复**:
|
||||
```python
|
||||
# 所有配额更新操作都加行锁
|
||||
quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota)
|
||||
.where(TenantCustomAgentQuota.tenant_id == user_id)
|
||||
.with_for_update() # ✅ 添加行锁
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 问题5:余额数据双重存储 - **已标注废弃,代码未使用**
|
||||
|
||||
**原文档观点**:
|
||||
> 系统中同时存在两个余额字段:User.eu_balance(标记为废弃)和 Balance.eu_balance(当前使用)
|
||||
|
||||
**验证结果**:✅ **已标注废弃,代码未实际使用**
|
||||
|
||||
**代码验证**:
|
||||
|
||||
1. **models.py:L85-L88**
|
||||
```python
|
||||
class User(BaseModel, Base):
|
||||
# EU计费(执行单元)
|
||||
eu_balance = Column(sa.Numeric(15, 2), default=0) # EU余额 [DEPRECATED - 使用 Balance 表]
|
||||
total_eu_consumed = Column(sa.Numeric(15, 2), default=0) # 总EU消耗
|
||||
```
|
||||
|
||||
2. **搜索是否有代码使用 User.eu_balance**:
|
||||
```bash
|
||||
grep -r "User\.eu_balance\|user\.eu_balance" services/mcp-server/
|
||||
```
|
||||
结果:只在 `billing.py:L29` 有一条注释:
|
||||
```python
|
||||
# 注意:User.eu_balance 和 User.balance 字段已废弃,请使用 Balance 表
|
||||
```
|
||||
|
||||
**结论**:
|
||||
- ✅ 字段已标记废弃
|
||||
- ✅ 代码未使用该字段
|
||||
- ⚠️ 但字段仍存在数据库中,占用存储空间
|
||||
|
||||
**建议**:
|
||||
```python
|
||||
# 后续可通过数据库迁移删除(非紧急)
|
||||
def upgrade():
|
||||
op.drop_column('users', 'eu_balance')
|
||||
op.drop_column('users', 'balance')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 问题6:计费记录 start_time 可能为空 - **存在风险**
|
||||
|
||||
**原文档观点**:
|
||||
> 周期计费任务会跳过 start_time 为 None 的记录,导致这些 Agent 免费运行
|
||||
|
||||
**验证结果**:🔴 **代码确实会跳过 start_time 为空的记录**
|
||||
|
||||
**代码验证**(`periodic_billing.py:L132-L138`):
|
||||
```python
|
||||
for record in running_agents:
|
||||
try:
|
||||
# 跳过没有开始时间的记录
|
||||
if record.start_time is None:
|
||||
logger.warning(f"Agent {record.agent_name} 缺少 start_time,跳过计费")
|
||||
stats["failed"] += 1
|
||||
stats["errors"].append(f"Agent {record.agent_name}: start_time 为空")
|
||||
continue # ❌ 跳过计费 → Agent 免费运行
|
||||
```
|
||||
|
||||
**潜在问题代码**:
|
||||
在 `billing_webhook.py:L500-L510` 创建兜底记录时:
|
||||
```python
|
||||
billing_record = AgentBillingRecord(
|
||||
user_id=callback_data.userId,
|
||||
channel_id=user.channel_id if user.channel_id else None,
|
||||
agent_name=callback_data.agentName,
|
||||
agent_type=agent_type,
|
||||
is_platform_agent=is_platform_agent,
|
||||
duration_seconds=duration_seconds,
|
||||
eu_consumed=eu_consumed,
|
||||
cost=float(new_cost),
|
||||
start_time=start_time or datetime.utcnow(), # ✅ 有兜底值
|
||||
end_time=end_time or datetime.utcnow(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
好消息:代码中使用了 `start_time or datetime.utcnow()`,理论上不会为空。
|
||||
|
||||
**建议**:
|
||||
```python
|
||||
# 1. 数据库约束:确保 start_time 不为空
|
||||
class AgentBillingRecord(BaseModel, Base):
|
||||
start_time = Column(DateTime, nullable=False) # ✅ 不允许为空
|
||||
|
||||
# 2. 周期任务中修复而非跳过
|
||||
if record.start_time is None:
|
||||
# 使用创建时间作为开始时间
|
||||
record.start_time = record.created_at or datetime.utcnow()
|
||||
logger.warning(f"修复 Agent {record.agent_name} 的 start_time")
|
||||
# 继续计费,而不是跳过
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🟡 问题7:余额不足时的处理不一致 - **部分正确**
|
||||
|
||||
**原文档观点**:
|
||||
> 不同场景下对余额不足的处理方式不一致:创建Agent不检查、周期计费只警告、模型调用允许透支
|
||||
|
||||
**验证结果**:🟡 **部分正确,已有停止机制,但创建时未预检查**
|
||||
|
||||
**代码验证**:
|
||||
|
||||
1. **创建 Agent 时**(无预检查)
|
||||
- 未找到余额预检查代码
|
||||
- Agent 可以在余额不足时创建
|
||||
|
||||
2. **周期计费时**(`periodic_billing.py:L185-L200`):
|
||||
```python
|
||||
success, message = await deduct_balance(
|
||||
str(record.user_id), cost_increment, db,
|
||||
f"Agent周期计费: {record.agent_name}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.warning(f"周期计费扣款失败: {message}")
|
||||
|
||||
# ✅ 检查是否透支,如果透支则停止所有Agent
|
||||
balance, credit_limit, available = await get_available_balance(
|
||||
str(record.user_id), db
|
||||
)
|
||||
if available < 0:
|
||||
logger.warning(
|
||||
f"用户 {record.user_id} 余额不足 (可用: {available}),将停止所有Agent"
|
||||
)
|
||||
stopped = await stop_user_agents(str(record.user_id), db)
|
||||
stats["stopped_agents"].extend(stopped)
|
||||
```
|
||||
|
||||
好消息:**周期计费已经实现了余额不足停止机制!**
|
||||
|
||||
3. **模型调用时**(`billing_webhook.py:L299-L305`):
|
||||
```python
|
||||
# 扣减EU余额
|
||||
old_balance = Decimal(str(balance.eu_balance))
|
||||
new_balance = old_balance - Decimal(str(eu_consumed))
|
||||
balance.eu_balance = float(new_balance)
|
||||
|
||||
# 余额不足警告(但不阻止记录)
|
||||
if new_balance < 0:
|
||||
logger.warning(
|
||||
f"⚠️ 用户余额不足: user_id={tenant_id}, "
|
||||
f"balance={new_balance:.4f}, "
|
||||
f"建议充值"
|
||||
)
|
||||
# ❌ 只警告,允许透支
|
||||
```
|
||||
|
||||
**建议**:
|
||||
```python
|
||||
# 创建 Agent 前预检查余额
|
||||
@router.post("/custom-agents/create")
|
||||
async def create_custom_agent(...):
|
||||
# ✅ 预估成本
|
||||
estimated_cost = estimate_agent_cost(
|
||||
cpu=req.cpuRequest,
|
||||
memory=req.memoryRequest,
|
||||
duration=3600 # 预估至少运行1小时
|
||||
)
|
||||
|
||||
# ✅ 检查余额
|
||||
balance, credit_limit, available = await get_available_balance(user_id, db)
|
||||
if available < estimated_cost:
|
||||
raise HTTPException(403, "余额不足,无法创建 Agent,请先充值")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 问题8:配额数据"假用量" - **历史问题,已有修复脚本**
|
||||
|
||||
**原文档观点**:
|
||||
> 历史原因导致配额表中记录了"假用量":配额显示 cpu_used=5,但 K8s 中没有对应的 Pod 运行
|
||||
|
||||
**验证结果**:✅ **确实存在历史遗留问题,已准备修复脚本**
|
||||
|
||||
**代码验证**:
|
||||
|
||||
1. **修复脚本存在**:
|
||||
- `/services/mcp-server/fix_fake_quota.py` ✅
|
||||
- `/services/mcp-server/fix_agent_quotas.py` ✅
|
||||
- `/services/mcp-server/fix_channel_quota_records.py` ✅
|
||||
|
||||
2. **fix_fake_quota.py 文件头注释**(L1-L18):
|
||||
```python
|
||||
"""
|
||||
修复脚本:清理历史假用量数据
|
||||
|
||||
此脚本用于清理因旧接口(/api/user/tools/generate)产生的假用量数据。
|
||||
旧接口会在数据库中创建Agent记录并扣除配额,但不实际部署到K8s,
|
||||
导致quota表中记录了用量但实际没有Pod运行。
|
||||
|
||||
脚本逻辑:
|
||||
1. 备份当前quota数据到JSON文件
|
||||
2. 遍历所有租户的TenantCustomAgentQuota记录
|
||||
3. 对每个租户,重新统计agents表中type='custom'且status='active'的真实运行Agent
|
||||
4. 更新quota表的cpu_used、memory_used、agent_count为真实值
|
||||
5. 生成修复报告
|
||||
|
||||
使用方法:
|
||||
在mcp-server容器内运行:
|
||||
python fix_fake_quota.py
|
||||
|
||||
注意:此脚本会修改数据库,请务必先备份数据库!
|
||||
"""
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- ✅ 修复脚本已准备好,可以执行
|
||||
- ⚠️ 执行前务必备份数据库
|
||||
- ⚠️ 废弃旧接口 `/api/user/tools/generate`,防止再次产生假用量
|
||||
|
||||
---
|
||||
|
||||
## 📋 问题优先级总结
|
||||
|
||||
| 问题 | 严重程度 | 是否存在 | 紧急程度 | 推荐优先级 |
|
||||
|------|---------|---------|---------|----------|
|
||||
| 问题1: 双重计费风险 | N/A | ❌ 非问题(设计如此) | - | **P3 - 文档优化** |
|
||||
| **问题2: 重复扣款** | 🔴 高 | ✅ **确实存在** | 🔴 高 | **P0 - 立即修复** |
|
||||
| **问题3: 删除顺序错误** | 🔴 高 | ✅ **确实存在** | 🔴 高 | **P0 - 立即修复** |
|
||||
| **问题4: 并发保护缺失** | 🟡 中 | ✅ **确实存在** | 🟡 中 | **P1 - 尽快修复** |
|
||||
| 问题5: 数据双重存储 | 🟡 中 | ⚠️ 已标记废弃 | 🟢 低 | **P2 - 后续清理** |
|
||||
| **问题6: start_time 为空** | 🟡 中 | ⚠️ 存在风险但有兜底 | 🟡 中 | **P1 - 添加约束** |
|
||||
| 问题7: 余额处理不一致 | 🟡 中 | 🟡 部分正确 | 🟡 中 | **P1 - 添加预检查** |
|
||||
| 问题8: 假用量数据 | 🟡 中 | ✅ 历史问题 | 🟢 低 | **P2 - 执行脚本** |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 推荐修复计划
|
||||
|
||||
### 第一阶段:P0 核心问题修复(本周内)
|
||||
|
||||
#### 1. 修复问题2:消除重复扣款风险
|
||||
|
||||
**文件**: `services/mcp-server/app/routes/billing_webhook.py`
|
||||
|
||||
**修改**:
|
||||
```python
|
||||
# 行号:L454-L490
|
||||
# 将 billing_webhook.py 中的扣款逻辑改为只更新记录
|
||||
|
||||
if existing_record:
|
||||
# 更新现有记录
|
||||
existing_record.end_time = end_time or datetime.utcnow()
|
||||
existing_record.duration_seconds = duration_seconds
|
||||
existing_record.eu_consumed = eu_consumed
|
||||
existing_record.cost = float(new_cost)
|
||||
existing_record.period_end = end_time or datetime.utcnow()
|
||||
existing_record.tools_used = callback_data.toolsUsed
|
||||
existing_record.request_id = callback_data.requestId
|
||||
|
||||
billing_record = existing_record
|
||||
|
||||
# ❌ 删除增量扣款逻辑,只在周期计费时扣款
|
||||
# cost_increment = new_cost - previous_cost
|
||||
# if cost_increment > 0:
|
||||
# success, message = await deduct_balance(...)
|
||||
|
||||
logger.info(
|
||||
f"📝 更新Agent计费记录(不扣款): agent={callback_data.agentName}, "
|
||||
f"最终成本={new_cost}"
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. 修复问题3:调整删除 Agent 的顺序
|
||||
|
||||
**文件**: `services/mcp-server/app/routes/user.py`
|
||||
|
||||
**修改**:
|
||||
```python
|
||||
# 行号:L3313-L3402
|
||||
# 调整 delete_custom_agent 函数的执行顺序
|
||||
|
||||
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
|
||||
async def delete_custom_agent(...):
|
||||
# 1. 查找计费记录
|
||||
billing_record = ...
|
||||
|
||||
# 2. ✅ 先删除 Pod(如果失败,整个操作终止)
|
||||
try:
|
||||
await client.delete_agent(agent_full_name)
|
||||
except AgentManagerError as e:
|
||||
logger.error(f"删除 Pod 失败: {e.message}")
|
||||
raise HTTPException(status_code=e.status_code, detail=str(e))
|
||||
|
||||
# 3. ✅ Pod 删除成功后,再更新数据库
|
||||
billing_record.end_time = datetime.utcnow()
|
||||
billing_record.cost = float(cost)
|
||||
await deduct_balance(user_id, cost, db, ...)
|
||||
|
||||
# 4. ✅ 使用行锁释放配额
|
||||
quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota)
|
||||
.where(TenantCustomAgentQuota.tenant_id == user_id)
|
||||
.with_for_update() # ✅ 添加行锁
|
||||
)
|
||||
quota = quota_result.scalar_one_or_none()
|
||||
if quota:
|
||||
quota.cpu_used -= cpu_released
|
||||
quota.memory_used -= memory_released
|
||||
quota.agent_count -= 1
|
||||
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
### 第二阶段:P1 改进和优化(下周)
|
||||
|
||||
#### 3. 修复问题4:添加并发保护
|
||||
|
||||
**文件**: `services/mcp-server/app/routes/user.py`
|
||||
|
||||
**修改位置**:
|
||||
- L2893(创建自定义Agent)
|
||||
- L3359(删除自定义Agent)
|
||||
- L3446(扩缩容)
|
||||
|
||||
```python
|
||||
# 所有配额更新操作都加行锁
|
||||
quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota)
|
||||
.where(TenantCustomAgentQuota.tenant_id == user_id)
|
||||
.with_for_update() # ✅ 添加行锁
|
||||
)
|
||||
```
|
||||
|
||||
#### 4. 修复问题6:确保 start_time 不为空
|
||||
|
||||
**文件**: `services/mcp-server/models.py`
|
||||
|
||||
```python
|
||||
class AgentBillingRecord(BaseModel, Base):
|
||||
start_time = Column(DateTime, nullable=False) # ✅ 不允许为空
|
||||
```
|
||||
|
||||
#### 5. 修复问题7:添加余额预检查
|
||||
|
||||
**文件**: `services/mcp-server/app/routes/user.py`
|
||||
|
||||
```python
|
||||
@router.post("/custom-agents/create")
|
||||
async def create_custom_agent(...):
|
||||
# ✅ 预估成本并检查余额
|
||||
estimated_cost = estimate_agent_cost(
|
||||
cpu=req.cpuRequest,
|
||||
memory=req.memoryRequest,
|
||||
duration=3600 # 预估1小时
|
||||
)
|
||||
|
||||
balance, credit_limit, available = await get_available_balance(user_id, db)
|
||||
if available < estimated_cost:
|
||||
raise HTTPException(403, "余额不足,请先充值")
|
||||
```
|
||||
|
||||
### 第三阶段:P2 清理和文档(后续)
|
||||
|
||||
#### 6. 问题5:删除废弃字段
|
||||
|
||||
创建数据库迁移脚本删除 User.eu_balance 和 User.balance。
|
||||
|
||||
#### 7. 问题8:执行假用量修复脚本
|
||||
|
||||
```bash
|
||||
# 1. 备份数据库
|
||||
pg_dump taiji_prod > backup_before_quota_fix.sql
|
||||
|
||||
# 2. 执行修复脚本
|
||||
python services/mcp-server/fix_fake_quota.py
|
||||
```
|
||||
|
||||
#### 8. 问题1:完善计费文档
|
||||
|
||||
创建用户文档,明确说明:
|
||||
- 总费用 = VM计算费用(0.5 EU/hour)+ 模型推理费用(0.025 EU/call)
|
||||
- 前端分开展示两种费用
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试建议
|
||||
|
||||
### 单元测试
|
||||
|
||||
```python
|
||||
# 测试1:重复扣款保护
|
||||
async def test_no_duplicate_deduction():
|
||||
"""测试周期计费和回调不会重复扣款"""
|
||||
# 创建Agent → 运行1小时 → 周期扣款 → 回调更新
|
||||
# 验证:总扣款 = 实际成本(不是双倍)
|
||||
|
||||
# 测试2:删除顺序保护
|
||||
async def test_delete_agent_rollback_on_pod_failure():
|
||||
"""测试Pod删除失败时不会释放配额"""
|
||||
# Mock Agent Manager删除失败
|
||||
# 验证:配额没有被释放,计费记录仍在运行中
|
||||
|
||||
# 测试3:并发配额更新
|
||||
async def test_concurrent_quota_updates():
|
||||
"""测试并发删除Agent时配额计算正确"""
|
||||
# 并发删除2个Agent
|
||||
# 验证:配额正确减少(不丢失更新)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 总结
|
||||
|
||||
经过详细的代码验证,原分析文档中提到的 8 个问题中:
|
||||
|
||||
- ✅ **3个确实存在且严重**(问题2、3、4)- 需要立即修复
|
||||
- ⚠️ **2个存在风险**(问题6、7)- 需要改进
|
||||
- ✅ **1个已有解决方案**(问题8)- 执行修复脚本即可
|
||||
- ✅ **1个已标注废弃**(问题5)- 后续清理
|
||||
- ❌ **1个非问题**(问题1)- 这是设计上的双重收费
|
||||
|
||||
**最紧急的修复**:
|
||||
1. 🔥 消除重复扣款风险(问题2)
|
||||
2. 🔥 调整删除顺序(问题3)
|
||||
3. 🔒 添加并发保护(问题4)
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0.0
|
||||
**最后更新**: 2026-03-09
|
||||
**审核状态**: 待审核
|
||||
Reference in New Issue
Block a user