feat: 智能Agent接入前连通性/鉴权校验
- 前端技能市场「接入单个Agent」新增「测试连接」按钮,接入前必须校验通过 - 后端 POST /market/apps/verify-agent:dify 走 chat-messages 鉴权探测(trust_env=False 直连),n8n/coze 走 HEAD 可达性探测 - upload_app 对 agent 类型做服务端守卫,密钥无效/不可达直接 400 拒绝 - MarketApp 新增 platform 列(dify/n8n/coze),009 迁移幂等加列
This commit is contained in:
1 parent
b0bfba5c16
commit
d374d464e8
6 files changed
+209
-5
No files matched your search
@@ -5,6 +5,8 @@ from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
import httpx
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
@@ -17,6 +19,84 @@ from ..middleware.log_service import LogService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===== Agent 接入前连通性 / 鉴权校验 =====
|
||||
def _normalize_dify_url(endpoint: str) -> str:
|
||||
ep = endpoint.rstrip("/")
|
||||
if ep.endswith("/chat-messages"):
|
||||
return ep
|
||||
if ep.endswith("/v1/chat-messages"):
|
||||
return ep
|
||||
if ep.endswith("/v1"):
|
||||
return ep + "/chat-messages"
|
||||
return ep + "/v1/chat-messages"
|
||||
|
||||
|
||||
def _short_err(e: Exception) -> str:
|
||||
return str(e)[:160]
|
||||
|
||||
|
||||
def verify_agent_connectivity(platform: str, endpoint: str, api_key: str) -> dict:
|
||||
"""
|
||||
接入 Agent 前的连通性 + 鉴权校验。
|
||||
返回 {ok, verified, detail}
|
||||
ok: 是否通过(dify=鉴权通过;n8n/coze/其他=地址可达)
|
||||
verified: 是否做了真正的鉴权验证(dify 为 True,其余为 False)
|
||||
"""
|
||||
ep = (endpoint or "").strip()
|
||||
if not ep or not ep.startswith(("http://", "https://")):
|
||||
return {"ok": False, "verified": False, "detail": "Endpoint 必须以 http:// 或 https:// 开头"}
|
||||
platform = (platform or "dify").lower()
|
||||
|
||||
# 关键:容器内 env 有坏代理(502),必须 trust_env=False 直连
|
||||
if platform == "dify":
|
||||
url = _normalize_dify_url(ep)
|
||||
payload = {
|
||||
"inputs": {},
|
||||
"query": "连通性测试",
|
||||
"user": "__verify__",
|
||||
"response_mode": "blocking",
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
try:
|
||||
r = httpx.post(url, headers=headers, json=payload, timeout=15.0, trust_env=False)
|
||||
except Exception as e:
|
||||
return {"ok": False, "verified": False, "detail": f"无法连接 Dify 地址:{_short_err(e)}"}
|
||||
if r.status_code == 200:
|
||||
return {"ok": True, "verified": True, "detail": "连通且 API Key 有效"}
|
||||
if r.status_code in (401, 403):
|
||||
return {"ok": False, "verified": False, "detail": "API Key 无效(401/403)"}
|
||||
if r.status_code == 404:
|
||||
return {"ok": False, "verified": False,
|
||||
"detail": "地址路径不正确(404),应为 .../v1/chat-messages"}
|
||||
return {"ok": False, "verified": False, "detail": f"Dify 返回状态码 {r.status_code}"}
|
||||
|
||||
# n8n / coze / 其他:仅做可达性探测(HEAD,避免触发 webhook 副作用)
|
||||
try:
|
||||
r = httpx.head(ep, timeout=12.0, trust_env=False, follow_redirects=True)
|
||||
if r.status_code < 500:
|
||||
return {"ok": True, "verified": False,
|
||||
"detail": f"地址可连通(HTTP {r.status_code}),运行时再做协议级鉴权"}
|
||||
return {"ok": False, "verified": False, "detail": f"服务返回 {r.status_code},地址可能不可用"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "verified": False, "detail": f"地址不可达:{_short_err(e)}"}
|
||||
|
||||
|
||||
class VerifyAgentRequest(BaseModel):
|
||||
platform: Optional[str] = None
|
||||
endpoint: str
|
||||
api_key: str
|
||||
|
||||
|
||||
@router.post("/apps/verify-agent")
|
||||
def verify_agent(
|
||||
req: VerifyAgentRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""接入 Agent 前手动测试连通性 / 鉴权,供前台「测试连接」按钮调用。"""
|
||||
result = verify_agent_connectivity(req.platform or "dify", req.endpoint, req.api_key)
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.get("/ads")
|
||||
def list_public_ads(db: Session = Depends(get_db)):
|
||||
"""
|
||||
@@ -59,6 +139,7 @@ class UploadAppRequest(BaseModel):
|
||||
join_promotion: Optional[bool] = False
|
||||
agent_endpoint: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
platform: Optional[str] = None
|
||||
|
||||
|
||||
class PurchaseAppRequest(BaseModel):
|
||||
@@ -209,6 +290,15 @@ def upload_app(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 接入 Agent 时先做连通性 / 鉴权校验,避免把无效连接存进库
|
||||
if (req.type or "") == "agent" and req.agent_endpoint and req.api_key:
|
||||
v = verify_agent_connectivity(req.platform or "dify", req.agent_endpoint, req.api_key)
|
||||
if not v["ok"]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Agent 连接校验未通过:{v['detail']}",
|
||||
)
|
||||
|
||||
app = MarketApp(
|
||||
id=gen_uuid(),
|
||||
name=req.name,
|
||||
@@ -216,6 +306,7 @@ def upload_app(
|
||||
icon=req.icon,
|
||||
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
||||
source=req.source if req.source in ("official","user") else "user",
|
||||
platform=req.platform,
|
||||
price=req.price,
|
||||
install_count=0,
|
||||
rating_avg=0,
|
||||
|
||||
@@ -294,6 +294,7 @@ class MarketApp(Base):
|
||||
install_count = Column(Integer, default=0)
|
||||
skill_content = Column(Text, default=None)
|
||||
mcp_transport = Column(String(20), default=None)
|
||||
platform = Column(String(20), default=None, comment="Agent 接入平台:dify/n8n/coze")
|
||||
agent_endpoint = Column(String(500), default=None)
|
||||
agent_api_key = Column(String(500), default=None, comment="单个Agent接入的API Key")
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
Reference in new issue
Block a user