From fe69a70a3aaf2f205592d280dbce02787ac56e96 Mon Sep 17 00:00:00 2001 From: zhanggangyong Date: Fri, 23 Jan 2026 10:57:13 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=A4=96=E9=83=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=B7=A5=E5=85=B7=E5=92=8C=E5=B7=A5=E5=85=B7=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../数据工具与自定义Agent-功能修复文档.md | 479 ------- Docs/外部数据工具接口文档.md | 843 +++++++++++++ .../Agent发送给AgentManager的数据结构文档.md | 0 .../LiteLLM和AgentManager回调接口文档.md | 0 Docs/{ => 项目文档}/全平台API接口清单.md | 0 Docs/{ => 项目文档}/删除Agent接口文档.md | 0 .../数据工具与自定义Agent-前端接口文档.md | 0 .../用户资源信息查询接口文档.md | 0 docs/agent-domain-access-改动计划.md | 501 -------- docs/dns-mapping-implementation-plan.md | 1121 ----------------- plans/自定义外部数据工具功能方案.md | 1117 ++++++++++++++++ .../mcp-server/app/agent_manager_client.py | 276 ++++ services/mcp-server/app/routes/__init__.py | 3 + .../mcp-server/app/routes/external_tools.py | 1030 +++++++++++++++ services/mcp-server/app/routes/user.py | 99 ++ services/mcp-server/app/schemas.py | 185 +++ .../017_add_external_data_tools.sql | 45 + .../migrations/018_add_external_toolkits.sql | 39 + .../migrations/run_017_migration.py | 63 + services/mcp-server/models.py | 82 ++ services/model-gateway/Dockerfile | 43 - services/model-gateway/config/litellm.yaml | 379 ------ .../model-gateway/config/litellm_simple.yaml | 107 -- 23 files changed, 3782 insertions(+), 2630 deletions(-) delete mode 100644 Docs/功能修复/数据工具与自定义Agent-功能修复文档.md create mode 100644 Docs/外部数据工具接口文档.md rename Docs/{ => 项目文档}/Agent发送给AgentManager的数据结构文档.md (100%) rename Docs/{ => 项目文档}/LiteLLM和AgentManager回调接口文档.md (100%) rename Docs/{ => 项目文档}/全平台API接口清单.md (100%) rename Docs/{ => 项目文档}/删除Agent接口文档.md (100%) rename Docs/{ => 项目文档}/数据工具与自定义Agent-前端接口文档.md (100%) rename Docs/{ => 项目文档}/用户资源信息查询接口文档.md (100%) delete mode 100644 docs/agent-domain-access-改动计划.md delete mode 100644 docs/dns-mapping-implementation-plan.md create mode 100644 plans/自定义外部数据工具功能方案.md create mode 100644 services/mcp-server/app/routes/external_tools.py create mode 100644 services/mcp-server/migrations/017_add_external_data_tools.sql create mode 100644 services/mcp-server/migrations/018_add_external_toolkits.sql create mode 100644 services/mcp-server/migrations/run_017_migration.py delete mode 100644 services/model-gateway/Dockerfile delete mode 100644 services/model-gateway/config/litellm.yaml delete mode 100644 services/model-gateway/config/litellm_simple.yaml diff --git a/Docs/功能修复/数据工具与自定义Agent-功能修复文档.md b/Docs/功能修复/数据工具与自定义Agent-功能修复文档.md deleted file mode 100644 index b33dc7a..0000000 --- a/Docs/功能修复/数据工具与自定义Agent-功能修复文档.md +++ /dev/null @@ -1,479 +0,0 @@ -# 数据工具与自定义Agent - 功能修复文档 - -> **创建日期**: 2026-01-13 -> **状态**: 待修复 -> **优先级**: 高 - ---- - -## 1. 问题概述 - -### 当前实现与预期业务逻辑不一致 - -**预期业务逻辑**: -1. 前端获取 `dataTemplates`(Agent 模板列表,如 `mysql_agent`、`postgresql_agent`) -2. 用户选择模板,根据模板的 `env_info` 填写配置值(如 MySQL 连接信息),保存为**工具** -3. 用户创建自定义 Agent 时选择已创建的工具 -4. MCP-Server 从工具中获取模板类型和配置,传递给 Agent Manager 创建对应的 Agent - -**当前实现问题**: -- Tool 模型设计为通用 API 调用配置(endpoint、method、schema) -- **缺少 `template` 字段**:无法关联 Agent 模板 -- **缺少 `env_config` 字段**:无法存储模板所需的环境变量配置 -- 创建 Agent 时,工具只作为附加的 API 能力,而不是决定 Agent 类型的关键信息 - ---- - -## 2. 正确的业务流程 - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ 正确的业务流程 │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ① 获取模板列表 ② 创建工具(基于模板) ③ 创建Agent(选择工具) │ -│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ │ GET │ │ POST │ │ POST │ │ -│ │ /templates │──────→│ /tools/create │───────────→│ /custom-agents│ │ -│ └───────────────┘ └───────────────┘ └───────────────┘ │ -│ │ │ │ │ -│ ↓ ↓ ↓ │ -│ 返回 dataTemplates 保存到 Tool 表 从工具获取 template │ -│ [mysql_agent, - template: mysql_agent 和 envConfig, │ -│ postgresql_agent] - envConfig: { 传递给 Agent Manager │ -│ MYSQL_HOST: "...", │ -│ 含 env_info: MYSQL_USER: "...", │ -│ - required ... │ -│ - optional } │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 关键概念 - -| 概念 | 说明 | -|------|------| -| **模板 (template)** | Agent Manager 提供的镜像类型,如 `mysql_agent`,包含 `env_info` 定义所需配置 | -| **工具 (Tool)** | 用户基于模板创建的配置实例,包含 `template` + 填写好的 `envConfig` | -| **自定义 Agent** | 根据工具的模板类型和配置创建的 K8s Pod | - ---- - -## 3. 需要修改的内容 - -### 3.1 数据库模型修改 - -**文件**: `services/mcp-server/models.py` - -**修改 Tool 模型,添加字段**: - -```python -class Tool(BaseModel, Base): - """工具模型""" - __tablename__ = "tools" - - name = Column(String(100), nullable=False) - description = Column(Text) - category = Column(String(50)) # database, api, function 等 - - # ========== 新增:模板相关字段 ========== - template = Column(String(100)) # 模板名称,如 "mysql_agent", "postgresql_agent" - env_config = Column(JSON, default=dict) # 环境变量配置,根据模板 env_info 填写 - # ========================================= - - # 工具定义(保留,用于自定义 API 工具) - schema = Column(JSON) # 改为可空,模板工具不需要 - endpoint = Column(String(500)) - method = Column(String(10), default="POST") - - # ... 其他字段保持不变 -``` - -### 3.2 创建工具接口修改 - -**文件**: `services/mcp-server/app/routes/user.py` - -**修改 `POST /api/user/tools/create` 接口**: - -```python -@router.post("/tools/create", response_model=SuccessResponse) -async def create_tool( - req: dict, - principal: dict = Depends(require_auth), - db: AsyncSession = Depends(get_db) -): - """ - 创建工具(基于模板) - - 请求体示例(MySQL 工具): - { - "name": "my-mysql-tool", - "description": "我的MySQL数据库连接工具", - "template": "mysql_agent", - "envConfig": { - "MYSQL_HOST": "mysql.example.com", - "MYSQL_USER": "root", - "MYSQL_PASSWORD": "password123", - "MYSQL_DATABASE": "mydb", - "MYSQL_PORT": "3306", - "OPENAI_API_KEY": "sk-xxx" - } - } - """ - user_id = principal.get("user_id") - - # 验证模板名称 - template_name = req.get("template") - if template_name: - # 从 Agent Manager 获取模板列表并验证 - try: - from app.agent_manager_client import get_agent_manager_client, AgentManagerError - client = get_agent_manager_client() - custom_templates = await client.list_custom_templates() - template_names = [t.template for t in custom_templates] - - if template_name not in template_names: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"无效的模板名称: {template_name}。支持的模板: {', '.join(template_names)}" - ) - except AgentManagerError: - pass # Agent Manager 不可用时跳过验证 - - # 检查工具名称是否已存在 - result = await db.execute( - select(Tool).where( - and_( - Tool.owner_id == user_id, - Tool.name == req.get("name") - ) - ) - ) - existing_tool = result.scalar_one_or_none() - - if existing_tool: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"工具名称 '{req.get('name')}' 已存在" - ) - - # 创建工具 - tool = Tool( - name=req.get("name"), - description=req.get("description"), - category="database" if template_name else req.get("type", "api"), - template=template_name, # 新增 - env_config=req.get("envConfig", {}), # 新增 - schema=req.get("config", {}).get("schema") if not template_name else None, - endpoint=req.get("config", {}).get("endpoint") if not template_name else None, - method=req.get("config", {}).get("method", "GET") if not template_name else None, - auth_config={"apiKey": req.get("config", {}).get("apiKey")} if req.get("config", {}).get("apiKey") else {}, - owner_id=user_id, - is_active=True, - is_public=False, - ) - - db.add(tool) - await db.commit() - await db.refresh(tool) - - return SuccessResponse( - data={ - "id": str(tool.id), - "name": tool.name, - "template": tool.template, - }, - message="工具创建成功" - ) -``` - -### 3.3 创建自定义 Agent 接口修改 - -**文件**: `services/mcp-server/app/routes/user.py` - -**修改 `POST /api/user/custom-agents` 接口的工具处理逻辑**: - -```python -@router.post("/custom-agents", response_model=SuccessResponse) -async def create_custom_agent( - req: CreateCustomAgentRequest, - principal: dict = Depends(require_auth), - db: AsyncSession = Depends(get_db) -): - """ - 创建自定义 Agent - - 新逻辑:从选中的工具获取模板类型和环境变量配置 - """ - # ... 配额检查等代码保持不变 ... - - # ========== 新增:从工具获取模板和配置 ========== - template_name = req.template # 默认使用请求中的 template - env_vars = req.envConfig or {} - - if req.tools and len(req.tools) > 0: - # 查询第一个工具(主工具,决定 Agent 类型) - primary_tool_id = req.tools[0] - try: - tool_result = await db.execute( - select(Tool).where( - Tool.id == PyUUID(primary_tool_id), - or_( - Tool.is_public == True, - Tool.owner_id == PyUUID(user_id) - ) - ) - ) - primary_tool = tool_result.scalar_one_or_none() - - if primary_tool and primary_tool.template: - # 使用工具的模板类型 - template_name = primary_tool.template - - # 合并工具的环境变量配置 - if primary_tool.env_config: - env_vars = {**primary_tool.env_config, **env_vars} - - logger.info( - f"从工具获取配置: tool_id={primary_tool_id}, " - f"template={template_name}, env_keys={list(env_vars.keys())}" - ) - except Exception as e: - logger.warning(f"查询工具失败: {str(e)}") - # ================================================ - - # 验证模板 - try: - client = get_agent_manager_client() - custom_templates = await client.list_custom_templates() - template_names = [t.template for t in custom_templates] - - if template_name not in template_names: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"无效的模板名称: {template_name}。支持的模板: {', '.join(template_names)}" - ) - except AgentManagerError as e: - logger.warning(f"无法获取模板列表,跳过验证: {str(e)}") - - # ... 后续代码使用 template_name 和 env_vars 创建 Agent ... -``` - -### 3.4 获取用户工具列表接口修改 - -**文件**: `services/mcp-server/app/routes/user.py` - -**修改 `GET /api/user/tools` 响应格式**: - -```python -@router.get("/tools", response_model=SuccessResponse) -async def get_user_tools( - principal: dict = Depends(require_auth), - db: AsyncSession = Depends(get_db) -): - """获取用户创建的所有工具""" - user_id = principal.get("user_id") - - result = await db.execute( - select(Tool) - .where(Tool.owner_id == user_id) - .order_by(desc(Tool.created_at)) - ) - tools = result.scalars().all() - - tools_data = [] - for tool in tools: - tools_data.append({ - "id": str(tool.id), - "name": tool.name, - "description": tool.description, - "template": tool.template, # 新增 - "category": tool.category, - "envConfig": tool.env_config, # 新增(注意:敏感信息如密码应脱敏) - "created_at": tool.created_at.isoformat() if tool.created_at else None, - "updated_at": tool.updated_at.isoformat() if tool.updated_at else None, - "is_active": tool.is_active, - }) - - return SuccessResponse(data={"tools": tools_data}) -``` - -### 3.5 数据库迁移 - -需要创建数据库迁移脚本,添加新字段: - -```sql --- 添加 template 字段 -ALTER TABLE tools ADD COLUMN template VARCHAR(100); - --- 添加 env_config 字段 -ALTER TABLE tools ADD COLUMN env_config JSON DEFAULT '{}'; - --- 创建索引 -CREATE INDEX idx_tool_template ON tools(template); -``` - ---- - -## 4. 更新后的接口文档 - -### 4.1 创建工具(基于模板) - -**接口**: `POST /api/user/tools/create` - -**请求参数**: - -```typescript -interface CreateToolRequest { - name: string; // 工具名称(必填) - description?: string; // 工具描述 - template: string; // 模板名称(必填,来自 dataTemplates[].template) - envConfig: Record; // 环境变量配置(必填,根据模板 env_info 填写) -} -``` - -**请求示例(MySQL 工具)**: - -```json -{ - "name": "my-mysql-tool", - "description": "我的MySQL数据库连接工具", - "template": "mysql_agent", - "envConfig": { - "MYSQL_HOST": "mysql.example.com", - "MYSQL_USER": "root", - "MYSQL_PASSWORD": "password123", - "MYSQL_DATABASE": "mydb", - "MYSQL_PORT": "3306", - "OPENAI_API_KEY": "sk-xxx" - } -} -``` - -**请求示例(PostgreSQL 工具)**: - -```json -{ - "name": "my-pgsql-tool", - "description": "我的PostgreSQL数据库连接工具", - "template": "postgresql_agent", - "envConfig": { - "POSTGRES_HOST": "postgres.example.com", - "POSTGRES_USER": "postgres", - "POSTGRES_PASSWORD": "password123", - "POSTGRES_DATABASE": "mydb", - "POSTGRES_PORT": "5432", - "OPENAI_API_KEY": "sk-xxx" - } -} -``` - -**响应示例**: - -```json -{ - "success": true, - "data": { - "id": "d14cd898-e2cf-4150-a7f9-53b962c1c9a2", - "name": "my-mysql-tool", - "template": "mysql_agent" - }, - "message": "工具创建成功" -} -``` - -### 4.2 创建自定义 Agent(选择工具) - -**接口**: `POST /api/user/custom-agents` - -**请求参数变化**: - -```typescript -interface CreateCustomAgentRequest { - name: string; // Agent名称(必填) - tools: string[]; // 工具ID列表(必填,第一个工具决定 Agent 类型) - - // 以下参数变为可选(可从工具自动获取) - template?: string; // 模板名称(可选,默认从工具获取) - envConfig?: Record; // 额外环境变量(可选,会与工具配置合并) - - // 资源配置 - cpuRequest?: string; // 默认 "100m" - memoryRequest?: string; // 默认 "128Mi" - - // 其他可选参数 - frameworkTemplate?: string; // 默认 "MCP" - model?: string; -} -``` - -**请求示例**: - -```json -{ - "name": "my-mysql-agent", - "tools": ["d14cd898-e2cf-4150-a7f9-53b962c1c9a2"], - "cpuRequest": "500m", - "memoryRequest": "1Gi", - "model": "gpt-4" -} -``` - -**说明**: -- `template` 和 `envConfig` 从工具自动获取 -- 请求中的 `envConfig` 会与工具配置合并(请求中的优先) - ---- - -## 5. 修改检查清单 - -- [ ] **models.py**: 添加 `template` 和 `env_config` 字段到 Tool 模型 -- [ ] **数据库迁移**: 创建迁移脚本添加新字段 -- [ ] **user.py - create_tool**: 修改创建工具接口,支持 template 和 envConfig -- [ ] **user.py - create_custom_agent**: 修改创建 Agent 接口,从工具获取模板和配置 -- [ ] **user.py - get_user_tools**: 修改响应格式,包含 template 和 envConfig -- [ ] **schemas.py**: 更新请求/响应模型定义 -- [ ] **接口文档**: 更新前端接口文档 - ---- - -## 6. 测试用例 - -### 6.1 创建工具 - -```bash -# 创建 MySQL 工具 -curl -X POST /api/user/tools/create \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-mysql-tool", - "template": "mysql_agent", - "envConfig": { - "MYSQL_HOST": "mysql.example.com", - "MYSQL_USER": "root", - "MYSQL_PASSWORD": "password123", - "MYSQL_DATABASE": "mydb" - } - }' -``` - -### 6.2 创建 Agent(选择工具) - -```bash -# 创建 Agent,选择已创建的工具 -curl -X POST /api/user/custom-agents \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-mysql-agent", - "tools": ["d14cd898-e2cf-4150-a7f9-53b962c1c9a2"], - "cpuRequest": "500m", - "memoryRequest": "1Gi" - }' -``` - ---- - -**如有问题,请联系大智开发团队。** - diff --git a/Docs/外部数据工具接口文档.md b/Docs/外部数据工具接口文档.md new file mode 100644 index 0000000..2be0fca --- /dev/null +++ b/Docs/外部数据工具接口文档.md @@ -0,0 +1,843 @@ +# 外部数据工具 - 接口文档 + +> **版本**: 2026-01-23 v1.0 +> **基础路径**: `/api/user/external-tools` +> **认证方式**: Bearer Token(在请求头添加 `Authorization: Bearer `) + +--- + +## 📊 业务流程 + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ 外部数据工具流程 │ +├─────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ① 创建外部工具 ② Agent Manager 生成 ③ 创建自定义 Agent │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ POST │ ───→ │ 生成 Pydantic │ ───→ │ POST │ │ +│ │ /external-tools│ │ 工具代码文件 │ │ /custom-agents │ │ +│ └───────────────┘ └───────────────┘ │ +externalTools │ │ +│ │ │ └───────────────┘ │ +│ ↓ ↓ │ │ +│ 保存基本信息 返回 tool_ref_id 传递 tool_ref_ids │ +│ 到 PostgreSQL 给 Agent Manager │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 核心概念 + +| 概念 | 说明 | +|------|------| +| **外部数据工具** | 用户创建的连接外部 API 的工具配置 | +| **tool_ref_id** | Agent Manager 生成工具后返回的标识,部署 Agent 时传递 | +| **工具状态** | pending(等待生成), active(可用), error(生成失败) | + +### 存储职责划分 + +| 存储位置 | 存储内容 | +|---------|---------| +| **MCP-Server (PostgreSQL)** | 工具基本信息(名称、URL、方法)、tool_ref_id、状态 | +| **Agent Manager** | Pydantic 工具代码文件、完整配置(含敏感信息) | + +--- + +## 🔐 通用请求头 + +```http +Content-Type: application/json +Authorization: Bearer +``` + +--- + +## 📑 接口列表 + +### 外部数据工具接口 + +| 序号 | 接口 | 方法 | 说明 | +|:---:|------|------|------| +| 1 | `/api/user/external-tools` | POST | 创建外部数据工具 | +| 2 | `/api/user/external-tools/upload` | POST | 上传 JSON 文件创建工具 | +| 3 | `/api/user/external-tools` | GET | 获取工具列表 | +| 4 | `/api/user/external-tools/{tool_id}` | GET | 获取工具详情 | +| 5 | `/api/user/external-tools/{tool_id}` | PUT | 更新工具配置 | +| 6 | `/api/user/external-tools/{tool_id}` | DELETE | 删除工具 | +| 7 | `/api/user/external-tools/{tool_id}/test` | POST | 测试工具连接 | + +### 工具集接口 + +| 序号 | 接口 | 方法 | 说明 | +|:---:|------|------|------| +| 9 | `/api/user/toolkits` | POST | 创建工具集(最多 8 个工具) | +| 10 | `/api/user/toolkits` | GET | 获取工具集列表 | +| 11 | `/api/user/toolkits/{toolkit_id}` | GET | 获取工具集详情 | +| 12 | `/api/user/toolkits/{toolkit_id}` | PUT | 更新工具集 | +| 13 | `/api/user/toolkits/{toolkit_id}` | DELETE | 删除工具集 | + +### 自定义 Agent 接口 + +| 序号 | 接口 | 方法 | 说明 | +|:---:|------|------|------| +| 8 | `/api/user/custom-agents` | POST | 创建自定义 Agent(支持外部工具/工具集) | + +--- + +## 1️⃣ 创建外部数据工具 + +### 接口 + +``` +POST /api/user/external-tools +``` + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|:---:|------| +| `name` | string | ✅ | 工具名称(1-100字符) | +| `description` | string | ❌ | 工具描述 | +| `url` | string | ✅ | API 端点 URL | +| `method` | string | ❌ | HTTP 方法,默认 POST | +| `headers` | object | ❌ | 自定义请求头 | +| `auth` | object | ❌ | 认证配置 | +| `request_params` | object | ❌ | URL 查询参数定义(JSON Schema) | +| `request_body` | object | ❌ | 请求体定义(JSON Schema) | +| `response_mapping` | object | ❌ | 响应字段映射 | +| `timeout` | integer | ❌ | 超时时间(秒),默认 30 | +| `retry` | object | ❌ | 重试配置 | + +### 认证配置 (auth) + +#### API Key 认证 +```json +{ + "auth": { + "type": "api_key", + "key": "sk-xxxxxxxxxxxx", + "in": "header", + "name": "X-API-Key" + } +} +``` + +#### Bearer Token 认证 +```json +{ + "auth": { + "type": "bearer", + "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +#### Basic Auth 认证 +```json +{ + "auth": { + "type": "basic", + "username": "admin", + "password": "password123" + } +} +``` + +### 请求示例 + +```json +{ + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "auth": { + "type": "api_key", + "key": "sk-xxxxxxxxxxxx", + "in": "header", + "name": "X-API-Key" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + } + } + }, + "timeout": 30 +} +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "tool_ref_id": "tool-weather-abc123", + "status": "active", + "created_at": "2026-01-23T10:00:00Z" + }, + "message": "外部数据工具创建成功" +} +``` + +--- + +## 2️⃣ 上传 JSON 文件创建工具 + +### 接口 + +``` +POST /api/user/external-tools/upload +Content-Type: multipart/form-data +``` + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `file` | File | ✅ | JSON 配置文件(.json) | + +### JSON 文件格式 + +```json +{ + "name": "weather-api", + "description": "查询城市天气信息", + "url": "https://api.weather.com/v1/forecast", + "method": "GET", + "auth": { + "type": "api_key", + "key": "your-weather-api-key", + "in": "query", + "name": "apikey" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + } + } + }, + "timeout": 10 +} +``` + +### 响应示例 + +同创建接口 + +--- + +## 3️⃣ 获取工具列表 + +### 接口 + +``` +GET /api/user/external-tools +``` + +### 查询参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `status` | string | ❌ | 过滤状态:active/pending/error | +| `page` | integer | ❌ | 页码,默认 1 | +| `page_size` | integer | ❌ | 每页数量,默认 20 | + +### 响应示例 + +```json +{ + "success": true, + "data": { + "tools": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "auth_type": "api_key", + "status": "active", + "usage_count": 15, + "created_at": "2026-01-23T10:00:00Z" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} +``` + +--- + +## 4️⃣ 获取工具详情 + +### 接口 + +``` +GET /api/user/external-tools/{tool_id} +``` + +### 路径参数 + +| 参数 | 类型 | 说明 | +|-----|------|------| +| `tool_id` | string | 工具ID(UUID) | + +### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "auth_type": "api_key", + "tool_ref_id": "tool-weather-abc123", + "status": "active", + "usage_count": 15, + "created_at": "2026-01-23T10:00:00Z", + "updated_at": "2026-01-23T10:00:00Z" + } +} +``` + +> **注意**:MCP-Server 只存储基本展示信息,不存储完整配置和敏感信息。 + +--- + +## 5️⃣ 更新工具配置 + +### 接口 + +``` +PUT /api/user/external-tools/{tool_id} +``` + +### 请求参数 + +与创建接口相同,需要传递完整配置(因为 MCP-Server 不存储完整配置)。 + +### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool-v2", + "tool_ref_id": "tool-weather-abc123-v2", + "status": "active", + "updated_at": "2026-01-23T11:00:00Z" + }, + "message": "外部数据工具更新成功" +} +``` + +--- + +## 6️⃣ 删除工具 + +### 接口 + +``` +DELETE /api/user/external-tools/{tool_id} +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000" + }, + "message": "外部数据工具删除成功" +} +``` + +--- + +## 7️⃣ 测试工具连接 + +### 接口 + +``` +POST /api/user/external-tools/{tool_id}/test +``` + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `test_params` | object | ❌ | 测试参数 | + +### 请求示例 + +```json +{ + "test_params": { + "city": "北京" + } +} +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "connected": true, + "response_time_ms": 156, + "status_code": 200, + "sample_response": { + "status": "ok", + "data": { + "city": "北京", + "temperature": "15°C" + } + } + }, + "message": "工具连接测试成功" +} +``` + +--- + +## 8️⃣ 创建带有外部工具的自定义 Agent + +> **注意**: 此功能已整合到原有的自定义 Agent 创建接口中 + +### 接口 + +``` +POST /api/user/custom-agents +``` + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|:---:|------| +| `name` | string | ✅ | Agent 名称(1-63字符) | +| `template` | string | ✅ | Agent 模板名称(从 Agent Manager 获取) | +| `frameworkTemplate` | string | ❌ | 框架模板类型(A2A/langchain/MCP),默认 MCP | +| `description` | string | ❌ | Agent 描述 | +| `externalTools` | string[] | ❌ | **外部数据工具 ID 列表**(使用新的外部工具) | +| `tools` | string[] | ❌ | 内置工具 ID 列表 | +| `cpuRequest` | string | ❌ | CPU 请求量,默认 "100m" | +| `cpuLimit` | string | ❌ | CPU 限制量 | +| `memoryRequest` | string | ❌ | 内存请求量,默认 "128Mi" | +| `memoryLimit` | string | ❌ | 内存限制量 | +| `model` | string | ❌ | 使用的模型名称(会自动注入 LiteLLM 配置) | +| `envConfig` | object | ❌ | 自定义环境变量 | + +### 请求示例(使用外部数据工具) + +```json +{ + "name": "my-data-agent", + "template": "custom_agent", + "description": "我的数据处理 Agent", + "externalTools": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ], + "cpuRequest": "500m", + "cpuLimit": "1000m", + "memoryRequest": "512Mi", + "memoryLimit": "1Gi", + "model": "gpt-4" +} +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "name": "my-data-agent", + "namespace": "ai-agents", + "status": "Pending", + "tools_attached": 2, + "servicePort": 8080, + "accessInfo": { + "domain": "my-data-agent.example.com", + "domain_url": "https://my-data-agent.example.com" + }, + "modelInjected": true, + "quotaRemaining": { + "cpu": 3.5, + "memory": 7.0 + } + }, + "message": "自定义 Agent my-data-agent 创建成功" +} +``` + +--- + +## ❌ 错误响应 + +### 通用格式 + +```json +{ + "detail": { + "error": "错误代码", + "message": "错误信息" + } +} +``` + +### 常见错误码 + +| HTTP状态码 | 错误代码 | 说明 | +|-----------|---------|------| +| 400 | `invalid_config` | 工具配置格式无效 | +| 400 | `invalid_url` | URL 格式无效 | +| 400 | `invalid_tool_id` | 无效的工具 ID 格式 | +| 400 | `tool_not_active` | 工具尚未就绪 | +| 400 | `missing_tools` | 必须选择至少一个工具 | +| 400 | `quota_insufficient` | CPU/内存配额不足 | +| 403 | `no_quota` | 没有自定义Agent配额 | +| 403 | `no_model_permission` | 没有指定模型的使用权限 | +| 404 | `tool_not_found` | 工具不存在 | +| 409 | `tool_name_exists` | 工具名称已存在 | +| 500 | `am_generate_failed` | Agent Manager 生成工具失败 | + +--- + +## 📊 Agent Manager 接口(内部使用) + +以下接口由 MCP-Server 内部调用,前端无需关注: + +| 接口 | 方法 | 说明 | +|------|------|------| +| `POST /tools/generate` | 生成 Pydantic 工具文件 | +| `PUT /tools/{tool_ref_id}` | 更新工具文件 | +| `DELETE /tools/{tool_ref_id}` | 删除工具文件 | +| `POST /tools/{tool_ref_id}/test` | 测试工具连接 | +| `POST /agents` | 创建 Agent(支持 tool_refs 参数) | + +--- + +## 📋 JSON 配置文件示例 + +### 示例 1:天气查询工具 + +```json +{ + "name": "weather-api", + "description": "查询城市天气信息", + "url": "https://api.weather.com/v1/forecast", + "method": "GET", + "auth": { + "type": "api_key", + "key": "your-weather-api-key", + "in": "query", + "name": "apikey" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + }, + "units": { + "type": "string", + "description": "温度单位", + "enum": ["metric", "imperial"], + "default": "metric" + } + } + }, + "timeout": 10 +} +``` + +### 示例 2:企业内部 API + +```json +{ + "name": "internal-crm-api", + "description": "查询客户信息", + "url": "https://internal.company.com/api/v2/customers", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "auth": { + "type": "bearer", + "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "request_body": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "客户ID", + "required": true + }, + "include_orders": { + "type": "boolean", + "description": "是否包含订单信息", + "default": false + } + } + }, + "response_mapping": { + "success_field": "code", + "success_value": 0, + "data_field": "data", + "error_field": "message" + }, + "timeout": 30 +} +``` + +### 示例 3:数据库查询服务 + +```json +{ + "name": "db-query-service", + "description": "执行 SQL 查询", + "url": "https://db-gateway.company.com/query", + "method": "POST", + "auth": { + "type": "basic", + "username": "readonly", + "password": "secure-password-123" + }, + "request_body": { + "type": "object", + "properties": { + "database": { + "type": "string", + "description": "数据库名称", + "required": true + }, + "sql": { + "type": "string", + "description": "SQL 查询语句", + "required": true + } + } + }, + "timeout": 60 +} +``` + +--- + +## 🧰 工具集接口 + +工具集允许用户将多个外部数据工具组合在一起,方便部署自定义 Agent。 + +### 9️⃣ 创建工具集 + +``` +POST /api/user/toolkits +``` + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|:---:|------| +| `name` | string | ✅ | 工具集名称(1-100字符) | +| `description` | string | ❌ | 工具集描述 | +| `tool_ids` | string[] | ✅ | 外部数据工具 ID 列表(1-8 个) | + +#### 请求示例 + +```json +{ + "name": "数据分析工具集", + "description": "包含数据查询和分析相关工具", + "tool_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ] +} +``` + +#### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "660e8400-e29b-41d4-a716-446655440002", + "name": "数据分析工具集", + "description": "包含数据查询和分析相关工具", + "tool_count": 2, + "created_at": "2026-01-23T10:00:00Z" + }, + "message": "工具集创建成功" +} +``` + +--- + +### 🔟 获取工具集列表 + +``` +GET /api/user/toolkits +``` + +#### 查询参数 + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|:---:|------| +| `page` | integer | ❌ | 页码,默认 1 | +| `page_size` | integer | ❌ | 每页数量,默认 20,最大 100 | + +#### 响应示例 + +```json +{ + "success": true, + "data": { + "toolkits": [ + { + "id": "660e8400-e29b-41d4-a716-446655440002", + "name": "数据分析工具集", + "description": "包含数据查询和分析相关工具", + "tool_count": 2, + "usage_count": 5, + "created_at": "2026-01-23T10:00:00Z" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} +``` + +--- + +### 1️⃣1️⃣ 获取工具集详情 + +``` +GET /api/user/toolkits/{toolkit_id} +``` + +#### 响应示例 + +```json +{ + "success": true, + "data": { + "id": "660e8400-e29b-41d4-a716-446655440002", + "name": "数据分析工具集", + "description": "包含数据查询和分析相关工具", + "tool_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ], + "tools": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-api", + "description": "天气查询 API", + "url": "https://api.weather.com/current", + "method": "GET", + "status": "active" + }, + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "stock-api", + "description": "股票查询 API", + "url": "https://api.stock.com/price", + "method": "GET", + "status": "active" + } + ], + "usage_count": 5, + "created_at": "2026-01-23T10:00:00Z", + "updated_at": "2026-01-23T12:00:00Z" + } +} +``` + +--- + +### 1️⃣2️⃣ 更新工具集 + +``` +PUT /api/user/toolkits/{toolkit_id} +``` + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|:---:|------| +| `name` | string | ❌ | 工具集名称 | +| `description` | string | ❌ | 工具集描述 | +| `tool_ids` | string[] | ❌ | 工具 ID 列表(1-8 个) | + +--- + +### 1️⃣3️⃣ 删除工具集 + +``` +DELETE /api/user/toolkits/{toolkit_id} +``` + +> ⚠️ **注意**:删除工具集不会删除其中的工具,只是解除组合关系。 + +--- + +## 📌 在自定义 Agent 中使用工具集 + +创建自定义 Agent 时,可以通过 `toolkit` 字段指定工具集: + +```json +{ + "name": "my-data-agent", + "template": "custom_agent", + "toolkit": "660e8400-e29b-41d4-a716-446655440002", + "cpuRequest": "500m", + "memoryRequest": "512Mi" +} +``` + +也可以同时使用工具集和单独的工具(会自动合并去重): + +```json +{ + "name": "my-data-agent", + "template": "custom_agent", + "toolkit": "660e8400-e29b-41d4-a716-446655440002", + "externalTools": ["770e8400-e29b-41d4-a716-446655440003"], + "cpuRequest": "500m", + "memoryRequest": "512Mi" +} +``` + +--- + +**如有问题,请联系开发团队。** + diff --git a/Docs/Agent发送给AgentManager的数据结构文档.md b/Docs/项目文档/Agent发送给AgentManager的数据结构文档.md similarity index 100% rename from Docs/Agent发送给AgentManager的数据结构文档.md rename to Docs/项目文档/Agent发送给AgentManager的数据结构文档.md diff --git a/Docs/LiteLLM和AgentManager回调接口文档.md b/Docs/项目文档/LiteLLM和AgentManager回调接口文档.md similarity index 100% rename from Docs/LiteLLM和AgentManager回调接口文档.md rename to Docs/项目文档/LiteLLM和AgentManager回调接口文档.md diff --git a/Docs/全平台API接口清单.md b/Docs/项目文档/全平台API接口清单.md similarity index 100% rename from Docs/全平台API接口清单.md rename to Docs/项目文档/全平台API接口清单.md diff --git a/Docs/删除Agent接口文档.md b/Docs/项目文档/删除Agent接口文档.md similarity index 100% rename from Docs/删除Agent接口文档.md rename to Docs/项目文档/删除Agent接口文档.md diff --git a/Docs/数据工具与自定义Agent-前端接口文档.md b/Docs/项目文档/数据工具与自定义Agent-前端接口文档.md similarity index 100% rename from Docs/数据工具与自定义Agent-前端接口文档.md rename to Docs/项目文档/数据工具与自定义Agent-前端接口文档.md diff --git a/Docs/用户资源信息查询接口文档.md b/Docs/项目文档/用户资源信息查询接口文档.md similarity index 100% rename from Docs/用户资源信息查询接口文档.md rename to Docs/项目文档/用户资源信息查询接口文档.md diff --git a/docs/agent-domain-access-改动计划.md b/docs/agent-domain-access-改动计划.md deleted file mode 100644 index 1b00c85..0000000 --- a/docs/agent-domain-access-改动计划.md +++ /dev/null @@ -1,501 +0,0 @@ -# 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 - diff --git a/docs/dns-mapping-implementation-plan.md b/docs/dns-mapping-implementation-plan.md deleted file mode 100644 index a703224..0000000 --- a/docs/dns-mapping-implementation-plan.md +++ /dev/null @@ -1,1121 +0,0 @@ -# Agent DNS 域名映射功能实现方案 - -## 1. 功能概述 - -### 1.1 目标 -实现 Agent Pod 的 IP 地址与 GoDaddy 域名子域名的自动映射,让用户可以通过域名访问 Agent 服务。 - -### 1.2 业务场景 -``` -用户创建 Agent → Agent Manager 返回 Pod IP → 自动创建 DNS A 记录 → 用户通过子域名访问 Agent - ↓ -用户删除 Agent → 删除 K8s Pod → 同步删除 DNS A 记录 -``` - -### 1.3 核心信息 -| 配置项 | 值 | -|--------|-----| -| 主域名 | `taiji.ai.com` | -| GoDaddy API Key | `gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4` | -| GoDaddy API Secret | `AMs5RxZeaW3mFZDwXvU3Bs` | -| 生产 API 地址 | `https://api.godaddy.com` | -| 测试 API 地址 | `https://api.ote-godaddy.com` | - ---- - -## 2. 技术架构 - -### 2.1 系统架构图 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ MCP Server │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ -│ │ agents.py │────▶│ godaddy_client │────▶│ GoDaddy API │ │ -│ │ (Agent CRUD) │ │ (DNS管理) │ │ (外部服务) │ │ -│ └────────┬────────┘ └──────────────────┘ └─────────────────┘ │ -│ │ │ -│ │ ┌──────────────────┐ │ -│ └─────────────▶│ Agent Manager │ │ -│ │ (K8s Pod管理) │ │ -│ │ 返回 Pod IP │ │ -│ └──────────────────┘ │ -│ │ -│ ┌─────────────────┐ ┌──────────────────┐ │ -│ │ config.py │ │ models.py │ │ -│ │ (GoDaddy配置) │ │ (subdomain字段) │ │ -│ └─────────────────┘ └──────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 数据流程图 - -``` -创建 Agent 流程: -┌──────────┐ ┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ -│ 用户请求 │───▶│ create_agent │───▶│ Agent Manager │───▶│ 获取 Pod IP │ -└──────────┘ └───────────────┘ │ 创建 K8s Pod │ └──────┬───────┘ - └─────────────────┘ │ - ▼ -┌──────────┐ ┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ -│ 返回结果 │◀───│ 更新数据库 │◀───│ 创建 DNS A记录 │◀───│GoDaddy Client│ -│(含子域名) │ │(存储subdomain)│ │ │ └──────────────┘ -└──────────┘ └───────────────┘ └─────────────────┘ - -删除 Agent 流程: -┌──────────┐ ┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ -│ 用户请求 │───▶│ delete_agent │───▶│ Agent Manager │───▶│ 删除 K8s Pod │ -└──────────┘ └───────────────┘ │ 删除 Pod │ └──────┬───────┘ - └─────────────────┘ │ - ▼ -┌──────────┐ ┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ -│ 返回结果 │◀───│ 删除数据库记录 │◀───│ 删除 DNS A记录 │◀───│GoDaddy Client│ -└──────────┘ └───────────────┘ └─────────────────┘ └──────────────┘ -``` - ---- - -## 3. 详细设计 - -### 3.1 子域名命名规则 - -``` -格式: {agent-name}-{agent-id-short}.taiji.ai.com - -示例: -- Agent 名称: my-chatbot -- Agent ID: 550e8400-e29b-41d4-a716-446655440000 -- 生成子域名: my-chatbot-550e8400.taiji.ai.com - -规则说明: -1. agent-name 转为小写,特殊字符替换为 '-' -2. agent-id 取前8位作为唯一标识 -3. 子域名长度限制: 最大63字符(DNS标准) -4. 仅允许: a-z, 0-9, '-'(不能以'-'开头或结尾) -``` - -### 3.2 GoDaddy API 调用规范 - -#### 3.2.1 认证方式 -```http -Authorization: sso-key {API_KEY}:{API_SECRET} -Content-Type: application/json -``` - -#### 3.2.2 核心接口 - -| 操作 | 方法 | 端点 | 说明 | -|------|------|------|------| -| 创建/更新记录 | PUT | `/v1/domains/{domain}/records/A/{name}` | 创建或替换 A 记录 | -| 删除记录 | DELETE | `/v1/domains/{domain}/records/A/{name}` | 删除指定 A 记录 | -| 查询记录 | GET | `/v1/domains/{domain}/records/A/{name}` | 获取指定 A 记录 | -| 批量添加 | PATCH | `/v1/domains/{domain}/records` | 批量添加记录 | - -#### 3.2.3 请求/响应示例 - -**创建 A 记录请求:** -```http -PUT https://api.godaddy.com/v1/domains/taiji.ai.com/records/A/my-chatbot-550e8400 -Authorization: sso-key gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4:AMs5RxZeaW3mFZDwXvU3Bs -Content-Type: application/json - -[ - { - "data": "203.0.113.50", - "ttl": 600 - } -] -``` - -**成功响应:** `200 OK` (无 body) - -**查询记录响应:** -```json -[ - { - "data": "203.0.113.50", - "name": "my-chatbot-550e8400", - "ttl": 600, - "type": "A" - } -] -``` - ---- - -## 4. 代码实现方案 - -### 4.1 文件结构 - -``` -services/mcp-server/ -├── app/ -│ ├── godaddy_client.py # [新增] GoDaddy DNS 客户端 -│ ├── dns_service.py # [新增] DNS 服务层(业务逻辑封装) -│ └── routes/ -│ └── agents.py # [修改] 集成 DNS 创建/删除 -├── config.py # [修改] 添加 GoDaddy 配置 -├── models.py # [修改] Agent 模型添加 subdomain 字段 -├── schemas.py # [修改] 响应模型添加 subdomain -└── migrations/ - └── versions/ - └── xxx_add_subdomain_to_agent.py # [新增] 数据库迁移 -``` - -### 4.2 配置项设计 (config.py) - -```python -class Settings(BaseSettings): - # ... 现有配置 ... - - # GoDaddy DNS 配置 - godaddy_api_key: str = os.getenv("GODADDY_API_KEY", "") - godaddy_api_secret: str = os.getenv("GODADDY_API_SECRET", "") - godaddy_base_url: str = os.getenv("GODADDY_BASE_URL", "https://api.godaddy.com") - godaddy_domain: str = os.getenv("GODADDY_DOMAIN", "taiji.ai.com") - godaddy_dns_ttl: int = int(os.getenv("GODADDY_DNS_TTL", "600")) # 10分钟 - godaddy_enabled: bool = os.getenv("GODADDY_ENABLED", "true").lower() == "true" -``` - -**环境变量配置:** -```bash -# .env 文件 -GODADDY_API_KEY=gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4 -GODADDY_API_SECRET=AMs5RxZeaW3mFZDwXvU3Bs -GODADDY_BASE_URL=https://api.godaddy.com -GODADDY_DOMAIN=taiji.ai.com -GODADDY_DNS_TTL=600 -GODADDY_ENABLED=true -``` - -### 4.3 数据库模型变更 (models.py) - -```python -class Agent(Base): - __tablename__ = "agents" - - # ... 现有字段 ... - - # DNS 相关字段(新增) - subdomain = Column(String(255), nullable=True, index=True, comment="子域名") - full_domain = Column(String(512), nullable=True, comment="完整域名") - dns_record_id = Column(String(255), nullable=True, comment="DNS记录标识") - dns_status = Column(String(50), default="pending", comment="DNS状态: pending/active/failed/deleted") - dns_created_at = Column(DateTime, nullable=True, comment="DNS记录创建时间") - dns_updated_at = Column(DateTime, nullable=True, comment="DNS记录更新时间") -``` - -**数据库迁移脚本:** -```python -# migrations/versions/xxx_add_subdomain_to_agent.py -"""add subdomain to agent - -Revision ID: xxx -""" - -def upgrade(): - op.add_column('agents', sa.Column('subdomain', sa.String(255), nullable=True)) - op.add_column('agents', sa.Column('full_domain', sa.String(512), nullable=True)) - op.add_column('agents', sa.Column('dns_record_id', sa.String(255), nullable=True)) - op.add_column('agents', sa.Column('dns_status', sa.String(50), server_default='pending')) - op.add_column('agents', sa.Column('dns_created_at', sa.DateTime, nullable=True)) - op.add_column('agents', sa.Column('dns_updated_at', sa.DateTime, nullable=True)) - - op.create_index('ix_agents_subdomain', 'agents', ['subdomain']) - -def downgrade(): - op.drop_index('ix_agents_subdomain', 'agents') - op.drop_column('agents', 'dns_updated_at') - op.drop_column('agents', 'dns_created_at') - op.drop_column('agents', 'dns_status') - op.drop_column('agents', 'dns_record_id') - op.drop_column('agents', 'full_domain') - op.drop_column('agents', 'subdomain') -``` - -### 4.4 GoDaddy 客户端设计 (godaddy_client.py) - -```python -""" -GoDaddy DNS API 客户端 - -功能: -- 创建/更新/删除 DNS A 记录 -- 查询 DNS 记录状态 -- 自动重试和错误处理 -""" - -import httpx -import structlog -from typing import Optional, List, Dict, Any -from dataclasses import dataclass -from config import settings - -logger = structlog.get_logger(__name__) - - -@dataclass -class DNSRecord: - """DNS 记录数据结构""" - name: str # 子域名名称 - data: str # IP 地址 - ttl: int = 600 # TTL(秒) - type: str = "A" # 记录类型 - - -class GoDaddyError(Exception): - """GoDaddy API 错误""" - def __init__(self, status_code: int, message: str, detail: Any = None): - self.status_code = status_code - self.message = message - self.detail = detail - super().__init__(f"GoDaddy API Error [{status_code}]: {message}") - - -class GoDaddyClient: - """GoDaddy DNS API 客户端""" - - def __init__( - self, - api_key: str = None, - api_secret: str = None, - base_url: str = None, - domain: str = None, - timeout: float = 30.0 - ): - self.api_key = api_key or settings.godaddy_api_key - self.api_secret = api_secret or settings.godaddy_api_secret - self.base_url = base_url or settings.godaddy_base_url - self.domain = domain or settings.godaddy_domain - self.timeout = timeout - self._client: Optional[httpx.AsyncClient] = None - - @property - def _headers(self) -> Dict[str, str]: - """构建请求头""" - return { - "Authorization": f"sso-key {self.api_key}:{self.api_secret}", - "Content-Type": "application/json", - "Accept": "application/json" - } - - async def _get_client(self) -> httpx.AsyncClient: - """获取或创建 HTTP 客户端""" - if self._client is None: - self._client = httpx.AsyncClient( - base_url=self.base_url, - headers=self._headers, - timeout=self.timeout - ) - return self._client - - async def close(self): - """关闭客户端""" - if self._client: - await self._client.aclose() - self._client = None - - async def create_a_record(self, name: str, ip: str, ttl: int = None) -> bool: - """ - 创建或更新 A 记录 - - Args: - name: 子域名名称(不含主域名) - ip: IP 地址 - ttl: TTL 秒数,默认使用配置值 - - Returns: - bool: 是否成功 - """ - client = await self._get_client() - ttl = ttl or settings.godaddy_dns_ttl - - url = f"/v1/domains/{self.domain}/records/A/{name}" - payload = [{"data": ip, "ttl": ttl}] - - try: - response = await client.put(url, json=payload) - - if response.status_code == 200: - logger.info("DNS A 记录创建成功", name=name, ip=ip, domain=self.domain) - return True - else: - error_detail = response.json() if response.content else None - raise GoDaddyError( - status_code=response.status_code, - message=f"创建 DNS 记录失败", - detail=error_detail - ) - except httpx.HTTPError as e: - logger.error("GoDaddy API 请求失败", error=str(e), name=name) - raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}") - - async def delete_a_record(self, name: str) -> bool: - """ - 删除 A 记录 - - Args: - name: 子域名名称 - - Returns: - bool: 是否成功 - """ - client = await self._get_client() - url = f"/v1/domains/{self.domain}/records/A/{name}" - - try: - response = await client.delete(url) - - if response.status_code in (200, 204, 404): - # 404 也视为成功(记录不存在) - logger.info("DNS A 记录删除成功", name=name, domain=self.domain) - return True - else: - error_detail = response.json() if response.content else None - raise GoDaddyError( - status_code=response.status_code, - message=f"删除 DNS 记录失败", - detail=error_detail - ) - except httpx.HTTPError as e: - logger.error("GoDaddy API 请求失败", error=str(e), name=name) - raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}") - - async def get_a_record(self, name: str) -> Optional[DNSRecord]: - """ - 查询 A 记录 - - Args: - name: 子域名名称 - - Returns: - DNSRecord 或 None(不存在时) - """ - client = await self._get_client() - url = f"/v1/domains/{self.domain}/records/A/{name}" - - try: - response = await client.get(url) - - if response.status_code == 200: - records = response.json() - if records and len(records) > 0: - r = records[0] - return DNSRecord( - name=r.get("name", name), - data=r.get("data"), - ttl=r.get("ttl", 600), - type="A" - ) - return None - elif response.status_code == 404: - return None - else: - error_detail = response.json() if response.content else None - raise GoDaddyError( - status_code=response.status_code, - message=f"查询 DNS 记录失败", - detail=error_detail - ) - except httpx.HTTPError as e: - logger.error("GoDaddy API 请求失败", error=str(e), name=name) - raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}") - - async def update_a_record(self, name: str, ip: str, ttl: int = None) -> bool: - """ - 更新 A 记录(实际上与 create 相同,PUT 会覆盖) - """ - return await self.create_a_record(name, ip, ttl) - - async def list_a_records(self) -> List[DNSRecord]: - """ - 列出所有 A 记录 - - Returns: - DNSRecord 列表 - """ - client = await self._get_client() - url = f"/v1/domains/{self.domain}/records/A" - - try: - response = await client.get(url) - - if response.status_code == 200: - records = response.json() - return [ - DNSRecord( - name=r.get("name"), - data=r.get("data"), - ttl=r.get("ttl", 600), - type="A" - ) - for r in records - ] - else: - error_detail = response.json() if response.content else None - raise GoDaddyError( - status_code=response.status_code, - message=f"列出 DNS 记录失败", - detail=error_detail - ) - except httpx.HTTPError as e: - logger.error("GoDaddy API 请求失败", error=str(e)) - raise GoDaddyError(500, f"HTTP 请求失败: {str(e)}") - - -# 全局客户端实例(单例模式) -_godaddy_client: Optional[GoDaddyClient] = None - - -def get_godaddy_client() -> GoDaddyClient: - """获取 GoDaddy 客户端实例""" - global _godaddy_client - if _godaddy_client is None: - _godaddy_client = GoDaddyClient() - return _godaddy_client -``` - -### 4.5 DNS 服务层设计 (dns_service.py) - -```python -""" -DNS 服务层 - -封装 DNS 相关业务逻辑: -- 子域名生成规则 -- DNS 记录生命周期管理 -- 与数据库同步 -""" - -import re -import structlog -from datetime import datetime -from typing import Optional, Tuple - -from config import settings -from .godaddy_client import get_godaddy_client, GoDaddyError - -logger = structlog.get_logger(__name__) - - -class DNSService: - """DNS 服务""" - - def __init__(self): - self.domain = settings.godaddy_domain - self.enabled = settings.godaddy_enabled - - def generate_subdomain(self, agent_name: str, agent_id: str) -> str: - """ - 生成子域名 - - 规则: - 1. agent_name 转小写 - 2. 特殊字符替换为 '-' - 3. 移除连续的 '-' - 4. 添加 agent_id 前8位 - 5. 确保不以 '-' 开头或结尾 - 6. 总长度限制 63 字符 - - Args: - agent_name: Agent 名称 - agent_id: Agent UUID - - Returns: - 子域名(不含主域名) - """ - # 清理名称 - clean_name = agent_name.lower() - clean_name = re.sub(r'[^a-z0-9-]', '-', clean_name) - clean_name = re.sub(r'-+', '-', clean_name) - clean_name = clean_name.strip('-') - - # 截取 ID 前8位 - short_id = agent_id.replace('-', '')[:8].lower() - - # 组合 - subdomain = f"{clean_name}-{short_id}" - - # 长度限制(DNS 标准最大63字符) - if len(subdomain) > 63: - max_name_len = 63 - len(short_id) - 1 - clean_name = clean_name[:max_name_len].rstrip('-') - subdomain = f"{clean_name}-{short_id}" - - return subdomain - - def get_full_domain(self, subdomain: str) -> str: - """获取完整域名""" - return f"{subdomain}.{self.domain}" - - async def create_dns_record( - self, - agent_name: str, - agent_id: str, - ip: str - ) -> Tuple[str, str]: - """ - 为 Agent 创建 DNS 记录 - - Args: - agent_name: Agent 名称 - agent_id: Agent UUID - ip: Pod IP 地址 - - Returns: - (subdomain, full_domain) - - Raises: - GoDaddyError: DNS 创建失败 - """ - if not self.enabled: - logger.warning("DNS 功能未启用,跳过创建") - return ("", "") - - if not ip: - logger.warning("IP 地址为空,跳过 DNS 创建", agent_id=agent_id) - return ("", "") - - subdomain = self.generate_subdomain(agent_name, agent_id) - full_domain = self.get_full_domain(subdomain) - - client = get_godaddy_client() - - try: - await client.create_a_record(subdomain, ip) - logger.info( - "DNS 记录创建成功", - agent_id=agent_id, - subdomain=subdomain, - full_domain=full_domain, - ip=ip - ) - return (subdomain, full_domain) - except GoDaddyError as e: - logger.error( - "DNS 记录创建失败", - agent_id=agent_id, - subdomain=subdomain, - ip=ip, - error=str(e) - ) - raise - - async def delete_dns_record(self, subdomain: str) -> bool: - """ - 删除 DNS 记录 - - Args: - subdomain: 子域名 - - Returns: - 是否成功 - """ - if not self.enabled: - logger.warning("DNS 功能未启用,跳过删除") - return True - - if not subdomain: - logger.warning("子域名为空,跳过 DNS 删除") - return True - - client = get_godaddy_client() - - try: - await client.delete_a_record(subdomain) - logger.info("DNS 记录删除成功", subdomain=subdomain) - return True - except GoDaddyError as e: - logger.error("DNS 记录删除失败", subdomain=subdomain, error=str(e)) - # 删除失败不阻塞 Agent 删除流程 - return False - - async def update_dns_record( - self, - subdomain: str, - new_ip: str - ) -> bool: - """ - 更新 DNS 记录的 IP 地址 - - Args: - subdomain: 子域名 - new_ip: 新的 IP 地址 - - Returns: - 是否成功 - """ - if not self.enabled: - return False - - client = get_godaddy_client() - - try: - await client.update_a_record(subdomain, new_ip) - logger.info("DNS 记录更新成功", subdomain=subdomain, new_ip=new_ip) - return True - except GoDaddyError as e: - logger.error("DNS 记录更新失败", subdomain=subdomain, error=str(e)) - return False - - async def check_dns_record(self, subdomain: str) -> Optional[str]: - """ - 检查 DNS 记录状态 - - Args: - subdomain: 子域名 - - Returns: - IP 地址,或 None(不存在) - """ - if not self.enabled: - return None - - client = get_godaddy_client() - - try: - record = await client.get_a_record(subdomain) - return record.data if record else None - except GoDaddyError: - return None - - -# 全局服务实例 -_dns_service: Optional[DNSService] = None - - -def get_dns_service() -> DNSService: - """获取 DNS 服务实例""" - global _dns_service - if _dns_service is None: - _dns_service = DNSService() - return _dns_service -``` - -### 4.6 Agent 路由修改 (agents.py) - -```python -# 在 create_agent 函数中集成 DNS 创建 - -from ..dns_service import get_dns_service, GoDaddyError as DNSError - -@router.post("", response_model=AgentCard) -async def create_agent( - request: AgentCreateRequest, - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), -) -> AgentCard: - """创建新的 Agent""" - - # ... 现有代码:创建 Agent 和 K8s Pod ... - - # ========== 新增: DNS 记录创建 ========== - if agent.pod_ip and settings.godaddy_enabled: - try: - dns_service = get_dns_service() - subdomain, full_domain = await dns_service.create_dns_record( - agent_name=agent.name, - agent_id=str(agent.id), - ip=agent.pod_ip - ) - - # 更新数据库 - agent.subdomain = subdomain - agent.full_domain = full_domain - agent.dns_status = "active" - agent.dns_created_at = datetime.utcnow() - - # 更新 access_url - if full_domain: - agent.access_url = f"http://{full_domain}:{agent.service_port or 8000}" - - logger.info( - "DNS 记录创建成功", - agent_id=str(agent.id), - subdomain=subdomain, - full_domain=full_domain - ) - - except DNSError as e: - # DNS 创建失败不阻塞 Agent 创建 - logger.warning( - "DNS 记录创建失败,Agent 仍可通过 IP 访问", - agent_id=str(agent.id), - error=str(e) - ) - agent.dns_status = "failed" - # ======================================== - - await db.commit() - await db.refresh(agent) - - return _build_agent_card(agent) - - -@router.delete("/{agent_id}") -async def delete_agent( - agent_id: str, - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), -) -> Dict[str, Any]: - """删除 Agent""" - - # ... 现有代码:权限检查 ... - - # ========== 新增: DNS 记录删除 ========== - if agent.subdomain and settings.godaddy_enabled: - try: - dns_service = get_dns_service() - await dns_service.delete_dns_record(agent.subdomain) - logger.info( - "DNS 记录删除成功", - agent_id=agent_id, - subdomain=agent.subdomain - ) - except Exception as e: - # DNS 删除失败不阻塞 Agent 删除 - logger.warning( - "DNS 记录删除失败", - agent_id=agent_id, - subdomain=agent.subdomain, - error=str(e) - ) - # ======================================== - - # ... 现有代码:删除 K8s Pod 和数据库记录 ... -``` - -### 4.7 响应模型更新 (schemas.py) - -```python -class AgentCard(BaseModel): - """Agent 卡片信息""" - - # ... 现有字段 ... - - # DNS 相关字段(新增) - subdomain: Optional[str] = None - full_domain: Optional[str] = None - dns_status: Optional[str] = None # pending/active/failed/deleted -``` - ---- - -## 5. API 接口设计 - -### 5.1 新增 DNS 管理接口 - -#### 5.1.1 获取 Agent DNS 状态 -```http -GET /api/agents/{agent_id}/dns - -Response 200: -{ - "agent_id": "550e8400-e29b-41d4-a716-446655440000", - "subdomain": "my-chatbot-550e8400", - "full_domain": "my-chatbot-550e8400.taiji.ai.com", - "ip": "203.0.113.50", - "ttl": 600, - "dns_status": "active", - "created_at": "2026-01-14T12:00:00Z" -} -``` - -#### 5.1.2 刷新 DNS 记录 -```http -POST /api/agents/{agent_id}/dns/refresh - -Response 200: -{ - "status": "success", - "message": "DNS 记录已刷新", - "old_ip": "10.0.0.5", - "new_ip": "203.0.113.50" -} -``` - -#### 5.1.3 手动删除 DNS 记录 -```http -DELETE /api/agents/{agent_id}/dns - -Response 200: -{ - "status": "success", - "message": "DNS 记录已删除" -} -``` - ---- - -## 6. 错误处理策略 - -### 6.1 错误码定义 - -| 错误码 | 场景 | 处理方式 | -|--------|------|----------| -| DNS_001 | GoDaddy API 认证失败 | 检查 API Key/Secret,告警 | -| DNS_002 | 域名不存在或无权限 | 检查域名配置,告警 | -| DNS_003 | 记录创建失败 | 重试3次,失败后设置 dns_status=failed | -| DNS_004 | 记录删除失败 | 记录日志,不阻塞主流程 | -| DNS_005 | IP 地址无效 | 跳过 DNS 创建,设置 dns_status=pending | - -### 6.2 失败恢复机制 - -```python -# 定时任务:修复失败的 DNS 记录 -async def repair_failed_dns_records(): - """ - 定时检查并修复 dns_status='failed' 的记录 - 执行频率:每5分钟 - """ - async with get_db() as db: - # 查询失败的记录 - result = await db.execute( - select(Agent).where( - Agent.dns_status == "failed", - Agent.pod_ip.isnot(None) - ) - ) - failed_agents = result.scalars().all() - - dns_service = get_dns_service() - - for agent in failed_agents: - try: - subdomain, full_domain = await dns_service.create_dns_record( - agent.name, str(agent.id), agent.pod_ip - ) - agent.subdomain = subdomain - agent.full_domain = full_domain - agent.dns_status = "active" - agent.dns_updated_at = datetime.utcnow() - logger.info("DNS 记录修复成功", agent_id=str(agent.id)) - except Exception as e: - logger.warning("DNS 记录修复失败", agent_id=str(agent.id), error=str(e)) - - await db.commit() -``` - ---- - -## 7. 测试方案 - -### 7.1 单元测试 - -```python -# tests/test_godaddy_client.py - -import pytest -from app.godaddy_client import GoDaddyClient, GoDaddyError - -@pytest.fixture -def client(): - return GoDaddyClient( - api_key="test_key", - api_secret="test_secret", - base_url="https://api.ote-godaddy.com", # 使用测试环境 - domain="taiji.ai.com" - ) - -@pytest.mark.asyncio -async def test_create_a_record(client): - """测试创建 A 记录""" - result = await client.create_a_record("test-agent", "203.0.113.50") - assert result is True - -@pytest.mark.asyncio -async def test_delete_a_record(client): - """测试删除 A 记录""" - result = await client.delete_a_record("test-agent") - assert result is True - -@pytest.mark.asyncio -async def test_get_a_record(client): - """测试查询 A 记录""" - await client.create_a_record("test-agent", "203.0.113.50") - record = await client.get_a_record("test-agent") - assert record is not None - assert record.data == "203.0.113.50" -``` - -### 7.2 集成测试 - -```python -# tests/test_dns_integration.py - -@pytest.mark.asyncio -async def test_agent_creation_with_dns(): - """测试 Agent 创建时自动创建 DNS 记录""" - # 1. 创建 Agent - response = await client.post("/api/agents", json={ - "name": "test-chatbot", - "template": "base-agent", - "description": "Test agent" - }) - assert response.status_code == 200 - - agent = response.json() - - # 2. 验证 DNS 字段 - assert agent["subdomain"] is not None - assert agent["full_domain"].endswith("taiji.ai.com") - assert agent["dns_status"] == "active" - - # 3. 验证 GoDaddy 记录存在 - dns_response = await client.get(f"/api/agents/{agent['id']}/dns") - assert dns_response.status_code == 200 - - # 4. 清理 - await client.delete(f"/api/agents/{agent['id']}") - -@pytest.mark.asyncio -async def test_agent_deletion_removes_dns(): - """测试 Agent 删除时同步删除 DNS 记录""" - # 1. 创建 Agent - response = await client.post("/api/agents", json={...}) - agent = response.json() - subdomain = agent["subdomain"] - - # 2. 删除 Agent - await client.delete(f"/api/agents/{agent['id']}") - - # 3. 验证 DNS 记录已删除 - dns_client = get_godaddy_client() - record = await dns_client.get_a_record(subdomain) - assert record is None -``` - -### 7.3 手动测试脚本 - -```python -# scripts/test_godaddy_api.py -""" -手动测试 GoDaddy API 连通性 - -使用方法: -python scripts/test_godaddy_api.py -""" - -import asyncio -import sys -sys.path.append(".") - -from app.godaddy_client import GoDaddyClient - -async def main(): - client = GoDaddyClient() - - print("=" * 50) - print("GoDaddy API 连通性测试") - print("=" * 50) - - # 1. 测试创建记录 - print("\n1. 创建测试 A 记录...") - try: - await client.create_a_record("test-dns-integration", "1.2.3.4") - print(" ✓ 创建成功") - except Exception as e: - print(f" ✗ 创建失败: {e}") - return - - # 2. 测试查询记录 - print("\n2. 查询 A 记录...") - try: - record = await client.get_a_record("test-dns-integration") - if record: - print(f" ✓ 查询成功: {record.name} -> {record.data}") - else: - print(" ✗ 记录不存在") - except Exception as e: - print(f" ✗ 查询失败: {e}") - - # 3. 测试删除记录 - print("\n3. 删除测试 A 记录...") - try: - await client.delete_a_record("test-dns-integration") - print(" ✓ 删除成功") - except Exception as e: - print(f" ✗ 删除失败: {e}") - - # 4. 列出所有 A 记录 - print("\n4. 列出所有 A 记录...") - try: - records = await client.list_a_records() - print(f" 共 {len(records)} 条记录:") - for r in records[:5]: - print(f" - {r.name} -> {r.data} (TTL: {r.ttl})") - if len(records) > 5: - print(f" ... 还有 {len(records) - 5} 条") - except Exception as e: - print(f" ✗ 列出失败: {e}") - - await client.close() - print("\n" + "=" * 50) - print("测试完成") - -if __name__ == "__main__": - asyncio.run(main()) -``` - ---- - -## 8. 部署检查清单 - -### 8.1 环境变量配置 -```bash -# 生产环境必须配置 -GODADDY_API_KEY=gGpZZZ2GxwVZ_7Qe1tr9rCZkErS46Za8uc4 -GODADDY_API_SECRET=AMs5RxZeaW3mFZDwXvU3Bs -GODADDY_DOMAIN=taiji.ai.com -GODADDY_BASE_URL=https://api.godaddy.com -GODADDY_ENABLED=true -GODADDY_DNS_TTL=600 -``` - -### 8.2 数据库迁移 -```bash -# 执行迁移 -alembic upgrade head - -# 验证字段 -psql -c "SELECT column_name FROM information_schema.columns WHERE table_name='agents' AND column_name LIKE 'dns%';" -``` - -### 8.3 功能验证 -- [ ] GoDaddy API 认证成功 -- [ ] 创建 Agent 后 subdomain 字段有值 -- [ ] full_domain 格式正确 (`xxx.taiji.ai.com`) -- [ ] 通过域名可以访问 Agent 服务 -- [ ] 删除 Agent 后 DNS 记录被清理 -- [ ] 失败情况下有正确的日志和 dns_status - ---- - -## 9. 后续优化建议 - -### 9.1 短期优化 -1. **DNS 缓存**: 本地缓存 DNS 记录状态,减少 API 调用 -2. **批量操作**: 支持批量创建/删除 DNS 记录 -3. **监控告警**: DNS 操作失败时发送告警 - -### 9.2 中期优化 -1. **HTTPS 支持**: 集成 Let's Encrypt 自动申请 SSL 证书 -2. **自定义域名**: 支持用户绑定自己的域名 -3. **DNS 健康检查**: 定期检查 DNS 解析是否正常 - -### 9.3 长期规划 -1. **多 DNS 提供商**: 支持 Cloudflare、AWS Route53 等 -2. **智能 DNS**: 基于地理位置的 DNS 解析 -3. **域名市场**: 提供子域名购买/租赁服务 - ---- - -## 10. 参考资料 - -- [GoDaddy API 文档](https://developer.godaddy.com/doc/endpoint/domains) -- [DNS A 记录规范](https://tools.ietf.org/html/rfc1035) -- [子域名命名规范](https://tools.ietf.org/html/rfc1123) - diff --git a/plans/自定义外部数据工具功能方案.md b/plans/自定义外部数据工具功能方案.md new file mode 100644 index 0000000..9bf0ef5 --- /dev/null +++ b/plans/自定义外部数据工具功能方案.md @@ -0,0 +1,1117 @@ +# 自定义外部数据工具功能方案 + +> **版本**: v1.0 +> **日期**: 2026-01-23 +> **状态**: ✅ 已实现 + +--- + +## 1. 功能概述 + +### 1.1 背景 + +用户需要能够创建自定义的外部数据工具,这些工具可以连接到外部 API 服务(如第三方数据接口、企业内部系统等),并在部署自定义 Agent 时使用这些工具。 + +### 1.2 核心流程 + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ 自定义外部数据工具流程 │ +├─────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ① 用户上传工具配置 ② MCP-Server 处理 ③ Agent Manager 生成工具 │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ 表单填写 或 │ ───→ │ 验证配置格式 │ ───→ │ 生成 Pydantic │ │ +│ │ 上传JSON文件 │ │ 发送给AM服务 │ │ 工具代码文件 │ │ +│ └───────────────┘ └───────────────┘ └───────────────┘ │ +│ │ │ │ +│ ↓ ↓ │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ 存储工具信息 │ ←─── │ 返回工具标识 │ │ +│ │ 到 PostgreSQL │ │ (tool_ref_id) │ │ +│ └───────────────┘ └───────────────┘ │ +│ │ +│ ④ 创建自定义Agent时 ⑤ 部署Agent │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ 选择已创建的 │ ───────────────→ │ 传递工具标识 │ │ +│ │ 外部数据工具 │ │ 给AgentManager │ │ +│ └───────────────┘ └───────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 变更说明 + +| 变更项 | 旧方案 | 新方案 | +|--------|--------|--------| +| 工具类型 | 模板工具 + API工具 | **统一为外部数据工具** | +| 工具代码生成 | 无 | **Agent Manager 生成并存储 Pydantic 工具文件** | +| MCP-Server 存储 | 存储完整配置 | **只存储工具基本信息 + tool_ref_id 标识** | +| 部署方式 | 传递环境变量配置 | **传递工具标识列表,AM 根据标识加载文件部署** | + +### 1.4 存储职责划分 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 存储职责划分 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MCP-Server (PostgreSQL) Agent Manager (文件系统/存储) │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ • 工具名称、描述 │ │ • Pydantic 工具代码文件 │ │ +│ │ • URL、方法、认证类型 │ │ • 工具运行时配置 │ │ +│ │ • tool_ref_id (标识) │ ←───→ │ • 根据 tool_ref_id 索引 │ │ +│ │ • 用户归属信息 │ │ │ │ +│ │ • 创建/更新时间 │ │ │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ 部署流程: │ +│ MCP-Server 传 tool_ref_ids → Agent Manager 查找对应文件 → 部署到 AKS │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 数据结构设计 + +### 2.1 外部数据工具 JSON 模板格式 + +用户上传的 JSON 文件格式定义: + +```json +{ + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Accept": "application/json" + }, + "auth": { + "type": "api_key", + "key": "your-api-key-here", + "in": "header", + "name": "X-API-Key" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + }, + "days": { + "type": "integer", + "description": "预报天数", + "default": 7, + "required": false + } + } + }, + "request_body": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "查询内容" + }, + "options": { + "type": "object", + "description": "查询选项", + "properties": { + "format": { + "type": "string", + "enum": ["json", "xml"], + "default": "json" + } + } + } + } + }, + "response_mapping": { + "success_field": "status", + "success_value": "ok", + "data_field": "data.result", + "error_field": "error.message" + }, + "timeout": 30, + "retry": { + "max_attempts": 3, + "delay_seconds": 1 + } +} +``` + +### 2.2 JSON 字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `name` | string | ✅ | 工具名称(唯一标识,1-100字符,支持中英文) | +| `description` | string | ❌ | 工具描述 | +| `url` | string | ✅ | API 端点 URL | +| `method` | string | ✅ | HTTP 方法(GET/POST/PUT/DELETE/PATCH) | +| `headers` | object | ❌ | 自定义请求头(键值对) | +| `auth` | object | ❌ | 认证配置 | +| `auth.type` | string | ✅ | 认证类型:`api_key`/`bearer`/`basic`/`none` | +| `auth.key` | string | 条件 | API Key 或 Bearer Token | +| `auth.in` | string | 条件 | Key 位置:`header`/`query`/`body` | +| `auth.name` | string | 条件 | Key 名称(如 `X-API-Key`、`Authorization`) | +| `auth.username` | string | 条件 | Basic Auth 用户名 | +| `auth.password` | string | 条件 | Basic Auth 密码 | +| `request_params` | object | ❌ | URL 查询参数定义(JSON Schema 格式) | +| `request_body` | object | ❌ | 请求体定义(JSON Schema 格式) | +| `response_mapping` | object | ❌ | 响应字段映射 | +| `timeout` | integer | ❌ | 超时时间(秒),默认 30 | +| `retry` | object | ❌ | 重试配置 | + +### 2.3 认证类型详解 + +#### API Key 认证 +```json +{ + "auth": { + "type": "api_key", + "key": "sk-xxxxxxxxxxxx", + "in": "header", + "name": "X-API-Key" + } +} +``` + +#### Bearer Token 认证 +```json +{ + "auth": { + "type": "bearer", + "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +#### Basic Auth 认证 +```json +{ + "auth": { + "type": "basic", + "username": "admin", + "password": "password123" + } +} +``` + +#### 无认证 +```json +{ + "auth": { + "type": "none" + } +} +``` + +### 2.4 数据库模型设计 + +#### ExternalDataTool 模型(新增) + +> **设计原则**:MCP-Server 只存储必要的元信息和关联标识,工具的实际代码和运行配置由 Agent Manager 管理。 + +```python +class ExternalDataTool(BaseModel, Base): + """外部数据工具模型 + + 存储工具的基本信息和 Agent Manager 返回的标识, + 工具的 Pydantic 代码文件由 Agent Manager 生成并存储。 + """ + __tablename__ = "external_data_tools" + + # 基本信息(用于前端展示) + name = Column(String(100), nullable=False) + description = Column(Text) + url = Column(String(500), nullable=False) # 仅用于展示,实际配置在 AM + method = Column(String(10), nullable=False, default="POST") + auth_type = Column(String(20), default="none") # 仅用于展示 + + # Agent Manager 关联(核心字段) + tool_ref_id = Column(String(100), unique=True) # AM 返回的工具标识,部署时传给 AM + status = Column(String(20), default="pending") # pending, active, error + error_message = Column(Text) # 生成失败时的错误信息 + + # 归属信息 + owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id")) + is_active = Column(Boolean, default=True) + + # 统计信息 + usage_count = Column(Integer, default=0) # 被 Agent 使用次数 + + # 关系 + owner = relationship("User", back_populates="external_data_tools") + + __table_args__ = ( + Index("idx_external_data_tool_owner", owner_id), + Index("idx_external_data_tool_ref", tool_ref_id), + ) +``` + +**说明**: +- `tool_ref_id` 是核心字段,部署 Agent 时传递给 Agent Manager +- URL、method、auth_type 仅用于前端列表展示,实际的完整配置在 Agent Manager 侧 +- 不存储敏感信息(API Key 等),这些信息只传给 Agent Manager,不在 MCP-Server 落库 + +#### 删除旧的 Tool 模型相关字段 + +需要删除 `Tool` 模型中的以下字段(或废弃整个模型): +- `template` +- `env_config` +- 其他模板工具相关字段 + +--- + +## 3. API 接口设计(MCP-Server 侧) + +### 3.1 接口列表 + +| 序号 | 接口 | 方法 | 说明 | +|:---:|------|------|------| +| 1 | `/api/user/external-tools` | POST | 创建外部数据工具 | +| 2 | `/api/user/external-tools` | GET | 获取用户的外部数据工具列表 | +| 3 | `/api/user/external-tools/{tool_id}` | GET | 获取工具详情 | +| 4 | `/api/user/external-tools/{tool_id}` | PUT | 更新工具配置 | +| 5 | `/api/user/external-tools/{tool_id}` | DELETE | 删除工具 | +| 6 | `/api/user/external-tools/upload` | POST | 上传 JSON 文件创建工具 | +| 7 | `/api/user/external-tools/{tool_id}/test` | POST | 测试工具连接 | +| 8 | `/api/user/custom-agents` | POST | 创建自定义 Agent(更新) | + +### 3.2 创建外部数据工具 + +#### 接口 + +``` +POST /api/user/external-tools +``` + +#### 请求体 + +```json +{ + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "auth": { + "type": "api_key", + "key": "sk-xxxxxxxxxxxx", + "in": "header", + "name": "X-API-Key" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + } + } + }, + "request_body": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "查询内容" + } + } + }, + "response_mapping": { + "data_field": "data.result" + }, + "timeout": 30 +} +``` + +#### 响应 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "tool_ref_id": "tool-weather-abc123", + "status": "active", + "created_at": "2026-01-23T10:00:00Z" + }, + "message": "外部数据工具创建成功" +} +``` + +### 3.3 上传 JSON 文件创建工具 + +#### 接口 + +``` +POST /api/user/external-tools/upload +Content-Type: multipart/form-data +``` + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `file` | File | ✅ | JSON 配置文件(.json) | + +#### 响应 + +同 3.2 创建接口 + +### 3.4 获取工具列表 + +#### 接口 + +``` +GET /api/user/external-tools +``` + +#### 查询参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `status` | string | ❌ | 过滤状态:active/pending/error | +| `page` | integer | ❌ | 页码,默认 1 | +| `page_size` | integer | ❌ | 每页数量,默认 20 | + +#### 响应 + +> **说明**:列表只返回基本展示信息,完整配置由 Agent Manager 管理。 + +```json +{ + "success": true, + "data": { + "tools": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "auth_type": "api_key", + "status": "active", + "usage_count": 15, + "created_at": "2026-01-23T10:00:00Z" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} +``` + +### 3.5 获取工具详情 + +#### 接口 + +``` +GET /api/user/external-tools/{tool_id} +``` + +#### 响应 + +> **注意**:MCP-Server 只存储基本展示信息,不存储完整配置和敏感信息。 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "auth_type": "api_key", + "tool_ref_id": "tool-weather-abc123", + "status": "active", + "usage_count": 15, + "created_at": "2026-01-23T10:00:00Z", + "updated_at": "2026-01-23T10:00:00Z" + } +} +``` + +> 如需查看工具的完整配置(包括参数定义等),可调用 Agent Manager 的接口获取。 + +### 3.6 更新工具配置 + +#### 接口 + +``` +PUT /api/user/external-tools/{tool_id} +``` + +#### 请求体 + +与创建接口格式相同,需要传递完整的工具配置(因为 MCP-Server 不存储完整配置)。 + +```json +{ + "name": "weather-query-tool-v2", + "description": "更新后的天气查询工具", + "url": "https://api.weather.com/v2/forecast", + "method": "POST", + "auth": { + "type": "api_key", + "key": "new-api-key", + "in": "header", + "name": "X-API-Key" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + } + } + }, + "timeout": 30 +} +``` + +#### 响应 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "weather-query-tool-v2", + "tool_ref_id": "tool-weather-abc123-v2", + "status": "active", + "updated_at": "2026-01-23T11:00:00Z" + }, + "message": "外部数据工具更新成功" +} +``` + +#### 处理流程 + +1. MCP-Server 接收完整配置 +2. 调用 Agent Manager 更新接口(传递新配置) +3. Agent Manager 重新生成工具文件,返回新的 `tool_ref_id` +4. MCP-Server 更新数据库中的基本信息和 `tool_ref_id` + +### 3.7 删除工具 + +#### 接口 + +``` +DELETE /api/user/external-tools/{tool_id} +``` + +#### 响应 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000" + }, + "message": "外部数据工具删除成功" +} +``` + +> **注意**:删除工具时需要通知 Agent Manager 删除对应的工具文件。 + +### 3.8 测试工具连接 + +#### 接口 + +``` +POST /api/user/external-tools/{tool_id}/test +``` + +#### 请求体(可选测试参数) + +```json +{ + "test_params": { + "city": "北京" + } +} +``` + +#### 响应 + +```json +{ + "success": true, + "data": { + "connected": true, + "response_time_ms": 156, + "status_code": 200, + "sample_response": { + "status": "ok", + "data": { + "city": "北京", + "temperature": "15°C" + } + } + }, + "message": "工具连接测试成功" +} +``` + +### 3.9 创建自定义 Agent(更新) + +#### 接口 + +``` +POST /api/user/custom-agents +``` + +#### 请求体 + +```json +{ + "name": "my-data-agent", + "description": "我的数据处理 Agent", + "external_tools": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ], + "cpuRequest": "500m", + "cpuLimit": "1000m", + "memoryRequest": "512Mi", + "memoryLimit": "1Gi", + "model": "gpt-4" +} +``` + +#### 字段说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `name` | string | ✅ | Agent 名称 | +| `description` | string | ❌ | Agent 描述 | +| `external_tools` | string[] | ✅ | **外部数据工具 ID 列表** | +| `cpuRequest` | string | ❌ | CPU 请求量,默认 "100m" | +| `cpuLimit` | string | ❌ | CPU 限制量 | +| `memoryRequest` | string | ❌ | 内存请求量,默认 "128Mi" | +| `memoryLimit` | string | ❌ | 内存限制量 | +| `model` | string | ❌ | 使用的模型名称 | + +#### 响应 + +```json +{ + "success": true, + "data": { + "name": "my-data-agent", + "namespace": "ai-agents", + "status": "Pending", + "tools_attached": 2, + "quotaRemaining": { + "cpu": 3.5, + "memory": 7.0 + } + }, + "message": "自定义 Agent 创建成功" +} +``` + +--- + +## 4. Agent Manager 接口设计(模拟) + +### 4.1 接口列表 + +| 序号 | 接口 | 方法 | 说明 | +|:---:|------|------|------| +| 1 | `/tools/generate` | POST | 生成 Pydantic 工具文件 | +| 2 | `/tools/{tool_ref_id}` | PUT | 更新工具文件 | +| 3 | `/tools/{tool_ref_id}` | DELETE | 删除工具文件 | +| 4 | `/agents` | POST | 创建 Agent(更新:支持工具标识列表) | + +### 4.2 生成工具文件 + +#### 接口 + +``` +POST {AGENT_MANAGER_URL}/tools/generate +``` + +#### 请求体 + +```json +{ + "name": "weather-query-tool", + "description": "查询天气信息的外部数据工具", + "url": "https://api.weather.com/v1/forecast", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "auth": { + "type": "api_key", + "key": "sk-xxxxxxxxxxxx", + "in": "header", + "name": "X-API-Key" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + } + } + }, + "request_body": {}, + "response_mapping": { + "data_field": "data.result" + }, + "timeout": 30, + "user_id": "user-123", + "tenant_id": "tenant-456" +} +``` + +#### 响应 + +```json +{ + "success": true, + "tool_ref_id": "tool-weather-abc123", + "tool_name": "weather_query_tool", + "status": "active", + "message": "Pydantic 工具文件生成成功" +} +``` + +### 4.3 更新工具文件 + +#### 接口 + +``` +PUT {AGENT_MANAGER_URL}/tools/{tool_ref_id} +``` + +#### 请求体 + +同 4.2 生成接口(需要传递完整的工具配置) + +#### 响应 + +```json +{ + "success": true, + "tool_ref_id": "tool-weather-abc123-v2", + "status": "active", + "message": "工具文件更新成功" +} +``` + +> **说明**:Agent Manager 会生成新的工具文件,可能返回新的 `tool_ref_id`(版本更新)。 + +### 4.4 删除工具文件 + +#### 接口 + +``` +DELETE {AGENT_MANAGER_URL}/tools/{tool_ref_id} +``` + +#### 响应 + +```json +{ + "success": true, + "message": "工具文件删除成功" +} +``` + +### 4.5 创建 Agent(更新) + +#### 接口 + +``` +POST {AGENT_MANAGER_URL}/agents +``` + +#### 请求体(新增 tool_refs 字段) + +```json +{ + "name": "my-data-agent", + "template": "custom_agent", + "config": { + "user_id": "user-123", + "cpu_request": "500m", + "cpu_limit": "1000m", + "memory_request": "512Mi", + "memory_limit": "1Gi", + "replicas": 1 + }, + "env": { + "LLM_BASE_URL": "https://litellm.xxx.io", + "OPENAI_API_KEY": "sk-xxx", + "MODEL_NAME": "gpt-4" + }, + "tool_refs": [ + "tool-weather-abc123", + "tool-stock-def456" + ] +} +``` + +#### 新增字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| `tool_refs` | string[] | ❌ | 外部数据工具标识列表,Agent 运行时加载这些工具 | + +--- + +## 5. 工作流程详解 + +### 5.1 创建工具流程 + +``` +┌──────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐ +│ 前端 │────→│ MCP-Server │────→│ Agent Manager │────→│ MCP-Server │ +└──────────┘ └──────────────┘ └───────────────┘ └──────────────┘ + │ │ │ │ + │ 1.提交配置 │ │ │ + │ (表单/JSON) │ │ │ + │ ───────────────>│ │ │ + │ │ │ │ + │ │ 2.验证配置格式 │ │ + │ │ 3.调用 AM 接口 │ │ + │ │ ───────────────────>│ │ + │ │ │ │ + │ │ │ 4.生成 Pydantic │ + │ │ │ 工具文件 │ + │ │ │ │ + │ │ 5.返回 tool_ref_id │ │ + │ │ <───────────────────│ │ + │ │ │ │ + │ │ 6.存储到数据库 │ │ + │ │ (关联 tool_ref_id) │ │ + │ │ │ │ + │ 7.返回结果 │ │ │ + │ <───────────────│ │ │ + │ │ │ │ +``` + +### 5.2 创建 Agent 并使用工具流程 + +``` +┌──────────┐ ┌──────────────┐ ┌───────────────┐ +│ 前端 │────→│ MCP-Server │────→│ Agent Manager │ +└──────────┘ └──────────────┘ └───────────────┘ + │ │ │ + │ 1.创建Agent │ │ + │ 选择工具列表 │ │ + │ ───────────────>│ │ + │ │ │ + │ │ 2.查询工具信息 │ + │ │ 获取 tool_ref_ids │ + │ │ │ + │ │ 3.调用 AM 创建 │ + │ │ 传递 tool_refs │ + │ │ ───────────────────>│ + │ │ │ + │ │ │ 4.部署 Agent + │ │ │ 加载工具文件 + │ │ │ + │ │ 5.返回部署结果 │ + │ │ <───────────────────│ + │ │ │ + │ 6.返回结果 │ │ + │ <───────────────│ │ + │ │ │ +``` + +--- + +## 6. 前端对接说明 + +### 6.1 工具管理页面 + +#### 功能需求 + +1. **工具列表展示** + - 显示用户创建的所有外部数据工具 + - 显示状态(active/pending/error) + - 显示使用次数 + - 支持搜索和筛选 + +2. **创建工具** + - 方式一:表单填写 + - 方式二:上传 JSON 文件 + - 实时验证配置格式 + +3. **工具详情** + - 查看完整配置(敏感信息脱敏) + - 编辑配置 + - 测试连接 + +4. **删除工具** + - 检查是否被 Agent 使用 + - 确认删除 + +#### 表单字段 + +| 字段 | 组件类型 | 说明 | +|------|---------|------| +| 工具名称 | Input | 必填,1-100字符 | +| 描述 | Textarea | 可选 | +| API URL | Input | 必填,URL 格式 | +| HTTP 方法 | Select | GET/POST/PUT/DELETE/PATCH | +| 请求头 | 键值对编辑器 | 可选 | +| 认证类型 | Select | api_key/bearer/basic/none | +| 认证配置 | 动态表单 | 根据认证类型显示 | +| 请求参数 | Schema 编辑器 | 可选 | +| 请求体 | Schema 编辑器 | 可选 | +| 超时时间 | InputNumber | 默认 30 秒 | + +### 6.2 创建 Agent 时选择工具 + +#### 流程 + +1. 获取用户的外部数据工具列表 +2. 多选工具 +3. 提交创建 Agent 请求时包含 `external_tools` 字段 + +#### UI 建议 + +- 使用穿梭框或多选列表 +- 显示工具名称、描述、状态 +- 禁用 error 状态的工具 + +--- + +## 7. 实现步骤 + +### 7.1 Phase 1: 数据库和模型(预计 1 天) + +- [ ] 创建 `ExternalDataTool` 模型 +- [ ] 编写数据库迁移脚本 +- [ ] 添加模型关系 +- [ ] 废弃/清理旧的 Tool 模型相关代码 + +### 7.2 Phase 2: Agent Manager 客户端(预计 0.5 天) + +- [ ] 在 `agent_manager_client.py` 中添加工具相关方法 + - `generate_tool()` + - `update_tool()` + - `delete_tool()` +- [ ] 更新 `create_agent()` 方法,支持 `tool_refs` 参数 + +### 7.3 Phase 3: API 接口实现(预计 2 天) + +- [ ] 实现 `POST /api/user/external-tools` 创建工具 +- [ ] 实现 `POST /api/user/external-tools/upload` 上传 JSON +- [ ] 实现 `GET /api/user/external-tools` 获取列表 +- [ ] 实现 `GET /api/user/external-tools/{tool_id}` 获取详情 +- [ ] 实现 `PUT /api/user/external-tools/{tool_id}` 更新工具 +- [ ] 实现 `DELETE /api/user/external-tools/{tool_id}` 删除工具 +- [ ] 实现 `POST /api/user/external-tools/{tool_id}/test` 测试连接 +- [ ] 更新 `POST /api/user/custom-agents` 支持外部工具 + +### 7.4 Phase 4: 删除旧接口和代码(预计 0.5 天) + +- [ ] 删除旧的 `/api/user/tools/*` 接口 +- [ ] 删除旧的 `/api/user/custom-agents/templates` 接口 +- [ ] 清理相关的 Schema 定义 +- [ ] 更新文档 + +### 7.5 Phase 5: 测试和文档(预计 1 天) + +- [ ] 编写单元测试 +- [ ] 编写集成测试 +- [ ] 更新 API 文档 +- [ ] 更新前端对接文档 + +--- + +## 8. 错误码定义 + +| HTTP状态码 | 错误代码 | 说明 | +|-----------|---------|------| +| 400 | `invalid_config` | 工具配置格式无效 | +| 400 | `invalid_url` | URL 格式无效 | +| 400 | `invalid_auth` | 认证配置无效 | +| 400 | `invalid_json_file` | JSON 文件格式无效 | +| 400 | `tool_in_use` | 工具正在被 Agent 使用,无法删除 | +| 404 | `tool_not_found` | 工具不存在 | +| 409 | `tool_name_exists` | 工具名称已存在 | +| 500 | `am_generate_failed` | Agent Manager 生成工具失败 | +| 503 | `am_unavailable` | Agent Manager 服务不可用 | + +--- + +## 9. 安全考虑 + +### 9.1 敏感信息处理 + +1. **敏感信息不落库** + - API Key、Bearer Token、密码等敏感信息**不存储在 MCP-Server** + - 敏感信息只在创建时传递给 Agent Manager,由 AM 安全存储 + +2. **传输安全** + - MCP-Server 与 Agent Manager 之间通过 HTTPS 传输 + - 敏感信息在传输层加密 + +### 9.2 权限控制 + +1. 用户只能操作自己创建的工具 +2. 删除工具前检查是否被 Agent 使用 +3. API 调用需要 JWT 认证 + +### 9.3 输入验证 + +1. URL 格式验证(必须是有效的 HTTP/HTTPS URL) +2. JSON Schema 格式验证 +3. 超时时间范围限制(1-300 秒) + +--- + +## 10. 附录 + +### 10.1 JSON 配置文件示例 + +#### 示例 1:天气查询工具 + +```json +{ + "name": "weather-api", + "description": "查询城市天气信息", + "url": "https://api.weather.com/v1/forecast", + "method": "GET", + "auth": { + "type": "api_key", + "key": "your-weather-api-key", + "in": "query", + "name": "apikey" + }, + "request_params": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称", + "required": true + }, + "units": { + "type": "string", + "description": "温度单位", + "enum": ["metric", "imperial"], + "default": "metric" + } + } + }, + "response_mapping": { + "data_field": "data.forecast" + }, + "timeout": 10 +} +``` + +#### 示例 2:企业内部 API + +```json +{ + "name": "internal-crm-api", + "description": "查询客户信息", + "url": "https://internal.company.com/api/v2/customers", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "X-Request-ID": "{{uuid}}" + }, + "auth": { + "type": "bearer", + "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "request_body": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "客户ID", + "required": true + }, + "include_orders": { + "type": "boolean", + "description": "是否包含订单信息", + "default": false + } + } + }, + "response_mapping": { + "success_field": "code", + "success_value": 0, + "data_field": "data", + "error_field": "message" + }, + "timeout": 30, + "retry": { + "max_attempts": 3, + "delay_seconds": 2 + } +} +``` + +#### 示例 3:数据库查询服务 + +```json +{ + "name": "db-query-service", + "description": "执行 SQL 查询", + "url": "https://db-gateway.company.com/query", + "method": "POST", + "auth": { + "type": "basic", + "username": "readonly", + "password": "secure-password-123" + }, + "request_body": { + "type": "object", + "properties": { + "database": { + "type": "string", + "description": "数据库名称", + "required": true + }, + "sql": { + "type": "string", + "description": "SQL 查询语句", + "required": true + }, + "params": { + "type": "array", + "description": "查询参数", + "items": { + "type": "string" + } + } + } + }, + "timeout": 60 +} +``` + +--- + +**文档结束** + diff --git a/services/mcp-server/app/agent_manager_client.py b/services/mcp-server/app/agent_manager_client.py index 8bf5e4f..82f84a1 100644 --- a/services/mcp-server/app/agent_manager_client.py +++ b/services/mcp-server/app/agent_manager_client.py @@ -1156,6 +1156,282 @@ class AgentManagerClient: count=data.get("count", 0) ) + # ==================== External Data Tool Management ==================== + # 外部数据工具管理接口 + # Agent Manager 负责生成和存储 Pydantic 工具文件,返回 tool_ref_id 给 MCP-Server + + async def generate_external_tool( + self, + name: str, + description: str, + url: str, + method: str, + user_id: str, + tenant_id: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + auth: Optional[Dict[str, Any]] = None, + request_params: Optional[Dict[str, Any]] = None, + request_body: Optional[Dict[str, Any]] = None, + response_mapping: Optional[Dict[str, Any]] = None, + timeout: int = 30, + retry: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + 生成外部数据工具 + + 调用: POST /tools/generate + + 将工具配置发送给 Agent Manager,AM 会: + 1. 根据配置生成 Pydantic 工具代码文件 + 2. 存储工具文件和配置 + 3. 返回 tool_ref_id(工具标识) + + Args: + name: 工具名称 + description: 工具描述 + url: API 端点 URL + method: HTTP 方法(GET/POST/PUT/DELETE/PATCH) + user_id: 用户 ID + tenant_id: 租户 ID(可选) + headers: 自定义请求头 + auth: 认证配置(type, key, in, name 等) + request_params: URL 查询参数定义(JSON Schema 格式) + request_body: 请求体定义(JSON Schema 格式) + response_mapping: 响应字段映射 + timeout: 超时时间(秒) + retry: 重试配置 + + Returns: + { + "success": true, + "tool_ref_id": "tool-xxx-123", + "tool_name": "weather_query_tool", + "status": "active", + "message": "工具生成成功" + } + """ + payload: Dict[str, Any] = { + "name": name, + "description": description, + "url": url, + "method": method, + "user_id": user_id, + } + + if tenant_id: + payload["tenant_id"] = tenant_id + if headers: + payload["headers"] = headers + if auth: + payload["auth"] = auth + if request_params: + payload["request_params"] = request_params + if request_body: + payload["request_body"] = request_body + if response_mapping: + payload["response_mapping"] = response_mapping + if timeout: + payload["timeout"] = timeout + if retry: + payload["retry"] = retry + + logger.info( + "generating_external_tool", + name=name, + url=url, + method=method, + user_id=user_id + ) + + return await self._request("POST", "/tools/generate", json=payload) + + async def update_external_tool( + self, + tool_ref_id: str, + name: str, + description: str, + url: str, + method: str, + user_id: str, + tenant_id: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + auth: Optional[Dict[str, Any]] = None, + request_params: Optional[Dict[str, Any]] = None, + request_body: Optional[Dict[str, Any]] = None, + response_mapping: Optional[Dict[str, Any]] = None, + timeout: int = 30, + retry: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + 更新外部数据工具 + + 调用: PUT /tools/{tool_ref_id} + + Agent Manager 会重新生成工具文件,可能返回新的 tool_ref_id。 + + Args: + tool_ref_id: 原工具标识 + 其他参数同 generate_external_tool + + Returns: + { + "success": true, + "tool_ref_id": "tool-xxx-123-v2", + "status": "active", + "message": "工具更新成功" + } + """ + payload: Dict[str, Any] = { + "name": name, + "description": description, + "url": url, + "method": method, + "user_id": user_id, + } + + if tenant_id: + payload["tenant_id"] = tenant_id + if headers: + payload["headers"] = headers + if auth: + payload["auth"] = auth + if request_params: + payload["request_params"] = request_params + if request_body: + payload["request_body"] = request_body + if response_mapping: + payload["response_mapping"] = response_mapping + if timeout: + payload["timeout"] = timeout + if retry: + payload["retry"] = retry + + logger.info( + "updating_external_tool", + tool_ref_id=tool_ref_id, + name=name, + user_id=user_id + ) + + return await self._request("PUT", f"/tools/{tool_ref_id}", json=payload) + + async def delete_external_tool(self, tool_ref_id: str) -> Dict[str, Any]: + """ + 删除外部数据工具 + + 调用: DELETE /tools/{tool_ref_id} + + Agent Manager 会删除对应的工具文件和配置。 + + Args: + tool_ref_id: 工具标识 + + Returns: + { + "success": true, + "message": "工具删除成功" + } + """ + logger.info("deleting_external_tool", tool_ref_id=tool_ref_id) + return await self._request("DELETE", f"/tools/{tool_ref_id}") + + async def test_external_tool( + self, + tool_ref_id: str, + test_params: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + 测试外部数据工具连接 + + 调用: POST /tools/{tool_ref_id}/test + + Agent Manager 会尝试调用工具的 API 并返回测试结果。 + + Args: + tool_ref_id: 工具标识 + test_params: 测试参数(可选) + + Returns: + { + "success": true, + "connected": true, + "response_time_ms": 156, + "status_code": 200, + "sample_response": {...} + } + """ + payload = {} + if test_params: + payload["test_params"] = test_params + + logger.info("testing_external_tool", tool_ref_id=tool_ref_id) + return await self._request("POST", f"/tools/{tool_ref_id}/test", json=payload) + + async def create_agent_with_tools( + self, + name: str, + template: str, + tool_refs: List[str], + config: Optional[AgentConfig] = None, + env: Optional[Dict[str, str]] = None + ) -> AgentCreateResult: + """ + 创建带有外部数据工具的 Agent + + 调用: POST /agents(新增 tool_refs 字段) + + Agent Manager 会根据 tool_refs 加载对应的工具文件,部署到 AKS。 + + Args: + name: Agent 名称 + template: Agent 模板(通常为 "custom_agent") + tool_refs: 外部数据工具标识列表 + config: 资源配置 + env: 环境变量 + + Returns: + 创建结果 + """ + payload: Dict[str, Any] = { + "name": name, + "template": template, + "tool_refs": tool_refs + } + + if config: + payload["config"] = config.to_dict() + + # 构建环境变量 + final_env = {"LLM_BASE_URL": LLM_BASE_URL} + if env: + final_env.update(env) + payload["env"] = final_env + + logger.info( + "creating_agent_with_tools", + name=name, + template=template, + tool_refs=tool_refs, + config=config.to_dict() if config else None + ) + + data = await self._request("POST", "/agents", json=payload) + + return AgentCreateResult( + name=data["name"], + namespace=data["namespace"], + status=data["status"], + created_at=data["created_at"], + template=data["template"], + service_port=data.get("service_port"), + access_info=data.get("access_info"), + pod_id=data.get("pod_id"), + pod_ip=data.get("pod_ip"), + host_ip=data.get("host_ip"), + node_name=data.get("node_name"), + owner_info=data.get("owner_info") + ) + # ==================== Unimplemented Interfaces (Not yet provided by Agent Manager) ==================== # The interfaces called by the following methods are not yet implemented in Agent Manager # Method signatures are retained for future extension, but will raise NotImplementedError when called diff --git a/services/mcp-server/app/routes/__init__.py b/services/mcp-server/app/routes/__init__.py index 2c3472e..6202205 100644 --- a/services/mcp-server/app/routes/__init__.py +++ b/services/mcp-server/app/routes/__init__.py @@ -11,6 +11,7 @@ from . import ( provider_health_management, # 阶段四:供应商健康检查管理 platform_agent_quota, # 平台 Agent 配额管理 billing_webhook, # LiteLLM Token计费webhook + external_tools, # 外部数据工具管理 ) @@ -35,6 +36,8 @@ def register_routes(app: FastAPI) -> None: agents.router, sessions.router, # 会话管理路由 tools.router, + external_tools.router, # 外部数据工具管理路由 + external_tools.toolkit_router, # 外部数据工具集管理路由 monitoring.router, metrics.router, websocket.router, diff --git a/services/mcp-server/app/routes/external_tools.py b/services/mcp-server/app/routes/external_tools.py new file mode 100644 index 0000000..ed827c4 --- /dev/null +++ b/services/mcp-server/app/routes/external_tools.py @@ -0,0 +1,1030 @@ +""" +外部数据工具 API 接口 + +用户可以创建外部数据工具,工具配置发送给 Agent Manager 生成 Pydantic 工具文件, +MCP-Server 只存储工具基本信息和关联标识(tool_ref_id)。 +""" + +import structlog +from typing import Optional +from uuid import UUID as PyUUID +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, and_ +import json + +from database import get_db +from models import ExternalDataTool, ExternalToolkit, User +from app.schemas import ( + SuccessResponse, + CreateExternalToolRequest, + UpdateExternalToolRequest, + TestExternalToolRequest, + ExternalToolInfo, + ExternalToolDetail, + CreateToolkitRequest, + UpdateToolkitRequest, + ToolkitInfo, + ToolkitDetail, +) +from app.auth import require_auth +from app.agent_manager_client import get_agent_manager_client + +logger = structlog.get_logger(__name__) + +router = APIRouter(prefix="/api/user/external-tools", tags=["External Data Tools"]) + + +# ============= 创建外部数据工具 ============= + +@router.post("", response_model=SuccessResponse) +async def create_external_tool( + req: CreateExternalToolRequest, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 创建外部数据工具 + + 1. 验证请求参数 + 2. 发送配置给 Agent Manager 生成 Pydantic 工具文件 + 3. 存储工具基本信息和 tool_ref_id 到数据库 + """ + user_id = principal.get("user_id") + tenant_id = principal.get("tenant_id") + channel_id = principal.get("channel_id") + + logger.info( + "creating_external_tool", + user_id=user_id, + name=req.name, + url=req.url, + method=req.method + ) + + # 检查工具名称是否已存在 + existing = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.owner_id == PyUUID(user_id), + ExternalDataTool.name == req.name + ) + ) + ) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=409, + detail={"error": "tool_name_exists", "message": f"工具名称 '{req.name}' 已存在"} + ) + + # 调用 Agent Manager 生成工具 + client = get_agent_manager_client() + + try: + am_result = await client.generate_external_tool( + name=req.name, + description=req.description or "", + url=req.url, + method=req.method, + user_id=user_id, + tenant_id=tenant_id, + headers=req.headers, + auth=req.auth.dict(by_alias=True) if req.auth else None, + request_params=req.request_params, + request_body=req.request_body, + response_mapping=req.response_mapping, + timeout=req.timeout, + retry=req.retry.dict() if req.retry else None + ) + + tool_ref_id = am_result.get("tool_ref_id") + am_status = "active" if am_result.get("success") else "error" + error_message = am_result.get("message") if not am_result.get("success") else None + + except Exception as e: + logger.error("agent_manager_generate_failed", error=str(e), name=req.name) + # Agent Manager 调用失败,仍然创建工具记录,状态为 error + tool_ref_id = None + am_status = "error" + error_message = f"Agent Manager 生成工具失败: {str(e)}" + + # 创建工具记录 + tool = ExternalDataTool( + name=req.name, + description=req.description, + url=req.url, + method=req.method, + auth_type=req.auth.type if req.auth else "none", + tool_ref_id=tool_ref_id, + status=am_status, + error_message=error_message, + owner_id=PyUUID(user_id), + tenant_id=PyUUID(tenant_id) if tenant_id else None, + channel_id=PyUUID(channel_id) if channel_id else None, + is_active=True, + usage_count=0 + ) + + db.add(tool) + await db.commit() + await db.refresh(tool) + + logger.info( + "external_tool_created", + tool_id=str(tool.id), + tool_ref_id=tool_ref_id, + status=am_status + ) + + return SuccessResponse( + data={ + "id": str(tool.id), + "name": tool.name, + "tool_ref_id": tool_ref_id, + "status": am_status, + "created_at": tool.created_at.isoformat() if tool.created_at else None + }, + message="外部数据工具创建成功" if am_status == "active" else f"工具创建完成,但生成失败: {error_message}" + ) + + +@router.post("/upload", response_model=SuccessResponse) +async def upload_external_tool( + file: UploadFile = File(...), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 上传 JSON 文件创建外部数据工具 + + JSON 文件格式参考 CreateExternalToolRequest + """ + user_id = principal.get("user_id") + + # 验证文件类型 + if not file.filename.endswith(".json"): + raise HTTPException( + status_code=400, + detail={"error": "invalid_file_type", "message": "只支持 .json 文件"} + ) + + # 读取并解析 JSON + try: + content = await file.read() + config = json.loads(content.decode("utf-8")) + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, + detail={"error": "invalid_json", "message": f"JSON 解析失败: {str(e)}"} + ) + except Exception as e: + raise HTTPException( + status_code=400, + detail={"error": "file_read_error", "message": f"文件读取失败: {str(e)}"} + ) + + # 验证必填字段 + required_fields = ["name", "url"] + missing_fields = [f for f in required_fields if f not in config] + if missing_fields: + raise HTTPException( + status_code=400, + detail={"error": "missing_fields", "message": f"缺少必填字段: {', '.join(missing_fields)}"} + ) + + # 构造请求对象 + try: + req = CreateExternalToolRequest(**config) + except Exception as e: + raise HTTPException( + status_code=400, + detail={"error": "invalid_config", "message": f"配置格式无效: {str(e)}"} + ) + + logger.info("uploading_external_tool", user_id=user_id, filename=file.filename) + + # 调用创建接口 + return await create_external_tool(req, principal, db) + + +# ============= 获取工具列表 ============= + +@router.get("", response_model=SuccessResponse) +async def list_external_tools( + status: Optional[str] = Query(None, description="过滤状态: active/pending/error"), + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取用户的外部数据工具列表 + """ + user_id = principal.get("user_id") + + # 构建查询 + query = select(ExternalDataTool).where( + ExternalDataTool.owner_id == PyUUID(user_id) + ) + + if status: + query = query.where(ExternalDataTool.status == status) + + # 获取总数 + count_query = select(ExternalDataTool).where( + ExternalDataTool.owner_id == PyUUID(user_id) + ) + if status: + count_query = count_query.where(ExternalDataTool.status == status) + + result = await db.execute(count_query) + total = len(result.scalars().all()) + + # 分页查询 + query = query.order_by(ExternalDataTool.created_at.desc()) + query = query.offset((page - 1) * page_size).limit(page_size) + + result = await db.execute(query) + tools = result.scalars().all() + + # 构建响应 + tools_data = [] + for tool in tools: + tools_data.append({ + "id": str(tool.id), + "name": tool.name, + "description": tool.description, + "url": tool.url, + "method": tool.method, + "auth_type": tool.auth_type, + "status": tool.status, + "usage_count": tool.usage_count, + "created_at": tool.created_at.isoformat() if tool.created_at else None + }) + + return SuccessResponse( + data={ + "tools": tools_data, + "total": total, + "page": page, + "page_size": page_size + } + ) + + +# ============= 获取工具详情 ============= + +@router.get("/{tool_id}", response_model=SuccessResponse) +async def get_external_tool( + tool_id: str, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取外部数据工具详情 + """ + user_id = principal.get("user_id") + + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"} + ) + + result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + tool = result.scalar_one_or_none() + + if not tool: + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": "工具不存在"} + ) + + return SuccessResponse( + data={ + "id": str(tool.id), + "name": tool.name, + "description": tool.description, + "url": tool.url, + "method": tool.method, + "auth_type": tool.auth_type, + "tool_ref_id": tool.tool_ref_id, + "status": tool.status, + "error_message": tool.error_message, + "usage_count": tool.usage_count, + "created_at": tool.created_at.isoformat() if tool.created_at else None, + "updated_at": tool.updated_at.isoformat() if tool.updated_at else None + } + ) + + +# ============= 更新工具 ============= + +@router.put("/{tool_id}", response_model=SuccessResponse) +async def update_external_tool( + tool_id: str, + req: UpdateExternalToolRequest, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 更新外部数据工具配置 + + 需要传递完整配置,会重新调用 Agent Manager 生成工具文件。 + """ + user_id = principal.get("user_id") + tenant_id = principal.get("tenant_id") + + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"} + ) + + result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + tool = result.scalar_one_or_none() + + if not tool: + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": "工具不存在"} + ) + + logger.info( + "updating_external_tool", + tool_id=tool_id, + user_id=user_id, + name=req.name + ) + + # 调用 Agent Manager 更新工具 + client = get_agent_manager_client() + + try: + if tool.tool_ref_id: + # 已有 tool_ref_id,调用更新接口 + am_result = await client.update_external_tool( + tool_ref_id=tool.tool_ref_id, + name=req.name, + description=req.description or "", + url=req.url, + method=req.method, + user_id=user_id, + tenant_id=tenant_id, + headers=req.headers, + auth=req.auth.dict(by_alias=True) if req.auth else None, + request_params=req.request_params, + request_body=req.request_body, + response_mapping=req.response_mapping, + timeout=req.timeout, + retry=req.retry.dict() if req.retry else None + ) + else: + # 没有 tool_ref_id,调用生成接口 + am_result = await client.generate_external_tool( + name=req.name, + description=req.description or "", + url=req.url, + method=req.method, + user_id=user_id, + tenant_id=tenant_id, + headers=req.headers, + auth=req.auth.dict(by_alias=True) if req.auth else None, + request_params=req.request_params, + request_body=req.request_body, + response_mapping=req.response_mapping, + timeout=req.timeout, + retry=req.retry.dict() if req.retry else None + ) + + new_tool_ref_id = am_result.get("tool_ref_id") + am_status = "active" if am_result.get("success") else "error" + error_message = am_result.get("message") if not am_result.get("success") else None + + except Exception as e: + logger.error("agent_manager_update_failed", error=str(e), tool_id=tool_id) + new_tool_ref_id = tool.tool_ref_id # 保持原有的 + am_status = "error" + error_message = f"Agent Manager 更新工具失败: {str(e)}" + + # 更新数据库记录 + tool.name = req.name + tool.description = req.description + tool.url = req.url + tool.method = req.method + tool.auth_type = req.auth.type if req.auth else "none" + tool.tool_ref_id = new_tool_ref_id + tool.status = am_status + tool.error_message = error_message + + await db.commit() + await db.refresh(tool) + + logger.info( + "external_tool_updated", + tool_id=str(tool.id), + tool_ref_id=new_tool_ref_id, + status=am_status + ) + + return SuccessResponse( + data={ + "id": str(tool.id), + "name": tool.name, + "tool_ref_id": new_tool_ref_id, + "status": am_status, + "updated_at": tool.updated_at.isoformat() if tool.updated_at else None + }, + message="外部数据工具更新成功" if am_status == "active" else f"工具更新完成,但生成失败: {error_message}" + ) + + +# ============= 删除工具 ============= + +@router.delete("/{tool_id}", response_model=SuccessResponse) +async def delete_external_tool( + tool_id: str, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 删除外部数据工具 + + 会同时通知 Agent Manager 删除对应的工具文件。 + """ + user_id = principal.get("user_id") + + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"} + ) + + result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + tool = result.scalar_one_or_none() + + if not tool: + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": "工具不存在"} + ) + + logger.info( + "deleting_external_tool", + tool_id=tool_id, + tool_ref_id=tool.tool_ref_id, + user_id=user_id + ) + + # 通知 Agent Manager 删除工具文件 + if tool.tool_ref_id: + client = get_agent_manager_client() + try: + await client.delete_external_tool(tool.tool_ref_id) + except Exception as e: + logger.warning( + "agent_manager_delete_failed", + error=str(e), + tool_ref_id=tool.tool_ref_id + ) + # 即使 AM 删除失败,也继续删除本地记录 + + # 删除数据库记录 + await db.delete(tool) + await db.commit() + + logger.info("external_tool_deleted", tool_id=tool_id) + + return SuccessResponse( + data={"id": tool_id}, + message="外部数据工具删除成功" + ) + + +# ============= 测试工具连接 ============= + +@router.post("/{tool_id}/test", response_model=SuccessResponse) +async def test_external_tool( + tool_id: str, + req: TestExternalToolRequest = None, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 测试外部数据工具连接 + + 调用 Agent Manager 的测试接口。 + """ + user_id = principal.get("user_id") + + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": "无效的工具 ID 格式"} + ) + + result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + tool = result.scalar_one_or_none() + + if not tool: + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": "工具不存在"} + ) + + if not tool.tool_ref_id: + raise HTTPException( + status_code=400, + detail={"error": "tool_not_ready", "message": "工具尚未生成成功,无法测试"} + ) + + logger.info( + "testing_external_tool", + tool_id=tool_id, + tool_ref_id=tool.tool_ref_id + ) + + # 调用 Agent Manager 测试接口 + client = get_agent_manager_client() + + try: + test_params = req.test_params if req else None + am_result = await client.test_external_tool(tool.tool_ref_id, test_params) + + return SuccessResponse( + data={ + "connected": am_result.get("connected", False), + "response_time_ms": am_result.get("response_time_ms"), + "status_code": am_result.get("status_code"), + "sample_response": am_result.get("sample_response") + }, + message="工具连接测试成功" if am_result.get("connected") else "工具连接测试失败" + ) + + except Exception as e: + logger.error("test_external_tool_failed", error=str(e), tool_id=tool_id) + return SuccessResponse( + data={ + "connected": False, + "error_message": str(e) + }, + message=f"工具连接测试失败: {str(e)}" + ) + + +# 注意:创建自定义 Agent 请使用 /api/user/custom-agents 接口 +# 在请求体中通过 externalTools 字段传入外部数据工具 ID 列表 +# 或通过 toolkit 字段传入工具集 ID + + +# ============= 工具集管理 ============= + +toolkit_router = APIRouter(prefix="/api/user/toolkits", tags=["External Toolkits"]) + + +@toolkit_router.post("", response_model=SuccessResponse) +async def create_toolkit( + req: CreateToolkitRequest, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 创建工具集 + + 将多个外部数据工具组合成一个工具集,最多 8 个工具。 + """ + user_id = principal.get("user_id") + tenant_id = principal.get("tenant_id") + channel_id = principal.get("channel_id") + + logger.info( + "creating_toolkit", + user_id=user_id, + name=req.name, + tool_count=len(req.tool_ids) + ) + + # 验证工具数量 + if len(req.tool_ids) > 8: + raise HTTPException( + status_code=400, + detail={"error": "too_many_tools", "message": "工具集最多包含 8 个工具"} + ) + + # 检查工具集名称是否已存在 + existing = await db.execute( + select(ExternalToolkit).where( + and_( + ExternalToolkit.owner_id == PyUUID(user_id), + ExternalToolkit.name == req.name + ) + ) + ) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=409, + detail={"error": "toolkit_name_exists", "message": f"工具集名称 '{req.name}' 已存在"} + ) + + # 验证所有工具是否存在且属于用户 + validated_tool_ids = [] + for tool_id in req.tool_ids: + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": f"无效的工具 ID: {tool_id}"} + ) + + result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + tool = result.scalar_one_or_none() + + if not tool: + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": f"工具不存在或无权限: {tool_id}"} + ) + + validated_tool_ids.append(str(tool.id)) + + # 创建工具集 + toolkit = ExternalToolkit( + name=req.name, + description=req.description, + tool_ids=validated_tool_ids, + owner_id=PyUUID(user_id), + tenant_id=PyUUID(tenant_id) if tenant_id else None, + channel_id=PyUUID(channel_id) if channel_id else None, + is_active=True, + usage_count=0 + ) + + db.add(toolkit) + await db.commit() + await db.refresh(toolkit) + + logger.info( + "toolkit_created", + toolkit_id=str(toolkit.id), + name=toolkit.name, + tool_count=len(validated_tool_ids) + ) + + return SuccessResponse( + data={ + "id": str(toolkit.id), + "name": toolkit.name, + "description": toolkit.description, + "tool_count": len(validated_tool_ids), + "created_at": toolkit.created_at.isoformat() if toolkit.created_at else None + }, + message="工具集创建成功" + ) + + +@toolkit_router.get("", response_model=SuccessResponse) +async def list_toolkits( + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取用户的工具集列表 + """ + user_id = principal.get("user_id") + + # 获取总数 + count_result = await db.execute( + select(ExternalToolkit).where( + ExternalToolkit.owner_id == PyUUID(user_id) + ) + ) + total = len(count_result.scalars().all()) + + # 分页查询 + query = select(ExternalToolkit).where( + ExternalToolkit.owner_id == PyUUID(user_id) + ).order_by(ExternalToolkit.created_at.desc()) + query = query.offset((page - 1) * page_size).limit(page_size) + + result = await db.execute(query) + toolkits = result.scalars().all() + + # 构建响应 + toolkits_data = [] + for tk in toolkits: + tool_ids = tk.tool_ids if tk.tool_ids else [] + toolkits_data.append({ + "id": str(tk.id), + "name": tk.name, + "description": tk.description, + "tool_count": len(tool_ids), + "usage_count": tk.usage_count or 0, + "created_at": tk.created_at.isoformat() if tk.created_at else None + }) + + return SuccessResponse( + data={ + "toolkits": toolkits_data, + "total": total, + "page": page, + "page_size": page_size + } + ) + + +@toolkit_router.get("/{toolkit_id}", response_model=SuccessResponse) +async def get_toolkit( + toolkit_id: str, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取工具集详情 + + 包含工具集的基本信息和包含的工具详情。 + """ + user_id = principal.get("user_id") + + try: + toolkit_uuid = PyUUID(toolkit_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"} + ) + + result = await db.execute( + select(ExternalToolkit).where( + and_( + ExternalToolkit.id == toolkit_uuid, + ExternalToolkit.owner_id == PyUUID(user_id) + ) + ) + ) + toolkit = result.scalar_one_or_none() + + if not toolkit: + raise HTTPException( + status_code=404, + detail={"error": "toolkit_not_found", "message": "工具集不存在"} + ) + + # 获取包含的工具详情 + tool_ids = toolkit.tool_ids if toolkit.tool_ids else [] + tools_data = [] + + for tid in tool_ids: + try: + tool_result = await db.execute( + select(ExternalDataTool).where(ExternalDataTool.id == PyUUID(tid)) + ) + tool = tool_result.scalar_one_or_none() + if tool: + tools_data.append({ + "id": str(tool.id), + "name": tool.name, + "description": tool.description, + "url": tool.url, + "method": tool.method, + "auth_type": tool.auth_type, + "status": tool.status, + "usage_count": tool.usage_count or 0 + }) + except Exception: + pass # 跳过无效的工具 ID + + return SuccessResponse( + data={ + "id": str(toolkit.id), + "name": toolkit.name, + "description": toolkit.description, + "tool_ids": tool_ids, + "tools": tools_data, + "usage_count": toolkit.usage_count or 0, + "created_at": toolkit.created_at.isoformat() if toolkit.created_at else None, + "updated_at": toolkit.updated_at.isoformat() if toolkit.updated_at else None + } + ) + + +@toolkit_router.put("/{toolkit_id}", response_model=SuccessResponse) +async def update_toolkit( + toolkit_id: str, + req: UpdateToolkitRequest, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 更新工具集 + """ + user_id = principal.get("user_id") + + try: + toolkit_uuid = PyUUID(toolkit_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"} + ) + + result = await db.execute( + select(ExternalToolkit).where( + and_( + ExternalToolkit.id == toolkit_uuid, + ExternalToolkit.owner_id == PyUUID(user_id) + ) + ) + ) + toolkit = result.scalar_one_or_none() + + if not toolkit: + raise HTTPException( + status_code=404, + detail={"error": "toolkit_not_found", "message": "工具集不存在"} + ) + + logger.info( + "updating_toolkit", + toolkit_id=toolkit_id, + user_id=user_id + ) + + # 更新名称(检查是否与其他工具集重名) + if req.name and req.name != toolkit.name: + existing = await db.execute( + select(ExternalToolkit).where( + and_( + ExternalToolkit.owner_id == PyUUID(user_id), + ExternalToolkit.name == req.name, + ExternalToolkit.id != toolkit_uuid + ) + ) + ) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=409, + detail={"error": "toolkit_name_exists", "message": f"工具集名称 '{req.name}' 已存在"} + ) + toolkit.name = req.name + + if req.description is not None: + toolkit.description = req.description + + # 更新工具列表 + if req.tool_ids is not None: + if len(req.tool_ids) > 8: + raise HTTPException( + status_code=400, + detail={"error": "too_many_tools", "message": "工具集最多包含 8 个工具"} + ) + + # 验证所有工具 + validated_tool_ids = [] + for tool_id in req.tool_ids: + try: + tool_uuid = PyUUID(tool_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_tool_id", "message": f"无效的工具 ID: {tool_id}"} + ) + + tool_result = await db.execute( + select(ExternalDataTool).where( + and_( + ExternalDataTool.id == tool_uuid, + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ) + if not tool_result.scalar_one_or_none(): + raise HTTPException( + status_code=404, + detail={"error": "tool_not_found", "message": f"工具不存在或无权限: {tool_id}"} + ) + + validated_tool_ids.append(str(tool_uuid)) + + toolkit.tool_ids = validated_tool_ids + + await db.commit() + await db.refresh(toolkit) + + logger.info( + "toolkit_updated", + toolkit_id=str(toolkit.id), + name=toolkit.name + ) + + return SuccessResponse( + data={ + "id": str(toolkit.id), + "name": toolkit.name, + "description": toolkit.description, + "tool_count": len(toolkit.tool_ids) if toolkit.tool_ids else 0, + "updated_at": toolkit.updated_at.isoformat() if toolkit.updated_at else None + }, + message="工具集更新成功" + ) + + +@toolkit_router.delete("/{toolkit_id}", response_model=SuccessResponse) +async def delete_toolkit( + toolkit_id: str, + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 删除工具集 + + 注意:删除工具集不会删除其中的工具。 + """ + user_id = principal.get("user_id") + + try: + toolkit_uuid = PyUUID(toolkit_id) + except ValueError: + raise HTTPException( + status_code=400, + detail={"error": "invalid_toolkit_id", "message": "无效的工具集 ID 格式"} + ) + + result = await db.execute( + select(ExternalToolkit).where( + and_( + ExternalToolkit.id == toolkit_uuid, + ExternalToolkit.owner_id == PyUUID(user_id) + ) + ) + ) + toolkit = result.scalar_one_or_none() + + if not toolkit: + raise HTTPException( + status_code=404, + detail={"error": "toolkit_not_found", "message": "工具集不存在"} + ) + + logger.info( + "deleting_toolkit", + toolkit_id=toolkit_id, + name=toolkit.name, + user_id=user_id + ) + + await db.delete(toolkit) + await db.commit() + + logger.info("toolkit_deleted", toolkit_id=toolkit_id) + + return SuccessResponse( + data={"id": toolkit_id}, + message="工具集删除成功" + ) + diff --git a/services/mcp-server/app/routes/user.py b/services/mcp-server/app/routes/user.py index 85ec6d9..15320c5 100644 --- a/services/mcp-server/app/routes/user.py +++ b/services/mcp-server/app/routes/user.py @@ -3144,6 +3144,105 @@ async def create_custom_agent( logger.warning(f"查询工具详情失败,仅传递工具 ID 列表: {str(e)}") # ================================================ + # ========== 外部数据工具配置传递给 Agent Manager ========== + from models import ExternalDataTool, ExternalToolkit + + external_tool_refs = [] # 存储 tool_ref_id 列表 + external_tool_ids_to_process = [] # 需要处理的工具 ID 列表 + + # 优先处理工具集(如果指定了 toolkit) + if req.toolkit: + logger.info(f"处理工具集: user_id={user_id}, toolkit={req.toolkit}") + try: + toolkit_result = await db.execute( + select(ExternalToolkit).where( + ExternalToolkit.id == PyUUID(req.toolkit), + ExternalToolkit.owner_id == PyUUID(user_id) + ) + ) + toolkit = toolkit_result.scalar_one_or_none() + + if not toolkit: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"工具集不存在或无权限: {req.toolkit}" + ) + + # 获取工具集中的工具 ID + if toolkit.tool_ids: + external_tool_ids_to_process.extend(toolkit.tool_ids) + logger.info(f"从工具集获取工具: toolkit={toolkit.name}, tools={toolkit.tool_ids}") + + # 更新工具集使用次数 + toolkit.usage_count = (toolkit.usage_count or 0) + 1 + + except HTTPException: + raise + except Exception as e: + logger.warning(f"查询工具集失败: {req.toolkit}, error={str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"无效的工具集 ID: {req.toolkit}" + ) + + # 合并直接指定的外部工具(如果同时指定了 externalTools) + if req.externalTools and len(req.externalTools) > 0: + for ext_id in req.externalTools: + if ext_id not in external_tool_ids_to_process: + external_tool_ids_to_process.append(ext_id) + + # 处理所有需要使用的外部数据工具 + if external_tool_ids_to_process: + logger.info(f"处理外部数据工具: user_id={user_id}, tools={external_tool_ids_to_process}") + + for ext_tool_id in external_tool_ids_to_process: + try: + ext_tool_result = await db.execute( + select(ExternalDataTool).where( + ExternalDataTool.id == PyUUID(ext_tool_id), + ExternalDataTool.owner_id == PyUUID(user_id) + ) + ) + ext_tool = ext_tool_result.scalar_one_or_none() + + if not ext_tool: + logger.warning(f"外部数据工具不存在或无权限: {ext_tool_id}") + continue + + if ext_tool.status != "active": + logger.warning(f"外部数据工具未就绪: {ext_tool_id}, status={ext_tool.status}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"外部数据工具 '{ext_tool.name}' 尚未就绪,状态: {ext_tool.status}" + ) + + if not ext_tool.tool_ref_id: + logger.warning(f"外部数据工具缺少 tool_ref_id: {ext_tool_id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"外部数据工具 '{ext_tool.name}' 缺少关联标识,请重新创建" + ) + + external_tool_refs.append(ext_tool.tool_ref_id) + + # 更新工具使用次数 + ext_tool.usage_count = (ext_tool.usage_count or 0) + 1 + + except HTTPException: + raise + except Exception as e: + logger.warning(f"查询外部数据工具失败: {ext_tool_id}, error={str(e)}") + + if external_tool_refs: + # 传递外部工具的 tool_ref_id 列表给 Agent Manager + env_vars["EXTERNAL_TOOL_REFS"] = json.dumps(external_tool_refs) + logger.info( + f"外部数据工具配置已准备: user_id={user_id}, " + f"tool_count={len(external_tool_refs)}, " + f"tool_refs={external_tool_refs}" + ) + # ================================================ + # ========== A2A 框架配置 ========== if framework_template == "A2A": import time as time_module diff --git a/services/mcp-server/app/schemas.py b/services/mcp-server/app/schemas.py index c2f627c..710b7b3 100644 --- a/services/mcp-server/app/schemas.py +++ b/services/mcp-server/app/schemas.py @@ -837,6 +837,12 @@ class CreateCustomAgentRequest(BaseModel): # 工具配置(第一个工具为主工具,可决定 Agent 类型) tools: Optional[List[str]] = Field(None, description="选择的工具ID列表,第一个工具为主工具,可从中获取模板和环境变量配置") + # 外部数据工具配置(用户创建的自定义外部 API 工具) + externalTools: Optional[List[str]] = Field(None, description="外部数据工具 ID 列表,部署时会将 tool_ref_id 传给 Agent Manager") + + # 工具集配置(用户创建的工具组合,最多 8 个工具) + toolkit: Optional[str] = Field(None, description="工具集 ID,使用工具集中的所有工具") + # 用户配置 endpoint: Optional[str] = Field(None, description="用户终结点") apiKey: Optional[str] = Field(None, description="用户 API 密钥") @@ -962,3 +968,182 @@ class AgentManagerCallbackResponse(BaseModel): message: str recordId: Optional[str] = None + +# ============= 外部数据工具 ============= + +class ExternalToolAuthConfig(BaseModel): + """外部工具认证配置""" + type: str = Field(..., pattern="^(api_key|bearer|basic|none)$", description="认证类型") + key: Optional[str] = Field(None, description="API Key 或 Bearer Token") + in_location: Optional[str] = Field(None, alias="in", description="Key 位置: header/query/body") + name: Optional[str] = Field(None, description="Key 名称(如 X-API-Key)") + username: Optional[str] = Field(None, description="Basic Auth 用户名") + password: Optional[str] = Field(None, description="Basic Auth 密码") + + class Config: + populate_by_name = True + + +class ExternalToolRetryConfig(BaseModel): + """外部工具重试配置""" + max_attempts: int = Field(3, ge=1, le=10, description="最大重试次数") + delay_seconds: int = Field(1, ge=0, le=60, description="重试延迟(秒)") + + +class CreateExternalToolRequest(BaseModel): + """创建外部数据工具请求 + + 用户上传的外部数据工具配置,将发送给 Agent Manager 生成 Pydantic 工具文件。 + """ + name: str = Field(..., min_length=1, max_length=100, description="工具名称") + description: Optional[str] = Field(None, description="工具描述") + url: str = Field(..., min_length=1, max_length=500, description="API 端点 URL") + method: str = Field("POST", pattern="^(GET|POST|PUT|DELETE|PATCH)$", description="HTTP 方法") + + # 请求配置 + headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头") + auth: Optional[ExternalToolAuthConfig] = Field(None, description="认证配置") + + # 参数定义(JSON Schema 格式) + request_params: Optional[Dict[str, Any]] = Field(None, description="URL 查询参数定义") + request_body: Optional[Dict[str, Any]] = Field(None, description="请求体定义") + + # 响应配置 + response_mapping: Optional[Dict[str, Any]] = Field(None, description="响应字段映射") + + # 运行配置 + timeout: int = Field(30, ge=1, le=300, description="超时时间(秒)") + retry: Optional[ExternalToolRetryConfig] = Field(None, description="重试配置") + + +class UpdateExternalToolRequest(BaseModel): + """更新外部数据工具请求 + + 需要传递完整配置,因为 MCP-Server 不存储完整配置。 + """ + name: str = Field(..., min_length=1, max_length=100, description="工具名称") + description: Optional[str] = Field(None, description="工具描述") + url: str = Field(..., min_length=1, max_length=500, description="API 端点 URL") + method: str = Field("POST", pattern="^(GET|POST|PUT|DELETE|PATCH)$", description="HTTP 方法") + + headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头") + auth: Optional[ExternalToolAuthConfig] = Field(None, description="认证配置") + request_params: Optional[Dict[str, Any]] = Field(None, description="URL 查询参数定义") + request_body: Optional[Dict[str, Any]] = Field(None, description="请求体定义") + response_mapping: Optional[Dict[str, Any]] = Field(None, description="响应字段映射") + timeout: int = Field(30, ge=1, le=300, description="超时时间(秒)") + retry: Optional[ExternalToolRetryConfig] = Field(None, description="重试配置") + + +class ExternalToolInfo(BaseModel): + """外部数据工具信息(列表响应)""" + id: str + name: str + description: Optional[str] = None + url: str + method: str + auth_type: str + status: str # pending, active, error + usage_count: int = 0 + created_at: str + updated_at: Optional[str] = None + + +class ExternalToolDetail(BaseModel): + """外部数据工具详情""" + id: str + name: str + description: Optional[str] = None + url: str + method: str + auth_type: str + tool_ref_id: Optional[str] = None + status: str + error_message: Optional[str] = None + usage_count: int = 0 + created_at: str + updated_at: Optional[str] = None + + +class TestExternalToolRequest(BaseModel): + """测试外部工具连接请求""" + test_params: Optional[Dict[str, Any]] = Field(None, description="测试参数") + + +class TestExternalToolResponse(BaseModel): + """测试外部工具连接响应""" + connected: bool + response_time_ms: Optional[int] = None + status_code: Optional[int] = None + sample_response: Optional[Dict[str, Any]] = None + error_message: Optional[str] = None + + +class CreateCustomAgentWithToolsRequest(BaseModel): + """创建带有外部数据工具的自定义 Agent 请求 + + 新版自定义 Agent 创建接口,使用外部数据工具(通过 tool_ref_id 关联)。 + """ + name: str = Field(..., min_length=1, max_length=63, description="Agent 名称") + description: Optional[str] = Field(None, description="描述") + + # 外部数据工具(核心字段) + external_tools: List[str] = Field(..., min_length=1, description="外部数据工具 ID 列表") + + # 资源配置 + cpuRequest: str = Field("100m", description="CPU 请求量") + cpuLimit: Optional[str] = Field(None, description="CPU 限制量") + memoryRequest: str = Field("128Mi", description="内存请求量") + memoryLimit: Optional[str] = Field(None, description="内存限制量") + + # 模型配置 + model: Optional[str] = Field(None, description="使用的模型名称") + + +# ==================== 外部数据工具集 ==================== + +class CreateToolkitRequest(BaseModel): + """创建工具集请求""" + name: str = Field(..., min_length=1, max_length=100, description="工具集名称") + description: Optional[str] = Field(None, description="工具集描述") + tool_ids: List[str] = Field( + ..., + min_length=1, + max_length=8, + description="外部数据工具 ID 列表(1-8 个)" + ) + + +class UpdateToolkitRequest(BaseModel): + """更新工具集请求""" + name: Optional[str] = Field(None, min_length=1, max_length=100, description="工具集名称") + description: Optional[str] = Field(None, description="工具集描述") + tool_ids: Optional[List[str]] = Field( + None, + min_length=1, + max_length=8, + description="外部数据工具 ID 列表(1-8 个)" + ) + + +class ToolkitInfo(BaseModel): + """工具集基本信息""" + id: str + name: str + description: Optional[str] = None + tool_count: int + usage_count: int = 0 + created_at: str + + +class ToolkitDetail(BaseModel): + """工具集详情""" + id: str + name: str + description: Optional[str] = None + tool_ids: List[str] + tools: List[ExternalToolInfo] = [] # 包含的工具信息 + usage_count: int = 0 + created_at: str + updated_at: Optional[str] = None + diff --git a/services/mcp-server/migrations/017_add_external_data_tools.sql b/services/mcp-server/migrations/017_add_external_data_tools.sql new file mode 100644 index 0000000..be04cc1 --- /dev/null +++ b/services/mcp-server/migrations/017_add_external_data_tools.sql @@ -0,0 +1,45 @@ +-- 017: 添加外部数据工具表 +-- 用于存储用户创建的外部数据工具基本信息和 Agent Manager 关联标识 + +-- 创建外部数据工具表 +CREATE TABLE IF NOT EXISTS external_data_tools ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- 基本信息(用于前端展示) + name VARCHAR(100) NOT NULL, + description TEXT, + + -- API 配置(仅用于展示,实际配置存储在 Agent Manager) + url VARCHAR(500) NOT NULL, + method VARCHAR(10) NOT NULL DEFAULT 'POST', + auth_type VARCHAR(20) DEFAULT 'none', -- api_key, bearer, basic, none + + -- Agent Manager 关联(核心字段) + tool_ref_id VARCHAR(100) UNIQUE, -- AM 返回的工具标识 + status VARCHAR(20) DEFAULT 'pending', -- pending, active, error + error_message TEXT, -- 生成失败时的错误信息 + + -- 归属信息 + owner_id UUID NOT NULL REFERENCES users(id), + tenant_id UUID REFERENCES users(id), + channel_id UUID REFERENCES channels(id), + is_active BOOLEAN DEFAULT TRUE, + + -- 统计信息 + usage_count INTEGER DEFAULT 0, + + -- 时间戳 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_external_data_tool_owner ON external_data_tools(owner_id); +CREATE INDEX IF NOT EXISTS idx_external_data_tool_ref ON external_data_tools(tool_ref_id); +CREATE INDEX IF NOT EXISTS idx_external_data_tool_status ON external_data_tools(status); + +-- 添加注释 +COMMENT ON TABLE external_data_tools IS '外部数据工具表 - 存储用户创建的外部 API 工具配置'; +COMMENT ON COLUMN external_data_tools.tool_ref_id IS 'Agent Manager 返回的工具标识,部署 Agent 时传递此标识'; +COMMENT ON COLUMN external_data_tools.status IS '工具状态:pending(等待生成), active(可用), error(生成失败)'; + diff --git a/services/mcp-server/migrations/018_add_external_toolkits.sql b/services/mcp-server/migrations/018_add_external_toolkits.sql new file mode 100644 index 0000000..2c4c0cd --- /dev/null +++ b/services/mcp-server/migrations/018_add_external_toolkits.sql @@ -0,0 +1,39 @@ +-- 018: 添加外部数据工具集表 +-- 用户可以将多个外部数据工具组合成一个工具集,方便部署自定义 Agent + +-- 创建外部数据工具集表 +CREATE TABLE IF NOT EXISTS external_toolkits ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- 基本信息 + name VARCHAR(100) NOT NULL, + description TEXT, + + -- 工具列表(存储工具 ID 的 JSON 数组,最多 8 个) + tool_ids JSONB NOT NULL DEFAULT '[]'::jsonb, + + -- 归属信息 + owner_id UUID NOT NULL REFERENCES users(id), + tenant_id UUID REFERENCES users(id), + channel_id UUID REFERENCES channels(id), + is_active BOOLEAN DEFAULT TRUE, + + -- 统计信息 + usage_count INTEGER DEFAULT 0, + + -- 时间戳 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- 约束:同一用户下工具集名称唯一 + CONSTRAINT uq_toolkit_name_owner UNIQUE (name, owner_id) +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_external_toolkit_owner ON external_toolkits(owner_id); + +-- 添加注释 +COMMENT ON TABLE external_toolkits IS '外部数据工具集表 - 存储用户创建的工具组合'; +COMMENT ON COLUMN external_toolkits.tool_ids IS '工具 ID 列表,JSON 数组格式,最多 8 个工具'; +COMMENT ON COLUMN external_toolkits.usage_count IS '工具集被 Agent 使用次数'; + diff --git a/services/mcp-server/migrations/run_017_migration.py b/services/mcp-server/migrations/run_017_migration.py new file mode 100644 index 0000000..f1630c3 --- /dev/null +++ b/services/mcp-server/migrations/run_017_migration.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +运行 017 迁移:添加外部数据工具表 +""" + +import asyncio +import os +import sys +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import asyncpg + + +async def run_migration(): + """执行迁移""" + # 获取数据库连接信息 + database_url = os.getenv( + "DATABASE_URL", + "postgresql://postgres:postgres@localhost:5432/mcp_server" + ) + + # 解析连接字符串 + if database_url.startswith("postgresql://"): + # 转换为 asyncpg 格式 + database_url = database_url.replace("postgresql://", "postgres://") + + print(f"连接数据库: {database_url.split('@')[1] if '@' in database_url else database_url}") + + try: + conn = await asyncpg.connect(database_url) + print("数据库连接成功") + + # 读取 SQL 文件 + sql_file = Path(__file__).parent / "017_add_external_data_tools.sql" + with open(sql_file, "r", encoding="utf-8") as f: + sql = f.read() + + print("执行迁移 SQL...") + await conn.execute(sql) + print("迁移执行成功!") + + # 验证表是否创建 + result = await conn.fetchval( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'external_data_tools'" + ) + if result > 0: + print("✅ external_data_tools 表已创建") + else: + print("❌ external_data_tools 表创建失败") + + await conn.close() + + except Exception as e: + print(f"迁移失败: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(run_migration()) + diff --git a/services/mcp-server/models.py b/services/mcp-server/models.py index c85a671..ff5c80d 100644 --- a/services/mcp-server/models.py +++ b/services/mcp-server/models.py @@ -1360,3 +1360,85 @@ class TenantModelKey(BaseModel, Base): Index("idx_tenant_model_key_status", status), UniqueConstraint("tenant_id", "model_name", name="uq_tenant_model"), ) + + +class ExternalDataTool(BaseModel, Base): + """外部数据工具模型 + + 用户创建的外部数据工具,通过 Agent Manager 生成 Pydantic 工具文件。 + MCP-Server 只存储工具的基本信息和 Agent Manager 返回的标识, + 工具的实际代码文件由 Agent Manager 管理。 + + 核心流程: + 1. 用户上传工具配置(表单或 JSON 文件) + 2. MCP-Server 发送配置给 Agent Manager 生成 Pydantic 工具文件 + 3. Agent Manager 返回 tool_ref_id(工具标识) + 4. MCP-Server 存储基本信息 + tool_ref_id + 5. 部署 Agent 时,传递 tool_ref_id 列表给 Agent Manager + """ + __tablename__ = "external_data_tools" + + # 基本信息(用于前端展示) + name = Column(String(100), nullable=False) + description = Column(Text) + + # API 配置(仅用于展示,实际配置存储在 Agent Manager) + url = Column(String(500), nullable=False) + method = Column(String(10), nullable=False, default="POST") + auth_type = Column(String(20), default="none") # api_key, bearer, basic, none + + # Agent Manager 关联(核心字段) + tool_ref_id = Column(String(100), unique=True) # AM 返回的工具标识,部署时传给 AM + status = Column(String(20), default="pending") # pending, active, error + error_message = Column(Text) # 生成失败时的错误信息 + + # 归属信息 + owner_id = Column(GUID(), ForeignKey("users.id"), nullable=False) + tenant_id = Column(GUID(), ForeignKey("users.id")) # 租户 ID(可选) + channel_id = Column(GUID(), ForeignKey("channels.id")) # 渠道 ID(可选) + is_active = Column(Boolean, default=True) + + # 统计信息 + usage_count = Column(Integer, default=0) # 被 Agent 使用次数 + + # 关联关系 + owner = relationship("User", foreign_keys=[owner_id]) + + __table_args__ = ( + Index("idx_external_data_tool_owner", owner_id), + Index("idx_external_data_tool_ref", tool_ref_id), + Index("idx_external_data_tool_status", status), + ) + + +class ExternalToolkit(BaseModel, Base): + """外部数据工具集模型 + + 用户可以将多个外部数据工具组合成一个工具集,方便部署自定义 Agent。 + 每个工具集最多包含 8 个工具。 + """ + __tablename__ = "external_toolkits" + + # 基本信息 + name = Column(String(100), nullable=False) + description = Column(Text) + + # 工具列表(存储工具 ID 的 JSON 数组,最多 8 个) + tool_ids = Column(JSON, nullable=False, default=list) + + # 归属信息 + owner_id = Column(GUID(), ForeignKey("users.id"), nullable=False) + tenant_id = Column(GUID(), ForeignKey("users.id")) + channel_id = Column(GUID(), ForeignKey("channels.id")) + is_active = Column(Boolean, default=True) + + # 统计信息 + usage_count = Column(Integer, default=0) # 被 Agent 使用次数 + + # 关联关系 + owner = relationship("User", foreign_keys=[owner_id]) + + __table_args__ = ( + Index("idx_external_toolkit_owner", owner_id), + UniqueConstraint("name", owner_id, name="uq_toolkit_name_owner"), # 同一用户下工具集名称唯一 + ) diff --git a/services/model-gateway/Dockerfile b/services/model-gateway/Dockerfile deleted file mode 100644 index 1cd2653..0000000 --- a/services/model-gateway/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM python:3.11-slim - -# 设置工作目录 -WORKDIR /app - -# 安装系统依赖与 Node.js (prisma需要) -RUN apt-get update && apt-get install -y \ - curl \ - nodejs \ - npm && \ - rm -rf /var/lib/apt/lists/* - -# 安装LiteLLM和prisma (使用国内源) -# 注意:Prisma Python 客户端 0.12.0 需要 Prisma CLI 5.8.0 -# 需要同时安装 Prisma CLI 和 Python 客户端 -RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple \ - litellm[proxy]==1.17.0 \ - redis==5.0.1 \ - prometheus-client==0.19.0 \ - prisma==0.12.0 && \ - (rm -f /usr/local/bin/prisma || true) && \ - npm install -g prisma@5.8.0 - -# 复制配置文件 -COPY config/ ./config/ - -# 创建logs目录 -RUN mkdir -p logs - -# 设置环境变量 -ENV LITELLM_MASTER_KEY=sk-taiji-master-key -ENV LITELLM_CONFIG_PATH=/app/config/litellm.yaml - -# 暴露端口 -EXPOSE 4000 - -# 健康检查(使用 API key,增加超时时间因为health端点需要检查所有模型) -HEALTHCHECK --interval=60s --timeout=30s --start-period=30s --retries=3 \ - CMD curl -f -H "Authorization: Bearer sk-taiji-master-key" --max-time 25 http://localhost:4000/health || exit 1 - -# 启动LiteLLM代理 -CMD ["litellm", "--config", "/app/config/litellm_simple.yaml", "--port", "4000", "--host", "0.0.0.0"] - diff --git a/services/model-gateway/config/litellm.yaml b/services/model-gateway/config/litellm.yaml deleted file mode 100644 index 1fff9f2..0000000 --- a/services/model-gateway/config/litellm.yaml +++ /dev/null @@ -1,379 +0,0 @@ -# LiteLLM 网关配置 -# taiji-AI-PAD 模型治理层配置 - -# 基础设置 -general_settings: - master_key: "os.environ/LITELLM_MASTER_KEY" - database_url: "os.environ/DATABASE_URL" - - # 日志设置 - set_verbose: true - json_logs: true - log_raw_request_response: false # 生产环境设为false - - # 缓存设置 - redis_host: "redis" - redis_port: 6379 - redis_password: null - - # 速率限制 - max_budget: 1000.0 # 美元 - budget_duration: "30d" - - # ✅ 回调和监控 - 添加webhook用于Token计费 - success_callback: ["langfuse", "webhook"] - failure_callback: ["langfuse", "webhook"] - - # Webhook配置 - 指向mcp-server的计费webhook端点 - webhook_url: "http://mcp-server:8002/api/v1/billing/litellm-callback" - webhook_headers: - Content-Type: "application/json" - - # 安全设置 - allowed_ips: ["127.0.0.1", "172.20.0.0/16"] # Docker网络 - -# 模型配置 -model_list: - # OpenAI 模型组 - - model_name: "gpt-3.5-turbo" - litellm_params: - model: "openai/gpt-3.5-turbo" - api_key: "os.environ/OPENAI_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - max_input_tokens: 16385 - max_output_tokens: 4096 - input_cost_per_token: 0.0000015 - output_cost_per_token: 0.000002 - - - model_name: "gpt-4" - litellm_params: - model: "openai/gpt-4" - api_key: "os.environ/OPENAI_API_KEY" - max_tokens: 8000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - max_input_tokens: 8192 - max_output_tokens: 8192 - input_cost_per_token: 0.00003 - output_cost_per_token: 0.00006 - - - model_name: "gpt-4-turbo" - litellm_params: - model: "openai/gpt-4-turbo-preview" - api_key: "os.environ/OPENAI_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 128000 - max_output_tokens: 4096 - input_cost_per_token: 0.00001 - output_cost_per_token: 0.00003 - - # Anthropic 模型组 - - model_name: "claude-3-haiku" - litellm_params: - model: "anthropic/claude-3-haiku-20240307" - api_key: "os.environ/ANTHROPIC_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 200000 - max_output_tokens: 4096 - input_cost_per_token: 0.00000025 - output_cost_per_token: 0.00000125 - - - model_name: "claude-3-sonnet" - litellm_params: - model: "anthropic/claude-3-sonnet-20240229" - api_key: "os.environ/ANTHROPIC_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 200000 - max_output_tokens: 4096 - input_cost_per_token: 0.000003 - output_cost_per_token: 0.000015 - - - model_name: "claude-3-opus" - litellm_params: - model: "anthropic/claude-3-opus-20240229" - api_key: "os.environ/ANTHROPIC_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 200000 - max_output_tokens: 4096 - input_cost_per_token: 0.000015 - output_cost_per_token: 0.000075 - - # 本地/开源模型(如果可用) - - model_name: "llama-3-8b" - litellm_params: - model: "ollama/llama3" - api_base: "http://ollama:11434" - max_tokens: 2000 - model_info: - mode: "chat" - supports_function_calling: false - supports_vision: false - max_input_tokens: 8192 - max_output_tokens: 2048 - input_cost_per_token: 0.0 # 本地模型无成本 - output_cost_per_token: 0.0 - - # OpenRouter 模型组 - 通过 OpenRouter 访问多种模型 - # 注意: 使用 openrouter/ 前缀时,LiteLLM 会自动使用 OpenRouter API - - model_name: "openrouter-gpt-4" - litellm_params: - model: "openrouter/openai/gpt-4" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 8000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - max_input_tokens: 8192 - max_output_tokens: 8192 - input_cost_per_token: 0.00003 - output_cost_per_token: 0.00006 - - - model_name: "openrouter-gpt-3.5-turbo" - litellm_params: - model: "openrouter/openai/gpt-3.5-turbo" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - max_input_tokens: 16385 - max_output_tokens: 4096 - input_cost_per_token: 0.0000015 - output_cost_per_token: 0.000002 - - - model_name: "openrouter-claude-3.5-sonnet" - litellm_params: - model: "openrouter/anthropic/claude-3.5-sonnet" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 200000 - max_output_tokens: 4096 - input_cost_per_token: 0.000003 - output_cost_per_token: 0.000015 - - - model_name: "openrouter-claude-3-opus" - litellm_params: - model: "openrouter/anthropic/claude-3-opus" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - max_input_tokens: 200000 - max_output_tokens: 4096 - input_cost_per_token: 0.000015 - output_cost_per_token: 0.000075 - -# 路由器配置 -router_settings: - routing_strategy: "least-busy" # 路由策略: least-busy, round-robin, latency-based - allowed_fails: 3 - cooldown_time: 30 - retry_after: 10 - - # 模型组定义 - model_group_configs: - - group_name: "gpt-3.5-group" - models: - - model_name: "gpt-3.5-turbo" - weight: 1.0 - - - group_name: "gpt-4-group" - models: - - model_name: "gpt-4" - weight: 0.7 - - model_name: "gpt-4-turbo" - weight: 0.3 - - - group_name: "claude-group" - models: - - model_name: "claude-3-haiku" - weight: 0.5 - - model_name: "claude-3-sonnet" - weight: 0.3 - - model_name: "claude-3-opus" - weight: 0.2 - - - group_name: "fast-models" - models: - - model_name: "gpt-3.5-turbo" - weight: 0.4 - - model_name: "claude-3-haiku" - weight: 0.4 - - model_name: "llama-3-8b" - weight: 0.2 - - - group_name: "premium-models" - models: - - model_name: "gpt-4-turbo" - weight: 0.4 - - model_name: "claude-3-opus" - weight: 0.3 - - model_name: "claude-3-sonnet" - weight: 0.3 - - - group_name: "openrouter-group" - models: - - model_name: "openrouter-gpt-4" - weight: 0.3 - - model_name: "openrouter-gpt-3.5-turbo" - weight: 0.3 - - model_name: "openrouter-claude-3.5-sonnet" - weight: 0.25 - - model_name: "openrouter-claude-3-opus" - weight: 0.15 - -# 用户和权限配置 -litellm_settings: - # API密钥管理 - api_keys: - - key: "sk-taiji-mcp-server" - models: ["gpt-3.5-turbo", "gpt-4", "claude-3-haiku", "claude-3-sonnet", "openrouter-gpt-4", "openrouter-gpt-3.5-turbo", "openrouter-claude-3.5-sonnet"] - max_budget: 100.0 - budget_duration: "1d" - metadata: - user_id: "mcp-server" - service: "mcp-server" - - - key: "sk-taiji-data-ingestion" - models: ["gpt-3.5-turbo", "claude-3-haiku", "llama-3-8b", "openrouter-gpt-3.5-turbo", "openrouter-claude-3.5-sonnet"] - max_budget: 50.0 - budget_duration: "1d" - metadata: - user_id: "data-ingestion" - service: "data-ingestion" - - - key: "sk-taiji-agent-dev" - models: ["gpt-3.5-group", "claude-group", "fast-models", "openrouter-group"] - max_budget: 20.0 - budget_duration: "1d" - metadata: - user_id: "agent-development" - service: "agent-development" - - - key: "sk-taiji-premium" - models: ["premium-models", "gpt-4-group", "openrouter-group", "openrouter-gpt-4", "openrouter-claude-3-opus", "openrouter-claude-3.5-sonnet"] - max_budget: 200.0 - budget_duration: "1d" - metadata: - user_id: "premium-user" - service: "premium" - -# 回调配置 -callbacks: - # 成功回调 - success_callback: - - callback_name: "langfuse" - callback_type: "success" - callback_vars: - langfuse_public_key: "os.environ/LANGFUSE_PUBLIC_KEY" - langfuse_secret_key: "os.environ/LANGFUSE_SECRET_KEY" - langfuse_host: "os.environ/LANGFUSE_HOST" - - # 失败回调 - failure_callback: - - callback_name: "langfuse" - callback_type: "failure" - callback_vars: - langfuse_public_key: "os.environ/LANGFUSE_PUBLIC_KEY" - langfuse_secret_key: "os.environ/LANGFUSE_SECRET_KEY" - langfuse_host: "os.environ/LANGFUSE_HOST" - -# 监控和指标 -monitoring: - prometheus_port: 4001 - health_check_interval: 30 - - # 自定义指标 - custom_metrics: - - name: "taiji_model_requests_total" - type: "counter" - description: "Total model requests" - labels: ["model", "user_id", "status"] - - - name: "taiji_model_latency" - type: "histogram" - description: "Model response latency" - labels: ["model", "user_id"] - - - name: "taiji_model_cost" - type: "gauge" - description: "Model cost tracking" - labels: ["model", "user_id"] - -# 错误处理 -error_handling: - # 重试配置 - retry_policy: - max_retries: 3 - retry_delay: 1.0 - exponential_backoff: true - - # 超时设置 - timeout: - request_timeout: 60 - - # 回退策略 - fallback: - enabled: true - fallback_models: - "gpt-4": ["openrouter-gpt-4", "gpt-4-turbo", "claude-3-sonnet", "openrouter-claude-3-sonnet"] - "claude-3-opus": ["openrouter-claude-3-opus", "claude-3-sonnet", "openrouter-claude-3-sonnet", "gpt-4", "openrouter-gpt-4"] - "gpt-3.5-turbo": ["openrouter-gpt-3.5-turbo", "claude-3-haiku", "llama-3-8b"] - "openrouter-gpt-4": ["gpt-4", "gpt-4-turbo", "openrouter-claude-3.5-sonnet"] - "openrouter-gpt-3.5-turbo": ["gpt-3.5-turbo", "claude-3-haiku"] - "openrouter-claude-3.5-sonnet": ["claude-3-sonnet", "gpt-4", "openrouter-gpt-4"] - "openrouter-claude-3-opus": ["claude-3-opus", "claude-3-sonnet", "openrouter-claude-3.5-sonnet"] - -# 日志配置 -logging: - level: "INFO" - format: "json" - - # 请求日志 - log_requests: true - log_responses: false # 生产环境关闭 - - # 敏感信息过滤 - redact_messages_in_logs: true - redact_user_api_key_info: true - diff --git a/services/model-gateway/config/litellm_simple.yaml b/services/model-gateway/config/litellm_simple.yaml deleted file mode 100644 index 6afdb47..0000000 --- a/services/model-gateway/config/litellm_simple.yaml +++ /dev/null @@ -1,107 +0,0 @@ -# LiteLLM 简化配置 - 用于测试和开发 -# taiji-AI-PAD 模型治理层配置 - -# 基础设置 -general_settings: - master_key: "sk-taiji-master-key" - - # 日志设置 - set_verbose: true - json_logs: false - log_raw_request_response: false - - # 安全设置 - allowed_ips: ["127.0.0.1", "172.20.0.0/16", "0.0.0.0/0"] # 允许所有IP用于测试 - -# 模型配置 - 使用测试模型和 OpenRouter -model_list: - # 测试模型 - 使用 OpenRouter 的 Qwen 模型 - - model_name: "test-model" - litellm_params: - model: "openrouter/qwen/qwen-2-7b-instruct" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 500 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: false - supports_vision: false - - # OpenRouter 模型 - 通过 OpenRouter 访问 - # 注意: 使用 openrouter/ 前缀时,LiteLLM 会自动使用 OpenRouter API - - model_name: "openrouter-gpt-4o-mini" - litellm_params: - model: "openrouter/openai/gpt-4o-mini" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 1000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - - - model_name: "openrouter-gpt-3.5-turbo" - litellm_params: - model: "openrouter/openai/gpt-3.5-turbo" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 1000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - - # gpt-3.5-turbo 别名 - 向后兼容 - - model_name: "gpt-3.5-turbo" - litellm_params: - model: "openrouter/openai/gpt-3.5-turbo" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: false - - - model_name: "openrouter-claude-3.5-sonnet" - litellm_params: - model: "openrouter/anthropic/claude-3.5-sonnet" - api_key: "os.environ/OPENROUTER_API_KEY" - max_tokens: 4000 - temperature: 0.7 - model_info: - mode: "chat" - supports_function_calling: true - supports_vision: true - -# 路由器配置 -router_settings: - routing_strategy: "round-robin" - allowed_fails: 1 - cooldown_time: 10 - -# 用户配置 -litellm_settings: - api_keys: - - key: "sk-test-key" - models: ["test-model", "openrouter-gpt-4o-mini", "openrouter-gpt-3.5-turbo", "openrouter-claude-3.5-sonnet", "gpt-3.5-turbo"] - metadata: - user_id: "test-user" - service: "testing" - - - key: "sk-taiji-master-key" - models: ["test-model", "openrouter-gpt-4o-mini", "openrouter-gpt-3.5-turbo", "openrouter-claude-3.5-sonnet", "gpt-3.5-turbo"] - metadata: - user_id: "master" - service: "all" - -# 监控 -monitoring: - health_check_interval: 30 - -# 日志配置 -logging: - level: "INFO" - format: "text" - log_requests: true - log_responses: false