oclaw/runtime/skills_market.py
oliver 4d9232f3b3 统一技能安装与 Skills 市场接入链路。
新增公开安装工具与多来源 provider 支持,补齐管理端与目录加载逻辑,并同步更新相关测试与文档以保证可见性和路径安全。

Made-with: Cursor
2026-04-30 22:17:50 +08:00

96 lines
3.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol
from oclaw.runtime.tools.skills.clawhub_client import get_skill_detail, search_skills
from oclaw.runtime.tools.skills.cocoloop_client import get_skill_detail_by_slug as cocoloop_get_skill_detail
from oclaw.runtime.tools.skills.cocoloop_client import search_store_skills as cocoloop_search_skills
def normalize_skill_market_provider_setting(raw: str | None) -> str:
"""Tenant setting value for ``AIA_SKILL_MARKET_PROVIDER``: ``clawhub`` or ``cocoloop``."""
p = str(raw or "").strip().lower()
if p in {"cocoloop", "cocoloop-cn", "cocoloop_cn"}:
return "cocoloop"
return "clawhub"
class SkillMarketAdapter(Protocol):
provider: str
def search(self, query: str, *, limit: int = 20) -> list[dict[str, Any]]: ...
def detail(self, slug: str) -> dict[str, Any]: ...
def resolve_archive_url(self, *, slug: str, version: str | None = None) -> tuple[str, str]:
"""Return (archive_url, resolved_version)."""
@dataclass(frozen=True)
class ClawHubMarketAdapter:
provider: str = "clawhub"
def search(self, query: str, *, limit: int = 20) -> list[dict[str, Any]]:
return search_skills(query, limit=limit)
def detail(self, slug: str) -> dict[str, Any]:
return get_skill_detail(slug)
def resolve_archive_url(self, *, slug: str, version: str | None = None) -> tuple[str, str]:
detail = self.detail(slug)
requested = str(version or "").strip()
if requested:
for row in detail.get("versions") or []:
if not isinstance(row, dict):
continue
if str(row.get("version") or "").strip() != requested:
continue
return str(row.get("archiveUrl") or "").strip(), requested
return "", requested
latest = str(detail.get("latestVersion") or "").strip()
return str(detail.get("archiveUrl") or "").strip(), latest
@dataclass(frozen=True)
class CocoloopMarketAdapter:
provider: str = "cocoloop"
def search(self, query: str, *, limit: int = 20) -> list[dict[str, Any]]:
return cocoloop_search_skills(query, limit=limit)
def detail(self, slug: str) -> dict[str, Any]:
return cocoloop_get_skill_detail(slug)
def resolve_archive_url(self, *, slug: str, version: str | None = None) -> tuple[str, str]:
detail = self.detail(slug)
requested = str(version or "").strip().lstrip("vV")
if requested:
for row in detail.get("versions") or []:
if not isinstance(row, dict):
continue
ver = str(row.get("version") or "").strip().lstrip("vV")
if ver != requested:
continue
return str(row.get("archiveUrl") or "").strip(), str(row.get("version") or requested)
latest = str(detail.get("latestVersion") or "").strip()
return str(detail.get("archiveUrl") or "").strip(), latest
def get_market_adapter(provider: str | None) -> SkillMarketAdapter:
p = normalize_skill_market_provider_setting(provider)
if p in {"clawhub", "openclaw"}:
return ClawHubMarketAdapter(provider="clawhub")
if p in {"cocoloop", "cocoloop-cn", "cocoloop_cn"}:
return CocoloopMarketAdapter(provider="cocoloop")
raise ValueError(f"unsupported_market_provider:{p}")
__all__ = [
"SkillMarketAdapter",
"ClawHubMarketAdapter",
"CocoloopMarketAdapter",
"get_market_adapter",
"normalize_skill_market_provider_setting",
]