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:
QihuAI Design committed 2026-07-15 13:01:19 +08:00
1 parent b0bfba5c16
commit d374d464e8
6 files changed
+209 -5

No files matched your search

+91
View File
@@ -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,
+1
View File
@@ -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)