更新agent域名

This commit is contained in:
zhanggangyong
2026-01-14 13:42:36 +00:00
parent 72ed8c1d29
commit 31ba6fb8f1
11 changed files with 1569 additions and 2403 deletions
+673
View File
@@ -0,0 +1,673 @@
# AI Agent Manager API 文档
## 📋 目录
- [概述](#概述)
- [快速开始](#快速开始)
- [核心概念](#核心概念)
- [API 端点](#api-端点)
- [数据模型](#数据模型)
- [使用示例](#使用示例)
- [错误处理](#错误处理)
- [最佳实践](#最佳实践)
---
## 概述
AI Agent Manager 是一个基于 Kubernetes 的 AI Agent 生命周期管理服务,提供完整的 Agent 创建、部署、监控和删除功能。
### 核心特性
- ✅ **独立命名空间**: 每个 Agent 部署在独立的 Kubernetes 命名空间中
- ✅ **自动外网访问**: 自动创建 LoadBalancer Service 和 Azure DNS 记录
- ✅ **多框架支持**: 支持 MCP、A2A、API 三种框架类型
- ✅ **资源监控**: 实时监控 Agent 资源使用情况
- ✅ **模板管理**: 预定义的 Agent 模板,快速部署
### 技术栈
- **Web 框架**: FastAPI
- **容器编排**: Kubernetes (AKS)
- **DNS 服务**: Azure DNS
- **监控**: Kubernetes Metrics Server
### 服务信息
- **版本**: v1.0.0
- **默认端口**: 8000
- **默认命名空间**: ai-agents
---
## 快速开始
### 验证服务
```bash
curl http://localhost:8000/
```
响应:
```json
{
"service": "AI Agent Manager",
"status": "running",
"namespace": "ai-agents"
}
```
---
## 核心概念
### 1. Agent
Agent 是运行在 Kubernetes 中的 AI 服务实例,每个 Agent:
- 运行在独立的命名空间中
- 拥有唯一的外网访问地址
- 支持自动扩缩容
- 可以实时监控资源使用
### 2. Template(模板)
模板定义了 Agent 的类型和配置,包括:
- 容器镜像
- 环境变量要求
- 资源配置
- 框架类型
### 3. Framework(框架)
支持三种框架类型:
- **MCP**: Model Context Protocol
- **A2A**: Agent-to-Agent
- **API**: REST API(默认)
### 4. Namespace(命名空间)
每个 Agent 创建时自动生成独立的 Kubernetes 命名空间,格式:`agent-{agent-name}`
---
## API 端点
### 基础信息
#### GET /
获取服务状态
**响应**:
```json
{
"service": "AI Agent Manager",
"status": "running",
"namespace": "ai-agents"
}
```
---
### Agent 管理
#### POST /agents
创建新的 Agent
**请求体**:
```json
{
"name": "my-agent",
"template": "jina_search_agent",
"framework": "API",
"config": {
"user_id": "user123"
},
"env": {
"JINA_API_KEY": "your-api-key"
}
}
```
**参数说明**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| name | string | ✅ | Agent 名称(1-63字符,小写字母、数字、连字符) |
| template | string | ✅ | 模板类型(见模板列表) |
| framework | string | ❌ | 框架类型:MCP、A2A、API(默认:API) |
| config | object | ❌ | 配置信息(如 user_id) |
| env | object | ❌ | 环境变量 |
| namespace | string | ❌ | 自定义命名空间(不推荐) |
**响应**:
```json
{
"name": "my-agent",
"namespace": "agent-my-agent",
"framework": "API",
"status": "Pending",
"template": "jina_search_agent",
"service_port": 8080,
"access_info": {
"external_ip": "135.171.210.24",
"ip_url": "http://135.171.210.24:80",
"domain": "my-agent.taijiagnet.com",
"domain_url": "http://my-agent.taijiagnet.com",
"recommended": "http://my-agent.taijiagnet.com"
},
"pod_id": "7160fcd9-fbbe-48a1-9675-2b111cc36bc8",
"pod_ip": "10.244.3.80",
"host_ip": "10.224.0.7",
"node_name": "aks-node-001",
"owner_info": {
"user_id": "user123",
"agent_name": "my-agent",
"namespace": "agent-my-agent",
"framework": "API",
"labels": {
"app": "my-agent",
"framework": "api",
"managed-by": "agent-manager",
"template": "jina_search_agent"
}
}
}
```
**状态码**:
- `200`: 创建成功
- `400`: 参数错误(无效的模板或框架类型)
- `500`: 服务器错误
---
#### GET /agents
列出所有 Agent
**查询参数**:
- `template` (可选): 按模板类型过滤
**示例**:
```bash
# 列出所有 Agent
curl http://localhost:8000/agents
# 按模板过滤
curl http://localhost:8000/agents?template=jina_search_agent
```
**响应**:
```json
{
"agents": [
{
"name": "my-agent",
"status": "Running",
"template": "jina_search_agent",
"created_at": "2026-01-14T10:00:00+00:00",
"pod_ip": "10.244.3.80"
}
],
"count": 1
}
```
---
#### GET /agents/{agent_name}/status
获取 Agent 详细状态
**路径参数**:
- `agent_name`: Agent 名称
**响应**:
```json
{
"name": "my-agent",
"namespace": "agent-my-agent",
"status": "Running",
"health_status": "healthy",
"template": "jina_search_agent",
"created_at": "2026-01-14T10:00:00+00:00",
"node": "aks-node-001",
"pod_ip": "10.244.3.80",
"containers": [
{
"name": "my-agent",
"ready": true,
"restart_count": 0,
"state": "running",
"started_at": "2026-01-14T10:00:05+00:00"
}
],
"resources": {
"requests": {
"cpu": "100m",
"memory": "128Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
},
"usage": {
"cpu": "50m",
"memory": "200Mi",
"available": true
}
},
"service_port": 8080,
"access_url": "http://10.244.3.80:8080",
"endpoints": {
"root": "http://10.244.3.80:8080/",
"health": "http://10.244.3.80:8080/health"
},
"conditions": [
{
"type": "Ready",
"status": "True",
"reason": "PodReady"
}
]
}
```
**健康状态**:
- `healthy`: 容器运行正常
- `unhealthy`: 容器未就绪或终止
- `degraded`: 重启次数过多
---
#### GET /agents/{agent_name}/metrics
获取 Agent 资源使用情况
**响应**:
```json
{
"name": "my-agent",
"namespace": "agent-my-agent",
"requests": {
"cpu": "100m",
"memory": "128Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
},
"usage": {
"cpu": "45m",
"memory": "256Mi"
},
"timestamp": "2026-01-14T10:30:00+00:00",
"metrics_available": true
}
```
**注意**: 需要 Kubernetes Metrics Server 支持
---
#### DELETE /agents/{agent_name}
删除 Agent
**路径参数**:
- `agent_name`: Agent 名称
**响应**:
```json
{
"status": "success",
"message": "Agent my-agent 的命名空间 agent-my-agent 及所有相关资源已删除",
"namespace": "agent-my-agent"
}
```
**删除内容**:
- ✅ Kubernetes 命名空间
- ✅ Pod
- ✅ LoadBalancer Service
- ✅ Azure DNS 记录(如果存在)
**状态码**:
- `200`: 删除成功
- `404`: Agent 不存在
- `500`: 服务器错误
---
### 模板管理
#### GET /templates
列出所有可用模板
**响应**:
```json
{
"templates": [
{
"template": "jina_search_agent",
"port": 8080,
"env_info": {
"required": {
"JINA_API_KEY": "Jina API密钥"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080"
}
}
}
],
"count": 10
}
```
---
#### GET /templates/platform
列出平台模板
**响应**: 与 `/templates` 类似,但只包含平台预定义模板
**平台模板列表**:
- `echo_agent`
- `chat_agent`
- `code_agent`
- `search_agent`
- `jina_search_agent`
- `azure_blob_agent`
- `azure_blob_agent_mcp`
- `azure_blob_agent_a2a`
---
#### GET /templates/custom
列出自定义模板
**自定义模板列表**:
- `mysql_agent`
- `postgresql_agent`
---
#### GET /templates/{template_name}
获取指定模板详情
**路径参数**:
- `template_name`: 模板名称
**响应**:
```json
{
"template": "jina_search_agent",
"port": 8080,
"env_info": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
}
}
```
---
## 数据模型
### CreateAgentRequest
创建 Agent 请求模型
```python
{
"name": str, # Agent名称(必需,1-63字符)
"template": str, # 模板类型(必需)
"framework": str, # 框架类型(可选,默认API)
"config": dict, # 配置信息(可选)
"env": dict, # 环境变量(可选)
"namespace": str # 命名空间(可选)
}
```
### AgentResponse
Agent 响应模型
```python
{
"name": str, # Agent名称
"namespace": str, # 命名空间
"framework": str, # 框架类型
"status": str, # 状态
"template": str, # 模板类型
"service_port": int, # 服务端口
"access_info": dict, # 访问信息
"pod_id": str, # Pod ID
"pod_ip": str, # Pod IP
"host_ip": str, # 宿主机IP
"node_name": str, # 节点名称
"owner_info": dict # 所有者信息
}
```
---
## 使用示例
### 示例 1: 创建 Jina 搜索 Agent
```bash
curl -X POST http://localhost:8000/agents \
-H "Content-Type: application/json" \
-d '{
"name": "search-bot",
"template": "jina_search_agent",
"framework": "API",
"config": {
"user_id": "alice"
},
"env": {
"JINA_API_KEY": "jina_xxx"
}
}'
```
### 示例 2: 创建 MCP 框架的 Azure Blob Agent
```bash
curl -X POST http://localhost:8000/agents \
-H "Content-Type: application/json" \
-d '{
"name": "blob-mcp",
"template": "azure_blob_agent_mcp",
"framework": "MCP",
"config": {
"user_id": "bob"
},
"env": {
"MODEL_PROVIDER": "openai",
"MODEL_NAME": "gpt-4",
"MODEL_API_KEY": "sk-xxx",
"AZURE_STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=https;..."
}
}'
```
### 示例 3: 查询 Agent 状态
```bash
curl http://localhost:8000/agents/search-bot/status
```
### 示例 4: 监控资源使用
```bash
curl http://localhost:8000/agents/search-bot/metrics
```
### 示例 5: 列出所有 Agent
```bash
# 所有 Agent
curl http://localhost:8000/agents
# 只列出 Jina 搜索 Agent
curl http://localhost:8000/agents?template=jina_search_agent
```
### 示例 6: 删除 Agent
```bash
curl -X DELETE http://localhost:8000/agents/search-bot
```
### 示例 7: Python 客户端
```python
import requests
# 基础 URL
BASE_URL = "http://localhost:8000"
# 创建 Agent
def create_agent(name, template, framework="API", env=None):
response = requests.post(
f"{BASE_URL}/agents",
json={
"name": name,
"template": template,
"framework": framework,
"config": {"user_id": "demo"},
"env": env or {}
}
)
return response.json()
# 获取状态
def get_agent_status(name):
response = requests.get(f"{BASE_URL}/agents/{name}/status")
return response.json()
# 删除 Agent
def delete_agent(name):
response = requests.delete(f"{BASE_URL}/agents/{name}")
return response.json()
# 使用示例
agent = create_agent(
name="my-search",
template="jina_search_agent",
env={"JINA_API_KEY": "jina_xxx"}
)
print(f"Agent 创建成功!")
print(f"访问地址: {agent['access_info']['recommended']}")
# 查询状态
status = get_agent_status("my-search")
print(f"状态: {status['status']}")
```
---
## 错误处理
### 错误响应格式
```json
{
"detail": "错误描述信息"
}
```
### 常见错误
#### 400 Bad Request
**原因**:
- 无效的模板类型
- 无效的框架类型
- Agent 名称不符合规范
**示例**:
```json
{
"detail": "无效的模板类型。支持的模板: echo_agent, chat_agent, ..."
}
```
#### 404 Not Found
**原因**:
- Agent 不存在
- 模板不存在
**示例**:
```json
{
"detail": "Pod my-agent 不存在"
}
```
#### 500 Internal Server Error
**原因**:
- Kubernetes API 错误
- DNS 配置错误
- 网络问题
**处理建议**:
1. 检查 Kubernetes 集群状态
2. 验证 kubeconfig 配置
3. 检查网络连接
4. 查看服务日志
---
---
## 附录
### A. 支持的模板列表
| 模板名称 | 类型 | 用途 | 必需环境变量 |
|---------|------|------|-------------|
| jina_search_agent | Platform | Jina AI 搜索 | JINA_API_KEY |
| azure_blob_agent | Platform | Azure Blob 存储 | LITELLM_API_BASE, LITELLM_MODEL, LITELLM_API_KEY |
| azure_blob_agent_mcp | Platform | Azure Blob MCP | MODEL_PROVIDER, MODEL_NAME, MODEL_API_KEY |
| azure_blob_agent_a2a | Platform | Azure Blob A2A | MODEL_PROVIDER, MODEL_NAME, MODEL_API_KEY, AGENT_ID, AGENT_ROLE |
| mysql_agent | Custom | MySQL 数据库 | MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, OPENAI_API_KEY |
| postgresql_agent | Custom | PostgreSQL 数据库 | POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DATABASE, OPENAI_API_KEY |
| search_agent | Platform | 通用搜索 | - |
| echo_agent | Platform | Echo 测试 | - |
| chat_agent | Platform | 聊天 | - |
| code_agent | Platform | 代码生成 | - |
### B. 框架类型说明
| 框架 | 全称 | 用途 |
|------|------|------|
| MCP | Model Context Protocol | 基于协议的模型上下文交互 |
| A2A | Agent-to-Agent | Agent 间通信 |
| API | REST API | 标准 HTTP API 接口 |
## 联系支持
如有问题或建议,请联系开发团队或查看项目文档。
**文档版本**: v1.0.0
**最后更新**: 2026-01-14
File diff suppressed because it is too large Load Diff
+87 -116
View File
@@ -1,7 +1,8 @@
# 用户资源信息查询接口文档
> **版本**: 2026-01-14 v2
> **版本**: 2026-01-14 v3
> **认证方式**: Bearer Token(JWT)
> **更新说明**: 新增域名访问支持(externalIp、domain、domainUrl、accessUrl 字段)
---
@@ -219,8 +220,10 @@ response = client.chat.completions.create(
该接口用于获取当前用户的 Agent 资源信息:
1. **平台 Agent**:已部署的平台 Agent 列表,包含实时 IP 和访问地址
2. **自定义 Agent**:已部署的自定义 Agent 列表,包含实时 IP 和访问地址
1. **平台 Agent**:已部署的平台 Agent 列表,包含域名、外网IP和访问地址
2. **自定义 Agent**:已部署的自定义 Agent 列表,包含域名、外网IP和访问地址
> 💡 **新特性**:每个 Agent 现在会自动分配域名和外网 IP,推荐使用域名访问 Agent 服务。
---
@@ -277,22 +280,26 @@ Content-Type: application/json
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `name` | string | Agent 实例名称 | `"echo-agent-b00a7b8e-fa5a66"` |
| `name` | string | Agent 实例名称 | `"echo-agent-b00a7b8e-5507d7"` |
| `template` | string | Agent 类型/模板 | `"echo_agent"` |
| `templateName` | string | 模板名称 | `"echo_agent"` |
| `status` | string | 运行状态(AKS 实时) | `"Running"` / `"Waiting"` / `"Pending"` / `"Failed"` |
| `healthStatus` | string | 健康状态(AKS 实时) | `"healthy"` / `"degraded"` / `"unhealthy"` / `"unknown"` |
| `podIp` | string \| null | **Pod IP 地址**(AKS 实时) | `"10.244.2.103"` |
| `accessUrl` | string \| null | **访问 URL**(AKS 实时) | `null` |
| `servicePort` | integer \| null | 服务端口 | `null` |
| `namespace` | string | K8s 命名空间 | `"ai-agents"` |
| `hostIp` | string \| null | 宿主机 IP(AKS 实时) | `null` |
| `nodeName` | string \| null | 节点名称(AKS 实时) | `null` |
| `status` | string | 运行状态 | `"Running"` / `"Pending"` / `"Failed"` / `"unknown"` |
| `healthStatus` | string | 健康状态 | `"healthy"` / `"degraded"` / `"unhealthy"` / `"unknown"` |
| `podIp` | string \| null | Pod 内部 IP 地址 | `"10.244.3.5"` |
| `externalIp` | string \| null | **🆕 外网 IP 地址** | `"20.195.113.211"` |
| `domain` | string \| null | **🆕 域名**(推荐访问方式) | `"echo-agent-b00a7b8e-5507d7.taijiagnet.com"` |
| `domainUrl` | string \| null | **🆕 域名访问地址** | `"http://echo-agent-b00a7b8e-5507d7.taijiagnet.com"` |
| `accessUrl` | string \| null | **🆕 推荐访问地址**(域名优先) | `"http://echo-agent-b00a7b8e-5507d7.taijiagnet.com"` |
| `servicePort` | integer \| null | 服务端口 | `8000` |
| `namespace` | string | K8s 命名空间 | `"agent-echo-agent-b00a7b8e-5507d7"` |
| `hostIp` | string \| null | 宿主机 IP | `"10.224.0.7"` |
| `nodeName` | string \| null | K8s 节点名称 | `"aks-taijipod-34569487-vmss00000b"` |
| `cpu` | string | CPU 配置 | `"100m"` |
| `memory` | string | 内存配置 | `"256Mi"` |
| `replicas` | integer | 副本数量 | `1` |
| `startTime` | string | 启动时间(ISO 8601) | `"2026-01-11T15:17:17.579421"` |
| `runningSeconds` | integer | 已运行秒数 | `227937` |
| `startTime` | string | 启动时间(ISO 8601) | `"2026-01-14T13:36:22.443568"` |
| `runningSeconds` | integer | 已运行秒数 | `3600` |
| `endpoints` | array \| undefined | 端点 URL 列表 | `["http://10.244.3.5:8000"]` |
---
@@ -315,119 +322,53 @@ Content-Type: application/json
"data": {
"platformAgents": [
{
"name": "echo-agent-b00a7b8e-fa5a66",
"name": "echo-agent-b00a7b8e-5507d7",
"template": "echo_agent",
"templateName": "echo_agent",
"status": "Running",
"healthStatus": "healthy",
"podIp": "10.244.2.103",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"podIp": "10.244.3.5",
"externalIp": "20.195.113.211",
"domain": "echo-agent-b00a7b8e-5507d7.taijiagnet.com",
"domainUrl": "http://echo-agent-b00a7b8e-5507d7.taijiagnet.com",
"accessUrl": "http://echo-agent-b00a7b8e-5507d7.taijiagnet.com",
"servicePort": 8000,
"namespace": "agent-echo-agent-b00a7b8e-5507d7",
"hostIp": "10.224.0.7",
"nodeName": "aks-taijipod-34569487-vmss00000b",
"cpu": "100m",
"memory": "256Mi",
"replicas": 1,
"startTime": "2026-01-11T15:17:17.579421",
"runningSeconds": 227937,
"hostIp": null,
"nodeName": null
},
{
"name": "echo-agent-b00a7b8e-362760",
"template": "echo_agent",
"templateName": "echo_agent",
"status": "Running",
"healthStatus": "healthy",
"podIp": "10.244.2.225",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"cpu": "100m",
"memory": "256Mi",
"replicas": 1,
"startTime": "2026-01-12T13:22:29.551127",
"runningSeconds": 148425,
"hostIp": null,
"nodeName": null
"startTime": "2026-01-14T13:36:22.443568",
"runningSeconds": 3600
}
],
"customAgents": [
{
"name": "test-pgsql-agent-curl",
"template": "postgresql_agent",
"templateName": "A2A",
"status": "Waiting",
"healthStatus": "degraded",
"podIp": "10.244.1.61",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"cpu": "100m",
"memory": "128Mi",
"replicas": 1,
"startTime": "2026-01-13T07:32:52.290231",
"runningSeconds": 83003,
"hostIp": null,
"nodeName": null
},
{
"name": "my-pgsql-agent",
"template": "postgresql_agent",
"templateName": "MCP",
"status": "Waiting",
"healthStatus": "degraded",
"podIp": "10.244.1.158",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"cpu": "100m",
"memory": "256Mi",
"replicas": 1,
"startTime": "2026-01-13T12:03:12.933103",
"runningSeconds": 66782,
"hostIp": null,
"nodeName": null
},
{
"name": "test-auto-apikey-agent",
"template": "postgresql_agent",
"templateName": "MCP",
"status": "Waiting",
"healthStatus": "degraded",
"podIp": "10.244.0.43",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"cpu": "100m",
"memory": "256Mi",
"replicas": 1,
"startTime": "2026-01-13T12:10:27.145811",
"runningSeconds": 66348,
"hostIp": null,
"nodeName": null
},
{
"name": "mysqltestagent",
"name": "my-mysql-agent",
"template": "mysql_agent",
"templateName": "MCP",
"status": "Waiting",
"healthStatus": "degraded",
"podIp": "10.244.2.42",
"accessUrl": null,
"servicePort": null,
"namespace": "ai-agents",
"cpu": "1000m",
"status": "Running",
"healthStatus": "healthy",
"podIp": "10.244.1.61",
"externalIp": "20.6.66.41",
"domain": "my-mysql-agent.taijiagnet.com",
"domainUrl": "http://my-mysql-agent.taijiagnet.com",
"accessUrl": "http://my-mysql-agent.taijiagnet.com",
"servicePort": 8000,
"namespace": "agent-my-mysql-agent",
"hostIp": "10.224.0.5",
"nodeName": "aks-taijipod-34569487-vmss00000a",
"cpu": "500m",
"memory": "1Gi",
"replicas": 1,
"startTime": "2026-01-13T12:38:00.340200",
"runningSeconds": 64695,
"hostIp": null,
"nodeName": null
"startTime": "2026-01-14T10:00:00.000000",
"runningSeconds": 14400
}
],
"summary": {
"totalPlatformAgents": 2,
"totalCustomAgents": 4
"totalPlatformAgents": 1,
"totalCustomAgents": 1
}
},
"message": "Agent 列表获取成功"
@@ -467,13 +408,40 @@ Content-Type: application/json
### Agent 访问方式
通过 `podIp` 可以在集群内部访问 Agent 服务:
#### 🌐 推荐:使用域名访问(稳定)
通过 `domain` 或 `accessUrl` 访问 Agent 服务,域名不会因 Pod 重启而变化:
```bash
# 直接通过 Pod IP 访问(默认端口 8080)
curl http://10.244.2.103:8080/api/chat
# 使用域名访问(推荐)
curl http://echo-agent-b00a7b8e-5507d7.taijiagnet.com/api/chat
# 或使用 accessUrl 字段的值
curl http://echo-agent-b00a7b8e-5507d7.taijiagnet.com/health
```
#### 🔗 使用外网 IP 访问
```bash
# 使用外网 IP 访问
curl http://20.195.113.211/api/chat
```
#### 📍 集群内部访问(仅限 K8s 集群内)
```bash
# 使用 Pod IP 访问(仅集群内部)
curl http://10.244.3.5:8000/api/chat
```
### 访问方式优先级
| 优先级 | 访问方式 | 字段 | 稳定性 | 说明 |
|:---:|---------|------|:---:|------|
| 1 | 域名 | `domain` / `accessUrl` | ⭐⭐⭐ | **推荐**,Pod 重启后不变 |
| 2 | 外网 IP | `externalIp` | ⭐⭐ | LoadBalancer IP,较稳定 |
| 3 | Pod IP | `podIp` | ⭐ | Pod 重启后会变化 |
---
## 📌 数据来源说明
@@ -482,8 +450,9 @@ curl http://10.244.2.103:8080/api/chat
|---------|------|--------|
| LiteLLM 密钥 | PostgreSQL 数据库 | 静态(创建时存储) |
| Agent 基本信息(名称、模板、CPU/内存) | PostgreSQL 数据库 | 静态(创建时存储) |
| Agent 访问信息(domain、externalIp) | PostgreSQL 数据库 | 静态(创建时存储) |
| Agent 状态(status, healthStatus) | AKS 集群(通过 Agent Manager) | **实时查询** |
| Agent 网络信息(podIp) | AKS 集群(通过 Agent Manager) | **实时查询** |
| Agent Pod 信息(podIp、hostIp) | AKS 集群(通过 Agent Manager) | **实时查询** |
### 调用链路
@@ -498,6 +467,8 @@ curl http://10.244.2.103:8080/api/chat
## ⚠️ 注意事项
1. **密钥安全**:返回的 `apiKey` 是完整的解密密钥,请妥善保管,不要泄露
2. **实时性**:Agent 的 `podIp`、`status` 等信息是实时从 AKS 查询的,可能有轻微延迟
3. **服务可用性**:如果 Agent Manager 服务不可用,Agent 的实时信息将显示为 `null` 或 `unknown`
4. **Pod IP 变化**:Pod 重启后 IP 地址会改变
2. **推荐域名访问**:使用 `domain` 或 `accessUrl` 访问 Agent,比 Pod IP 更稳定
3. **DNS 生效时间**:新创建的 Agent 域名可能需要 1-5 分钟 DNS 传播时间
4. **实时性**:`status`、`podIp` 等信息是实时从 AKS 查询的,可能有轻微延迟
5. **服务可用性**:如果 Agent Manager 服务不可用,实时信息将显示为 `null` 或 `unknown`
6. **旧 Agent 兼容**:在 2026-01-14 之前创建的 Agent,`domain`、`externalIp` 等字段可能为 `null`
+501
View File
@@ -0,0 +1,501 @@
# Agent 域名访问改动计划
> **版本**: 2026-01-14 v1
> **状态**: ✅ 已完成
> **相关服务**: mcp-server, agent-manager
> **完成时间**: 2026-01-14
---
## 📋 背景与需求
### 业务变更说明
1. **Agent Manager 服务升级**:部署好的每个 Pod 现在会自动绑定域名和外网 IP
2. **访问方式变更**:租户后续将使用域名访问属于自己的平台 Agent 和自定义 Agent
3. **数据存储需求**:需要记录 Agent Manager 返回的访问信息(domain、external_ip 等)
### Agent Manager 返回的 access_info 结构
```json
{
"access_info": {
"external_ip": "135.171.210.24",
"ip_url": "http://135.171.210.24:80",
"domain": "my-agent.taijiagent.com",
"domain_url": "http://my-agent.taijiagent.com",
"recommended": "http://my-agent.taijiagent.com"
}
}
```
---
## 🔍 当前代码分析
### 1. 数据库模型现状
#### Agent 模型 (`models.py:125`)
```python
class Agent(BaseModel, Base):
# ... 已有字段
access_url = Column(String(500)) # 访问 URL(单个字段)
endpoints = Column(JSON, default=dict) # 端点信息
# ❌ 缺少 domain、external_ip 等字段
```
#### AgentBillingRecord 模型 (`models.py:1157`)
```python
class AgentBillingRecord(BaseModel, Base):
# ... 已有字段
agent_name = Column(String(100), nullable=False)
# ❌ 缺少 access_info 相关字段(domain、external_ip、access_url)
```
### 2. Agent Manager Client 现状
#### AgentCreateResult (`agent_manager_client.py:97`)
```python
@dataclass
class AgentCreateResult:
name: str
namespace: str
status: str
access_info: Optional[Dict[str, Any]] = None # ✅ 已有,但未完整使用
```
#### AgentStatusResult (`agent_manager_client.py:119`)
```python
@dataclass
class AgentStatusResult:
# ❌ 缺少 domain、external_ip 等字段
access_url: Optional[str] = None # 只有单个 access_url
endpoints: Optional[List[str]] = None
```
### 3. 创建 Agent 代码现状
#### 平台 Agent 创建 (`user.py:1427-1439`)
```python
result = await client.create_agent(...)
# 创建 Agent 记录时
agent = Agent(
name=instance_name,
# ❌ 未保存 result.access_info 中的 domain、external_ip
)
```
#### 自定义 Agent 创建 (`user.py:3146-3157`)
```python
result = await client.create_custom_agent(...)
# ❌ 响应中只返回了 accessInfo,未持久化 domain 到数据库
return SuccessResponse(
data={
"accessInfo": result.access_info, # 只是透传,未存储
}
)
```
### 4. 查询 Agent 列表代码现状
#### 用户 Agent 资源查询 (`user.py:3904-3999`)
```python
@router.get("/resources/agents")
async def get_user_agents_info(...):
# 从 Agent Manager 获取状态
agent_status = await client.get_agent_status(record.agent_name)
agent_info["accessUrl"] = agent_status.access_url # ❌ 只返回 access_url
# ❌ 缺少 domain、external_ip 等字段
```
---
## ✅ 改动方案
### 阶段一:数据库模型改动
#### 1.1 修改 AgentBillingRecord 模型
**文件**: `services/mcp-server/models.py`
```python
class AgentBillingRecord(BaseModel, Base):
# ... 已有字段
# ========== 新增:访问信息字段 ==========
external_ip = Column(String(45), nullable=True) # 外网 IP 地址
domain = Column(String(255), nullable=True) # 域名
domain_url = Column(String(500), nullable=True) # 域名访问地址
access_url = Column(String(500), nullable=True) # 推荐访问地址
service_port = Column(Integer, nullable=True) # 服务端口
namespace = Column(String(100), nullable=True) # K8s 命名空间
```
#### 1.2 创建数据库迁移脚本
**文件**: `services/mcp-server/alembic/versions/xxxx_add_agent_access_info.py`
```python
"""Add agent access info fields
Revision ID: xxxx
"""
def upgrade():
op.add_column('agent_billing_records',
sa.Column('external_ip', sa.String(45), nullable=True))
op.add_column('agent_billing_records',
sa.Column('domain', sa.String(255), nullable=True))
op.add_column('agent_billing_records',
sa.Column('domain_url', sa.String(500), nullable=True))
op.add_column('agent_billing_records',
sa.Column('access_url', sa.String(500), nullable=True))
op.add_column('agent_billing_records',
sa.Column('service_port', sa.Integer, nullable=True))
op.add_column('agent_billing_records',
sa.Column('namespace', sa.String(100), nullable=True))
# 添加索引(可选,用于按域名查询)
op.create_index('idx_agent_billing_domain', 'agent_billing_records', ['domain'])
def downgrade():
op.drop_index('idx_agent_billing_domain', 'agent_billing_records')
op.drop_column('agent_billing_records', 'namespace')
op.drop_column('agent_billing_records', 'service_port')
op.drop_column('agent_billing_records', 'access_url')
op.drop_column('agent_billing_records', 'domain_url')
op.drop_column('agent_billing_records', 'domain')
op.drop_column('agent_billing_records', 'external_ip')
```
---
### 阶段二:Agent Manager Client 改动
#### 2.1 修改 AgentStatusResult
**文件**: `services/mcp-server/app/agent_manager_client.py`
```python
@dataclass
class AgentStatusResult:
# ... 已有字段
# ========== 新增:访问信息字段 ==========
external_ip: Optional[str] = None # 外网 IP
domain: Optional[str] = None # 域名
domain_url: Optional[str] = None # 域名访问地址
```
#### 2.2 修改 get_agent_status 方法解析逻辑
**文件**: `services/mcp-server/app/agent_manager_client.py`
在 `get_agent_status` 方法中,需要解析 Agent Manager 返回的 access_info:
```python
async def get_agent_status(self, name: str) -> AgentStatusResult:
# ... 现有逻辑
# 解析 access_info
access_info = data.get("access_info", {})
return AgentStatusResult(
# ... 现有字段
external_ip=access_info.get("external_ip"),
domain=access_info.get("domain"),
domain_url=access_info.get("domain_url"),
access_url=access_info.get("recommended") or access_info.get("domain_url"),
)
```
---
### 阶段三:创建 Agent 代码改动
#### 3.1 平台 Agent 创建改动
**文件**: `services/mcp-server/app/routes/user.py`
**位置**: `list_platform_agents` / `deploy_platform_agent` 函数
```python
# 在创建 billing_record 时保存 access_info
billing_record = AgentBillingRecord(
# ... 已有字段
# ========== 新增:保存访问信息 ==========
external_ip=result.access_info.get("external_ip") if result.access_info else None,
domain=result.access_info.get("domain") if result.access_info else None,
domain_url=result.access_info.get("domain_url") if result.access_info else None,
access_url=result.access_info.get("recommended") if result.access_info else None,
service_port=result.service_port,
namespace=result.namespace,
)
```
#### 3.2 自定义 Agent 创建改动
**文件**: `services/mcp-server/app/routes/user.py`
**位置**: `create_custom_agent` 函数(约第 3146-3213 行)
```python
# 在创建 billing_record 时保存 access_info
billing_record = AgentBillingRecord(
# ... 已有字段
# ========== 新增:保存访问信息 ==========
external_ip=result.access_info.get("external_ip") if result.access_info else None,
domain=result.access_info.get("domain") if result.access_info else None,
domain_url=result.access_info.get("domain_url") if result.access_info else None,
access_url=result.access_info.get("recommended") if result.access_info else None,
service_port=result.service_port,
namespace=result.namespace,
)
```
---
### 阶段四:查询 Agent 列表改动
#### 4.1 用户 Agent 资源查询改动
**文件**: `services/mcp-server/app/routes/user.py`
**位置**: `get_user_agents_info` 函数(约第 3904-3999 行)
```python
@router.get("/resources/agents", response_model=SuccessResponse)
async def get_user_agents_info(...):
for record in billing_records:
agent_info = {
# ... 已有字段
# ========== 新增:访问信息字段(优先使用数据库存储的值) ==========
"externalIp": record.external_ip,
"domain": record.domain,
"domainUrl": record.domain_url,
"accessUrl": record.access_url, # 推荐访问地址
}
# 从 Agent Manager 获取最新状态(实时更新 IP 等信息)
if agent_manager_available and client:
try:
agent_status = await client.get_agent_status(record.agent_name)
# 更新实时状态
agent_info["status"] = agent_status.status
agent_info["healthStatus"] = agent_status.health_status
agent_info["podIp"] = agent_status.pod_ip
# 更新访问信息(如果 Agent Manager 返回了新的值)
if agent_status.external_ip:
agent_info["externalIp"] = agent_status.external_ip
if agent_status.domain:
agent_info["domain"] = agent_status.domain
if agent_status.domain_url:
agent_info["domainUrl"] = agent_status.domain_url
if agent_status.access_url:
agent_info["accessUrl"] = agent_status.access_url
except Exception as e:
logger.warning(f"获取 Agent {record.agent_name} 状态失败: {e}")
```
#### 4.2 自定义 Agent 列表查询改动
**文件**: `services/mcp-server/app/routes/user.py`
**位置**: `list_my_custom_agents` 函数(约第 3462-3516 行)
同样需要添加 domain 等字段的返回。
---
### 阶段五:响应模型改动
#### 5.1 添加/修改 Schema
**文件**: `services/mcp-server/app/schemas.py` 或 `services/mcp-server/schemas.py`
```python
class AgentAccessInfo(BaseModel):
"""Agent 访问信息"""
external_ip: Optional[str] = Field(None, description="外网 IP 地址")
domain: Optional[str] = Field(None, description="域名")
domain_url: Optional[str] = Field(None, description="域名访问地址")
ip_url: Optional[str] = Field(None, description="IP 访问地址")
recommended: Optional[str] = Field(None, description="推荐访问地址")
class AgentResourceInfo(BaseModel):
"""用户 Agent 资源信息"""
name: str
template: str
templateName: Optional[str] = None
status: str
healthStatus: str
# Pod 信息
podIp: Optional[str] = None
hostIp: Optional[str] = None
nodeName: Optional[str] = None
# ========== 新增:访问信息 ==========
externalIp: Optional[str] = Field(None, description="外网 IP 地址")
domain: Optional[str] = Field(None, description="域名")
domainUrl: Optional[str] = Field(None, description="域名访问地址")
accessUrl: Optional[str] = Field(None, description="推荐访问地址(域名优先)")
# 资源配置
servicePort: Optional[int] = None
namespace: str = "ai-agents"
cpu: Optional[str] = None
memory: Optional[str] = None
replicas: int = 1
# 运行信息
startTime: Optional[str] = None
runningSeconds: int = 0
endpoints: Optional[List[str]] = None
```
---
## 📄 接口文档更新
### GET /api/user/resources/agents 响应更新
```json
{
"success": true,
"data": {
"platformAgents": [
{
"name": "echo-agent-b00a7b8e-fa5a66",
"template": "echo_agent",
"templateName": "echo_agent",
"status": "Running",
"healthStatus": "healthy",
"podIp": "10.244.2.103",
"externalIp": "135.171.210.24",
"domain": "echo-agent-b00a7b8e-fa5a66.taijiagent.com",
"domainUrl": "http://echo-agent-b00a7b8e-fa5a66.taijiagent.com",
"accessUrl": "http://echo-agent-b00a7b8e-fa5a66.taijiagent.com",
"servicePort": 80,
"namespace": "agent-echo-agent-b00a7b8e-fa5a66",
"cpu": "100m",
"memory": "256Mi",
"replicas": 1,
"startTime": "2026-01-11T15:17:17.579421",
"runningSeconds": 227937
}
],
"customAgents": [
{
"name": "my-mysql-agent",
"template": "mysql_agent",
"templateName": "MCP",
"status": "Running",
"healthStatus": "healthy",
"podIp": "10.244.1.61",
"externalIp": "135.171.210.25",
"domain": "my-mysql-agent.taijiagent.com",
"domainUrl": "http://my-mysql-agent.taijiagent.com",
"accessUrl": "http://my-mysql-agent.taijiagent.com",
"servicePort": 80,
"namespace": "agent-my-mysql-agent",
"cpu": "500m",
"memory": "1Gi",
"replicas": 1,
"startTime": "2026-01-13T07:32:52.290231",
"runningSeconds": 83003
}
],
"summary": {
"totalPlatformAgents": 1,
"totalCustomAgents": 1
}
},
"message": "Agent 列表获取成功"
}
```
---
## 📝 改动文件清单
| 序号 | 文件路径 | 改动类型 | 改动说明 |
|:---:|---------|---------|---------|
| 1 | `services/mcp-server/models.py` | 修改 | AgentBillingRecord 添加访问信息字段 |
| 2 | `services/mcp-server/alembic/versions/xxx.py` | 新增 | 数据库迁移脚本 |
| 3 | `services/mcp-server/app/agent_manager_client.py` | 修改 | AgentStatusResult 添加 domain 等字段 |
| 4 | `services/mcp-server/app/routes/user.py` | 修改 | 创建 Agent 时保存 access_info |
| 5 | `services/mcp-server/app/routes/user.py` | 修改 | 查询 Agent 列表返回 domain 等字段 |
| 6 | `services/mcp-server/app/schemas.py` | 修改 | 添加/修改响应模型 |
| 7 | `Docs/用户资源信息查询接口文档.md` | 修改 | 更新接口文档 |
| 8 | `Docs/数据工具与自定义Agent-前端接口文档.md` | 修改 | 更新接口文档 |
---
## 🔄 实施步骤
### Step 1: 数据库改动(需要停机)
1. 备份数据库
2. 执行数据库迁移脚本
3. 验证迁移结果
### Step 2: 代码改动
1. 修改 `models.py`
2. 修改 `agent_manager_client.py`
3. 修改 `user.py` 中的创建 Agent 逻辑
4. 修改 `user.py` 中的查询 Agent 逻辑
5. 修改响应模型
### Step 3: 测试验证
1. 单元测试
2. 集成测试(创建 Agent → 查询列表 → 访问域名)
3. 前端联调
### Step 4: 文档更新
1. 更新 API 接口文档
2. 更新前端接口文档
---
## ⚠️ 注意事项
1. **向后兼容**:新增字段均为可选(nullable=True),不影响现有数据
2. **域名生效时间**:域名 DNS 解析可能有延迟(通常 1-5 分钟)
3. **访问优先级**:推荐使用 `accessUrl`(域名优先),Pod IP 会随重启变化
4. **安全考虑**:域名访问可能需要配置 HTTPS(后续考虑)
---
## 📊 预估工时
| 阶段 | 工时估算 |
|-----|---------|
| 数据库改动 | 0.5 天 |
| Agent Manager Client 改动 | 0.5 天 |
| 创建 Agent 代码改动 | 1 天 |
| 查询 Agent 列表改动 | 0.5 天 |
| 测试与联调 | 1 天 |
| 文档更新 | 0.5 天 |
| **总计** | **4 天** |
---
**文档编写**: AI Assistant
**最后更新**: 2026-01-14
@@ -1,271 +0,0 @@
# Agent 启动业务流程分析报告
> **版本**: v1.0.0
> **创建时间**: 2026-01-08
> **分析目标**: 确认平台 Agent 和自定义 Agent 的启动业务流程实现状态
---
## 1. 业务流程概述
### 1.1 业务逻辑1:平台 Agent 启动流程
**流程描述**:渠道分配平台 Agent 给租户后,平台 Agent 通过 Agent Manager 服务启动到 AKS 中开始运行。
```mermaid
sequenceDiagram
participant CA as 渠道管理员
participant MCP as mcp-server
participant AM as Agent Manager
participant AKS as Azure Kubernetes
CA->>MCP: 1. 申请平台 Agent 配额
MCP->>MCP: 2. 创建 ResourceApplication
Note over MCP: 状态: pending
rect rgb(200, 220, 255)
Note over MCP: 管理员审批流程
MCP->>MCP: 3. 管理员审批通过
MCP->>MCP: 4. 创建渠道 PlatformAgentQuota
end
CA->>MCP: 5. 分配平台 Agent 给租户
MCP->>MCP: 6. 检查渠道配额
MCP->>MCP: 7. 创建租户 PlatformAgentQuota
MCP->>AM: 8. 调用 POST /agents 创建 Pod
AM->>AKS: 9. 创建 K8s Pod
AKS-->>AM: 10. 返回 Pod 状态
AM-->>MCP: 11. 返回创建结果
MCP->>MCP: 12. 创建 Agent 记录
MCP-->>CA: 13. 返回分配成功
```
### 1.2 业务逻辑2:自定义 Agent 启动流程
**流程描述**:渠道分配自定义 Agent 配额给租户后,租户通过 mcp-server 平台配置相关配置文件后,自定义 Agent 通过 Agent Manager 服务启动到 AKS 中。
```mermaid
sequenceDiagram
participant CA as 渠道管理员
participant T as 租户
participant MCP as mcp-server
participant AM as Agent Manager
participant AKS as Azure Kubernetes
CA->>MCP: 1. 分配自定义 Agent 配额给租户
MCP->>MCP: 2. 创建 TenantCustomAgentQuota
Note over MCP: CPU/内存配额
T->>MCP: 3. 创建自定义 Agent
Note over T,MCP: 提供模板、环境变量、资源配置
MCP->>MCP: 4. 检查租户配额
MCP->>MCP: 5. 查询租户 LiteLLM Key(如指定模型)
MCP->>MCP: 6. 构建环境变量(注入 LiteLLM 配置)
MCP->>AM: 7. 调用 POST /agents 创建 Pod
AM->>AKS: 8. 创建 K8s Pod
AKS-->>AM: 9. 返回 Pod 状态
AM-->>MCP: 10. 返回创建结果
MCP->>MCP: 11. 更新配额使用量
MCP->>MCP: 12. 创建 AgentBillingRecord
MCP-->>T: 13. 返回创建成功
```
---
## 2. 实现状态分析
### 2.1 平台 Agent 启动流程 - ✅ 已实现
| 步骤 | 功能 | 实现文件 | 实现状态 |
|------|------|----------|----------|
| 1 | 渠道申请平台 Agent 配额 | [`platform_agent_quota.py:239-316`](services/mcp-server/app/routes/platform_agent_quota.py:239) | ✅ 已实现 |
| 2 | 管理员审批申请 | [`platform_agent_quota.py:626-721`](services/mcp-server/app/routes/platform_agent_quota.py:626) | ✅ 已实现 |
| 3 | 渠道分配平台 Agent 给租户 | [`platform_agent_quota.py:404-579`](services/mcp-server/app/routes/platform_agent_quota.py:404) | ✅ 已实现 |
| 4 | 调用 Agent Manager 创建 Pod | [`platform_agent_quota.py:520-524`](services/mcp-server/app/routes/platform_agent_quota.py:520) | ✅ 已实现 |
| 5 | 创建 Agent 数据库记录 | [`platform_agent_quota.py:540-551`](services/mcp-server/app/routes/platform_agent_quota.py:540) | ✅ 已实现 |
| 6 | 更新配额使用量 | [`platform_agent_quota.py:527-537`](services/mcp-server/app/routes/platform_agent_quota.py:527) | ✅ 已实现 |
**关键代码位置**:
- **渠道分配接口**: [`POST /api/channel/tenants/{tenant_id}/platform-agents`](services/mcp-server/app/routes/platform_agent_quota.py:404)
- **Agent Manager 客户端**: [`agent_manager_client.py:751-830`](services/mcp-server/app/agent_manager_client.py:751)
**实现细节**:
```python
# platform_agent_quota.py:520-524 - 调用 Agent Manager 创建 Pod
result = await client.create_agent(
name=pod_name,
template=request.templateName,
config=agent_config
)
```
### 2.2 自定义 Agent 启动流程 - ✅ 已实现
| 步骤 | 功能 | 实现文件 | 实现状态 |
|------|------|----------|----------|
| 1 | 渠道分配自定义 Agent 配额 | [`channel.py:564-651`](services/mcp-server/app/routes/channel.py:564) | ✅ 已实现 |
| 2 | 租户查看配额 | [`user.py:342-400`](services/mcp-server/app/routes/user.py:342) | ✅ 已实现 |
| 3 | 租户创建自定义 Agent | [`user.py:1539-1712`](services/mcp-server/app/routes/user.py:1539) | ✅ 已实现 |
| 4 | 检查配额 | [`user.py:1563-1600`](services/mcp-server/app/routes/user.py:1563) | ✅ 已实现 |
| 5 | 查询 LiteLLM Key 并注入环境变量 | [`user.py:1609-1645`](services/mcp-server/app/routes/user.py:1609) | ✅ 已实现 |
| 6 | 调用 Agent Manager 创建 Pod | [`user.py:1660-1666`](services/mcp-server/app/routes/user.py:1660) | ✅ 已实现 |
| 7 | 更新配额使用量 | [`user.py:1669-1671`](services/mcp-server/app/routes/user.py:1669) | ✅ 已实现 |
| 8 | 创建计费记录 | [`user.py:1674-1684`](services/mcp-server/app/routes/user.py:1674) | ✅ 已实现 |
**关键代码位置**:
- **创建自定义 Agent 接口**: [`POST /api/user/custom-agents`](services/mcp-server/app/routes/user.py:1539)
- **LiteLLM Key 注入**: [`user.py:1609-1645`](services/mcp-server/app/routes/user.py:1609)
**实现细节**:
```python
# user.py:1631-1639 - 注入 LiteLLM 环境变量
env_vars["OPENAI_API_BASE"] = settings.litellm_url
env_vars["OPENAI_API_KEY"] = decrypted_key
env_vars["MODEL_NAME"] = model_name
env_vars["LITELLM_MODEL"] = model_name
# user.py:1660-1666 - 调用 Agent Manager 创建 Pod
result = await client.create_custom_agent(
name=req.name,
template=req.template,
user_id=str(user_id),
env_vars=env_vars,
config=agent_config
)
```
---
## 3. 接口清单
### 3.1 平台 Agent 相关接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 查看可用平台 Agent | GET | `/api/channel/available-platform-agents` | 渠道查看可申请的平台 Agent |
| 申请平台 Agent 配额 | POST | `/api/channel/applications/platform-agents` | 渠道申请配额 |
| 查看申请列表 | GET | `/api/channel/applications/platform-agents` | 渠道查看自己的申请 |
| 审批申请 | PUT | `/api/admin/applications/platform-agents/{id}/review` | 管理员审批 |
| 分配给租户 | POST | `/api/channel/tenants/{tenant_id}/platform-agents` | **核心接口:分配并启动 Pod** |
| 查看渠道配额 | GET | `/api/channel/platform-agents` | 渠道查看已有配额 |
| 租户查看配额 | GET | `/api/user/platform-agents` | 租户查看自己的配额 |
| 停止 Agent | DELETE | `/api/user/platform-agents/{agent_name}` | 租户停止 Agent |
### 3.2 自定义 Agent 相关接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 分配配额给租户 | PUT | `/api/channel/tenants/{tenant_id}/resources` | 渠道分配 CPU/内存配额 |
| 查看配额 | GET | `/api/user/custom-agent-quota` | 租户查看自己的配额 |
| 查看模板 | GET | `/api/user/custom-agents/templates` | 租户查看可用模板 |
| 创建自定义 Agent | POST | `/api/user/custom-agents` | **核心接口:创建并启动 Pod** |
| 查看 Agent 列表 | GET | `/api/user/custom-agents` | 租户查看自己的 Agent |
| 删除 Agent | DELETE | `/api/user/custom-agents/{name}` | 租户删除 Agent |
| 扩缩容 | PUT | `/api/user/custom-agents/{name}/scale` | 租户调整资源 |
---
## 4. Agent Manager 客户端接口
mcp-server 通过 [`AgentManagerClient`](services/mcp-server/app/agent_manager_client.py:535) 与 Agent Manager 服务交互:
| 方法 | Agent Manager 接口 | 说明 |
|------|-------------------|------|
| `create_agent()` | POST /agents | 创建 Agent Pod |
| `create_platform_agent()` | POST /agents | 创建平台 Agent(便捷方法) |
| `create_custom_agent()` | POST /agents | 创建自定义 Agent(便捷方法) |
| `delete_agent()` | DELETE /agents/{name} | 删除 Agent Pod |
| `get_agent_status()` | GET /agents/{name}/status | 获取 Agent 状态 |
| `get_agent_metrics()` | GET /agents/{name}/metrics | 获取资源使用情况 |
| `list_agents()` | GET /agents | 列出所有 Agent |
| `list_platform_templates()` | GET /templates/platform | 获取平台模板 |
| `list_custom_templates()` | GET /templates/custom | 获取自定义模板 |
---
## 5. 数据模型
### 5.1 配额管理
```
PlatformAgentQuota
├── target_id: UUID (渠道ID 或 租户ID)
├── target_type: str (channel 或 tenant)
├── template_name: str (模板名称)
├── pod_quota: int (配额上限)
├── pod_used: int (已使用)
└── allocated_at: datetime
TenantCustomAgentQuota
├── tenant_id: UUID
├── cpu_quota: float (CPU 配额,核心数)
├── memory_quota: float (内存配额,GB)
├── cpu_used: float
├── memory_used: float
└── agent_count: int
```
### 5.2 Agent 记录
```
Agent
├── name: str
├── type: str (platform 或 custom)
├── template: str
├── pod_name: str
├── k8s_namespace: str
├── k8s_status: str
├── service_port: int
├── owner_id: UUID (租户ID)
└── status: str
```
---
## 6. 结论
### 6.1 实现状态总结
| 业务流程 | 实现状态 | 说明 |
|----------|----------|------|
| 平台 Agent 启动流程 | ✅ **完整实现** | 渠道分配时立即启动 Pod |
| 自定义 Agent 启动流程 | ✅ **完整实现** | 租户创建时启动 Pod,支持 LiteLLM 集成 |
### 6.2 关键实现特点
1. **平台 Agent**:
- 渠道分配配额给租户时,**立即调用 Agent Manager 启动 Pod**
- 配额管理采用两级结构:渠道配额 → 租户配额
- 支持配额使用量追踪(pod_used)
2. **自定义 Agent**:
- 租户创建 Agent 时,**立即调用 Agent Manager 启动 Pod**
- 支持 LiteLLM 环境变量自动注入(OPENAI_API_BASE, OPENAI_API_KEY, MODEL_NAME)
- 配额管理基于 CPU/内存资源
3. **LiteLLM 集成**:
- 根据 [`Agent-Manager接口变动需求文档.md`](plans/Agent-Manager接口变动需求文档.md) 的设计
- 自定义 Agent 创建时会自动注入 LiteLLM 相关环境变量
- 环境变量通过 `env_vars` 参数传递给 Agent Manager
### 6.3 待确认事项
1. **Agent Manager 服务**:需要确认 Agent Manager 服务是否已部署并正常运行
2. **敏感信息处理**:根据需求文档,建议 Agent Manager 使用 K8s Secret 存储敏感环境变量(如 OPENAI_API_KEY)
3. **模板配置**:管理员需要通过 `/api/admin/platform-agents/templates/{name}/config` 接口配置平台 Agent 模板的资源限制
---
## 7. 相关文件索引
| 文件 | 说明 |
|------|------|
| [`services/mcp-server/app/routes/platform_agent_quota.py`](services/mcp-server/app/routes/platform_agent_quota.py) | 平台 Agent 配额管理路由 |
| [`services/mcp-server/app/routes/user.py`](services/mcp-server/app/routes/user.py) | 用户侧 API(含自定义 Agent) |
| [`services/mcp-server/app/routes/channel.py`](services/mcp-server/app/routes/channel.py) | 渠道 API(含配额分配) |
| [`services/mcp-server/app/agent_manager_client.py`](services/mcp-server/app/agent_manager_client.py) | Agent Manager 客户端 |
| [`plans/Agent-Manager接口变动需求文档.md`](plans/Agent-Manager接口变动需求文档.md) | Agent Manager 接口变动需求 |
-269
View File
@@ -1,269 +0,0 @@
# Agent 计费完整流程图
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Agent 计费完整流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ MCP Server │ │Agent Manager │ │ Kubernetes │
│ │ │ │ │ │
└───────┬──────┘ └───────┬──────┘ └───────┬──────┘
│ │ │
│ │ │
│ 1. POST /agents │ │
│ { │ │
│ name: "alice-echo"│ │
│ template: "echo" │ │
│ config: { │ │
│ user_id: "xxx" │ │
│ } │ │
│ } │ │
├───────────────────────>│ │
│ │ │
│ │ 2. Create Pod │
│ │ with Labels: │
│ │ user-id=xxx │
│ ├───────────────────────>│
│ │ │
│ │ 3. Pod Created │
│ │<───────────────────────┤
│ │ │
│ 4. Agent Created │ │
│<───────────────────────┤ │
│ │ │
│ │ │
│ ⏱️ Agent 运行中... │
│ │ │
│ │ │
│ │ 5. Agent 停止 │
│ │ (用户调用完成) │
│ │ │
│ │ 6. Read Pod Labels │
│ ├───────────────────────>│
│ │ │
│ │ 7. Return Labels │
│ │ user-id=xxx │
│ │<───────────────────────┤
│ │ │
│ │ 8. Calculate Time │
│ │ running_time = │
│ │ stop - start │
│ │ │
│ 9. POST /billing/ │ │
│ agent-callback │ │
│ { │ │
│ agentName: "..." │ │
│ userId: "xxx" │◄── 从 Labels 获取 │
│ podRunningTime: 120 │
│ toolsUsed: [...] │ │
│ } │ │
│<───────────────────────┤ │
│ │ │
│ 10. Create Billing │ │
│ Record │ │
│ - Calculate EU │ │
│ - Calculate Cost │ │
│ - Deduct Balance │ │
│ │ │
│ 11. Response │ │
│ { │ │
│ success: true │ │
│ recordId: "..." │ │
│ } │ │
├───────────────────────>│ │
│ │ │
│ │ 12. Delete Pod │
│ ├───────────────────────>│
│ │ │
▼ ▼ ▼
═══════════════════════════════════════════════════════════════════════════════
关键点:用户身份识别
═══════════════════════════════════════════════════════════════════════════════
步骤 2: Agent Manager 创建 Pod 时
↓
将 user_id 存储在 Pod Labels 中
↓
labels:
user-id: "xxx"
↓
步骤 6-7: Agent 停止时读取 Pod Labels
↓
获取 user_id
↓
步骤 9: 回调时使用该 user_id
═══════════════════════════════════════════════════════════════════════════════
数据库记录结构
═══════════════════════════════════════════════════════════════════════════════
agent_billing_records 表:
┌─────────────────┬──────────────────────┬─────────────────────┐
│ 字段 │ 值示例 │ 来源 │
├─────────────────┼──────────────────────┼─────────────────────┤
│ user_id │ user-123-456 │ Pod Labels │
│ agent_name │ alice-echo-agent │ Agent Manager │
│ duration_seconds│ 120 │ 计算: stop - start │
│ eu_consumed │ 12 │ ceil(120/10) │
│ cost │ 0.10 │ 基于费率计算 │
│ tools_used │ ["web_search"] │ Agent Manager │
│ request_id │ req-abc-123 │ Agent Manager │
│ start_time │ 2026-01-11T10:00:00Z │ Pod 创建时间 │
│ end_time │ 2026-01-11T10:02:00Z │ Pod 删除时间 │
└─────────────────┴──────────────────────┴─────────────────────┘
═══════════════════════════════════════════════════════════════════════════════
错误处理流程
═══════════════════════════════════════════════════════════════════════════════
场景 1: Pod Labels 中没有 user_id
↓
Agent Manager 应该:
1. 从内部映射表查询(如果维护了)
2. 记录错误日志
3. 使用默认/系统用户 ID(可选)
4. 继续回调(避免计费丢失)
场景 2: 回调 MCP Server 失败
↓
Agent Manager 应该:
1. 重试 2-3 次(指数退避)
2. 将失败的回调存储到队列
3. 定期重试队列中的失败回调
4. 记录详细错误日志
场景 3: MCP Server 返回用户不存在
↓
Agent Manager 应该:
1. 记录严重错误
2. 通知管理员
3. 不再重试该条记录
═══════════════════════════════════════════════════════════════════════════════
```
## 关键技术点
### 1. 用户身份识别
```yaml
# Pod 定义示例
apiVersion: v1
kind: Pod
metadata:
name: alice-echo-agent
labels:
app: agent
template: echo_agent
user-id: user-123-456 # ✅ 关键标签
annotations:
taiji.ai/user-id: user-123-456
taiji.ai/channel-id: channel-789
taiji.ai/created-at: "2026-01-11T10:00:00Z"
spec:
containers:
- name: agent
image: taiji/echo-agent:latest
```
### 2. 运行时间计算
```python
# Agent Manager
from datetime import datetime
# Pod 创建时
pod_start_time = pod.status.start_time
# Pod 停止时
pod_stop_time = pod.metadata.deletion_timestamp or datetime.utcnow()
# 计算运行时间(秒)
running_time_seconds = int((pod_stop_time - pod_start_time).total_seconds())
```
### 3. 工具使用记录
```python
# Agent 运行时记录工具调用
tools_used = []
def call_tool(tool_name: str):
tools_used.append(tool_name)
# 执行工具...
# 回调时发送
await report_billing(
agent_name=agent_name,
user_id=user_id,
pod_running_time=120,
tools_used=list(set(tools_used)) # 去重
)
```
## 性能优化建议
### 1. 使用 Redis 缓存 user_id
```python
# 创建时
redis.setex(f"agent:{agent_name}:user_id", 86400, user_id)
# 读取时
user_id = redis.get(f"agent:{agent_name}:user_id")
if not user_id:
# 回退到读取 Pod Labels
pod = v1.read_namespaced_pod(...)
user_id = pod.metadata.labels["user-id"]
```
### 2. 异步回调
```python
# 不阻塞 Pod 删除
asyncio.create_task(report_billing(...))
```
### 3. 批量处理
```python
# 收集一批回调请求
pending_callbacks = []
# 定期批量发送
async def flush_callbacks():
if pending_callbacks:
await mcp_client.batch_report_billing(pending_callbacks)
```
## 监控指标
### Agent Manager 侧
- `agent_callback_total` - 回调总次数
- `agent_callback_success` - 回调成功次数
- `agent_callback_failed` - 回调失败次数
- `agent_callback_retry` - 回调重试次数
- `agent_callback_duration_seconds` - 回调耗时
### MCP Server 侧
- `billing_callback_received` - 收到的回调
- `billing_record_created` - 创建的计费记录
- `billing_callback_errors` - 回调错误数
## 相关文档
- [Agent用户身份识别机制说明.md](../Docs/Agent用户身份识别机制说明.md)
- [Agent-Manager回调配置指南.md](../Docs/Agent-Manager回调配置指南.md)
- [Agent-EU计费改进实施报告.md](./Agent-EU计费改进实施报告.md)
@@ -183,6 +183,12 @@ class AgentStatusResult:
conditions: Optional[List[Dict[str, Any]]] = None
# Compatible with old fields
template: Optional[str] = None
# ========== 访问信息字段(从 access_info 解析) ==========
external_ip: Optional[str] = None # 外网 IP 地址
domain: Optional[str] = None # 域名
domain_url: Optional[str] = None # 域名访问地址
ip_url: Optional[str] = None # IP 访问地址
# ======================================================
@property
def is_healthy(self) -> bool:
@@ -1024,6 +1030,16 @@ class AgentManagerClient:
# New format: list
endpoints = endpoints_data
# ========== 解析 access_info(域名和外网IP) ==========
access_info = data.get("access_info", {})
external_ip = access_info.get("external_ip")
domain = access_info.get("domain")
domain_url = access_info.get("domain_url")
ip_url = access_info.get("ip_url")
# 优先使用 access_info 中的推荐地址,否则使用旧的 access_url
access_url = access_info.get("recommended") or data.get("access_url")
# ======================================================
return AgentStatusResult(
name=data["name"],
namespace=data["namespace"],
@@ -1035,13 +1051,18 @@ class AgentManagerClient:
node_name=data.get("node_name"),
labels=data.get("labels"),
service_port=data.get("service_port"),
access_url=data.get("access_url"),
access_url=access_url,
containers=containers,
resources=data.get("resources"),
endpoints=endpoints,
conditions=data.get("conditions"),
# Extract template from labels (for compatibility)
template=data.get("labels", {}).get("template") if data.get("labels") else None
template=data.get("labels", {}).get("template") if data.get("labels") else None,
# ========== 新增:访问信息字段 ==========
external_ip=external_ip,
domain=domain,
domain_url=domain_url,
ip_url=ip_url,
)
async def get_agent_metrics(self, agent_name: str) -> AgentMetricsResult:
+70 -21
View File
@@ -1457,7 +1457,8 @@ async def deploy_agent(
)
db.add(agent)
# 记录计费
# 记录计费(包含访问信息)
access_info = result.access_info or {}
billing_record = AgentBillingRecord(
user_id=user_id,
channel_id=channel_id,
@@ -1470,6 +1471,13 @@ async def deploy_agent(
cpu_used=quota.cpu_per_pod or "100m",
memory_used=quota.memory_per_pod or "256Mi",
replicas=req.instances,
# ========== 保存访问信息 ==========
external_ip=access_info.get("external_ip"),
domain=access_info.get("domain"),
domain_url=access_info.get("domain_url"),
access_url=access_info.get("recommended") or access_info.get("domain_url"),
service_port=result.service_port,
namespace=result.namespace,
)
db.add(billing_record)
@@ -2400,7 +2408,8 @@ async def deploy_platform_agent(
)
db.add(agent)
# 记录计费
# 记录计费(包含访问信息)
access_info = result.access_info or {}
billing_record = AgentBillingRecord(
user_id=user_id,
channel_id=channel_id,
@@ -2413,6 +2422,13 @@ async def deploy_platform_agent(
cpu_used=quota.cpu_per_pod or "100m",
memory_used=quota.memory_per_pod or "256Mi",
replicas=1,
# ========== 保存访问信息 ==========
external_ip=access_info.get("external_ip"),
domain=access_info.get("domain"),
domain_url=access_info.get("domain_url"),
access_url=access_info.get("recommended") or access_info.get("domain_url"),
service_port=result.service_port,
namespace=result.namespace,
)
db.add(billing_record)
@@ -2425,6 +2441,8 @@ async def deploy_platform_agent(
"status": result.status,
"servicePort": result.service_port,
"accessInfo": result.access_info,
"domain": access_info.get("domain"),
"domainUrl": access_info.get("domain_url"),
"quotaRemaining": quota.pod_quota - quota.pod_used,
},
message=f"平台 Agent {req.agentType} 部署成功"
@@ -2521,7 +2539,8 @@ async def use_platform_agent(
# 更新配额使用量
quota.pod_used += 1
# 记录计费
# 记录计费(包含访问信息)
access_info = result.access_info or {}
billing_record = AgentBillingRecord(
user_id=user_id,
channel_id=channel_id,
@@ -2534,6 +2553,13 @@ async def use_platform_agent(
cpu_used=quota.cpu_per_pod or "100m",
memory_used=quota.memory_per_pod or "256Mi",
replicas=1,
# ========== 保存访问信息 ==========
external_ip=access_info.get("external_ip"),
domain=access_info.get("domain"),
domain_url=access_info.get("domain_url"),
access_url=access_info.get("recommended") or access_info.get("domain_url"),
service_port=result.service_port,
namespace=result.namespace,
)
db.add(billing_record)
@@ -2546,6 +2572,8 @@ async def use_platform_agent(
"status": result.status,
"servicePort": result.service_port,
"accessInfo": result.access_info,
"domain": access_info.get("domain"),
"domainUrl": access_info.get("domain_url"),
"quotaRemaining": quota.pod_quota - quota.pod_used,
},
message=f"平台 Agent {req.agentType} 启动成功"
@@ -3162,7 +3190,8 @@ async def create_custom_agent(
quota.memory_used = memory_used + memory_request
quota.agent_count = (quota.agent_count or 0) + 1
# 记录计费
# 记录计费(包含访问信息)
access_info = result.access_info or {}
billing_record = AgentBillingRecord(
user_id=user_id,
channel_id=channel_id,
@@ -3174,6 +3203,13 @@ async def create_custom_agent(
cpu_used=req.cpuRequest,
memory_used=req.memoryRequest,
tools_used=req.tools if req.tools else [],
# ========== 保存访问信息 ==========
external_ip=access_info.get("external_ip"),
domain=access_info.get("domain"),
domain_url=access_info.get("domain_url"),
access_url=access_info.get("recommended") or access_info.get("domain_url"),
service_port=result.service_port,
namespace=result.namespace,
)
db.add(billing_record)
@@ -3203,6 +3239,8 @@ async def create_custom_agent(
"status": result.status,
"servicePort": result.service_port,
"accessInfo": result.access_info,
"domain": access_info.get("domain"),
"domainUrl": access_info.get("domain_url"),
"modelInjected": model_name is not None,
"quotaRemaining": {
"cpu": remaining_cpu - cpu_request,
@@ -3951,9 +3989,16 @@ async def get_user_agents_info(
"status": "unknown",
"healthStatus": "unknown",
"podIp": None,
"accessUrl": None,
"servicePort": None,
"namespace": "ai-agents",
# ========== 访问信息(优先使用数据库存储的值) ==========
"externalIp": record.external_ip,
"domain": record.domain,
"domainUrl": record.domain_url,
"accessUrl": record.access_url,
"servicePort": record.service_port,
"namespace": record.namespace or "ai-agents",
# ======================================================
"hostIp": None,
"nodeName": None,
"cpu": record.cpu_used,
"memory": record.memory_used,
"replicas": record.replicas,
@@ -3961,30 +4006,34 @@ async def get_user_agents_info(
"runningSeconds": int((datetime.utcnow() - record.start_time).total_seconds()) if record.start_time else 0,
}
# 从 Agent Manager 获取详细状态
# 从 Agent Manager 获取详细状态(实时更新)
if agent_manager_available and client:
try:
agent_status = await client.get_agent_status(record.agent_name)
agent_info["status"] = agent_status.status
agent_info["healthStatus"] = agent_status.health_status
agent_info["podIp"] = agent_status.pod_ip
agent_info["accessUrl"] = agent_status.access_url
agent_info["servicePort"] = agent_status.service_port
agent_info["namespace"] = agent_status.namespace
agent_info["hostIp"] = agent_status.host_ip
agent_info["nodeName"] = agent_status.node_name
# 端点信息
# ========== 更新访问信息(如果 Agent Manager 返回了最新值) ==========
if agent_status.external_ip:
agent_info["externalIp"] = agent_status.external_ip
if agent_status.domain:
agent_info["domain"] = agent_status.domain
if agent_status.domain_url:
agent_info["domainUrl"] = agent_status.domain_url
if agent_status.access_url:
agent_info["accessUrl"] = agent_status.access_url
if agent_status.service_port:
agent_info["servicePort"] = agent_status.service_port
if agent_status.namespace:
agent_info["namespace"] = agent_status.namespace
# ==================================================================
# 端点信息(字符串列表,如 ["http://10.244.1.100:8080"])
if agent_status.endpoints:
agent_info["endpoints"] = [
{
"name": ep.name,
"port": ep.port,
"protocol": ep.protocol,
"targetPort": ep.target_port,
}
for ep in agent_status.endpoints
]
agent_info["endpoints"] = agent_status.endpoints
except Exception as e:
logger.warning(f"获取 Agent {record.agent_name} 状态失败: {e}")
@@ -0,0 +1,61 @@
-- Migration 016: Add Agent Access Info Fields
-- Description: 为 agent_billing_records 表添加访问信息字段(域名、外网IP等)
-- Date: 2026-01-14
-- ========== 添加访问信息字段 ==========
-- 外网 IP 地址
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS external_ip VARCHAR(45);
-- 域名
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS domain VARCHAR(255);
-- 域名访问地址
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS domain_url VARCHAR(500);
-- 推荐访问地址
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS access_url VARCHAR(500);
-- 服务端口
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS service_port INTEGER;
-- K8s 命名空间
ALTER TABLE agent_billing_records
ADD COLUMN IF NOT EXISTS namespace VARCHAR(100);
-- ========== 添加索引 ==========
-- 按域名查询索引(用于根据域名查找 Agent)
CREATE INDEX IF NOT EXISTS idx_agent_billing_domain
ON agent_billing_records(domain);
-- ========== 添加注释 ==========
COMMENT ON COLUMN agent_billing_records.external_ip IS '外网 IP 地址(Agent Manager 分配)';
COMMENT ON COLUMN agent_billing_records.domain IS '域名(如 my-agent.taijiagent.com)';
COMMENT ON COLUMN agent_billing_records.domain_url IS '域名访问地址(如 http://my-agent.taijiagent.com)';
COMMENT ON COLUMN agent_billing_records.access_url IS '推荐访问地址(域名优先)';
COMMENT ON COLUMN agent_billing_records.service_port IS '服务端口';
COMMENT ON COLUMN agent_billing_records.namespace IS 'Kubernetes 命名空间';
-- ========== 验证迁移 ==========
-- 检查字段是否添加成功
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'agent_billing_records'
AND column_name = 'domain'
) THEN
RAISE NOTICE 'Migration 016 completed successfully: domain field added';
ELSE
RAISE EXCEPTION 'Migration 016 failed: domain field not found';
END IF;
END $$;
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Migration 016: Add Agent Access Info Fields
为 agent_billing_records 表添加访问信息字段(域名、外网IP等)
Usage:
python migrations/run_016_migration.py
"""
import asyncio
import os
import sys
# 添加项目根目录到 Python 路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import text
from database import engine
async def run_migration():
"""运行迁移"""
print("=" * 60)
print("Migration 016: Add Agent Access Info Fields")
print("=" * 60)
# 读取 SQL 文件
sql_file = os.path.join(os.path.dirname(__file__), "016_add_agent_access_info_fields.sql")
with open(sql_file, "r", encoding="utf-8") as f:
sql_content = f.read()
# 分割 SQL 语句
statements = []
current_stmt = []
in_do_block = False
for line in sql_content.split("\n"):
line_stripped = line.strip()
# 跳过注释和空行(但保留 DO 块中的内容)
if not in_do_block:
if line_stripped.startswith("--") or not line_stripped:
continue
# 检测 DO 块开始
if line_stripped.upper().startswith("DO $$"):
in_do_block = True
current_stmt.append(line)
continue
# 检测 DO 块结束
if in_do_block and line_stripped == "END $$;":
current_stmt.append(line)
statements.append("\n".join(current_stmt))
current_stmt = []
in_do_block = False
continue
if in_do_block:
current_stmt.append(line)
continue
# 普通语句处理
current_stmt.append(line)
if line_stripped.endswith(";"):
stmt = "\n".join(current_stmt)
if stmt.strip():
statements.append(stmt)
current_stmt = []
# 执行迁移
async with engine.begin() as conn:
for i, stmt in enumerate(statements, 1):
try:
# 打印语句摘要
stmt_preview = stmt.strip()[:80].replace("\n", " ")
if len(stmt.strip()) > 80:
stmt_preview += "..."
print(f"\n[{i}/{len(statements)}] Executing: {stmt_preview}")
await conn.execute(text(stmt))
print(f" ✓ Success")
except Exception as e:
error_msg = str(e)
# 忽略 "column already exists" 错误
if "already exists" in error_msg.lower():
print(f" ⚠ Skipped (already exists)")
else:
print(f" ✗ Error: {error_msg}")
raise
print("\n" + "=" * 60)
print("Migration 016 completed successfully!")
print("=" * 60)
async def verify_migration():
"""验证迁移结果"""
print("\nVerifying migration...")
async with engine.begin() as conn:
# 检查新字段是否存在
result = await conn.execute(text("""
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'agent_billing_records'
AND column_name IN ('external_ip', 'domain', 'domain_url', 'access_url', 'service_port', 'namespace')
ORDER BY column_name
"""))
columns = result.fetchall()
print(f"\nNew columns in agent_billing_records table:")
print("-" * 50)
for col in columns:
col_name, data_type, max_len = col
type_info = f"{data_type}({max_len})" if max_len else data_type
print(f" ✓ {col_name}: {type_info}")
expected_columns = {'external_ip', 'domain', 'domain_url', 'access_url', 'service_port', 'namespace'}
found_columns = {col[0] for col in columns}
if found_columns == expected_columns:
print(f"\n✓ All {len(expected_columns)} columns verified successfully!")
else:
missing = expected_columns - found_columns
if missing:
print(f"\n✗ Missing columns: {missing}")
return False
return True
if __name__ == "__main__":
try:
asyncio.run(run_migration())
asyncio.run(verify_migration())
except KeyboardInterrupt:
print("\nMigration cancelled.")
sys.exit(1)
except Exception as e:
print(f"\nMigration failed: {e}")
sys.exit(1)
+10
View File
@@ -1198,6 +1198,15 @@ class AgentBillingRecord(BaseModel, Base):
tools_used = Column(JSON, nullable=True) # 使用的工具列表
request_id = Column(String(100), nullable=True) # 请求 ID
# ========== 访问信息字段(Agent Manager 返回) ==========
external_ip = Column(String(45), nullable=True) # 外网 IP 地址
domain = Column(String(255), nullable=True) # 域名
domain_url = Column(String(500), nullable=True) # 域名访问地址
access_url = Column(String(500), nullable=True) # 推荐访问地址
service_port = Column(Integer, nullable=True) # 服务端口
namespace = Column(String(100), nullable=True) # K8s 命名空间
# ======================================================
# 关联关系
user = relationship("User")
channel = relationship("Channel")
@@ -1207,6 +1216,7 @@ class AgentBillingRecord(BaseModel, Base):
Index("idx_agent_billing_channel", channel_id),
Index("idx_agent_billing_period", period_start, period_end),
Index("idx_agent_billing_type", agent_type),
Index("idx_agent_billing_domain", domain), # 按域名查询索引
)