forked from xiaohei/taiji-AI-PAD
更新渠道申请
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
# MCP-Server 设计问题审查报告
|
||||
|
||||
## 概述
|
||||
|
||||
本报告检查了 MCP-Server 的设计代码,发现了多个设计错误和冲突问题。
|
||||
|
||||
---
|
||||
|
||||
## 一、严重问题(会导致运行时错误)
|
||||
|
||||
### 1.1 PlatformAgentQuota 模型字段缺失 ❌
|
||||
|
||||
**问题描述**:`user.py` 中使用了 `PlatformAgentQuota` 的多个字段,但这些字段在 `models.py` 中**不存在**。
|
||||
|
||||
**models.py 中的定义(第 1095-1131 行)**:
|
||||
```python
|
||||
class PlatformAgentQuota(BaseModel, Base):
|
||||
__tablename__ = "platform_agent_quotas"
|
||||
|
||||
target_id = Column(GUID(), nullable=False)
|
||||
target_type = Column(String(20), nullable=False) # channel, tenant
|
||||
template_name = Column(String(100), nullable=False)
|
||||
pod_quota = Column(Integer, nullable=False, default=0)
|
||||
pod_used = Column(Integer, default=0)
|
||||
allocated_by = Column(GUID(), ForeignKey("users.id"))
|
||||
allocated_at = Column(DateTime, default=datetime.utcnow)
|
||||
```
|
||||
|
||||
**user.py 中使用的缺失字段**:
|
||||
|
||||
| 缺失字段 | 使用位置 | 用途 |
|
||||
|----------|----------|------|
|
||||
| `is_active` | user.py:835, 881, 1011 | 过滤活跃配额 |
|
||||
| `agent_type` | user.py:844, 880, 1010 | Agent 类型标识 |
|
||||
| `cpu_per_pod` | user.py:849, 909, 935 | 每个 Pod 的 CPU 配置 |
|
||||
| `memory_per_pod` | user.py:850, 911, 936 | 每个 Pod 的内存配置 |
|
||||
|
||||
**影响**:
|
||||
- 用户调用 `/api/user/platform-agents/available` 会报错
|
||||
- 用户调用 `/api/user/platform-agents/use` 会报错
|
||||
- 用户停止平台 Agent 时会报错
|
||||
|
||||
**修复建议**:在 `models.py` 的 `PlatformAgentQuota` 类中添加缺失字段:
|
||||
|
||||
```python
|
||||
class PlatformAgentQuota(BaseModel, Base):
|
||||
__tablename__ = "platform_agent_quotas"
|
||||
|
||||
target_id = Column(GUID(), nullable=False)
|
||||
target_type = Column(String(20), nullable=False) # channel, tenant
|
||||
template_name = Column(String(100), nullable=False)
|
||||
|
||||
# 新增字段
|
||||
agent_type = Column(String(100)) # Agent 类型名称(与 template_name 可能相同)
|
||||
is_active = Column(Boolean, default=True) # 是否活跃
|
||||
cpu_per_pod = Column(String(20), default="100m") # 每个 Pod 的 CPU
|
||||
memory_per_pod = Column(String(20), default="128Mi") # 每个 Pod 的内存
|
||||
|
||||
# 原有字段
|
||||
pod_quota = Column(Integer, nullable=False, default=0)
|
||||
pod_used = Column(Integer, default=0)
|
||||
allocated_by = Column(GUID(), ForeignKey("users.id"))
|
||||
allocated_at = Column(DateTime, default=datetime.utcnow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 AgentBillingRecord 模型字段缺失 ❌
|
||||
|
||||
**问题描述**:`user.py` 中使用了 `AgentBillingRecord` 的多个字段,但这些字段在 `models.py` 中**不存在**。
|
||||
|
||||
**models.py 中的定义(第 1134-1172 行)**:
|
||||
```python
|
||||
class AgentBillingRecord(BaseModel, Base):
|
||||
__tablename__ = "agent_billing_records"
|
||||
|
||||
user_id = Column(GUID(), ForeignKey("users.id"), nullable=False)
|
||||
channel_id = Column(GUID(), ForeignKey("channels.id"))
|
||||
agent_name = Column(String(100), nullable=False)
|
||||
agent_type = Column(String(20), nullable=False) # platform, custom
|
||||
template_name = Column(String(100), nullable=False)
|
||||
duration_seconds = Column(Integer, nullable=False)
|
||||
cpu_seconds = Column(sa.Float, default=0)
|
||||
memory_gb_seconds = Column(sa.Float, default=0)
|
||||
request_count = Column(Integer, default=0)
|
||||
cost = Column(sa.Numeric(12, 4), nullable=False)
|
||||
currency = Column(String(10), default="EU")
|
||||
period_start = Column(DateTime, nullable=False)
|
||||
period_end = Column(DateTime, nullable=False)
|
||||
```
|
||||
|
||||
**user.py 中使用的缺失字段**:
|
||||
|
||||
| 缺失字段 | 使用位置 | 用途 |
|
||||
|----------|----------|------|
|
||||
| `is_platform_agent` | user.py:933, 1054, 1207, 1264, 1469, 1659, 1682 | 区分平台/自定义 Agent |
|
||||
| `start_time` | user.py:934, 1071, 1209, 1285, 1488, 1684 | 开始时间 |
|
||||
| `end_time` | user.py:992, 1054, 1265, 1469, 1685 | 结束时间 |
|
||||
| `cpu_used` | user.py:935, 1211, 1291, 1378, 1421, 1487, 1687 | CPU 使用量 |
|
||||
| `memory_used` | user.py:936, 1212, 1291, 1379, 1424, 1488, 1688 | 内存使用量 |
|
||||
| `eu_consumed` | user.py:1002, 1287, 1687 | EU 消耗量 |
|
||||
|
||||
**影响**:
|
||||
- 用户使用平台 Agent 时计费记录创建失败
|
||||
- 用户创建自定义 Agent 时计费记录创建失败
|
||||
- 计费历史查询失败
|
||||
|
||||
**修复建议**:在 `models.py` 的 `AgentBillingRecord` 类中添加缺失字段:
|
||||
|
||||
```python
|
||||
class AgentBillingRecord(BaseModel, Base):
|
||||
__tablename__ = "agent_billing_records"
|
||||
|
||||
user_id = Column(GUID(), ForeignKey("users.id"), nullable=False)
|
||||
channel_id = Column(GUID(), ForeignKey("channels.id"))
|
||||
|
||||
agent_name = Column(String(100), nullable=False)
|
||||
agent_type = Column(String(20), nullable=False) # 模板名称
|
||||
template_name = Column(String(100), nullable=False)
|
||||
|
||||
# 新增字段
|
||||
is_platform_agent = Column(Boolean, nullable=False) # 是否为平台 Agent
|
||||
start_time = Column(DateTime) # 开始时间
|
||||
end_time = Column(DateTime) # 结束时间(None 表示运行中)
|
||||
cpu_used = Column(String(20)) # CPU 使用量(如 "100m")
|
||||
memory_used = Column(String(20)) # 内存使用量(如 "128Mi")
|
||||
eu_consumed = Column(sa.Float, default=0) # EU 消耗量
|
||||
|
||||
# 原有字段
|
||||
duration_seconds = Column(Integer) # 改为可空,运行中时为 None
|
||||
cpu_seconds = Column(sa.Float, default=0)
|
||||
memory_gb_seconds = Column(sa.Float, default=0)
|
||||
request_count = Column(Integer, default=0)
|
||||
cost = Column(sa.Numeric(12, 4)) # 改为可空
|
||||
currency = Column(String(10), default="EU")
|
||||
period_start = Column(DateTime) # 改为可空
|
||||
period_end = Column(DateTime) # 改为可空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、设计冲突问题
|
||||
|
||||
### 2.1 平台 Agent 模板硬编码 ⚠️
|
||||
|
||||
**问题描述**:平台 Agent 模板在 `admin.py` 和 `channel.py` 中**重复硬编码**,而不是从 Agent Manager 获取。
|
||||
|
||||
**位置**:
|
||||
- [`admin.py:2507-2547`](services/mcp-server/app/routes/admin.py:2507) - `PLATFORM_AGENT_TEMPLATES`
|
||||
- [`channel.py:1737-1777`](services/mcp-server/app/routes/channel.py:1737) - `PLATFORM_AGENT_TEMPLATES`
|
||||
|
||||
**问题**:
|
||||
1. 两处代码需要保持同步,容易出错
|
||||
2. 新增模板需要修改代码并重新部署
|
||||
3. 与 Agent Manager 的模板管理功能冲突
|
||||
|
||||
**修复建议**:
|
||||
1. 删除硬编码的 `PLATFORM_AGENT_TEMPLATES`
|
||||
2. 调用 `agent_manager_client.list_platform_templates()` 获取模板
|
||||
3. 或者在数据库中存储模板配置
|
||||
|
||||
### 2.2 字段命名不一致 ⚠️
|
||||
|
||||
**问题描述**:同一概念在不同地方使用不同的字段名。
|
||||
|
||||
| 概念 | models.py | user.py | 说明 |
|
||||
|------|-----------|---------|------|
|
||||
| Agent 类型 | `agent_type` | `agent_type` / `template_name` | 有时混用 |
|
||||
| 开始时间 | `period_start` | `start_time` | 不一致 |
|
||||
| 结束时间 | `period_end` | `end_time` | 不一致 |
|
||||
|
||||
**修复建议**:统一字段命名,或者在模型中同时保留两个字段作为别名。
|
||||
|
||||
---
|
||||
|
||||
## 三、业务逻辑问题
|
||||
|
||||
### 3.1 平台 Agent 启动时机不符合需求 ⚠️
|
||||
|
||||
**当前实现**:
|
||||
- `user.py` 中的 `/api/user/platform-agents/use` 接口是**用户主动调用**才启动 Pod
|
||||
|
||||
**需求**:
|
||||
- 平台 Agent 应该在**渠道分配给用户时立即启动**,用户可以直接使用
|
||||
|
||||
**影响位置**:
|
||||
- [`channel.py`](services/mcp-server/app/routes/channel.py) 中的租户资源分配接口
|
||||
|
||||
**修复建议**:
|
||||
在渠道分配平台 Agent 配额给租户时,自动调用 Agent Manager 创建 Pod:
|
||||
|
||||
```python
|
||||
# channel.py 中分配平台 Agent 给租户时
|
||||
async def allocate_platform_agent_to_tenant(...):
|
||||
# 1. 创建配额记录
|
||||
tenant_quota = PlatformAgentQuota(...)
|
||||
|
||||
# 2. 立即调用 Agent Manager 启动 Pod
|
||||
client = get_agent_manager_client()
|
||||
result = await client.create_platform_agent(
|
||||
name=f"{template_name}-{tenant_id[:8]}",
|
||||
template=template_name,
|
||||
user_id=tenant_id,
|
||||
channel_id=channel_id,
|
||||
config=AgentConfig(
|
||||
cpu_request=cpu_per_pod,
|
||||
memory_request=memory_per_pod,
|
||||
...
|
||||
)
|
||||
)
|
||||
|
||||
# 3. 更新配额使用量
|
||||
tenant_quota.pod_used = 1
|
||||
tenant_quota.pod_name = result.name
|
||||
```
|
||||
|
||||
### 3.2 自定义 Agent 配额检查逻辑正确 ✅
|
||||
|
||||
**当前实现**:
|
||||
- 渠道分配配额给租户时只记录配额,不启动 Pod
|
||||
- 用户创建自定义 Agent 时检查配额并启动 Pod
|
||||
|
||||
**符合需求**:自定义 Agent 需要用户填写配置后才启动。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据库迁移需求
|
||||
|
||||
根据上述问题,需要创建数据库迁移脚本:
|
||||
|
||||
### 4.1 迁移脚本 007_fix_platform_agent_quota.sql
|
||||
|
||||
```sql
|
||||
-- 为 platform_agent_quotas 表添加缺失字段
|
||||
ALTER TABLE platform_agent_quotas
|
||||
ADD COLUMN IF NOT EXISTS agent_type VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS cpu_per_pod VARCHAR(20) DEFAULT '100m',
|
||||
ADD COLUMN IF NOT EXISTS memory_per_pod VARCHAR(20) DEFAULT '128Mi';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_agent_quota_active
|
||||
ON platform_agent_quotas(is_active);
|
||||
```
|
||||
|
||||
### 4.2 迁移脚本 008_fix_agent_billing_record.sql
|
||||
|
||||
```sql
|
||||
-- 为 agent_billing_records 表添加缺失字段
|
||||
ALTER TABLE agent_billing_records
|
||||
ADD COLUMN IF NOT EXISTS is_platform_agent BOOLEAN,
|
||||
ADD COLUMN IF NOT EXISTS start_time TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS end_time TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS cpu_used VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS memory_used VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS eu_consumed FLOAT DEFAULT 0;
|
||||
|
||||
-- 修改原有字段为可空
|
||||
ALTER TABLE agent_billing_records
|
||||
ALTER COLUMN duration_seconds DROP NOT NULL,
|
||||
ALTER COLUMN cost DROP NOT NULL,
|
||||
ALTER COLUMN period_start DROP NOT NULL,
|
||||
ALTER COLUMN period_end DROP NOT NULL;
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_billing_is_platform
|
||||
ON agent_billing_records(is_platform_agent);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_billing_end_time
|
||||
ON agent_billing_records(end_time);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、问题汇总
|
||||
|
||||
| 问题类型 | 问题描述 | 严重程度 | 状态 |
|
||||
|----------|----------|----------|------|
|
||||
| 模型字段缺失 | PlatformAgentQuota 缺少 is_active, agent_type, cpu_per_pod, memory_per_pod | 严重 | 待修复 |
|
||||
| 模型字段缺失 | AgentBillingRecord 缺少 is_platform_agent, start_time, end_time, cpu_used, memory_used, eu_consumed | 严重 | 待修复 |
|
||||
| 代码重复 | PLATFORM_AGENT_TEMPLATES 在两处硬编码 | 中等 | 待修复 |
|
||||
| 字段命名不一致 | period_start/start_time, period_end/end_time | 低 | 待修复 |
|
||||
| 业务逻辑 | 平台 Agent 启动时机不符合需求 | 中等 | 待修复 |
|
||||
|
||||
---
|
||||
|
||||
## 六、修复优先级
|
||||
|
||||
1. **高优先级**:修复 `PlatformAgentQuota` 和 `AgentBillingRecord` 模型字段缺失
|
||||
2. **中优先级**:修改平台 Agent 启动时机(渠道分配时启动)
|
||||
3. **低优先级**:移除硬编码模板,改为从 Agent Manager 获取
|
||||
|
||||
---
|
||||
|
||||
## 七、版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2026-01-05 | 初始审查报告 |
|
||||
@@ -677,10 +677,50 @@ async def list_channels(
|
||||
|
||||
for alloc in allocations:
|
||||
# 查询Agent的资源配置
|
||||
agent_result = await db.execute(
|
||||
select(Agent).where(Agent.id == alloc.resource_id)
|
||||
)
|
||||
agent = agent_result.scalar_one_or_none()
|
||||
# resource_id 可能是 UUID 字符串或模板名称(如 'code-reviewer')
|
||||
agent = None
|
||||
|
||||
# 首先尝试按 UUID 查询
|
||||
try:
|
||||
resource_uuid = uuid.UUID(alloc.resource_id)
|
||||
agent_result = await db.execute(
|
||||
select(Agent).where(Agent.id == resource_uuid)
|
||||
)
|
||||
agent = agent_result.scalar_one_or_none()
|
||||
except (ValueError, TypeError):
|
||||
# 如果不是有效的 UUID,按名称查询
|
||||
pass
|
||||
|
||||
# 如果按 UUID 没找到,尝试按名称查询
|
||||
if not agent:
|
||||
agent_result = await db.execute(
|
||||
select(Agent).where(Agent.name == alloc.resource_id)
|
||||
)
|
||||
agent = agent_result.scalar_one_or_none()
|
||||
|
||||
# 如果仍然没找到,检查是否是平台 Agent 模板
|
||||
if not agent and alloc.resource_id in PLATFORM_AGENT_TEMPLATES:
|
||||
template = PLATFORM_AGENT_TEMPLATES[alloc.resource_id]
|
||||
# 使用模板的默认资源配置
|
||||
quantity = alloc.quantity or 1
|
||||
# 解析 CPU (如 "500m" -> 0.5 核)
|
||||
cpu_limit = template.get("cpuLimit", "0")
|
||||
if isinstance(cpu_limit, str) and cpu_limit.endswith("m"):
|
||||
total_cpu += float(cpu_limit[:-1]) / 1000 * quantity
|
||||
elif cpu_limit:
|
||||
try:
|
||||
total_cpu += float(cpu_limit) * quantity
|
||||
except ValueError:
|
||||
pass
|
||||
# 解析内存 (如 "512Mi" -> 0.5 GB)
|
||||
memory_limit = template.get("memoryLimit", "0")
|
||||
if isinstance(memory_limit, str):
|
||||
if memory_limit.endswith("Mi"):
|
||||
total_memory += float(memory_limit[:-2]) / 1024 * quantity
|
||||
elif memory_limit.endswith("Gi"):
|
||||
total_memory += float(memory_limit[:-2]) * quantity
|
||||
continue
|
||||
|
||||
if agent:
|
||||
quantity = alloc.quantity or 1
|
||||
total_cpu += float(agent.cpu or 0) * quantity
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Migration: 007_change_resource_id_to_string
|
||||
-- Description: 将 resource_allocations 表的 resource_id 字段从 UUID 改为 VARCHAR(100)
|
||||
-- 原因: resource_id 需要存储平台 Agent 模板名称(如 'code-reviewer')而不仅仅是 UUID
|
||||
-- Date: 2026-01-05
|
||||
|
||||
-- 1. 删除旧索引(如果存在)
|
||||
DROP INDEX IF EXISTS idx_resource_allocation_resource;
|
||||
|
||||
-- 2. 修改 resource_id 字段类型
|
||||
-- 注意:PostgreSQL 需要使用 USING 子句来转换类型
|
||||
ALTER TABLE resource_allocations
|
||||
ALTER COLUMN resource_id TYPE VARCHAR(100)
|
||||
USING resource_id::VARCHAR(100);
|
||||
|
||||
-- 3. 重新创建索引
|
||||
CREATE INDEX idx_resource_allocation_resource ON resource_allocations(resource_id, resource_type);
|
||||
|
||||
-- 验证修改
|
||||
-- SELECT column_name, data_type, character_maximum_length
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_name = 'resource_allocations' AND column_name = 'resource_id';
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
执行数据库迁移:将 resource_allocations.resource_id 从 UUID 改为 VARCHAR(100)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
|
||||
async def run_migration():
|
||||
"""执行迁移"""
|
||||
# 从环境变量获取数据库 URL
|
||||
database_url = os.getenv("DATABASE_URL") or os.getenv("ASYNC_DATABASE_URL")
|
||||
|
||||
if not database_url:
|
||||
print("错误: 请设置 DATABASE_URL 或 ASYNC_DATABASE_URL 环境变量")
|
||||
sys.exit(1)
|
||||
|
||||
# 确保使用 asyncpg 驱动
|
||||
if database_url.startswith("postgresql://") and "+asyncpg" not in database_url:
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# 移除 URL 中的 sslmode 参数(asyncpg 使用不同的 SSL 配置方式)
|
||||
if "sslmode=" in database_url:
|
||||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
parsed = urlparse(database_url)
|
||||
query_params = parse_qs(parsed.query)
|
||||
query_params.pop('sslmode', None)
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
database_url = urlunparse(parsed._replace(query=new_query))
|
||||
|
||||
print(f"连接数据库...")
|
||||
|
||||
# 创建 SSL 上下文
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
echo=True,
|
||||
connect_args={"ssl": ssl_context}
|
||||
)
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
print("执行迁移...")
|
||||
|
||||
# 1. 删除旧索引
|
||||
print("步骤 1: 删除旧索引...")
|
||||
await conn.execute(text("DROP INDEX IF EXISTS idx_resource_allocation_resource"))
|
||||
|
||||
# 2. 修改字段类型
|
||||
print("步骤 2: 修改 resource_id 字段类型...")
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE resource_allocations
|
||||
ALTER COLUMN resource_id TYPE VARCHAR(100)
|
||||
USING resource_id::VARCHAR(100)
|
||||
"""))
|
||||
|
||||
# 3. 重新创建索引
|
||||
print("步骤 3: 重新创建索引...")
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_allocation_resource
|
||||
ON resource_allocations(resource_id, resource_type)
|
||||
"""))
|
||||
|
||||
print("迁移成功完成!")
|
||||
|
||||
# 验证修改
|
||||
result = await conn.execute(text("""
|
||||
SELECT column_name, data_type, character_maximum_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'resource_allocations' AND column_name = 'resource_id'
|
||||
"""))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
print(f"验证结果: column={row[0]}, type={row[1]}, max_length={row[2]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"迁移失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_migration())
|
||||
@@ -190,9 +190,10 @@ class Agent(BaseModel, Base):
|
||||
owner = relationship("User", back_populates="agents")
|
||||
|
||||
executions = relationship("Execution", back_populates="agent", cascade="all, delete-orphan")
|
||||
resource_allocations = relationship("ResourceAllocation",
|
||||
foreign_keys="[ResourceAllocation.resource_id]",
|
||||
primaryjoin="and_(Agent.id==ResourceAllocation.resource_id, ResourceAllocation.resource_type=='agent')")
|
||||
# 注意:resource_allocations 关系已移除,因为 resource_id 现在是 String 类型
|
||||
# 可以存储模板名称(如 'code-reviewer')或 UUID 字符串
|
||||
# 如需查询 Agent 的资源分配,请使用显式查询:
|
||||
# select(ResourceAllocation).where(ResourceAllocation.resource_id == str(agent.id))
|
||||
|
||||
# 索引和约束
|
||||
__table_args__ = (
|
||||
@@ -340,6 +341,7 @@ class Channel(BaseModel, Base):
|
||||
|
||||
# 关联关系
|
||||
tenants = relationship("User", back_populates="channel")
|
||||
# 注意:resource_allocations 关系使用 target_id(GUID 类型)进行关联,仍然有效
|
||||
resource_allocations = relationship("ResourceAllocation",
|
||||
foreign_keys="[ResourceAllocation.target_id]",
|
||||
primaryjoin="and_(Channel.id==ResourceAllocation.target_id, ResourceAllocation.target_type=='channel')")
|
||||
@@ -374,14 +376,20 @@ class ModelProvider(BaseModel, Base):
|
||||
|
||||
|
||||
class ResourceAllocation(BaseModel, Base):
|
||||
"""资源分配表(渠道和租户的资源分配)"""
|
||||
"""资源分配表(渠道和租户的资源分配)
|
||||
|
||||
注意:resource_id 字段使用 String 类型,因为它可能存储:
|
||||
- 平台 Agent 模板名称(如 'code-reviewer', 'gpt-assistant')
|
||||
- 模型供应商 UUID
|
||||
- 其他资源标识符
|
||||
"""
|
||||
|
||||
__tablename__ = "resource_allocations"
|
||||
|
||||
target_id = Column(GUID(), nullable=False)
|
||||
target_type = Column(String(20), nullable=False) # channel, tenant
|
||||
resource_type = Column(String(20), nullable=False) # agent, model
|
||||
resource_id = Column(GUID(), nullable=False)
|
||||
resource_id = Column(String(100), nullable=False) # 资源标识符(模板名称或UUID字符串)
|
||||
|
||||
# Agent资源配置
|
||||
quantity = Column(Integer) # Agent数量
|
||||
|
||||
@@ -1,647 +0,0 @@
|
||||
"""
|
||||
Agent 管理功能测试
|
||||
|
||||
测试内容:
|
||||
1. 平台 Agent 管理
|
||||
2. 自定义 Agent 管理
|
||||
3. 资源申请审批
|
||||
4. Agent 计费
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
# ============= 测试数据 =============
|
||||
|
||||
def create_test_user(
|
||||
user_id: str = None,
|
||||
name: str = "测试用户",
|
||||
email: str = "test@example.com",
|
||||
role: str = "user",
|
||||
channel_id: str = None,
|
||||
):
|
||||
"""创建测试用户对象"""
|
||||
return MagicMock(
|
||||
id=uuid.UUID(user_id) if user_id else uuid.uuid4(),
|
||||
name=name,
|
||||
email=email,
|
||||
role=role,
|
||||
channel_id=uuid.UUID(channel_id) if channel_id else uuid.uuid4(),
|
||||
status="active",
|
||||
balance=Decimal("100.00"),
|
||||
credit_limit=Decimal("50.00"),
|
||||
)
|
||||
|
||||
|
||||
def create_test_channel(
|
||||
channel_id: str = None,
|
||||
name: str = "测试渠道",
|
||||
):
|
||||
"""创建测试渠道对象"""
|
||||
return MagicMock(
|
||||
id=uuid.UUID(channel_id) if channel_id else uuid.uuid4(),
|
||||
name=name,
|
||||
status="active",
|
||||
)
|
||||
|
||||
|
||||
def create_test_quota(
|
||||
target_id: str,
|
||||
target_type: str = "tenant",
|
||||
template_name: str = "gpt-assistant",
|
||||
pod_quota: int = 5,
|
||||
pod_used: int = 0,
|
||||
):
|
||||
"""创建测试配额对象"""
|
||||
return MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
target_id=uuid.UUID(target_id),
|
||||
target_type=target_type,
|
||||
template_name=template_name,
|
||||
pod_quota=pod_quota,
|
||||
pod_used=pod_used,
|
||||
allocated_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
def create_test_billing_record(
|
||||
user_id: str,
|
||||
agent_type: str = "platform",
|
||||
template_name: str = "gpt-assistant",
|
||||
duration: int = 3600,
|
||||
cost: float = 0.5,
|
||||
):
|
||||
"""创建测试计费记录"""
|
||||
return MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=uuid.UUID(user_id),
|
||||
billing_type="agent",
|
||||
agent_type=agent_type,
|
||||
template_name=template_name,
|
||||
agent_name=f"test-{template_name}",
|
||||
instance_id=f"instance-{uuid.uuid4().hex[:8]}",
|
||||
duration=duration,
|
||||
eu=duration // 10,
|
||||
cost=Decimal(str(cost)),
|
||||
status="completed",
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
# ============= 计费逻辑测试 =============
|
||||
|
||||
class TestAgentBilling:
|
||||
"""Agent 计费逻辑测试"""
|
||||
|
||||
def test_calculate_platform_agent_cost(self):
|
||||
"""测试平台 Agent 计费计算"""
|
||||
from app.billing import calculate_platform_agent_cost, PLATFORM_AGENT_PRICING
|
||||
|
||||
# 测试 gpt-assistant 模板,运行 1 小时
|
||||
cost = calculate_platform_agent_cost("gpt-assistant", 3600)
|
||||
expected_price = PLATFORM_AGENT_PRICING.get("gpt-assistant", 0.5)
|
||||
assert cost == expected_price
|
||||
|
||||
# 测试 code-reviewer 模板,运行 2 小时
|
||||
cost = calculate_platform_agent_cost("code-reviewer", 7200)
|
||||
expected_price = PLATFORM_AGENT_PRICING.get("code-reviewer", 0.8) * 2
|
||||
assert cost == expected_price
|
||||
|
||||
# 测试未知模板,使用默认价格
|
||||
cost = calculate_platform_agent_cost("unknown-template", 3600)
|
||||
assert cost == 0.5 # 默认价格
|
||||
|
||||
def test_calculate_custom_agent_cost(self):
|
||||
"""测试自定义 Agent 计费计算"""
|
||||
from app.billing import calculate_agent_cost_by_resources, AGENT_RESOURCE_PRICING
|
||||
|
||||
# 测试 1 核 CPU + 1GB 内存,运行 1 小时
|
||||
cost = calculate_agent_cost_by_resources(
|
||||
cpu_cores=1.0,
|
||||
memory_gb=1.0,
|
||||
duration_seconds=3600
|
||||
)
|
||||
|
||||
cpu_price = AGENT_RESOURCE_PRICING["cpu_per_core_hour"]
|
||||
memory_price = AGENT_RESOURCE_PRICING["memory_per_gb_hour"]
|
||||
expected_cost = cpu_price + memory_price
|
||||
|
||||
assert abs(cost - expected_cost) < 0.001
|
||||
|
||||
# 测试 2 核 CPU + 4GB 内存,运行 30 分钟
|
||||
cost = calculate_agent_cost_by_resources(
|
||||
cpu_cores=2.0,
|
||||
memory_gb=4.0,
|
||||
duration_seconds=1800
|
||||
)
|
||||
|
||||
expected_cost = (cpu_price * 2 + memory_price * 4) * 0.5
|
||||
assert abs(cost - expected_cost) < 0.001
|
||||
|
||||
def test_parse_cpu_to_cores(self):
|
||||
"""测试 CPU 解析"""
|
||||
from app.billing import _parse_cpu_to_cores
|
||||
|
||||
# 测试毫核
|
||||
assert _parse_cpu_to_cores("100m") == 0.1
|
||||
assert _parse_cpu_to_cores("500m") == 0.5
|
||||
assert _parse_cpu_to_cores("1000m") == 1.0
|
||||
|
||||
# 测试核心数
|
||||
assert _parse_cpu_to_cores("1") == 1.0
|
||||
assert _parse_cpu_to_cores("2") == 2.0
|
||||
assert _parse_cpu_to_cores("0.5") == 0.5
|
||||
|
||||
# 测试数字输入
|
||||
assert _parse_cpu_to_cores(1) == 1.0
|
||||
assert _parse_cpu_to_cores(0.5) == 0.5
|
||||
|
||||
def test_parse_memory_to_gb(self):
|
||||
"""测试内存解析"""
|
||||
from app.billing import _parse_memory_to_gb
|
||||
|
||||
# 测试 Mi
|
||||
assert _parse_memory_to_gb("128Mi") == 0.125
|
||||
assert _parse_memory_to_gb("512Mi") == 0.5
|
||||
assert _parse_memory_to_gb("1024Mi") == 1.0
|
||||
|
||||
# 测试 Gi
|
||||
assert _parse_memory_to_gb("1Gi") == 1.0
|
||||
assert _parse_memory_to_gb("2Gi") == 2.0
|
||||
assert _parse_memory_to_gb("0.5Gi") == 0.5
|
||||
|
||||
# 测试数字输入(假设为 GB)
|
||||
assert _parse_memory_to_gb(1) == 1.0
|
||||
assert _parse_memory_to_gb(0.5) == 0.5
|
||||
|
||||
|
||||
# ============= 配额管理测试 =============
|
||||
|
||||
class TestQuotaManagement:
|
||||
"""配额管理测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_platform_agent_quota(self):
|
||||
"""测试平台 Agent 配额检查"""
|
||||
# 模拟配额:5 个 Pod,已使用 3 个
|
||||
quota = create_test_quota(
|
||||
target_id=str(uuid.uuid4()),
|
||||
template_name="gpt-assistant",
|
||||
pod_quota=5,
|
||||
pod_used=3,
|
||||
)
|
||||
|
||||
# 剩余 2 个,请求 1 个应该成功
|
||||
remaining = quota.pod_quota - quota.pod_used
|
||||
assert remaining >= 1
|
||||
|
||||
# 请求 3 个应该失败
|
||||
assert remaining < 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_custom_agent_quota(self):
|
||||
"""测试自定义 Agent 配额检查"""
|
||||
# 模拟配额:4 核 CPU,8GB 内存
|
||||
quota = MagicMock(
|
||||
cpu_quota=Decimal("4.0"),
|
||||
memory_quota=Decimal("8.0"),
|
||||
cpu_used=Decimal("1.0"),
|
||||
memory_used=Decimal("2.0"),
|
||||
)
|
||||
|
||||
# 剩余 3 核 CPU,6GB 内存
|
||||
cpu_remaining = float(quota.cpu_quota - quota.cpu_used)
|
||||
memory_remaining = float(quota.memory_quota - quota.memory_used)
|
||||
|
||||
assert cpu_remaining == 3.0
|
||||
assert memory_remaining == 6.0
|
||||
|
||||
# 请求 2 核 CPU,4GB 内存应该成功
|
||||
assert cpu_remaining >= 2.0
|
||||
assert memory_remaining >= 4.0
|
||||
|
||||
# 请求 4 核 CPU 应该失败
|
||||
assert cpu_remaining < 4.0
|
||||
|
||||
|
||||
# ============= 资源申请测试 =============
|
||||
|
||||
class TestResourceApplication:
|
||||
"""资源申请测试"""
|
||||
|
||||
def test_create_platform_agent_application(self):
|
||||
"""测试创建平台 Agent 申请"""
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
channel_id=uuid.uuid4(),
|
||||
resource_type="platform_agent",
|
||||
template_name="gpt-assistant",
|
||||
requested_pod_quota=10,
|
||||
reason="业务需要",
|
||||
status="pending",
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
assert application.resource_type == "platform_agent"
|
||||
assert application.status == "pending"
|
||||
assert application.requested_pod_quota == 10
|
||||
|
||||
def test_create_custom_agent_quota_application(self):
|
||||
"""测试创建自定义 Agent 配额申请"""
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
channel_id=uuid.uuid4(),
|
||||
resource_type="custom_agent_quota",
|
||||
requested_cpu_quota=Decimal("8.0"),
|
||||
requested_memory_quota=Decimal("16.0"),
|
||||
reason="扩展业务",
|
||||
status="pending",
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
assert application.resource_type == "custom_agent_quota"
|
||||
assert application.status == "pending"
|
||||
assert float(application.requested_cpu_quota) == 8.0
|
||||
assert float(application.requested_memory_quota) == 16.0
|
||||
|
||||
def test_approve_application(self):
|
||||
"""测试审批申请"""
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
status="pending",
|
||||
requested_pod_quota=10,
|
||||
approved_pod_quota=None,
|
||||
reviewed_by=None,
|
||||
review_reason=None,
|
||||
reviewed_at=None,
|
||||
)
|
||||
|
||||
# 模拟审批
|
||||
application.status = "approved"
|
||||
application.approved_pod_quota = 8 # 批准 8 个,少于请求的 10 个
|
||||
application.reviewed_by = str(uuid.uuid4())
|
||||
application.review_reason = "批准 8 个 Pod"
|
||||
application.reviewed_at = datetime.utcnow()
|
||||
|
||||
assert application.status == "approved"
|
||||
assert application.approved_pod_quota == 8
|
||||
assert application.reviewed_at is not None
|
||||
|
||||
def test_reject_application(self):
|
||||
"""测试拒绝申请"""
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
status="pending",
|
||||
)
|
||||
|
||||
# 模拟拒绝
|
||||
application.status = "rejected"
|
||||
application.review_reason = "资源不足"
|
||||
application.reviewed_at = datetime.utcnow()
|
||||
|
||||
assert application.status == "rejected"
|
||||
assert application.review_reason == "资源不足"
|
||||
|
||||
|
||||
# ============= Agent Manager 客户端测试 =============
|
||||
|
||||
class TestAgentManagerClient:
|
||||
"""Agent Manager 客户端测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_platform_templates(self):
|
||||
"""测试获取平台 Agent 模板列表"""
|
||||
from app.agent_manager_client import AgentManagerClient
|
||||
|
||||
client = AgentManagerClient()
|
||||
|
||||
with patch.object(client, '_request', new_callable=AsyncMock) as mock_request:
|
||||
mock_request.return_value = {
|
||||
"templates": [
|
||||
{
|
||||
"name": "gpt-assistant",
|
||||
"displayName": "GPT 智能助手",
|
||||
"category": "assistant",
|
||||
},
|
||||
{
|
||||
"name": "code-reviewer",
|
||||
"displayName": "代码审查助手",
|
||||
"category": "development",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
result = await client.list_platform_templates()
|
||||
|
||||
assert "templates" in result
|
||||
assert len(result["templates"]) == 2
|
||||
assert result["templates"][0]["name"] == "gpt-assistant"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_platform_agent(self):
|
||||
"""测试创建平台 Agent"""
|
||||
from app.agent_manager_client import AgentManagerClient
|
||||
|
||||
client = AgentManagerClient()
|
||||
|
||||
with patch.object(client, '_request', new_callable=AsyncMock) as mock_request:
|
||||
mock_request.return_value = {
|
||||
"instanceName": "gpt-assistant-abc123",
|
||||
"status": "running",
|
||||
"endpoint": "http://gpt-assistant-abc123.agents.svc.cluster.local",
|
||||
}
|
||||
|
||||
result = await client.create_platform_agent(
|
||||
user_id="user-123",
|
||||
template_name="gpt-assistant",
|
||||
instance_name="gpt-assistant-abc123",
|
||||
)
|
||||
|
||||
assert result["instanceName"] == "gpt-assistant-abc123"
|
||||
assert result["status"] == "running"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_custom_agent(self):
|
||||
"""测试创建自定义 Agent"""
|
||||
from app.agent_manager_client import AgentManagerClient
|
||||
|
||||
client = AgentManagerClient()
|
||||
|
||||
with patch.object(client, '_request', new_callable=AsyncMock) as mock_request:
|
||||
mock_request.return_value = {
|
||||
"name": "my-custom-agent",
|
||||
"status": "running",
|
||||
"endpoint": "http://my-custom-agent.user-ns.svc.cluster.local",
|
||||
}
|
||||
|
||||
result = await client.create_custom_agent(
|
||||
user_id="user-123",
|
||||
template_name="openai-compatible",
|
||||
agent_name="my-custom-agent",
|
||||
cpu_request="500m",
|
||||
memory_request="512Mi",
|
||||
env_vars={
|
||||
"OPENAI_API_KEY": "sk-xxx",
|
||||
"OPENAI_API_BASE": "https://api.openai.com/v1",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["name"] == "my-custom-agent"
|
||||
assert result["status"] == "running"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scale_custom_agent(self):
|
||||
"""测试扩缩容自定义 Agent"""
|
||||
from app.agent_manager_client import AgentManagerClient
|
||||
|
||||
client = AgentManagerClient()
|
||||
|
||||
with patch.object(client, '_request', new_callable=AsyncMock) as mock_request:
|
||||
mock_request.return_value = {
|
||||
"name": "my-custom-agent",
|
||||
"replicas": 3,
|
||||
"status": "scaling",
|
||||
}
|
||||
|
||||
result = await client.scale_custom_agent(
|
||||
user_id="user-123",
|
||||
agent_name="my-custom-agent",
|
||||
replicas=3,
|
||||
)
|
||||
|
||||
assert result["replicas"] == 3
|
||||
assert result["status"] == "scaling"
|
||||
|
||||
|
||||
# ============= API 路由测试 =============
|
||||
|
||||
class TestUserAgentAPIs:
|
||||
"""用户 Agent API 测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_available_platform_agents(self):
|
||||
"""测试获取可用平台 Agent 列表"""
|
||||
# 模拟用户有配额
|
||||
quotas = [
|
||||
create_test_quota(
|
||||
target_id=str(uuid.uuid4()),
|
||||
template_name="gpt-assistant",
|
||||
pod_quota=5,
|
||||
pod_used=2,
|
||||
),
|
||||
create_test_quota(
|
||||
target_id=str(uuid.uuid4()),
|
||||
template_name="code-reviewer",
|
||||
pod_quota=3,
|
||||
pod_used=0,
|
||||
),
|
||||
]
|
||||
|
||||
# 验证返回数据格式
|
||||
result = []
|
||||
for quota in quotas:
|
||||
result.append({
|
||||
"templateName": quota.template_name,
|
||||
"podQuota": quota.pod_quota,
|
||||
"podUsed": quota.pod_used,
|
||||
"podRemaining": quota.pod_quota - quota.pod_used,
|
||||
})
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["podRemaining"] == 3
|
||||
assert result[1]["podRemaining"] == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_use_platform_agent(self):
|
||||
"""测试使用平台 Agent"""
|
||||
# 模拟请求
|
||||
request = {
|
||||
"templateName": "gpt-assistant",
|
||||
"agentType": "chat",
|
||||
}
|
||||
|
||||
# 验证请求格式
|
||||
assert "templateName" in request
|
||||
assert request["templateName"] == "gpt-assistant"
|
||||
|
||||
|
||||
class TestChannelAgentAPIs:
|
||||
"""渠道 Agent API 测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allocate_platform_agent_to_tenant(self):
|
||||
"""测试分配平台 Agent 给租户"""
|
||||
# 模拟渠道配额
|
||||
channel_quota = create_test_quota(
|
||||
target_id=str(uuid.uuid4()),
|
||||
target_type="channel",
|
||||
template_name="gpt-assistant",
|
||||
pod_quota=20,
|
||||
pod_used=5,
|
||||
)
|
||||
|
||||
# 模拟分配请求
|
||||
request = {
|
||||
"templateName": "gpt-assistant",
|
||||
"podQuota": 5,
|
||||
}
|
||||
|
||||
# 验证配额足够
|
||||
remaining = channel_quota.pod_quota - channel_quota.pod_used
|
||||
assert remaining >= request["podQuota"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_billing_stats(self):
|
||||
"""测试获取 Agent 计费统计"""
|
||||
# 模拟计费记录
|
||||
records = [
|
||||
create_test_billing_record(
|
||||
user_id=str(uuid.uuid4()),
|
||||
agent_type="platform",
|
||||
template_name="gpt-assistant",
|
||||
duration=3600,
|
||||
cost=0.5,
|
||||
),
|
||||
create_test_billing_record(
|
||||
user_id=str(uuid.uuid4()),
|
||||
agent_type="custom",
|
||||
template_name="openai-compatible",
|
||||
duration=7200,
|
||||
cost=1.2,
|
||||
),
|
||||
]
|
||||
|
||||
# 计算统计
|
||||
total_cost = sum(float(r.cost) for r in records)
|
||||
total_duration = sum(r.duration for r in records)
|
||||
|
||||
assert total_cost == 1.7
|
||||
assert total_duration == 10800
|
||||
|
||||
|
||||
# ============= 集成测试 =============
|
||||
|
||||
class TestIntegration:
|
||||
"""集成测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_platform_agent_workflow(self):
|
||||
"""测试完整的平台 Agent 工作流程"""
|
||||
# 1. 渠道申请平台 Agent
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
channel_id=uuid.uuid4(),
|
||||
resource_type="platform_agent",
|
||||
template_name="gpt-assistant",
|
||||
requested_pod_quota=10,
|
||||
status="pending",
|
||||
)
|
||||
|
||||
# 2. 管理员审批
|
||||
application.status = "approved"
|
||||
application.approved_pod_quota = 10
|
||||
|
||||
# 3. 渠道获得配额
|
||||
channel_quota = create_test_quota(
|
||||
target_id=str(application.channel_id),
|
||||
target_type="channel",
|
||||
template_name="gpt-assistant",
|
||||
pod_quota=10,
|
||||
pod_used=0,
|
||||
)
|
||||
|
||||
# 4. 渠道分配给租户
|
||||
tenant_id = str(uuid.uuid4())
|
||||
tenant_quota = create_test_quota(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
template_name="gpt-assistant",
|
||||
pod_quota=5,
|
||||
pod_used=0,
|
||||
)
|
||||
|
||||
# 5. 租户使用平台 Agent
|
||||
tenant_quota.pod_used = 1
|
||||
|
||||
# 6. 创建计费记录
|
||||
billing_record = create_test_billing_record(
|
||||
user_id=tenant_id,
|
||||
agent_type="platform",
|
||||
template_name="gpt-assistant",
|
||||
)
|
||||
|
||||
# 验证流程
|
||||
assert application.status == "approved"
|
||||
assert channel_quota.pod_quota == 10
|
||||
assert tenant_quota.pod_used == 1
|
||||
assert billing_record.agent_type == "platform"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_custom_agent_workflow(self):
|
||||
"""测试完整的自定义 Agent 工作流程"""
|
||||
# 1. 渠道申请自定义 Agent 配额
|
||||
application = MagicMock(
|
||||
id=uuid.uuid4(),
|
||||
channel_id=uuid.uuid4(),
|
||||
resource_type="custom_agent_quota",
|
||||
requested_cpu_quota=Decimal("8.0"),
|
||||
requested_memory_quota=Decimal("16.0"),
|
||||
status="pending",
|
||||
)
|
||||
|
||||
# 2. 管理员审批
|
||||
application.status = "approved"
|
||||
application.approved_cpu_quota = Decimal("8.0")
|
||||
application.approved_memory_quota = Decimal("16.0")
|
||||
|
||||
# 3. 渠道获得配额
|
||||
channel_quota = MagicMock(
|
||||
channel_id=application.channel_id,
|
||||
cpu_quota=Decimal("8.0"),
|
||||
memory_quota=Decimal("16.0"),
|
||||
cpu_allocated=Decimal("0"),
|
||||
memory_allocated=Decimal("0"),
|
||||
)
|
||||
|
||||
# 4. 渠道分配给租户
|
||||
tenant_id = str(uuid.uuid4())
|
||||
tenant_quota = MagicMock(
|
||||
tenant_id=uuid.UUID(tenant_id),
|
||||
cpu_quota=Decimal("4.0"),
|
||||
memory_quota=Decimal("8.0"),
|
||||
cpu_used=Decimal("0"),
|
||||
memory_used=Decimal("0"),
|
||||
)
|
||||
|
||||
# 5. 租户创建自定义 Agent
|
||||
tenant_quota.cpu_used = Decimal("1.0")
|
||||
tenant_quota.memory_used = Decimal("2.0")
|
||||
|
||||
# 6. 创建计费记录
|
||||
billing_record = create_test_billing_record(
|
||||
user_id=tenant_id,
|
||||
agent_type="custom",
|
||||
template_name="openai-compatible",
|
||||
)
|
||||
|
||||
# 验证流程
|
||||
assert application.status == "approved"
|
||||
assert float(channel_quota.cpu_quota) == 8.0
|
||||
assert float(tenant_quota.cpu_used) == 1.0
|
||||
assert billing_record.agent_type == "custom"
|
||||
|
||||
|
||||
# ============= 运行测试 =============
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--tb=short"])
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试分配平台 Agent 给租户的接口"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
BASE_URL = "http://localhost:8002"
|
||||
|
||||
def test_allocate_platform_agent():
|
||||
# 1. 渠道管理员登录
|
||||
print("=" * 50)
|
||||
print("1. 渠道管理员登录")
|
||||
login_resp = requests.post(
|
||||
f"{BASE_URL}/api/channel/auth/login",
|
||||
json={"email": "66@66.com", "password": "66"}
|
||||
)
|
||||
print(f"状态码: {login_resp.status_code}")
|
||||
print(f"响应: {json.dumps(login_resp.json(), indent=2, ensure_ascii=False)}")
|
||||
|
||||
if login_resp.status_code != 200:
|
||||
print("登录失败!")
|
||||
return
|
||||
|
||||
token = login_resp.json().get("token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 解析 JWT token 查看内容
|
||||
import base64
|
||||
parts = token.split(".")
|
||||
if len(parts) >= 2:
|
||||
payload = parts[1]
|
||||
# 添加填充
|
||||
payload += "=" * (4 - len(payload) % 4)
|
||||
decoded = base64.urlsafe_b64decode(payload)
|
||||
print(f"\nJWT Payload: {decoded.decode('utf-8')}")
|
||||
|
||||
# 2. 获取渠道下的租户列表
|
||||
print("\n" + "=" * 50)
|
||||
print("2. 获取渠道下的租户列表")
|
||||
tenants_resp = requests.get(
|
||||
f"{BASE_URL}/api/channel/tenants",
|
||||
headers=headers
|
||||
)
|
||||
print(f"状态码: {tenants_resp.status_code}")
|
||||
print(f"响应: {json.dumps(tenants_resp.json(), indent=2, ensure_ascii=False)}")
|
||||
|
||||
# 3. 查看可用的平台 Agent 模板
|
||||
print("\n" + "=" * 50)
|
||||
print("3. 查看可用的平台 Agent 模板")
|
||||
templates_resp = requests.get(
|
||||
f"{BASE_URL}/api/channel/available-platform-agents",
|
||||
headers=headers
|
||||
)
|
||||
print(f"状态码: {templates_resp.status_code}")
|
||||
print(f"响应: {json.dumps(templates_resp.json(), indent=2, ensure_ascii=False)}")
|
||||
|
||||
# 4. 查看渠道的平台 Agent 配额
|
||||
print("\n" + "=" * 50)
|
||||
print("4. 查看渠道的平台 Agent 配额")
|
||||
quotas_resp = requests.get(
|
||||
f"{BASE_URL}/api/channel/platform-agents",
|
||||
headers=headers
|
||||
)
|
||||
print(f"状态码: {quotas_resp.status_code}")
|
||||
print(f"响应: {json.dumps(quotas_resp.json(), indent=2, ensure_ascii=False)}")
|
||||
|
||||
# 5. 如果有租户,尝试分配平台 Agent
|
||||
if tenants_resp.status_code == 200:
|
||||
tenants_data = tenants_resp.json()
|
||||
tenants = tenants_data.get("data", {}).get("tenants", [])
|
||||
|
||||
if tenants:
|
||||
tenant_id = tenants[0]["id"]
|
||||
print("\n" + "=" * 50)
|
||||
print(f"5. 分配平台 Agent 给租户 {tenant_id}")
|
||||
|
||||
allocate_resp = requests.post(
|
||||
f"{BASE_URL}/api/channel/tenants/{tenant_id}/platform-agents",
|
||||
headers=headers,
|
||||
json={
|
||||
"templateName": "gpt-assistant",
|
||||
"podQuota": 2
|
||||
}
|
||||
)
|
||||
print(f"状态码: {allocate_resp.status_code}")
|
||||
print(f"响应: {json.dumps(allocate_resp.json(), indent=2, ensure_ascii=False)}")
|
||||
else:
|
||||
print("\n没有租户,跳过分配测试")
|
||||
print("请先创建一个租户")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_allocate_platform_agent()
|
||||
Executable
+447
@@ -0,0 +1,447 @@
|
||||
#!/bin/bash
|
||||
# ============================================================================
|
||||
# 渠道申请平台 Agent 和分配给租户的完整测试脚本
|
||||
# ============================================================================
|
||||
#
|
||||
# 测试流程:
|
||||
# 1. 创建超级管理员和渠道66
|
||||
# 2. 超级管理员登录
|
||||
# 3. 渠道66登录
|
||||
# 4. 渠道申请平台 Agent
|
||||
# 5. 超级管理员审批申请
|
||||
# 6. 渠道创建租户
|
||||
# 7. 渠道分配平台 Agent 给租户
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
BASE_URL="http://localhost:8002"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印函数
|
||||
print_step() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}步骤 $1: $2${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${YELLOW}→ $1${NC}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 1: 创建超级管理员和渠道66(通过数据库脚本)
|
||||
# ============================================================================
|
||||
print_step "1" "创建超级管理员和渠道66"
|
||||
|
||||
# 运行 Python 脚本创建用户
|
||||
python3 << 'EOF'
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import bcrypt
|
||||
|
||||
# 添加路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'services', 'mcp-server'))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy import select
|
||||
from models import User, Channel
|
||||
from config import settings
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""加密密码"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||
return hashed.decode('utf-8')
|
||||
|
||||
async def create_users():
|
||||
"""创建超级管理员和渠道66"""
|
||||
database_url = settings.database_url
|
||||
if database_url.startswith("postgresql://") and "+asyncpg" not in database_url:
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 1. 创建超级管理员
|
||||
result = await session.execute(
|
||||
select(User).where(User.email == "superadmin@taiji-ai.com")
|
||||
)
|
||||
existing_user = result.scalar_one_or_none()
|
||||
|
||||
if existing_user:
|
||||
print(" ⚠ 超级管理员已存在,更新密码和角色")
|
||||
password_hash = get_password_hash("Admin@123456")
|
||||
existing_user.password_hash = password_hash
|
||||
existing_user.hashed_password = password_hash
|
||||
existing_user.role = "super_admin"
|
||||
existing_user.is_admin = True
|
||||
existing_user.is_active = True
|
||||
else:
|
||||
password_hash = get_password_hash("Admin@123456")
|
||||
user = User(
|
||||
name="超级管理员",
|
||||
email="superadmin@taiji-ai.com",
|
||||
password_hash=password_hash,
|
||||
hashed_password=password_hash,
|
||||
username="superadmin",
|
||||
full_name="超级管理员",
|
||||
role="super_admin",
|
||||
is_active=True,
|
||||
is_admin=True,
|
||||
status="active",
|
||||
balance=0,
|
||||
credit_limit=0,
|
||||
)
|
||||
session.add(user)
|
||||
print(" ✓ 创建超级管理员: superadmin@taiji-ai.com")
|
||||
|
||||
# 2. 创建渠道66
|
||||
result = await session.execute(
|
||||
select(Channel).where(Channel.email == "66@66.com")
|
||||
)
|
||||
existing_channel = result.scalar_one_or_none()
|
||||
|
||||
if existing_channel:
|
||||
print(" ⚠ 渠道66已存在")
|
||||
channel_id = str(existing_channel.id)
|
||||
else:
|
||||
password_hash = get_password_hash("66")
|
||||
channel = Channel(
|
||||
name="渠道66",
|
||||
email="66@66.com",
|
||||
password_hash=password_hash,
|
||||
commission_rate=10.0,
|
||||
channel_credit=100000,
|
||||
custom_agent_cpu=8,
|
||||
custom_agent_memory=16,
|
||||
status="active",
|
||||
)
|
||||
session.add(channel)
|
||||
await session.flush()
|
||||
channel_id = str(channel.id)
|
||||
print(f" ✓ 创建渠道66: 66@66.com (ID: {channel_id})")
|
||||
|
||||
# 3. 创建渠道管理员(关联到渠道66)
|
||||
result = await session.execute(
|
||||
select(Channel).where(Channel.email == "66@66.com")
|
||||
)
|
||||
channel = result.scalar_one_or_none()
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.email == "channel66@taiji-ai.com")
|
||||
)
|
||||
existing_channel_admin = result.scalar_one_or_none()
|
||||
|
||||
if existing_channel_admin:
|
||||
print(" ⚠ 渠道66管理员已存在,更新关联")
|
||||
existing_channel_admin.channel_id = channel.id
|
||||
existing_channel_admin.role = "channel_admin"
|
||||
else:
|
||||
password_hash = get_password_hash("Channel66@123")
|
||||
channel_admin = User(
|
||||
name="渠道66管理员",
|
||||
email="channel66@taiji-ai.com",
|
||||
password_hash=password_hash,
|
||||
hashed_password=password_hash,
|
||||
username="channel66",
|
||||
full_name="渠道66管理员",
|
||||
role="channel_admin",
|
||||
channel_id=channel.id,
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
status="active",
|
||||
balance=0,
|
||||
credit_limit=0,
|
||||
)
|
||||
session.add(channel_admin)
|
||||
print(f" ✓ 创建渠道66管理员: channel66@taiji-ai.com")
|
||||
|
||||
await session.commit()
|
||||
print(f"\n 渠道66 ID: {channel_id}")
|
||||
|
||||
# 输出渠道ID供后续使用
|
||||
with open('/tmp/channel66_id.txt', 'w') as f:
|
||||
f.write(channel_id)
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
asyncio.run(create_users())
|
||||
EOF
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "用户创建完成"
|
||||
else
|
||||
print_error "用户创建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 读取渠道ID
|
||||
CHANNEL_ID=$(cat /tmp/channel66_id.txt 2>/dev/null || echo "")
|
||||
print_info "渠道66 ID: $CHANNEL_ID"
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 2: 超级管理员登录
|
||||
# ============================================================================
|
||||
print_step "2" "超级管理员登录"
|
||||
|
||||
SUPER_ADMIN_RESPONSE=$(curl -s -X POST "$BASE_URL/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "superadmin@taiji-ai.com",
|
||||
"password": "Admin@123456",
|
||||
"role": "super_admin"
|
||||
}')
|
||||
|
||||
echo "响应: $SUPER_ADMIN_RESPONSE"
|
||||
|
||||
SUPER_ADMIN_TOKEN=$(echo $SUPER_ADMIN_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('token', ''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$SUPER_ADMIN_TOKEN" ]; then
|
||||
print_success "超级管理员登录成功"
|
||||
print_info "Token: ${SUPER_ADMIN_TOKEN:0:50}..."
|
||||
else
|
||||
print_error "超级管理员登录失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 3: 渠道66管理员登录
|
||||
# ============================================================================
|
||||
print_step "3" "渠道66管理员登录"
|
||||
|
||||
CHANNEL_ADMIN_RESPONSE=$(curl -s -X POST "$BASE_URL/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "channel66@taiji-ai.com",
|
||||
"password": "Channel66@123",
|
||||
"role": "channel_admin"
|
||||
}')
|
||||
|
||||
echo "响应: $CHANNEL_ADMIN_RESPONSE"
|
||||
|
||||
CHANNEL_ADMIN_TOKEN=$(echo $CHANNEL_ADMIN_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('token', ''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$CHANNEL_ADMIN_TOKEN" ]; then
|
||||
print_success "渠道66管理员登录成功"
|
||||
print_info "Token: ${CHANNEL_ADMIN_TOKEN:0:50}..."
|
||||
else
|
||||
print_error "渠道66管理员登录失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 4: 查看可用的平台 Agent 模板
|
||||
# ============================================================================
|
||||
print_step "4" "查看可用的平台 Agent 模板"
|
||||
|
||||
curl -s -X GET "$BASE_URL/api/channel/available-platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
print_success "查看平台 Agent 模板完成"
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 5: 渠道申请平台 Agent (gpt-assistant)
|
||||
# ============================================================================
|
||||
print_step "5" "渠道申请平台 Agent (gpt-assistant)"
|
||||
|
||||
APPLICATION_RESPONSE=$(curl -s -X POST "$BASE_URL/api/channel/applications/platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"templateName": "gpt-assistant",
|
||||
"requestedPodQuota": 5,
|
||||
"reason": "业务需要使用 GPT 智能助手进行客户服务"
|
||||
}')
|
||||
|
||||
echo "响应: $APPLICATION_RESPONSE" | python3 -m json.tool
|
||||
|
||||
APPLICATION_ID=$(echo $APPLICATION_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('id', ''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$APPLICATION_ID" ]; then
|
||||
print_success "申请提交成功,申请ID: $APPLICATION_ID"
|
||||
else
|
||||
print_info "申请可能已存在或提交失败"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 6: 查看渠道的申请列表
|
||||
# ============================================================================
|
||||
print_step "6" "查看渠道的申请列表"
|
||||
|
||||
curl -s -X GET "$BASE_URL/api/channel/applications/platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
# 获取待审批的申请ID
|
||||
PENDING_APP_RESPONSE=$(curl -s -X GET "$BASE_URL/api/channel/applications/platform-agents?status=pending" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN")
|
||||
|
||||
APPLICATION_ID=$(echo $PENDING_APP_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); apps=data.get('data', {}).get('applications', []); print(apps[0]['id'] if apps else '')" 2>/dev/null || echo "")
|
||||
|
||||
print_info "待审批申请ID: $APPLICATION_ID"
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 7: 超级管理员查看所有申请
|
||||
# ============================================================================
|
||||
print_step "7" "超级管理员查看所有申请"
|
||||
|
||||
curl -s -X GET "$BASE_URL/api/admin/applications/platform-agents" \
|
||||
-H "Authorization: Bearer $SUPER_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 8: 超级管理员审批申请
|
||||
# ============================================================================
|
||||
print_step "8" "超级管理员审批申请"
|
||||
|
||||
if [ -n "$APPLICATION_ID" ]; then
|
||||
REVIEW_RESPONSE=$(curl -s -X PUT "$BASE_URL/api/admin/applications/platform-agents/$APPLICATION_ID/review" \
|
||||
-H "Authorization: Bearer $SUPER_ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approved": true,
|
||||
"approvedPodQuota": 5,
|
||||
"reason": "批准使用,配额5个Pod"
|
||||
}')
|
||||
|
||||
echo "响应: $REVIEW_RESPONSE" | python3 -m json.tool
|
||||
print_success "申请审批完成"
|
||||
else
|
||||
print_info "没有待审批的申请,尝试直接分配配额"
|
||||
|
||||
# 直接分配配额给渠道
|
||||
ALLOCATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/platform-agents/allocate?channel_id=$CHANNEL_ID&template_name=gpt-assistant&pod_quota=5" \
|
||||
-H "Authorization: Bearer $SUPER_ADMIN_TOKEN")
|
||||
|
||||
echo "响应: $ALLOCATE_RESPONSE" | python3 -m json.tool
|
||||
print_success "直接分配配额完成"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 9: 查看渠道的平台 Agent 配额
|
||||
# ============================================================================
|
||||
print_step "9" "查看渠道的平台 Agent 配额"
|
||||
|
||||
curl -s -X GET "$BASE_URL/api/channel/platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
print_success "查看配额完成"
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 10: 渠道创建租户
|
||||
# ============================================================================
|
||||
print_step "10" "渠道创建租户"
|
||||
|
||||
TENANT_RESPONSE=$(curl -s -X POST "$BASE_URL/api/channel/tenants/create" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "测试租户A",
|
||||
"email": "tenant-a@test.com",
|
||||
"password": "TenantA@123",
|
||||
"subscriptionTier": "premium"
|
||||
}')
|
||||
|
||||
echo "响应: $TENANT_RESPONSE" | python3 -m json.tool
|
||||
|
||||
TENANT_ID=$(echo $TENANT_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('id', ''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$TENANT_ID" ]; then
|
||||
print_success "租户创建成功,租户ID: $TENANT_ID"
|
||||
else
|
||||
# 如果租户已存在,尝试获取租户ID
|
||||
print_info "租户可能已存在,尝试获取租户列表"
|
||||
TENANTS_RESPONSE=$(curl -s -X GET "$BASE_URL/api/channel/tenants" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN")
|
||||
|
||||
TENANT_ID=$(echo $TENANTS_RESPONSE | python3 -c "import sys, json; data=json.load(sys.stdin); tenants=data.get('data', {}).get('tenants', []); print(tenants[0]['id'] if tenants else '')" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$TENANT_ID" ]; then
|
||||
print_info "使用现有租户ID: $TENANT_ID"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 11: 渠道分配平台 Agent 给租户
|
||||
# ============================================================================
|
||||
print_step "11" "渠道分配平台 Agent 给租户"
|
||||
|
||||
if [ -n "$TENANT_ID" ]; then
|
||||
ALLOCATE_TENANT_RESPONSE=$(curl -s -X POST "$BASE_URL/api/channel/tenants/$TENANT_ID/platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"templateName": "gpt-assistant",
|
||||
"podQuota": 3
|
||||
}')
|
||||
|
||||
echo "响应: $ALLOCATE_TENANT_RESPONSE" | python3 -m json.tool
|
||||
print_success "平台 Agent 分配给租户完成"
|
||||
else
|
||||
print_error "没有可用的租户ID"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 12: 查看租户的平台 Agent 使用情况
|
||||
# ============================================================================
|
||||
print_step "12" "查看租户的平台 Agent 使用情况"
|
||||
|
||||
if [ -n "$TENANT_ID" ]; then
|
||||
curl -s -X GET "$BASE_URL/api/channel/tenants/$TENANT_ID/platform-agents/usage" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
print_success "查看租户使用情况完成"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 步骤 13: 再次查看渠道的平台 Agent 配额(验证分配后的变化)
|
||||
# ============================================================================
|
||||
print_step "13" "再次查看渠道的平台 Agent 配额"
|
||||
|
||||
curl -s -X GET "$BASE_URL/api/channel/platform-agents" \
|
||||
-H "Authorization: Bearer $CHANNEL_ADMIN_TOKEN" | python3 -m json.tool
|
||||
|
||||
# ============================================================================
|
||||
# 测试完成
|
||||
# ============================================================================
|
||||
echo -e "\n${GREEN}============================================${NC}"
|
||||
echo -e "${GREEN}测试完成!${NC}"
|
||||
echo -e "${GREEN}============================================${NC}"
|
||||
echo -e "\n${YELLOW}账户信息汇总:${NC}"
|
||||
echo -e " 超级管理员: superadmin@taiji-ai.com / Admin@123456"
|
||||
echo -e " 渠道66管理员: channel66@taiji-ai.com / Channel66@123"
|
||||
echo -e " 渠道66 ID: $CHANNEL_ID"
|
||||
if [ -n "$TENANT_ID" ]; then
|
||||
echo -e " 测试租户ID: $TENANT_ID"
|
||||
fi
|
||||
echo -e "\n${YELLOW}测试流程:${NC}"
|
||||
echo -e " 1. ✓ 创建超级管理员和渠道66"
|
||||
echo -e " 2. ✓ 超级管理员登录"
|
||||
echo -e " 3. ✓ 渠道66管理员登录"
|
||||
echo -e " 4. ✓ 查看可用的平台 Agent 模板"
|
||||
echo -e " 5. ✓ 渠道申请平台 Agent"
|
||||
echo -e " 6. ✓ 查看渠道的申请列表"
|
||||
echo -e " 7. ✓ 超级管理员查看所有申请"
|
||||
echo -e " 8. ✓ 超级管理员审批申请"
|
||||
echo -e " 9. ✓ 查看渠道的平台 Agent 配额"
|
||||
echo -e " 10. ✓ 渠道创建租户"
|
||||
echo -e " 11. ✓ 渠道分配平台 Agent 给租户"
|
||||
echo -e " 12. ✓ 查看租户的平台 Agent 使用情况"
|
||||
echo -e " 13. ✓ 再次查看渠道的平台 Agent 配额"
|
||||
Reference in New Issue
Block a user