From d374d464e87e38e4e9075f098658564b0f2c024e Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Wed, 15 Jul 2026 13:01:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BDAgent=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=89=8D=E8=BF=9E=E9=80=9A=E6=80=A7/=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端技能市场「接入单个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 迁移幂等加列 --- backend/python/app/api/market.py | 91 +++++++++++++++++++++++ backend/python/app/models.py | 1 + frontend/src/api/index.ts | 2 + frontend/src/pages/front/MarketPage.css | 45 +++++++++++ frontend/src/pages/front/MarketPage.tsx | 65 ++++++++++++++-- migrations/009_add_marketapp_platform.sql | 10 +++ 6 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 migrations/009_add_marketapp_platform.sql diff --git a/backend/python/app/api/market.py b/backend/python/app/api/market.py index 20c9142..6723e8a 100644 --- a/backend/python/app/api/market.py +++ b/backend/python/app/api/market.py @@ -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, diff --git a/backend/python/app/models.py b/backend/python/app/models.py index 6399e71..b0414d6 100644 --- a/backend/python/app/models.py +++ b/backend/python/app/models.py @@ -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) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 882640a..f0fbdac 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -313,6 +313,8 @@ export const marketAPI = { createApp: (data: any) => post('/market/apps', data), + verifyAgent: (data: any) => post('/market/apps/verify-agent', data), + purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), installApp: (id: string) => post(`/market/apps/${id}/install`), diff --git a/frontend/src/pages/front/MarketPage.css b/frontend/src/pages/front/MarketPage.css index a0d28c5..f032216 100644 --- a/frontend/src/pages/front/MarketPage.css +++ b/frontend/src/pages/front/MarketPage.css @@ -856,6 +856,51 @@ cursor: not-allowed; } +/* Agent 接入:连通性 / 鉴权校验 */ +.agent-verify-row { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-bottom: var(--space-sm); + flex-wrap: wrap; +} + +.form-test-btn { + padding: var(--space-sm) var(--space-lg); + background: transparent; + color: var(--color-accent); + border: 1px solid var(--color-accent); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); +} + +.form-test-btn:hover:not(:disabled) { + background: var(--color-accent); + color: #fff; +} + +.form-test-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.agent-verify-msg { + font-size: var(--text-xs); + line-height: 1.4; + word-break: break-all; +} + +.agent-verify-msg.ok { + color: #16a34a; +} + +.agent-verify-msg.err { + color: #dc2626; +} + /* Audit Progress */ .audit-progress { display: flex; diff --git a/frontend/src/pages/front/MarketPage.tsx b/frontend/src/pages/front/MarketPage.tsx index 2bcef7c..d36bb7b 100644 --- a/frontend/src/pages/front/MarketPage.tsx +++ b/frontend/src/pages/front/MarketPage.tsx @@ -1172,14 +1172,53 @@ const AgentConnectModal: React.FC = ({ onClose, onConnec const [endpoint, setEndpoint] = useState(''); const [price, setPrice] = useState(0); const [connecting, setConnecting] = useState(false); + const [verifying, setVerifying] = useState(false); + const [verifyMsg, setVerifyMsg] = useState(''); + const [verifyOk, setVerifyOk] = useState(null); - const handleConnect = () => { + const runVerify = async (): Promise => { + if (!endpoint.trim() || !apiKey.trim()) { + setVerifyMsg('请先填写 API Endpoint 与 API Key'); + setVerifyOk(false); + return false; + } + setVerifying(true); + setVerifyMsg('正在校验连通性与密钥...'); + setVerifyOk(null); + try { + const res: any = await marketAPI.verifyAgent({ + platform, + endpoint: endpoint.trim(), + api_key: apiKey.trim(), + }); + if (res && res.code === 0 && res.data && res.data.ok) { + setVerifyOk(true); + setVerifyMsg('✓ ' + (res.data.detail || '校验通过')); + return true; + } + setVerifyOk(false); + setVerifyMsg('✗ ' + ((res && res.data && res.data.detail) || '校验未通过')); + return false; + } catch (e: any) { + setVerifyOk(false); + setVerifyMsg('✗ 校验请求失败:' + (e?.message || '网络错误')); + return false; + } finally { + setVerifying(false); + } + }; + + const handleConnect = async () => { if (!name.trim() || !endpoint.trim() || !apiKey.trim()) return; + // 接入前必须先通过连通性 / 鉴权校验 + const ok = await runVerify(); + if (!ok) return; setConnecting(true); - setTimeout(() => { + try { + await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim()); + } finally { setConnecting(false); - onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim()); - }, 800); + } }; return ( @@ -1254,10 +1293,25 @@ const AgentConnectModal: React.FC = ({ onClose, onConnec /> 用户对接的 Agent 是否收费由您决定;管理员导入默认免费且审核通过。 +
+ + {verifyMsg && ( + + {verifyMsg} + + )} +
@@ -1393,6 +1447,7 @@ const MarketPage: React.FC = () => { source: 'user', agent_endpoint: config.endpoint || '', api_key: config.apiKey || '', + platform: platformId || 'dify', }); // 重新加载列表 loadApps(); diff --git a/migrations/009_add_marketapp_platform.sql b/migrations/009_add_marketapp_platform.sql new file mode 100644 index 0000000..e5de31c --- /dev/null +++ b/migrations/009_add_marketapp_platform.sql @@ -0,0 +1,10 @@ +-- 009: 为 market_apps 增加 platform 列(Agent 接入平台 dify/n8n/coze) +-- 幂等:列已存在则跳过 +SET @col = (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'market_apps' AND column_name = 'platform'); +SET @sql = IF(@col = 0, + 'ALTER TABLE market_apps ADD COLUMN platform VARCHAR(20) NULL COMMENT \'Agent 接入平台:dify/n8n/coze\'', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;