1306 lines
48 KiB
Python
1306 lines
48 KiB
Python
"""
|
||
外部工具 API 模块
|
||
符合 MCP-Server 调用规范
|
||
参考: http://gitee.ath.cx:3000/xiaohei/taiji-AI-PAD/src/branch/feature/chenchen/Docs/Agent-Manager外部工具接口规范.md
|
||
|
||
职责:
|
||
- 接收 MCP-Server 的工具配置请求
|
||
- 生成 Pydantic AI 工具代码
|
||
- 存储工具配置和代码文件
|
||
- 支持工具的 CRUD 操作
|
||
- 支持带工具的 Agent 创建
|
||
"""
|
||
|
||
import os
|
||
import uuid
|
||
import logging
|
||
import requests
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Any
|
||
from fastapi import APIRouter, HTTPException, Query
|
||
from pydantic import BaseModel, Field
|
||
|
||
from agent_code_generator import agent_code_generator
|
||
from tool_storage import tool_storage
|
||
from gitee_manager import gitee_manager
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 创建路由 - 按照规范使用 /tools 前缀
|
||
router = APIRouter(prefix="/external-tools", tags=["External Tool API (MCP-Server 规范)"])
|
||
|
||
|
||
# ==================== 请求/响应模型 ====================
|
||
|
||
class AuthConfig(BaseModel):
|
||
"""认证配置"""
|
||
type: str = Field(..., description="认证类型: api_key, bearer, basic")
|
||
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
|
||
token: Optional[str] = Field(None, description="Bearer Token(与 key 等效,兼容字段)")
|
||
username: Optional[str] = Field(None, description="Basic Auth 用户名")
|
||
password: Optional[str] = Field(None, description="Basic Auth 密码")
|
||
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
|
||
name: Optional[str] = Field("X-API-Key", description="API Key 参数名")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
def get_token_or_key(self) -> Optional[str]:
|
||
"""获取 token 或 key(兼容两种字段名)"""
|
||
return self.token or self.key
|
||
|
||
|
||
class RetryConfig(BaseModel):
|
||
"""重试配置"""
|
||
max_retries: int = Field(3, description="最大重试次数")
|
||
retry_delay: int = Field(1, description="重试间隔(秒)")
|
||
|
||
|
||
class GenerateToolRequest(BaseModel):
|
||
"""
|
||
生成外部数据工具请求
|
||
符合接口规范文档
|
||
"""
|
||
name: str = Field(..., min_length=1, max_length=100, description="工具名称(将用于生成 Python 函数名)")
|
||
description: str = Field(..., description="工具描述(将作为工具的 docstring)")
|
||
url: str = Field(..., description="API 端点 URL")
|
||
method: str = Field(..., description="HTTP 方法:GET/POST/PUT/DELETE/PATCH")
|
||
user_id: str = Field(..., description="用户 ID(UUID 格式)")
|
||
tenant_id: Optional[str] = Field(None, description="租户 ID(UUID 格式)")
|
||
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
|
||
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(JSON Schema 格式)")
|
||
request_body: Optional[Dict] = Field(None, description="请求体定义(JSON Schema 格式)")
|
||
input_schema: Optional[Dict] = Field(None, description="输入参数定义(兼容字段,等同于 request_params)")
|
||
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
|
||
timeout: int = Field(30, description="超时时间(秒),默认 30")
|
||
retry: Optional[RetryConfig] = Field(None, description="重试配置")
|
||
use_ai: bool = Field(True, description="是否使用 AI 智能生成代码(默认开启,支持复杂场景如 URL 拼接)")
|
||
api_key: Optional[str] = Field(None, description="LLM API Key(use_ai=true 时需要)")
|
||
|
||
|
||
class SimpleGenerateToolRequest(BaseModel):
|
||
"""
|
||
简化版工具生成请求
|
||
只需要三个核心字段:url、auth、request_body_schema
|
||
默认使用 AI 辅助生成
|
||
"""
|
||
name: str = Field(..., min_length=1, max_length=100, description="工具名称")
|
||
description: Optional[str] = Field(None, description="工具描述(可选,AI 会自动推断)")
|
||
url: str = Field(..., description="API 端点 URL")
|
||
method: str = Field("POST", description="HTTP 方法,默认 POST")
|
||
user_id: str = Field("default", description="用户 ID")
|
||
|
||
# 核心三要素
|
||
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||
request_body_schema: Optional[Dict] = Field(None, description="请求体 Schema (JSON Schema 格式)")
|
||
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(可选)")
|
||
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头(可选)")
|
||
|
||
# AI 生成相关
|
||
api_key: Optional[str] = Field(None, description="LLM API Key(可选,使用系统默认)")
|
||
|
||
class Config:
|
||
# 允许额外字段,让 AI 可以处理任意用户输入
|
||
extra = "allow"
|
||
|
||
|
||
class UpdateToolRequest(BaseModel):
|
||
"""更新外部数据工具请求"""
|
||
description: Optional[str] = Field(None, description="工具描述")
|
||
url: Optional[str] = Field(None, description="API URL")
|
||
method: Optional[str] = Field(None, description="HTTP 方法")
|
||
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
|
||
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||
request_params: Optional[Dict] = Field(None, description="请求参数定义")
|
||
request_body: Optional[Dict] = Field(None, description="请求体定义")
|
||
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
|
||
timeout: Optional[int] = Field(None, description="超时时间")
|
||
|
||
|
||
class TestToolRequest(BaseModel):
|
||
"""测试工具连接请求"""
|
||
test_params: Optional[Dict] = Field(None, description="测试参数")
|
||
test_body: Optional[Dict] = Field(None, description="测试请求体")
|
||
|
||
|
||
class ResourceConfig(BaseModel):
|
||
"""资源配置"""
|
||
cpu_request: str = Field("100m", description="CPU 请求(如 100m, 500m)")
|
||
cpu_limit: str = Field("500m", description="CPU 限制(如 500m, 1000m)")
|
||
memory_request: str = Field("128Mi", description="内存请求(如 128Mi, 256Mi)")
|
||
memory_limit: str = Field("512Mi", description="内存限制(如 512Mi, 1Gi)")
|
||
|
||
|
||
class CreateAgentWithToolsRequest(BaseModel):
|
||
"""创建带有外部工具的 Agent 请求"""
|
||
name: str = Field(..., min_length=1, max_length=63, description="Agent 名称(符合 K8s 命名规范)")
|
||
template: str = Field(..., description="Agent 模板名称")
|
||
tool_refs: List[str] = Field(default=[], description="外部数据工具标识列表")
|
||
config: Optional[Dict] = Field(default_factory=dict, description="其他配置")
|
||
env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量")
|
||
# 资源配置 - 参考原有模板创建方式
|
||
cpu_request: Optional[str] = Field("100m", description="CPU 请求(如 100m, 500m)")
|
||
cpu_limit: Optional[str] = Field("500m", description="CPU 限制(如 500m, 1000m)")
|
||
memory_request: Optional[str] = Field("128Mi", description="内存请求(如 128Mi, 256Mi)")
|
||
memory_limit: Optional[str] = Field("512Mi", description="内存限制(如 512Mi, 1Gi)")
|
||
replicas: Optional[int] = Field(1, ge=1, le=10, description="副本数量")
|
||
|
||
|
||
# ==================== 工具 API 接口 ====================
|
||
|
||
@router.post("/generate")
|
||
async def generate_tool(request: GenerateToolRequest):
|
||
"""
|
||
1️⃣ 生成外部数据工具
|
||
|
||
MCP-Server 调用此接口,Agent Manager 需要:
|
||
1. 验证配置格式
|
||
2. 根据配置生成 Pydantic AI 工具代码文件
|
||
3. 存储工具代码文件和完整配置(包含敏感信息如 API Key)
|
||
4. 返回唯一的 tool_ref_id 供后续引用
|
||
"""
|
||
try:
|
||
# 验证 HTTP 方法
|
||
valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||
if request.method.upper() not in valid_methods:
|
||
return {
|
||
"success": False,
|
||
"error": "invalid_method",
|
||
"message": f"无效的 HTTP 方法。支持: {', '.join(valid_methods)}"
|
||
}
|
||
|
||
# 验证 URL 格式
|
||
if not request.url.startswith(("http://", "https://")):
|
||
return {
|
||
"success": False,
|
||
"error": "invalid_url",
|
||
"message": "URL 必须以 http:// 或 https:// 开头"
|
||
}
|
||
|
||
# 验证认证配置
|
||
if request.auth:
|
||
valid_auth_types = ["api_key", "bearer", "basic"]
|
||
if request.auth.type not in valid_auth_types:
|
||
return {
|
||
"success": False,
|
||
"error": "invalid_auth",
|
||
"message": f"无效的认证类型。支持: {', '.join(valid_auth_types)}"
|
||
}
|
||
|
||
# 生成唯一 tool_ref_id
|
||
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
|
||
|
||
# 合并 request_params 和 input_schema(兼容两种字段名)
|
||
merged_params = request.request_params or request.input_schema
|
||
|
||
# 处理认证配置 - 兼容 token 和 key 字段
|
||
auth_config = None
|
||
if request.auth:
|
||
auth_config = request.auth.model_dump(by_alias=True)
|
||
# 兼容 token 字段:如果用户使用 token,将其映射到 key
|
||
if auth_config.get("token") and not auth_config.get("key"):
|
||
auth_config["key"] = auth_config["token"]
|
||
|
||
# 构建工具配置
|
||
tool_config = {
|
||
"name": request.name,
|
||
"description": request.description,
|
||
"url": request.url,
|
||
"method": request.method.upper(),
|
||
"headers": request.headers,
|
||
"auth": auth_config,
|
||
"request_params": merged_params,
|
||
"input_schema": merged_params, # 保留两种格式供 AI 理解
|
||
"request_body": request.request_body,
|
||
"response_mapping": request.response_mapping,
|
||
"timeout": request.timeout,
|
||
"retry": request.retry.model_dump() if request.retry else None
|
||
}
|
||
|
||
# 生成 Pydantic AI 工具代码
|
||
if request.use_ai:
|
||
# 使用 AI 智能生成(支持复杂场景)
|
||
import asyncio
|
||
import concurrent.futures
|
||
logger.info(f"🤖 使用 AI 生成工具代码: {request.name}")
|
||
|
||
# 在新线程中运行异步代码
|
||
def run_async():
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
try:
|
||
return loop.run_until_complete(
|
||
agent_code_generator.generate_tool_code_with_ai(tool_config, request.api_key)
|
||
)
|
||
finally:
|
||
loop.close()
|
||
|
||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||
future = executor.submit(run_async)
|
||
tool_code = future.result(timeout=90)
|
||
else:
|
||
# 使用模板生成
|
||
tool_code = agent_code_generator.generate_tool_code(tool_config)
|
||
|
||
# 存储工具配置和代码
|
||
save_result = tool_storage.save_tool(
|
||
tool_ref_id=tool_ref_id,
|
||
name=request.name,
|
||
description=request.description,
|
||
config=tool_config,
|
||
code=tool_code,
|
||
user_id=request.user_id,
|
||
tenant_id=request.tenant_id
|
||
)
|
||
|
||
if not save_result.get("success"):
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": save_result.get("error", "工具保存失败")
|
||
}
|
||
|
||
logger.info(f"✅ 工具生成成功: {tool_ref_id} (用户: {request.user_id})")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"name": request.name,
|
||
"description": request.description,
|
||
"created_at": datetime.utcnow().isoformat()
|
||
},
|
||
"message": "工具生成成功"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"工具生成失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.post("/generate-simple")
|
||
async def generate_tool_simple(request: SimpleGenerateToolRequest):
|
||
"""
|
||
🚀 简化版工具生成接口 - 默认使用 AI 辅助
|
||
|
||
只需要提供三个核心字段:
|
||
1. url - API 端点 URL
|
||
2. auth - 认证配置(bearer/api_key/basic)
|
||
3. request_body_schema - 请求体 Schema
|
||
|
||
AI 会智能理解您的配置并生成高质量的 Pydantic AI 工具代码。
|
||
|
||
示例请求:
|
||
```json
|
||
{
|
||
"name": "jina_reader",
|
||
"url": "https://r.jina.ai/",
|
||
"method": "POST",
|
||
"user_id": "test-user",
|
||
"auth": {
|
||
"type": "bearer",
|
||
"token": "your-token-here"
|
||
},
|
||
"request_body_schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"url": {"type": "string", "description": "要爬取的网页URL"}
|
||
},
|
||
"required": ["url"]
|
||
}
|
||
}
|
||
```
|
||
"""
|
||
try:
|
||
# 验证 URL 格式
|
||
if not request.url.startswith(("http://", "https://")):
|
||
return {
|
||
"success": False,
|
||
"error": "invalid_url",
|
||
"message": "URL 必须以 http:// 或 https:// 开头"
|
||
}
|
||
|
||
# 生成唯一 tool_ref_id
|
||
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
|
||
|
||
# 处理认证配置 - 兼容 token 和 key 字段
|
||
auth_config = None
|
||
if request.auth:
|
||
auth_config = request.auth.model_dump(by_alias=True)
|
||
# 兼容 token 字段:如果用户使用 token,将其映射到 key
|
||
if auth_config.get("token") and not auth_config.get("key"):
|
||
auth_config["key"] = auth_config["token"]
|
||
|
||
# 自动推断描述(如果未提供)
|
||
description = request.description
|
||
if not description:
|
||
description = f"调用 {request.name} API"
|
||
if request.request_body_schema:
|
||
props = request.request_body_schema.get("properties", {})
|
||
if props:
|
||
param_names = list(props.keys())[:3]
|
||
description += f",参数: {', '.join(param_names)}"
|
||
|
||
# 构建完整的工具配置 - 保留用户原始输入供 AI 理解
|
||
tool_config = {
|
||
"name": request.name,
|
||
"description": description,
|
||
"url": request.url,
|
||
"method": request.method.upper(),
|
||
"headers": request.headers,
|
||
"auth": auth_config,
|
||
# 支持两种参数格式
|
||
"request_body": request.request_body_schema,
|
||
"request_params": request.request_params,
|
||
"input_schema": request.request_body_schema or request.request_params,
|
||
"timeout": 30,
|
||
# 保存原始请求供 AI 参考(可能包含额外字段)
|
||
"_original_request": request.model_dump(exclude_unset=False)
|
||
}
|
||
|
||
# 🤖 默认使用 AI 生成(更智能、更灵活)
|
||
import asyncio
|
||
import concurrent.futures
|
||
|
||
logger.info(f"🤖 [简化模式] 使用 AI 生成工具代码: {request.name}")
|
||
|
||
def run_async():
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
try:
|
||
return loop.run_until_complete(
|
||
agent_code_generator.generate_tool_code_with_ai(tool_config, request.api_key)
|
||
)
|
||
finally:
|
||
loop.close()
|
||
|
||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||
future = executor.submit(run_async)
|
||
tool_code = future.result(timeout=90)
|
||
|
||
# 存储工具配置和代码
|
||
save_result = tool_storage.save_tool(
|
||
tool_ref_id=tool_ref_id,
|
||
name=request.name,
|
||
description=description,
|
||
config=tool_config,
|
||
code=tool_code,
|
||
user_id=request.user_id,
|
||
tenant_id=None
|
||
)
|
||
|
||
if not save_result.get("success"):
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": save_result.get("error", "工具保存失败")
|
||
}
|
||
|
||
logger.info(f"✅ [简化模式] 工具生成成功: {tool_ref_id}")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"name": request.name,
|
||
"description": description,
|
||
"url": request.url,
|
||
"method": request.method.upper(),
|
||
"has_auth": bool(request.auth),
|
||
"created_at": datetime.utcnow().isoformat()
|
||
},
|
||
"message": "工具生成成功 (AI 辅助)"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[简化模式] 工具生成失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
return {
|
||
"success": False,
|
||
"error": "generation_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.put("/{tool_ref_id}")
|
||
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
|
||
"""
|
||
2️⃣ 更新外部数据工具
|
||
|
||
更新已存在的工具配置,重新生成代码
|
||
"""
|
||
try:
|
||
# 检查工具是否存在
|
||
existing_tool = tool_storage.get_tool(tool_ref_id)
|
||
if not existing_tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
# 构建更新内容
|
||
updates = {}
|
||
if request.description is not None:
|
||
updates["description"] = request.description
|
||
if request.url is not None:
|
||
updates["url"] = request.url
|
||
if request.method is not None:
|
||
# 验证 HTTP 方法
|
||
valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||
if request.method.upper() not in valid_methods:
|
||
return {
|
||
"success": False,
|
||
"error": "invalid_method",
|
||
"message": f"无效的 HTTP 方法。支持: {', '.join(valid_methods)}"
|
||
}
|
||
updates["method"] = request.method.upper()
|
||
if request.headers is not None:
|
||
updates["headers"] = request.headers
|
||
if request.auth is not None:
|
||
updates["auth"] = request.auth.model_dump(by_alias=True)
|
||
if request.request_params is not None:
|
||
updates["request_params"] = request.request_params
|
||
if request.request_body is not None:
|
||
updates["request_body"] = request.request_body
|
||
if request.response_mapping is not None:
|
||
updates["response_mapping"] = request.response_mapping
|
||
if request.timeout is not None:
|
||
updates["timeout"] = request.timeout
|
||
|
||
# 更新工具配置
|
||
updated_tool = tool_storage.update_tool(tool_ref_id, updates)
|
||
|
||
if not updated_tool:
|
||
return {
|
||
"success": False,
|
||
"error": "update_failed",
|
||
"message": "工具更新失败"
|
||
}
|
||
|
||
# 重新生成代码
|
||
tool_code = agent_code_generator.generate_tool_code(updated_tool["config"])
|
||
|
||
# 更新代码文件
|
||
tool_storage.save_tool(
|
||
tool_ref_id=tool_ref_id,
|
||
name=updated_tool["name"],
|
||
description=updated_tool.get("description", ""),
|
||
config=updated_tool["config"],
|
||
code=tool_code,
|
||
user_id=updated_tool["user_id"],
|
||
tenant_id=updated_tool.get("tenant_id")
|
||
)
|
||
|
||
logger.info(f"✅ 工具更新成功: {tool_ref_id}")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"name": updated_tool["name"],
|
||
"updated_at": updated_tool.get("updated_at", datetime.utcnow().isoformat())
|
||
},
|
||
"message": "工具更新成功"
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"工具更新失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "update_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.delete("/{tool_ref_id}")
|
||
async def delete_tool(tool_ref_id: str):
|
||
"""
|
||
3️⃣ 删除外部数据工具
|
||
|
||
删除工具配置和代码文件
|
||
"""
|
||
try:
|
||
# 检查工具是否存在
|
||
existing_tool = tool_storage.get_tool(tool_ref_id)
|
||
if not existing_tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
# 检查工具是否正在被使用
|
||
if tool_storage.check_tool_in_use(tool_ref_id):
|
||
return {
|
||
"success": False,
|
||
"error": "tool_in_use",
|
||
"message": "工具正在被 Agent 使用,无法删除"
|
||
}
|
||
|
||
# 删除工具
|
||
tool_storage.delete_tool(tool_ref_id)
|
||
|
||
logger.info(f"✅ 工具删除成功: {tool_ref_id}")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id
|
||
},
|
||
"message": "工具删除成功"
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"工具删除失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "delete_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.post("/{tool_ref_id}/test")
|
||
async def test_tool(tool_ref_id: str, request: TestToolRequest = None):
|
||
"""
|
||
4️⃣ 测试工具连接
|
||
|
||
使用存储的配置发送测试请求,验证 API 连通性
|
||
"""
|
||
try:
|
||
# 获取工具配置
|
||
tool = tool_storage.get_tool(tool_ref_id)
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
config = tool.get("config", {})
|
||
|
||
# 构建请求
|
||
url = config.get("url", "")
|
||
method = config.get("method", "GET")
|
||
timeout = config.get("timeout", 30)
|
||
|
||
headers = config.get("headers", {}) or {}
|
||
params = (request.test_params if request else None) or {}
|
||
body = (request.test_body if request else None) or None
|
||
|
||
# 处理认证
|
||
auth = config.get("auth")
|
||
if auth:
|
||
auth_type = auth.get("type", "")
|
||
|
||
if auth_type == "api_key":
|
||
location = auth.get("in", "header")
|
||
key_name = auth.get("name", "X-API-Key")
|
||
# 兼容 token 和 key 字段
|
||
key_value = auth.get("token") or auth.get("key", "")
|
||
|
||
if location == "header":
|
||
headers[key_name] = key_value
|
||
elif location == "query":
|
||
params[key_name] = key_value
|
||
|
||
elif auth_type == "bearer":
|
||
# 兼容 token 和 key 字段
|
||
token_value = auth.get("token") or auth.get("key", "")
|
||
headers["Authorization"] = f"Bearer {token_value}"
|
||
|
||
elif auth_type == "basic":
|
||
import base64
|
||
credentials = base64.b64encode(
|
||
f"{auth.get('username', '')}:{auth.get('password', '')}".encode()
|
||
).decode()
|
||
headers["Authorization"] = f"Basic {credentials}"
|
||
|
||
# 发送测试请求
|
||
start_time = datetime.utcnow()
|
||
|
||
response = requests.request(
|
||
method=method,
|
||
url=url,
|
||
headers=headers,
|
||
params=params if method.upper() == "GET" else None,
|
||
json=body if method.upper() != "GET" else None,
|
||
timeout=timeout
|
||
)
|
||
|
||
elapsed_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
|
||
|
||
# 构建响应
|
||
result = {
|
||
"status_code": response.status_code,
|
||
"response_time_ms": round(elapsed_ms, 2),
|
||
"content_type": response.headers.get("Content-Type"),
|
||
"headers": dict(response.headers)
|
||
}
|
||
|
||
# 响应预览(截断)
|
||
if response.text:
|
||
result["response_preview"] = response.text[:500]
|
||
if len(response.text) > 500:
|
||
result["response_preview"] += "... (truncated)"
|
||
|
||
# 判断成功
|
||
is_success = 200 <= response.status_code < 300
|
||
|
||
return {
|
||
"success": is_success,
|
||
"data": result,
|
||
"message": "连接测试成功" if is_success else f"连接测试失败 (HTTP {response.status_code})"
|
||
}
|
||
|
||
except requests.exceptions.Timeout:
|
||
return {
|
||
"success": False,
|
||
"error": "timeout",
|
||
"message": "连接超时"
|
||
}
|
||
except requests.exceptions.ConnectionError:
|
||
return {
|
||
"success": False,
|
||
"error": "connection_error",
|
||
"message": "无法连接到目标服务"
|
||
}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"工具测试失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "test_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/{tool_ref_id}")
|
||
async def get_tool(tool_ref_id: str):
|
||
"""
|
||
获取工具详情
|
||
|
||
返回工具的完整配置信息(不含敏感信息)
|
||
"""
|
||
try:
|
||
tool = tool_storage.get_tool(tool_ref_id)
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
# 脱敏处理
|
||
safe_tool = {
|
||
"tool_ref_id": tool["tool_ref_id"],
|
||
"name": tool["name"],
|
||
"description": tool.get("description"),
|
||
"url": tool["config"].get("url"),
|
||
"method": tool["config"].get("method"),
|
||
"status": tool.get("status", "created"),
|
||
"created_at": tool.get("created_at"),
|
||
"updated_at": tool.get("updated_at"),
|
||
"user_id": tool.get("user_id"),
|
||
"tenant_id": tool.get("tenant_id"),
|
||
"has_auth": bool(tool["config"].get("auth")),
|
||
"used_by_agents": tool.get("used_by_agents", [])
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"data": safe_tool
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"获取工具失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "get_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/")
|
||
async def list_tools(
|
||
user_id: Optional[str] = Query(None, description="用户 ID 过滤"),
|
||
tenant_id: Optional[str] = Query(None, description="租户 ID 过滤")
|
||
):
|
||
"""
|
||
列出所有工具
|
||
|
||
支持按用户和租户过滤
|
||
"""
|
||
try:
|
||
tools = tool_storage.list_tools(user_id=user_id, tenant_id=tenant_id)
|
||
|
||
# 简化输出
|
||
tool_list = [
|
||
{
|
||
"tool_ref_id": t["tool_ref_id"],
|
||
"name": t["name"],
|
||
"description": t.get("description"),
|
||
"status": t.get("status", "created"),
|
||
"created_at": t.get("created_at")
|
||
}
|
||
for t in tools
|
||
]
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tools": tool_list,
|
||
"count": len(tool_list)
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"列出工具失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "list_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/{tool_ref_id}/code")
|
||
async def get_tool_code(tool_ref_id: str):
|
||
"""
|
||
获取工具生成的代码
|
||
|
||
返回 Pydantic AI 工具代码
|
||
"""
|
||
try:
|
||
code = tool_storage.get_tool_code(tool_ref_id)
|
||
if not code:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"code": code
|
||
}
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"获取工具代码失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "get_code_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
# ==================== 工具集成到 Agent ====================
|
||
|
||
@router.post("/agents/create-with-tools")
|
||
async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
|
||
"""
|
||
5️⃣ 创建带有外部工具的 Agent
|
||
|
||
这是扩展的 Agent 创建接口,支持 tool_refs 字段。
|
||
当传递 tool_refs 时,Agent Manager 需要:
|
||
1. 加载对应的工具代码文件
|
||
2. 将工具集成到 Agent 中
|
||
3. 生成完整项目并部署到 AKS
|
||
"""
|
||
try:
|
||
# 验证工具引用
|
||
tools = []
|
||
missing_tools = []
|
||
|
||
for ref in request.tool_refs:
|
||
tool = tool_storage.get_tool(ref)
|
||
if tool:
|
||
tools.append(tool)
|
||
else:
|
||
missing_tools.append(ref)
|
||
|
||
if missing_tools:
|
||
return {
|
||
"success": False,
|
||
"error": "tool_not_found",
|
||
"message": f"以下工具不存在: {', '.join(missing_tools)}"
|
||
}
|
||
|
||
# 转换工具配置为代码生成器需要的格式
|
||
tools_config = []
|
||
for tool in tools:
|
||
config = tool.get("config", {})
|
||
# 获取 AI 生成的工具代码
|
||
tool_code = tool_storage.get_tool_code(tool.get("tool_ref_id", ""))
|
||
tools_config.append({
|
||
"name": tool["name"],
|
||
"description": tool.get("description", ""),
|
||
"url": config.get("url"),
|
||
"method": config.get("method", "GET"),
|
||
"auth": config.get("auth"),
|
||
"request_params": config.get("request_params"),
|
||
"request_body": config.get("request_body"),
|
||
"timeout": config.get("timeout", 30),
|
||
"generated_code": tool_code # 传递 AI 生成的代码
|
||
})
|
||
|
||
# 生成唯一后缀,确保不同用户创建同名 Agent 不会冲突
|
||
unique_suffix = uuid.uuid4().hex[:6]
|
||
base_name = request.name.lower().replace("_", "-").replace(" ", "-")
|
||
|
||
# k8s_name 带唯一后缀,避免域名/namespace 冲突
|
||
k8s_name = f"{base_name}-{unique_suffix}"
|
||
repo_name = f"agent-{k8s_name}"
|
||
agent_ref_id = f"agent-{repo_name}"
|
||
|
||
# 生成完整项目文件(传递资源配置)
|
||
# 使用 k8s_name 作为 agent_name,确保 CI/CD 中创建的 DNS/namespace 与 API 返回一致
|
||
project_files = agent_code_generator.generate_full_project(
|
||
agent_name=k8s_name,
|
||
description=f"Agent with {len(tools)} external tools",
|
||
tools_config=tools_config,
|
||
auto_deploy=True,
|
||
cpu_request=request.cpu_request or "100m",
|
||
cpu_limit=request.cpu_limit or "500m",
|
||
memory_request=request.memory_request or "128Mi",
|
||
memory_limit=request.memory_limit or "512Mi",
|
||
replicas=request.replicas or 1
|
||
)
|
||
|
||
# 创建 Gitee 仓库
|
||
repo_result = gitee_manager.create_repository(
|
||
repo_name=repo_name,
|
||
description=f"{request.name} - Agent with external tools",
|
||
private=False
|
||
)
|
||
|
||
if not repo_result.get("success"):
|
||
return {
|
||
"success": False,
|
||
"error": "repo_creation_failed",
|
||
"message": repo_result.get("error", "仓库创建失败")
|
||
}
|
||
|
||
# 获取仓库所有者
|
||
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
|
||
|
||
# ⚠️ 重要:先设置 CI/CD Secrets,再推送文件
|
||
# 因为推送文件会触发 CI/CD,必须确保 secrets 已经就绪
|
||
cicd_secrets = {
|
||
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
|
||
"ACR_USERNAME": "agnettaiji",
|
||
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
|
||
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
|
||
"AZ_CLIENT_SECRET": "S0J8Q~DE.DEu29nreaBn2EbeuGOg7GEIkonRMbYj",
|
||
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
|
||
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
|
||
"AZ_RG": "taiji-ai-pda",
|
||
"AZ_AKS": "taiji-ai-pda",
|
||
"AZURE_DNS_ZONE": "taijiagnet.com",
|
||
"AZURE_DNS_RG": "taiji-Ai-v0"
|
||
}
|
||
|
||
logger.info(f"📝 设置 CI/CD Secrets(共 {len(cicd_secrets)} 个)...")
|
||
secrets_result = gitee_manager.set_repo_secrets(
|
||
repo_name=repo_name,
|
||
secrets=cicd_secrets,
|
||
owner=repo_owner
|
||
)
|
||
|
||
if not secrets_result.get("success"):
|
||
logger.warning(f"⚠️ 部分 Secrets 设置可能失败: {secrets_result}")
|
||
|
||
# 等待一小段时间确保 secrets 生效
|
||
import time
|
||
time.sleep(1)
|
||
|
||
# 推送文件(这会触发 CI/CD)
|
||
logger.info(f"📤 推送项目文件...")
|
||
push_result = gitee_manager.push_files(
|
||
repo_name=repo_name,
|
||
files=project_files,
|
||
commit_message=f"Initial commit: {request.name} with {len(tools)} tools",
|
||
owner=repo_owner
|
||
)
|
||
|
||
# 标记工具被使用
|
||
for ref in request.tool_refs:
|
||
tool_storage.mark_tool_in_use(ref, request.name)
|
||
|
||
# 计算域名和 namespace(k8s_name 已在上面定义,带唯一后缀)
|
||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||
namespace = f"agent-{k8s_name}"
|
||
|
||
# 存储 Agent 信息以便后续查询
|
||
AGENT_REFS[agent_ref_id] = {
|
||
"agent_ref_id": agent_ref_id,
|
||
"name": request.name, # 原始名称(用户输入)
|
||
"display_name": request.name, # 显示名称
|
||
"k8s_name": k8s_name, # K8s 名称(带唯一后缀,用于域名/namespace)
|
||
"repo_name": repo_name,
|
||
"repo_url": repo_result.get("html_url"),
|
||
"repo_owner": repo_owner,
|
||
"namespace": namespace,
|
||
"domain": expected_domain,
|
||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
|
||
"tools": [t["name"] for t in tools],
|
||
"tool_refs": request.tool_refs,
|
||
"user_id": request.config.get("user_id") if request.config else None,
|
||
"created_at": datetime.utcnow().isoformat(),
|
||
"status": "building"
|
||
}
|
||
|
||
logger.info(f"✅ Agent 创建成功: {request.name} (工具: {len(tools)} 个)")
|
||
logger.info(f" Agent Ref ID: {agent_ref_id}")
|
||
logger.info(f" 查询构建状态: GET /external-tools/agents/{agent_ref_id}/build-status")
|
||
|
||
return {
|
||
"success": True,
|
||
"name": request.name,
|
||
"agent_ref_id": agent_ref_id,
|
||
"namespace": namespace,
|
||
"status": "Building",
|
||
"created_at": datetime.utcnow().isoformat(),
|
||
"template": request.template,
|
||
"service_port": 8000,
|
||
"access_info": {
|
||
"domain": expected_domain,
|
||
"domain_url": f"http://{expected_domain}",
|
||
"ip_url": "pending"
|
||
},
|
||
"resources": {
|
||
"cpu_request": request.cpu_request or "100m",
|
||
"cpu_limit": request.cpu_limit or "500m",
|
||
"memory_request": request.memory_request or "128Mi",
|
||
"memory_limit": request.memory_limit or "512Mi",
|
||
"replicas": request.replicas or 1
|
||
},
|
||
"tools_attached": len(tools),
|
||
"repo_url": repo_result.get("html_url"),
|
||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
|
||
"status_query_url": f"/external-tools/agents/{agent_ref_id}/build-status",
|
||
"deployment_info_url": f"/external-tools/agents/{agent_ref_id}/deployment-info"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建带工具的 Agent 失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
return {
|
||
"success": False,
|
||
"error": "deployment_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/{tool_ref_id}/preview-code")
|
||
async def preview_generated_code(tool_ref_id: str):
|
||
"""
|
||
预览工具生成的 Pydantic AI 代码
|
||
|
||
用于调试和验证代码生成是否正确
|
||
"""
|
||
try:
|
||
tool = tool_storage.get_tool(tool_ref_id)
|
||
if not tool:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
|
||
)
|
||
|
||
code = tool_storage.get_tool_code(tool_ref_id)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"tool_ref_id": tool_ref_id,
|
||
"name": tool["name"],
|
||
"generated_code": code,
|
||
"code_preview": code[:1000] if code else None
|
||
}
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"预览代码失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "preview_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
# ==================== Agent 状态查询 ====================
|
||
|
||
# 内存存储 Agent 引用(用于查询构建状态)
|
||
AGENT_REFS: Dict[str, Dict] = {}
|
||
|
||
|
||
@router.get("/agents/{agent_ref_id}/build-status")
|
||
async def get_agent_build_status(agent_ref_id: str):
|
||
"""
|
||
查询 Agent CI/CD 构建状态
|
||
|
||
返回:
|
||
- Gitee Action 运行状态
|
||
- ACR 镜像是否已构建
|
||
- 整体构建状态
|
||
"""
|
||
try:
|
||
# 检查是否有存储的 agent 信息
|
||
agent_info = AGENT_REFS.get(agent_ref_id)
|
||
|
||
if not agent_info:
|
||
# 尝试从 repo_name 推断
|
||
if agent_ref_id.startswith("agent-"):
|
||
repo_name = agent_ref_id.replace("agent-", "", 1)
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"error": "agent_not_found",
|
||
"message": f"Agent {agent_ref_id} 不存在或信息已丢失"
|
||
}
|
||
else:
|
||
repo_name = agent_info.get("repo_name", agent_ref_id.replace("agent-", "", 1))
|
||
|
||
# 查询 Gitee Action 状态
|
||
action_status = gitee_manager.get_action_status(repo_name)
|
||
|
||
# 查询 ACR 镜像状态
|
||
image_name = f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest"
|
||
acr_status = _check_acr_image(image_name)
|
||
|
||
# 判断整体状态
|
||
overall_status = "unknown"
|
||
if acr_status.get("exists"):
|
||
overall_status = "ready"
|
||
elif action_status.get("conclusion") == "failure":
|
||
overall_status = "build_failed"
|
||
elif action_status.get("status") == "in_progress":
|
||
overall_status = "building"
|
||
elif action_status.get("status") == "completed" and action_status.get("conclusion") == "success":
|
||
overall_status = "deployed"
|
||
elif action_status.get("status") == "no_runs":
|
||
overall_status = "pending"
|
||
|
||
# 计算域名 - 优先使用存储的 k8s_name
|
||
if agent_info and agent_info.get("k8s_name"):
|
||
k8s_name = agent_info.get("k8s_name")
|
||
else:
|
||
# 无法从 repo_name 准确推断,使用 agent 名称
|
||
agent_name = agent_info.get("name") if agent_info else None
|
||
if agent_name:
|
||
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||
else:
|
||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||
|
||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||
namespace = f"agent-{k8s_name}"
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"agent_ref_id": agent_ref_id,
|
||
"repo_name": repo_name,
|
||
"overall_status": overall_status,
|
||
"gitee_action": {
|
||
"status": action_status.get("status"),
|
||
"conclusion": action_status.get("conclusion"),
|
||
"run_id": action_status.get("run_id"),
|
||
"html_url": action_status.get("html_url"),
|
||
"message": action_status.get("message")
|
||
},
|
||
"acr_image": {
|
||
"image_name": image_name,
|
||
"exists": acr_status.get("exists", False),
|
||
"tags": acr_status.get("all_tags", [])
|
||
},
|
||
"access_info": {
|
||
"expected_domain": expected_domain,
|
||
"expected_url": f"http://{expected_domain}",
|
||
"expected_namespace": namespace
|
||
}
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询构建状态失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "query_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
@router.get("/agents/{agent_ref_id}/deployment-info")
|
||
async def get_agent_deployment_info(agent_ref_id: str):
|
||
"""
|
||
获取 Agent 部署后的详细信息
|
||
|
||
返回:
|
||
- K8s 部署状态
|
||
- 服务端点信息
|
||
- DNS 域名信息
|
||
- 访问 URL
|
||
"""
|
||
try:
|
||
# 检查是否有存储的 agent 信息
|
||
agent_info = AGENT_REFS.get(agent_ref_id)
|
||
|
||
# 从 agent_ref_id 推断 repo_name
|
||
if agent_info:
|
||
repo_name = agent_info.get("repo_name")
|
||
k8s_name = agent_info.get("k8s_name")
|
||
namespace = agent_info.get("namespace")
|
||
else:
|
||
if agent_ref_id.startswith("agent-"):
|
||
repo_name = agent_ref_id.replace("agent-", "", 1)
|
||
else:
|
||
repo_name = agent_ref_id
|
||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||
namespace = f"agent-{k8s_name}"
|
||
|
||
# 导入 K8sManager 查询实际状态
|
||
from k8s_manager import K8sManager
|
||
|
||
deployment_info = {
|
||
"agent_ref_id": agent_ref_id,
|
||
"repo_name": repo_name,
|
||
"namespace": namespace,
|
||
"k8s_status": {},
|
||
"access_info": {},
|
||
"image_info": {}
|
||
}
|
||
|
||
try:
|
||
k8s_mgr = K8sManager(namespace=namespace)
|
||
|
||
# 查询 namespace 是否存在
|
||
try:
|
||
ns = k8s_mgr.v1.read_namespace(name=namespace)
|
||
deployment_info["k8s_status"]["namespace_exists"] = True
|
||
deployment_info["k8s_status"]["namespace_status"] = ns.status.phase
|
||
except Exception:
|
||
deployment_info["k8s_status"]["namespace_exists"] = False
|
||
deployment_info["k8s_status"]["status"] = "not_deployed"
|
||
return {
|
||
"success": True,
|
||
"data": deployment_info,
|
||
"message": "Agent 尚未部署到 K8s"
|
||
}
|
||
|
||
# 查询 Pod 状态
|
||
try:
|
||
pods = k8s_mgr.v1.list_namespaced_pod(namespace=namespace)
|
||
if pods.items:
|
||
pod = pods.items[0]
|
||
deployment_info["k8s_status"]["pod_name"] = pod.metadata.name
|
||
deployment_info["k8s_status"]["pod_status"] = pod.status.phase
|
||
deployment_info["k8s_status"]["pod_ip"] = pod.status.pod_ip
|
||
deployment_info["k8s_status"]["node_name"] = pod.spec.node_name
|
||
deployment_info["k8s_status"]["status"] = "running" if pod.status.phase == "Running" else pod.status.phase.lower()
|
||
except Exception as e:
|
||
deployment_info["k8s_status"]["pod_error"] = str(e)
|
||
|
||
# 查询 Service 状态
|
||
try:
|
||
services = k8s_mgr.v1.list_namespaced_service(namespace=namespace)
|
||
for svc in services.items:
|
||
if svc.spec.type == "LoadBalancer":
|
||
deployment_info["access_info"]["service_name"] = svc.metadata.name
|
||
deployment_info["access_info"]["cluster_ip"] = svc.spec.cluster_ip
|
||
|
||
# 获取外网 IP
|
||
if svc.status.load_balancer.ingress:
|
||
external_ip = svc.status.load_balancer.ingress[0].ip
|
||
deployment_info["access_info"]["external_ip"] = external_ip
|
||
deployment_info["access_info"]["ip_url"] = f"http://{external_ip}"
|
||
except Exception as e:
|
||
deployment_info["access_info"]["service_error"] = str(e)
|
||
|
||
# DNS 域名信息
|
||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||
deployment_info["access_info"]["domain"] = expected_domain
|
||
deployment_info["access_info"]["domain_url"] = f"http://{expected_domain}"
|
||
deployment_info["access_info"]["recommended_url"] = f"http://{expected_domain}"
|
||
|
||
# 镜像信息
|
||
deployment_info["image_info"] = {
|
||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
|
||
"registry": "agnettaiji.azurecr.io"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询 K8s 状态失败: {e}")
|
||
deployment_info["k8s_status"]["error"] = str(e)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": deployment_info
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取部署信息失败: {e}")
|
||
return {
|
||
"success": False,
|
||
"error": "query_failed",
|
||
"message": str(e)
|
||
}
|
||
|
||
|
||
def _check_acr_image(image_name: str) -> Dict[str, Any]:
|
||
"""检查 ACR 镜像是否存在"""
|
||
try:
|
||
import subprocess
|
||
|
||
# 解析镜像名称
|
||
parts = image_name.split("/")
|
||
if len(parts) < 3:
|
||
return {"exists": False, "error": "Invalid image name"}
|
||
|
||
registry = parts[0].replace(".azurecr.io", "")
|
||
repo = "/".join(parts[1:]).split(":")[0]
|
||
tag = parts[-1].split(":")[-1] if ":" in parts[-1] else "latest"
|
||
|
||
# 使用 az acr 命令检查
|
||
result = subprocess.run(
|
||
["az", "acr", "repository", "show-tags",
|
||
"--name", registry,
|
||
"--repository", repo,
|
||
"--output", "json"],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
import json
|
||
tags = json.loads(result.stdout)
|
||
if tag in tags:
|
||
return {
|
||
"exists": True,
|
||
"tag": tag,
|
||
"all_tags": tags
|
||
}
|
||
|
||
return {"exists": False}
|
||
|
||
except Exception as e:
|
||
logger.debug(f"检查 ACR 镜像失败: {e}")
|
||
return {"exists": False, "error": str(e)}
|