feat(market): skills/mcp 能力分类 + AI验证期自动识别 + 导入自动入座
This commit is contained in:
1 parent
2ae5d9f9b5
commit
6f3fb400a4
7 files changed
+1407
-1140
No files matched your search
@@ -14,10 +14,11 @@ import os
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, MarketApp, AppPurchase, AppReview, AppInstallation, Order,
|
||||
AdBanner, MCPServer, gen_uuid
|
||||
AdBanner, MCPServer, ModelProvider, gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional
|
||||
from ..middleware.log_service import LogService
|
||||
from ..capability_registry import CAPABILITY_TYPES, resolve_auto_model as _resolve_auto_model
|
||||
from app.api.admin.mcp_admin import _test_mcp_server, parse_mcp_config
|
||||
|
||||
router = APIRouter()
|
||||
@@ -206,6 +207,53 @@ def verify_agent_connectivity(platform: str, endpoint: str, api_key: str) -> dic
|
||||
return {"ok": False, "verified": False, "detail": f"地址不可达:{_short_err(e)}"}
|
||||
|
||||
|
||||
# ===== 能力分类(AI 自动归类,任何失败都返回 None,不阻断导入) =====
|
||||
def _classify_capability(name: str, description: Optional[str], content: Optional[str], db: Session) -> Optional[str]:
|
||||
"""用平台文本模型把应用归类到 11 种能力之一;失败返回 None(不阻断导入)。"""
|
||||
try:
|
||||
text_model = _resolve_auto_model("text", db)
|
||||
if not text_model:
|
||||
return None
|
||||
provider = None
|
||||
if getattr(text_model, "provider_id", None):
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == text_model.provider_id).first()
|
||||
api_key = text_model.api_key or (provider.api_key if provider else None)
|
||||
if not api_key:
|
||||
return None
|
||||
api_url = text_model.endpoint or (
|
||||
f"{provider.api_base_url}/v1/chat/completions" if provider and provider.api_base_url else None
|
||||
)
|
||||
if not api_url:
|
||||
return None
|
||||
options = "、".join(f"{c['id']}({c['label']})" for c in CAPABILITY_TYPES)
|
||||
sys_prompt = (
|
||||
"你是应用市场的能力分类器。根据应用名称、描述与内容,把它精确归类到下列能力之一:"
|
||||
f"{options}。\n只返回一个 JSON 对象:{{\"capability\": \"<id>\"}},不要任何额外解释或 Markdown。"
|
||||
)
|
||||
user_text = f"名称:{name}\n描述:{description or ''}\n内容摘要:{(content or '')[:2000]}"
|
||||
messages = [
|
||||
{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": user_text},
|
||||
]
|
||||
proxy = _skill_http_proxy()
|
||||
with httpx.Client(timeout=25, follow_redirects=True, trust_env=False, proxy=proxy) as c:
|
||||
r = c.post(
|
||||
api_url,
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
json={"model": text_model.name, "messages": messages, "max_tokens": 64, "temperature": 0, "enable_thinking": False},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
data = r.json()
|
||||
text_out = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
m = re.search(r'"capability"\s*:\s*"([a-z_]+)"', text_out)
|
||||
if m and m.group(1) in {c["id"] for c in CAPABILITY_TYPES}:
|
||||
return m.group(1)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class VerifyAgentRequest(BaseModel):
|
||||
platform: Optional[str] = None
|
||||
endpoint: str
|
||||
@@ -222,6 +270,25 @@ def verify_agent(
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
class DetectCapabilityRequest(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
skill_content: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/apps/detect-capability")
|
||||
def detect_capability(
|
||||
req: DetectCapabilityRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""导入前 AI 验证阶段:根据名称/描述/内容识别能力分类,供前台预填并允许用户修改。"""
|
||||
cap = _classify_capability(req.name, req.description, req.skill_content, db)
|
||||
label = next((c["label"] for c in CAPABILITY_TYPES if c["id"] == cap), None)
|
||||
return {"code": 0, "data": {"capability": cap, "label": label}, "message": "ok"}
|
||||
|
||||
|
||||
@router.get("/ads")
|
||||
def list_public_ads(db: Session = Depends(get_db)):
|
||||
"""
|
||||
@@ -260,6 +327,7 @@ class UploadAppRequest(BaseModel):
|
||||
download_url: Optional[str] = None
|
||||
skill_content: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
capability: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
is_member_free: Optional[bool] = False
|
||||
join_promotion: Optional[bool] = False
|
||||
@@ -281,7 +349,7 @@ class ReviewAppRequest(BaseModel):
|
||||
def list_apps(
|
||||
type: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
capability: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
sort: str = Query(default="latest"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
@@ -295,8 +363,8 @@ def list_apps(
|
||||
query = query.filter(MarketApp.type == type)
|
||||
if source:
|
||||
query = query.filter(MarketApp.source == source)
|
||||
if category:
|
||||
query = query.filter(MarketApp.type == category)
|
||||
if capability:
|
||||
query = query.filter(MarketApp.capability == capability)
|
||||
if search:
|
||||
query = query.filter(
|
||||
and_(
|
||||
@@ -346,6 +414,7 @@ def list_apps(
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"created_at": str(app.created_at) if app.created_at else None,
|
||||
"capability": app.capability or None,
|
||||
"installed": (app.id in installed_app_ids or is_user_owned) if user_id else False,
|
||||
}
|
||||
result.append(item)
|
||||
@@ -400,8 +469,9 @@ def get_app(
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"publisher_id": app.publisher_id,
|
||||
"download_url": app.download_url,
|
||||
"detail": app.detail,
|
||||
"download_url": getattr(app, "download_url", None),
|
||||
"detail": getattr(app, "detail", None),
|
||||
"capability": app.capability or None,
|
||||
"created_at": str(app.created_at),
|
||||
"purchased": purchased,
|
||||
"installed": installed,
|
||||
@@ -456,6 +526,9 @@ def upload_app(
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
# 能力分类:前端显式指定优先;否则导入阶段由 AI 自动归类;都不行则留空(不阻断导入)
|
||||
_classify_content = req.skill_content or req.download_url or ""
|
||||
app.capability = req.capability or _classify_capability(req.name, req.description, _classify_content, db) or None
|
||||
db.add(app)
|
||||
db.commit()
|
||||
db.refresh(app)
|
||||
@@ -536,6 +609,7 @@ def upload_app(
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": app.id, "name": app.name, "status": app.status,
|
||||
"capability": app.capability,
|
||||
"created_at": str(app.created_at),
|
||||
},
|
||||
"message": "提交成功,等待审核",
|
||||
|
||||
Reference in new issue
Block a user