mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 18:10:15 +08:00
feat: add searchable skills marketplace
This commit is contained in:
@@ -27,6 +27,8 @@ class PendingSkillsInteraction:
|
||||
view: str = "root"
|
||||
local_page: int = 0
|
||||
market_page: int = 0
|
||||
market_query: str = ""
|
||||
awaiting_input: Optional[str] = None
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
|
||||
@@ -162,7 +164,14 @@ class SkillsChain(ChainBase):
|
||||
source=source,
|
||||
username=None,
|
||||
)
|
||||
force = (arg_str or "").strip().lower() in {"refresh", "刷新"}
|
||||
normalized_arg = (arg_str or "").strip()
|
||||
force = normalized_arg.lower() in {"refresh", "刷新"}
|
||||
search_query = self._extract_market_search_query(normalized_arg) or (
|
||||
"" if force else normalized_arg
|
||||
)
|
||||
if search_query:
|
||||
request.view = "market"
|
||||
request.market_query = search_query
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
@@ -242,13 +251,22 @@ class SkillsChain(ChainBase):
|
||||
|
||||
if action == "root":
|
||||
request.view = "root"
|
||||
request.awaiting_input = None
|
||||
elif action == "installed":
|
||||
request.view = "installed"
|
||||
request.local_page = 0
|
||||
request.awaiting_input = None
|
||||
elif action == "market":
|
||||
request.view = "market"
|
||||
request.market_page = 0
|
||||
request.awaiting_input = None
|
||||
elif action == "search":
|
||||
request.view = "market"
|
||||
request.awaiting_input = "market-search"
|
||||
elif action == "clear-search":
|
||||
self._clear_market_search(request)
|
||||
elif action == "refresh":
|
||||
request.awaiting_input = None
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
@@ -261,16 +279,19 @@ class SkillsChain(ChainBase):
|
||||
)
|
||||
return True
|
||||
elif action == "page-next":
|
||||
request.awaiting_input = None
|
||||
if request.view == "installed":
|
||||
request.local_page += 1
|
||||
elif request.view == "market":
|
||||
request.market_page += 1
|
||||
elif action == "page-prev":
|
||||
request.awaiting_input = None
|
||||
if request.view == "installed":
|
||||
request.local_page = max(0, request.local_page - 1)
|
||||
elif request.view == "market":
|
||||
request.market_page = max(0, request.market_page - 1)
|
||||
elif action == "install" and index:
|
||||
request.awaiting_input = None
|
||||
success, message = self._install_market_skill(request, index)
|
||||
if success:
|
||||
self.post_message(
|
||||
@@ -293,6 +314,7 @@ class SkillsChain(ChainBase):
|
||||
)
|
||||
)
|
||||
elif action == "remove" and index:
|
||||
request.awaiting_input = None
|
||||
success, message = self._remove_local_skill(request, index)
|
||||
self.post_message(
|
||||
Notification(
|
||||
@@ -354,6 +376,7 @@ class SkillsChain(ChainBase):
|
||||
|
||||
if lowered in {"返回", "back"}:
|
||||
request.view = "root"
|
||||
request.awaiting_input = None
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
@@ -364,6 +387,7 @@ class SkillsChain(ChainBase):
|
||||
return True
|
||||
|
||||
if lowered in {"刷新", "refresh"}:
|
||||
request.awaiting_input = None
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
@@ -375,6 +399,7 @@ class SkillsChain(ChainBase):
|
||||
return True
|
||||
|
||||
if lowered in {"p", "prev", "上一页"}:
|
||||
request.awaiting_input = None
|
||||
if request.view == "installed":
|
||||
request.local_page = max(0, request.local_page - 1)
|
||||
elif request.view == "market":
|
||||
@@ -389,6 +414,7 @@ class SkillsChain(ChainBase):
|
||||
return True
|
||||
|
||||
if lowered in {"n", "next", "下一页"}:
|
||||
request.awaiting_input = None
|
||||
if request.view == "installed":
|
||||
request.local_page += 1
|
||||
elif request.view == "market":
|
||||
@@ -407,6 +433,11 @@ class SkillsChain(ChainBase):
|
||||
request.view = "installed"
|
||||
elif lowered in {"2", "市场", "market"}:
|
||||
request.view = "market"
|
||||
elif self._extract_market_search_query(normalized):
|
||||
self._apply_market_search(
|
||||
request,
|
||||
self._extract_market_search_query(normalized),
|
||||
)
|
||||
else:
|
||||
self.post_message(
|
||||
Notification(
|
||||
@@ -427,9 +458,49 @@ class SkillsChain(ChainBase):
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"清除搜索", "取消搜索", "clear", "clear search"}:
|
||||
if request.view == "market" or request.market_query:
|
||||
self._clear_market_search(request)
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if request.awaiting_input == "market-search":
|
||||
if lowered in {"取消", "cancel"}:
|
||||
request.awaiting_input = None
|
||||
else:
|
||||
search_query = self._extract_market_search_query(normalized) or normalized
|
||||
self._apply_market_search(request, search_query)
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
search_query = self._extract_market_search_query(normalized)
|
||||
if search_query:
|
||||
self._apply_market_search(request, search_query)
|
||||
self._render_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
install_match = re.match(r"^(?:安装|装)\s*(\d+)$", normalized)
|
||||
remove_match = re.match(r"^(?:删除|删)\s*(\d+)$", normalized)
|
||||
if request.view == "market" and install_match:
|
||||
request.awaiting_input = None
|
||||
success, message = self._install_market_skill(
|
||||
request=request,
|
||||
page_index=int(install_match.group(1)),
|
||||
@@ -452,6 +523,7 @@ class SkillsChain(ChainBase):
|
||||
)
|
||||
return True
|
||||
if request.view == "installed" and remove_match:
|
||||
request.awaiting_input = None
|
||||
success, message = self._remove_local_skill(
|
||||
request=request,
|
||||
page_index=int(remove_match.group(1)),
|
||||
@@ -493,7 +565,7 @@ class SkillsChain(ChainBase):
|
||||
"""
|
||||
按当前市场页的可见序号安装技能,避免跨页序号歧义。
|
||||
"""
|
||||
market_skills = [skill for skill in self.skillhelper.list_market_skills() if not skill.installed]
|
||||
market_skills = self._get_market_skills(request=request)
|
||||
page_items, page, _ = self._page_items(
|
||||
items=market_skills,
|
||||
page=request.market_page,
|
||||
@@ -683,11 +755,10 @@ class SkillsChain(ChainBase):
|
||||
"""
|
||||
构建技能市场视图,仅展示尚未安装的技能。
|
||||
"""
|
||||
market_skills = [
|
||||
skill
|
||||
for skill in self.skillhelper.list_market_skills(force=force_market_refresh)
|
||||
if not skill.installed
|
||||
]
|
||||
market_skills = self._get_market_skills(
|
||||
request=request,
|
||||
force_market_refresh=force_market_refresh,
|
||||
)
|
||||
page_items, page, total_pages = self._page_items(
|
||||
items=market_skills,
|
||||
page=request.market_page,
|
||||
@@ -696,9 +767,21 @@ class SkillsChain(ChainBase):
|
||||
request.market_page = page
|
||||
|
||||
text_lines = [f"第 {page + 1}/{total_pages} 页,共 {len(market_skills)} 个可安装技能"]
|
||||
if request.market_query:
|
||||
text_lines.append(f"当前搜索:{request.market_query}")
|
||||
if request.awaiting_input == "market-search":
|
||||
text_lines.extend(
|
||||
[
|
||||
"",
|
||||
"搜索输入中:直接回复关键词即可筛选市场技能,回复 取消 结束输入。",
|
||||
]
|
||||
)
|
||||
if not page_items:
|
||||
text_lines.append("")
|
||||
text_lines.append("当前没有可安装的市场技能")
|
||||
if request.market_query:
|
||||
text_lines.append("当前搜索没有匹配的市场技能")
|
||||
else:
|
||||
text_lines.append("当前没有可安装的市场技能")
|
||||
else:
|
||||
for index, skill in enumerate(page_items, start=1):
|
||||
text_lines.extend(
|
||||
@@ -722,13 +805,30 @@ class SkillsChain(ChainBase):
|
||||
text_lines.extend(
|
||||
[
|
||||
"",
|
||||
"回复 安装 <序号> 安装技能,回复 刷新 重新拉取市场,回复 n/p 翻页,回复 返回 回到菜单,回复 退出 结束交互",
|
||||
"回复 搜索 <关键词> 筛选技能,回复 清除搜索 恢复全量列表,回复 安装 <序号> 安装技能,回复 刷新 重新拉取市场,回复 n/p 翻页,回复 返回 回到菜单,回复 退出 结束交互",
|
||||
]
|
||||
)
|
||||
|
||||
buttons = None
|
||||
if self._supports_interactive_buttons(request.channel):
|
||||
buttons = []
|
||||
search_row = []
|
||||
if request.market_query:
|
||||
search_row.append(
|
||||
{
|
||||
"text": "清除搜索",
|
||||
"callback_data": f"skills:{request.request_id}:clear-search",
|
||||
}
|
||||
)
|
||||
else:
|
||||
search_row.append(
|
||||
{
|
||||
"text": "搜索",
|
||||
"callback_data": f"skills:{request.request_id}:search",
|
||||
}
|
||||
)
|
||||
if search_row:
|
||||
buttons.append(search_row)
|
||||
for index, _skill in enumerate(page_items, start=1):
|
||||
buttons.append(
|
||||
[
|
||||
@@ -872,7 +972,60 @@ class SkillsChain(ChainBase):
|
||||
根据当前视图返回可执行的文本命令提示。
|
||||
"""
|
||||
if view == "market":
|
||||
return "请输入 安装 <序号>、刷新、n、p、返回 或 退出"
|
||||
return "请输入 搜索 <关键词>、清除搜索、安装 <序号>、刷新、n、p、返回 或 退出"
|
||||
if view == "installed":
|
||||
return "请输入 删除 <序号>、n、p、返回 或 退出"
|
||||
return "请输入 1、2、刷新 或 退出"
|
||||
return "请输入 1、2、搜索 <关键词>、刷新 或 退出"
|
||||
|
||||
def _get_market_skills(
|
||||
self,
|
||||
request: PendingSkillsInteraction,
|
||||
force_market_refresh: bool = False,
|
||||
) -> List[SkillInfo]:
|
||||
"""
|
||||
获取当前 /skills 会话可见的市场技能,并应用搜索词过滤。
|
||||
"""
|
||||
skills = [
|
||||
skill
|
||||
for skill in self.skillhelper.list_market_skills(force=force_market_refresh)
|
||||
if not skill.installed
|
||||
]
|
||||
if not request.market_query:
|
||||
return skills
|
||||
return self.skillhelper.filter_market_skills(
|
||||
skills=skills,
|
||||
query=request.market_query,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_market_search_query(text: str) -> str:
|
||||
"""
|
||||
从文本命令中提取市场搜索词,兼容“搜索/查找/查”前缀。
|
||||
"""
|
||||
normalized = (text or "").strip()
|
||||
if not normalized:
|
||||
return ""
|
||||
match = re.match(r"^(?:搜索|查找|查)\s+(.+)$", normalized)
|
||||
return match.group(1).strip() if match else ""
|
||||
|
||||
@staticmethod
|
||||
def _apply_market_search(
|
||||
request: PendingSkillsInteraction,
|
||||
query: str,
|
||||
) -> None:
|
||||
"""
|
||||
将会话切到市场搜索结果视图,并重置分页状态。
|
||||
"""
|
||||
request.view = "market"
|
||||
request.market_query = (query or "").strip()
|
||||
request.market_page = 0
|
||||
request.awaiting_input = None
|
||||
|
||||
@staticmethod
|
||||
def _clear_market_search(request: PendingSkillsInteraction) -> None:
|
||||
"""
|
||||
清除当前市场搜索状态,恢复全量市场列表。
|
||||
"""
|
||||
request.market_query = ""
|
||||
request.market_page = 0
|
||||
request.awaiting_input = None
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from urllib.parse import urlencode, urljoin, urlparse
|
||||
|
||||
from app.agent.middleware.skills import _parse_skill_metadata
|
||||
from app.core.cache import cached, fresh
|
||||
@@ -20,6 +21,10 @@ _SOURCE_META_FILENAME = ".moviepilot-skill-source.json"
|
||||
_DEFAULT_BRANCHES = ("main", "master")
|
||||
_MARKET_CACHE_TTL = 60 * 30
|
||||
_CLAWHUB_HOSTS = {"clawhub.ai", "www.clawhub.ai"}
|
||||
_CLAWHUB_CONVEX_CLIENT = "npm-1.35.1"
|
||||
_CLAWHUB_LIST_QUERY = "skills:listPublicPageV4"
|
||||
_CLAWHUB_LIST_PAGE_SIZE = 100
|
||||
_CLAWHUB_LIST_MAX_PAGES = 2
|
||||
_OFFICIAL_SKILL_REPOS = {
|
||||
"openai/skills",
|
||||
"anthropics/skills",
|
||||
@@ -316,6 +321,44 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def filter_market_skills(
|
||||
skills: List[SkillInfo],
|
||||
query: str,
|
||||
) -> List[SkillInfo]:
|
||||
"""
|
||||
按关键词过滤市场技能。
|
||||
|
||||
搜索范围覆盖技能 ID、名称、描述以及来源标签;多词查询按 AND 语义匹配,
|
||||
便于用户逐步缩小候选范围。
|
||||
"""
|
||||
normalized_query = (query or "").strip().lower()
|
||||
if not normalized_query:
|
||||
return skills
|
||||
|
||||
terms = [term for term in re.split(r"\s+", normalized_query) if term]
|
||||
if not terms:
|
||||
return skills
|
||||
|
||||
results: List[SkillInfo] = []
|
||||
for skill in skills:
|
||||
haystack = " ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
skill.id,
|
||||
skill.name,
|
||||
skill.description,
|
||||
skill.source_label,
|
||||
skill.repo_name,
|
||||
skill.registry_name,
|
||||
],
|
||||
)
|
||||
).lower()
|
||||
if all(term in haystack for term in terms):
|
||||
results.append(skill)
|
||||
return results
|
||||
|
||||
@cached(maxsize=24, ttl=_MARKET_CACHE_TTL, skip_empty=True)
|
||||
def _list_market_source_skills(self, source: str) -> List[SkillInfo]:
|
||||
"""
|
||||
@@ -405,8 +448,17 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
|
||||
def _list_market_registry_skills(self, registry: dict) -> List[SkillInfo]:
|
||||
"""
|
||||
从注册表拉取技能列表,目前用于 ClawHub。
|
||||
从注册表拉取技能列表。
|
||||
|
||||
ClawHub 官方前端当前通过 Convex query 拉取公开技能列表,因此这里优先复用
|
||||
同一条查询链路;若运行时信息解析失败,再回退到旧的 REST 风格接口。
|
||||
"""
|
||||
parsed = urlparse((registry.get("registry_url") or "").rstrip("/"))
|
||||
if parsed.netloc.lower() in _CLAWHUB_HOSTS:
|
||||
skills = self._list_clawhub_registry_skills(registry)
|
||||
if skills:
|
||||
return skills
|
||||
|
||||
response = self._request_registry(
|
||||
url=f"{registry['api_base']}/skills",
|
||||
params={"limit": 200, "sort": "installsAllTime"},
|
||||
@@ -599,29 +651,94 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
return []
|
||||
|
||||
def _list_clawhub_registry_skills(self, registry: dict) -> List[SkillInfo]:
|
||||
"""
|
||||
按 ClawHub 官方前端的调用方式,通过 Convex query 获取公开技能列表。
|
||||
"""
|
||||
runtime_env = self._discover_clawhub_runtime_env(registry["registry_url"])
|
||||
convex_url = (runtime_env or {}).get("convex_url")
|
||||
if not convex_url:
|
||||
return []
|
||||
|
||||
results: Dict[str, SkillInfo] = {}
|
||||
cursor = None
|
||||
for _ in range(_CLAWHUB_LIST_MAX_PAGES):
|
||||
args = {
|
||||
"numItems": _CLAWHUB_LIST_PAGE_SIZE,
|
||||
"sort": "downloads",
|
||||
"dir": "desc",
|
||||
"nonSuspiciousOnly": True,
|
||||
}
|
||||
if cursor:
|
||||
args["cursor"] = cursor
|
||||
|
||||
response = self._request_convex_query(
|
||||
deployment_url=convex_url,
|
||||
path=_CLAWHUB_LIST_QUERY,
|
||||
args=args,
|
||||
)
|
||||
if not response:
|
||||
break
|
||||
|
||||
payload = self._load_json_response(response)
|
||||
value = payload.get("value")
|
||||
if not isinstance(value, dict):
|
||||
break
|
||||
|
||||
page = value.get("page")
|
||||
if not isinstance(page, list):
|
||||
break
|
||||
|
||||
for item in page:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
skill = self._build_registry_skill(item, registry)
|
||||
if skill and skill.id not in results:
|
||||
results[skill.id] = skill
|
||||
|
||||
if not value.get("hasMore") or not value.get("nextCursor"):
|
||||
break
|
||||
cursor = value["nextCursor"]
|
||||
|
||||
return list(results.values())
|
||||
|
||||
def _build_registry_skill(
|
||||
self, item: dict, registry: dict
|
||||
) -> Optional[SkillInfo]:
|
||||
"""
|
||||
将注册表返回的条目转换为统一的 SkillInfo。
|
||||
"""
|
||||
skill_data = item.get("skill") if isinstance(item.get("skill"), dict) else item
|
||||
slug = (
|
||||
item.get("slug")
|
||||
skill_data.get("slug")
|
||||
or item.get("slug")
|
||||
or skill_data.get("id")
|
||||
or item.get("id")
|
||||
or item.get("name")
|
||||
or item.get("title")
|
||||
or skill_data.get("name")
|
||||
or skill_data.get("displayName")
|
||||
or skill_data.get("title")
|
||||
)
|
||||
if not slug:
|
||||
return None
|
||||
|
||||
name = item.get("name") or item.get("title") or slug
|
||||
name = (
|
||||
skill_data.get("name")
|
||||
or skill_data.get("displayName")
|
||||
or item.get("displayName")
|
||||
or skill_data.get("title")
|
||||
or item.get("title")
|
||||
or slug
|
||||
)
|
||||
description = (
|
||||
item.get("description")
|
||||
skill_data.get("description")
|
||||
or skill_data.get("summary")
|
||||
or item.get("description")
|
||||
or item.get("summary")
|
||||
or skill_data.get("excerpt")
|
||||
or item.get("excerpt")
|
||||
or ""
|
||||
)
|
||||
owner_handle = self._extract_registry_owner_handle(item)
|
||||
owner_handle = item.get("ownerHandle") or self._extract_registry_owner_handle(item)
|
||||
page_path = f"/{owner_handle}/{slug}" if owner_handle else f"/skills/{slug}"
|
||||
|
||||
return SkillInfo(
|
||||
@@ -637,7 +754,8 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
registry_url=registry["registry_url"],
|
||||
registry_name=registry["registry_name"],
|
||||
registry_slug=str(slug),
|
||||
download_url=item.get("downloadUrl")
|
||||
download_url=skill_data.get("downloadUrl")
|
||||
or item.get("downloadUrl")
|
||||
or self._build_registry_download_url(registry["api_base"], str(slug)),
|
||||
installed=False,
|
||||
removable=False,
|
||||
@@ -685,6 +803,74 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
return None
|
||||
return response.content
|
||||
|
||||
@cached(maxsize=4, ttl=_MARKET_CACHE_TTL, skip_empty=True)
|
||||
def _discover_clawhub_runtime_env(self, registry_url: str) -> Optional[dict]:
|
||||
"""
|
||||
从 ClawHub 首页的 runtime env 脚本中提取当前生效的 Convex 部署地址。
|
||||
"""
|
||||
response = self._request_registry(url=registry_url)
|
||||
if response is None or response.status_code != 200:
|
||||
return None
|
||||
|
||||
html = self._read_response_text(response)
|
||||
runtime_asset_path = self._extract_runtime_env_asset_path(html)
|
||||
if not runtime_asset_path:
|
||||
return None
|
||||
|
||||
runtime_asset_url = urljoin(f"{registry_url.rstrip('/')}/", runtime_asset_path)
|
||||
asset_response = self._request_registry(url=runtime_asset_url)
|
||||
if asset_response is None or asset_response.status_code != 200:
|
||||
return None
|
||||
|
||||
script = self._read_response_text(asset_response)
|
||||
convex_url = self._extract_runtime_env_value(script, "VITE_CONVEX_URL")
|
||||
convex_site_url = self._extract_runtime_env_value(script, "VITE_CONVEX_SITE_URL")
|
||||
if not convex_url and not convex_site_url:
|
||||
return None
|
||||
return {
|
||||
"convex_url": convex_url.rstrip("/") if convex_url else None,
|
||||
"convex_site_url": convex_site_url.rstrip("/") if convex_site_url else None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _read_response_text(response) -> str:
|
||||
"""
|
||||
尽量稳定地把 requests 响应或测试桩响应转换成文本。
|
||||
"""
|
||||
text = getattr(response, "text", None)
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
|
||||
content = getattr(response, "content", b"")
|
||||
if isinstance(content, bytes):
|
||||
return content.decode("utf-8", errors="ignore")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _extract_runtime_env_asset_path(html: str) -> Optional[str]:
|
||||
"""
|
||||
从 ClawHub 首页 HTML 中定位 runtime env 资源路径。
|
||||
"""
|
||||
match = re.search(r'"/assets/runtimeEnv-[^"]+\.js"', html or "")
|
||||
if not match:
|
||||
return None
|
||||
return match.group(0).strip('"')
|
||||
|
||||
@staticmethod
|
||||
def _extract_runtime_env_value(script: str, key: str) -> Optional[str]:
|
||||
"""
|
||||
从运行时脚本中提取指定环境变量的值。
|
||||
"""
|
||||
match = re.search(
|
||||
rf"{re.escape(key)}:\s*['\"`]([^'\"`]+)['\"`]",
|
||||
script or "",
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
return match.group(1).strip()
|
||||
|
||||
@staticmethod
|
||||
def _extract_skill_archive(zf: zipfile.ZipFile, target_dir: Path) -> bool:
|
||||
"""
|
||||
@@ -744,6 +930,48 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
logger.warning("下载技能市场仓库失败:%s", repo["repo_url"])
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _request_convex_query(
|
||||
deployment_url: str,
|
||||
path: str,
|
||||
args: dict,
|
||||
timeout: int = 30,
|
||||
):
|
||||
"""
|
||||
以官方前端相同的请求格式调用 Convex query 接口。
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Convex-Client": _CLAWHUB_CONVEX_CLIENT,
|
||||
}
|
||||
payload = {
|
||||
"path": path,
|
||||
"format": "convex_encoded_json",
|
||||
"args": [args or {}],
|
||||
}
|
||||
|
||||
strategies = []
|
||||
if settings.PROXY_HOST:
|
||||
strategies.append({"proxies": settings.PROXY, "timeout": timeout})
|
||||
strategies.append({"timeout": timeout})
|
||||
|
||||
for kwargs in strategies:
|
||||
try:
|
||||
response = RequestUtils(headers=headers, **kwargs).post_res(
|
||||
url=f"{deployment_url.rstrip('/')}/api/query",
|
||||
json=payload,
|
||||
raise_exception=True,
|
||||
)
|
||||
if response is not None and response.status_code == 200:
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"请求 Convex 技能列表失败:%s/api/query - %s",
|
||||
deployment_url.rstrip("/"),
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _request_registry(
|
||||
url: str,
|
||||
|
||||
Reference in New Issue
Block a user