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 from typing import Optional
import uuid import uuid
from datetime import datetime from datetime import datetime
import httpx
from urllib.parse import urlparse
from ..database import get_db from ..database import get_db
from ..models import ( from ..models import (
@@ -17,6 +19,84 @@ from ..middleware.log_service import LogService
router = APIRouter() 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") @router.get("/ads")
def list_public_ads(db: Session = Depends(get_db)): def list_public_ads(db: Session = Depends(get_db)):
""" """
@@ -59,6 +139,7 @@ class UploadAppRequest(BaseModel):
join_promotion: Optional[bool] = False join_promotion: Optional[bool] = False
agent_endpoint: Optional[str] = None agent_endpoint: Optional[str] = None
api_key: Optional[str] = None api_key: Optional[str] = None
platform: Optional[str] = None
class PurchaseAppRequest(BaseModel): class PurchaseAppRequest(BaseModel):
@@ -209,6 +290,15 @@ def upload_app(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), 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( app = MarketApp(
id=gen_uuid(), id=gen_uuid(),
name=req.name, name=req.name,
@@ -216,6 +306,7 @@ def upload_app(
icon=req.icon, icon=req.icon,
type=req.type if req.type in ("skill","agent","mcp") else "skill", type=req.type if req.type in ("skill","agent","mcp") else "skill",
source=req.source if req.source in ("official","user") else "user", source=req.source if req.source in ("official","user") else "user",
platform=req.platform,
price=req.price, price=req.price,
install_count=0, install_count=0,
rating_avg=0, rating_avg=0,
+1
View File
@@ -294,6 +294,7 @@ class MarketApp(Base):
install_count = Column(Integer, default=0) install_count = Column(Integer, default=0)
skill_content = Column(Text, default=None) skill_content = Column(Text, default=None)
mcp_transport = Column(String(20), 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_endpoint = Column(String(500), default=None)
agent_api_key = Column(String(500), default=None, comment="单个Agent接入的API Key") agent_api_key = Column(String(500), default=None, comment="单个Agent接入的API Key")
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
+2
View File
@@ -313,6 +313,8 @@ export const marketAPI = {
createApp: (data: any) => post<any>('/market/apps', data), createApp: (data: any) => post<any>('/market/apps', data),
verifyAgent: (data: any) => post<any>('/market/apps/verify-agent', data),
purchaseApp: (id: string) => post<any>(`/market/apps/${id}/purchase`), purchaseApp: (id: string) => post<any>(`/market/apps/${id}/purchase`),
installApp: (id: string) => post<any>(`/market/apps/${id}/install`), installApp: (id: string) => post<any>(`/market/apps/${id}/install`),
+45
View File
@@ -856,6 +856,51 @@
cursor: not-allowed; 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 */
.audit-progress { .audit-progress {
display: flex; display: flex;
+60 -5
View File
@@ -1172,14 +1172,53 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
const [endpoint, setEndpoint] = useState(''); const [endpoint, setEndpoint] = useState('');
const [price, setPrice] = useState<number>(0); const [price, setPrice] = useState<number>(0);
const [connecting, setConnecting] = useState(false); const [connecting, setConnecting] = useState(false);
const [verifying, setVerifying] = useState(false);
const [verifyMsg, setVerifyMsg] = useState<string>('');
const [verifyOk, setVerifyOk] = useState<boolean | null>(null);
const handleConnect = () => { const runVerify = async (): Promise<boolean | null> => {
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; if (!name.trim() || !endpoint.trim() || !apiKey.trim()) return;
// 接入前必须先通过连通性 / 鉴权校验
const ok = await runVerify();
if (!ok) return;
setConnecting(true); setConnecting(true);
setTimeout(() => { try {
await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim());
} finally {
setConnecting(false); setConnecting(false);
onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim()); }
}, 800);
}; };
return ( return (
@@ -1254,10 +1293,25 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
/> />
<span className="hub-price-hint">用户对接的 Agent 是否收费由您决定;管理员导入默认免费且审核通过。</span> <span className="hub-price-hint">用户对接的 Agent 是否收费由您决定;管理员导入默认免费且审核通过。</span>
</div> </div>
<div className="form-section agent-verify-row">
<button
type="button"
className="form-test-btn"
onClick={runVerify}
disabled={!endpoint.trim() || !apiKey.trim() || verifying}
>
{verifying ? '校验中...' : '测试连接'}
</button>
{verifyMsg && (
<span className={`agent-verify-msg ${verifyOk === true ? 'ok' : verifyOk === false ? 'err' : ''}`}>
{verifyMsg}
</span>
)}
</div>
<button <button
className="form-submit-btn" className="form-submit-btn"
onClick={handleConnect} onClick={handleConnect}
disabled={!name.trim() || !endpoint.trim() || !apiKey.trim() || connecting} disabled={!name.trim() || !endpoint.trim() || !apiKey.trim() || connecting || verifying}
> >
{connecting ? '接入中...' : '接入 Agent'} {connecting ? '接入中...' : '接入 Agent'}
</button> </button>
@@ -1393,6 +1447,7 @@ const MarketPage: React.FC = () => {
source: 'user', source: 'user',
agent_endpoint: config.endpoint || '', agent_endpoint: config.endpoint || '',
api_key: config.apiKey || '', api_key: config.apiKey || '',
platform: platformId || 'dify',
}); });
// 重新加载列表 // 重新加载列表
loadApps(); loadApps();
+10
View File
@@ -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;