Merge pull request #695 from supreme0597/feat/skill-batch-install
feat(skills): 优化远程 Skill 批量安装,单次克隆完成多 skill 安装
This commit is contained in:
commit
46bd6d22ef
@ -209,3 +209,104 @@ async def install_remote_skill(
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(temp_home, ignore_errors=True)
|
shutil.rmtree(temp_home, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def install_remote_skills_batch(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
skills: list[str],
|
||||||
|
created_by: str | None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""批量从同一个远程仓库安装多个 skills(仅一次克隆)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话。
|
||||||
|
source: 远程仓库来源,如 ``owner/repo`` 或 GitHub URL。
|
||||||
|
skills: 需要安装的 skill 名称列表。
|
||||||
|
created_by: 操作者标识。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
每个 skill 的安装结果列表,顺序与请求一致: ``[{slug, success, error?}, ...]``
|
||||||
|
"""
|
||||||
|
normalized_source = _normalize_source(source)
|
||||||
|
if not skills:
|
||||||
|
raise ValueError("skills 列表不能为空")
|
||||||
|
|
||||||
|
# 预分配结果数组(按请求顺序),校验非法名并记录失败
|
||||||
|
results: list[dict] = [{"slug": "", "success": False, "error": "unset"}] * len(skills)
|
||||||
|
normalized_skills: list[str] = []
|
||||||
|
valid_indices: list[int] = []
|
||||||
|
for i, skill in enumerate(skills):
|
||||||
|
try:
|
||||||
|
normalized_skills.append(_normalize_skill_name(skill))
|
||||||
|
valid_indices.append(i)
|
||||||
|
except ValueError as e:
|
||||||
|
results[i] = {"slug": skill, "success": False, "error": str(e)}
|
||||||
|
|
||||||
|
if not normalized_skills:
|
||||||
|
return results
|
||||||
|
|
||||||
|
temp_home, env, workdir = _create_isolated_workdir()
|
||||||
|
try:
|
||||||
|
# Step 1: 一次 npx 调用安装所有 skill(克隆一次)
|
||||||
|
skill_args: list[str] = []
|
||||||
|
for name in normalized_skills:
|
||||||
|
skill_args.extend(["--skill", name])
|
||||||
|
|
||||||
|
cli_failed = False
|
||||||
|
try:
|
||||||
|
await _run_skills_cli(
|
||||||
|
[
|
||||||
|
"npx",
|
||||||
|
"-y",
|
||||||
|
"skills",
|
||||||
|
"add",
|
||||||
|
normalized_source,
|
||||||
|
*skill_args,
|
||||||
|
"-g",
|
||||||
|
"-y",
|
||||||
|
"--copy",
|
||||||
|
],
|
||||||
|
env=env,
|
||||||
|
cwd=workdir,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# CLI 对不匹配的 skill 会退出码非零,但已安装的目录仍在
|
||||||
|
cli_failed = True
|
||||||
|
|
||||||
|
# Step 2: 从临时目录中找到各 skill 的安装目录并逐个导入
|
||||||
|
base_dir = Path(temp_home).resolve()
|
||||||
|
skills_dir = base_dir / ".agents" / "skills"
|
||||||
|
|
||||||
|
for original_index, name in zip(valid_indices, normalized_skills):
|
||||||
|
installed_dir = _find_skill_dir(skills_dir, name)
|
||||||
|
if installed_dir is None:
|
||||||
|
error_msg = "CLI 安装失败" if cli_failed else "skills CLI 未生成预期的技能目录"
|
||||||
|
results[original_index] = {"slug": name, "success": False, "error": error_msg}
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = await import_skill_dir(
|
||||||
|
db,
|
||||||
|
source_dir=installed_dir,
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
results[original_index] = {"slug": item.slug, "success": True}
|
||||||
|
except Exception as e:
|
||||||
|
await db.rollback()
|
||||||
|
results[original_index] = {"slug": name, "success": False, "error": str(e)}
|
||||||
|
|
||||||
|
return results
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(temp_home, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_skill_dir(skills_dir: Path, name: str) -> Path | None:
|
||||||
|
"""在 skills 安装目录下按名称查找 skill 子目录。"""
|
||||||
|
if not skills_dir.is_dir():
|
||||||
|
return None
|
||||||
|
for candidate in skills_dir.iterdir():
|
||||||
|
if candidate.name == name and candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from server.utils.auth_middleware import get_admin_user, get_db
|
from server.utils.auth_middleware import get_admin_user, get_db
|
||||||
from yuxi.services.remote_skill_install_service import install_remote_skill, list_remote_skills
|
from yuxi.services.remote_skill_install_service import install_remote_skill, install_remote_skills_batch, list_remote_skills
|
||||||
from yuxi.services.skill_service import (
|
from yuxi.services.skill_service import (
|
||||||
BuiltinSkillUpdateConflictError,
|
BuiltinSkillUpdateConflictError,
|
||||||
create_skill_node,
|
create_skill_node,
|
||||||
@ -63,6 +63,10 @@ class RemoteSkillInstallRequest(RemoteSkillSourceRequest):
|
|||||||
skill: str = Field(..., description="需要安装的 skill 名称")
|
skill: str = Field(..., description="需要安装的 skill 名称")
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteSkillBatchInstallRequest(RemoteSkillSourceRequest):
|
||||||
|
skills: list[str] = Field(..., description="需要安装的 skill 名称列表(批量,共享一次克隆)")
|
||||||
|
|
||||||
|
|
||||||
def _raise_from_value_error(e: ValueError) -> None:
|
def _raise_from_value_error(e: ValueError) -> None:
|
||||||
message = str(e)
|
message = str(e)
|
||||||
status_code = 404 if "不存在" in message else 400
|
status_code = 404 if "不存在" in message else 400
|
||||||
@ -251,6 +255,38 @@ async def install_remote_skill_route(
|
|||||||
raise HTTPException(status_code=500, detail="安装远程 skill 失败")
|
raise HTTPException(status_code=500, detail="安装远程 skill 失败")
|
||||||
|
|
||||||
|
|
||||||
|
@skills.post("/remote/install-batch")
|
||||||
|
async def install_remote_skills_batch_route(
|
||||||
|
payload: RemoteSkillBatchInstallRequest,
|
||||||
|
current_user: User = Depends(get_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""批量从同一远程仓库安装多个 skills(仅一次克隆,不存在的 skill 静默跳过)。"""
|
||||||
|
try:
|
||||||
|
results = await install_remote_skills_batch(
|
||||||
|
db,
|
||||||
|
source=payload.source,
|
||||||
|
skills=payload.skills,
|
||||||
|
created_by=current_user.username,
|
||||||
|
)
|
||||||
|
success_count = sum(1 for r in results if r["success"])
|
||||||
|
failed_count = sum(1 for r in results if not r["success"])
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": results,
|
||||||
|
"summary": {"total": len(results), "success": success_count, "failed": failed_count},
|
||||||
|
}
|
||||||
|
except ValueError as e:
|
||||||
|
_raise_from_value_error(e)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to install remote skills batch from '{payload.source}': {e}"
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=500, detail="批量安装远程 skills 失败")
|
||||||
|
|
||||||
|
|
||||||
@skills.get("/{slug}/tree")
|
@skills.get("/{slug}/tree")
|
||||||
async def get_skill_tree_route(
|
async def get_skill_tree_route(
|
||||||
slug: str,
|
slug: str,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -143,3 +144,167 @@ async def test_install_remote_skill_rejects_missing_remote_skill(monkeypatch: py
|
|||||||
skill="frontend-design",
|
skill="frontend-design",
|
||||||
created_by="root",
|
created_by="root",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_install_remote_skills_batch_installs_all(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
calls: list[tuple[list[str], str]] = []
|
||||||
|
imported_skills: list[str] = []
|
||||||
|
|
||||||
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
||||||
|
calls.append((args, env["HOME"]))
|
||||||
|
home = Path(env["HOME"])
|
||||||
|
skill_dir_base = home / ".agents" / "skills"
|
||||||
|
for skill_name in ("frontend-design", "claude-api", "code-review"):
|
||||||
|
(skill_dir_base / skill_name).mkdir(parents=True, exist_ok=True)
|
||||||
|
(skill_dir_base / skill_name / "SKILL.md").write_text(
|
||||||
|
f"---\nname: {skill_name}\ndescription: demo\n---\n# {skill_name}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return "installed"
|
||||||
|
|
||||||
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
||||||
|
imported_skills.append(source_dir.name)
|
||||||
|
return SimpleNamespace(slug=source_dir.name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
||||||
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
||||||
|
|
||||||
|
results = await svc.install_remote_skills_batch(
|
||||||
|
None,
|
||||||
|
source="anthropics/skills",
|
||||||
|
skills=["frontend-design", "claude-api", "code-review"],
|
||||||
|
created_by="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should only have 1 CLI call (no --list, direct batch install)
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0][0] == [
|
||||||
|
"npx",
|
||||||
|
"-y",
|
||||||
|
"skills",
|
||||||
|
"add",
|
||||||
|
"anthropics/skills",
|
||||||
|
"--skill",
|
||||||
|
"frontend-design",
|
||||||
|
"--skill",
|
||||||
|
"claude-api",
|
||||||
|
"--skill",
|
||||||
|
"code-review",
|
||||||
|
"-g",
|
||||||
|
"-y",
|
||||||
|
"--copy",
|
||||||
|
]
|
||||||
|
|
||||||
|
assert len(results) == 3
|
||||||
|
assert all(r["success"] for r in results)
|
||||||
|
assert [r["slug"] for r in results] == ["frontend-design", "claude-api", "code-review"]
|
||||||
|
assert imported_skills == ["frontend-design", "claude-api", "code-review"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_install_remote_skills_batch_skips_missing(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
||||||
|
home = Path(env["HOME"])
|
||||||
|
skill_dir_base = home / ".agents" / "skills"
|
||||||
|
(skill_dir_base / "frontend-design").mkdir(parents=True, exist_ok=True)
|
||||||
|
(skill_dir_base / "frontend-design" / "SKILL.md").write_text(
|
||||||
|
"---\nname: frontend-design\ndescription: demo\n---\n# Demo\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return "installed"
|
||||||
|
|
||||||
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
||||||
|
return SimpleNamespace(slug=source_dir.name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
||||||
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
||||||
|
|
||||||
|
results = await svc.install_remote_skills_batch(
|
||||||
|
None,
|
||||||
|
source="anthropics/skills",
|
||||||
|
skills=["frontend-design", "nonexistent-skill"],
|
||||||
|
created_by="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) == 2
|
||||||
|
assert results[0] == {"slug": "frontend-design", "success": True}
|
||||||
|
assert results[1] == {"slug": "nonexistent-skill", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_install_remote_skills_batch_partial_failure(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
calls: list[tuple[list[str], str]] = []
|
||||||
|
|
||||||
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
||||||
|
calls.append((args, env["HOME"]))
|
||||||
|
home = Path(env["HOME"])
|
||||||
|
skill_dir_base = home / ".agents" / "skills"
|
||||||
|
(skill_dir_base / "skill-a").mkdir(parents=True, exist_ok=True)
|
||||||
|
(skill_dir_base / "skill-a" / "SKILL.md").write_text(
|
||||||
|
"---\nname: skill-a\ndescription: demo\n---\n# A\n", encoding="utf-8",
|
||||||
|
)
|
||||||
|
# skill-b directory missing (simulate install failure from CLI side)
|
||||||
|
(skill_dir_base / "skill-c").mkdir(parents=True, exist_ok=True)
|
||||||
|
(skill_dir_base / "skill-c" / "SKILL.md").write_text(
|
||||||
|
"---\nname: skill-c\ndescription: demo\n---\n# C\n", encoding="utf-8",
|
||||||
|
)
|
||||||
|
return "installed"
|
||||||
|
|
||||||
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
||||||
|
return SimpleNamespace(slug=source_dir.name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
||||||
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
||||||
|
|
||||||
|
results = await svc.install_remote_skills_batch(
|
||||||
|
None,
|
||||||
|
source="test/repo",
|
||||||
|
skills=["skill-a", "skill-b", "skill-c"],
|
||||||
|
created_by="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) == 3
|
||||||
|
assert results[0] == {"slug": "skill-a", "success": True}
|
||||||
|
assert results[1] == {"slug": "skill-b", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
||||||
|
assert results[2] == {"slug": "skill-c", "success": True}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_install_remote_skills_batch_handles_invalid_names(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
calls: list[tuple[list[str], str]] = []
|
||||||
|
|
||||||
|
async def fake_run_skills_cli(args: list[str], *, env: dict[str, str], cwd: str) -> str:
|
||||||
|
calls.append((args, env["HOME"]))
|
||||||
|
home = Path(env["HOME"])
|
||||||
|
skill_dir_base = home / ".agents" / "skills"
|
||||||
|
(skill_dir_base / "valid-skill").mkdir(parents=True, exist_ok=True)
|
||||||
|
(skill_dir_base / "valid-skill" / "SKILL.md").write_text(
|
||||||
|
"---\nname: valid-skill\ndescription: demo\n---\n# Valid\n", encoding="utf-8",
|
||||||
|
)
|
||||||
|
return "installed"
|
||||||
|
|
||||||
|
async def fake_import_skill_dir(_db, *, source_dir, created_by):
|
||||||
|
return SimpleNamespace(slug=source_dir.name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(svc, "_run_skills_cli", fake_run_skills_cli)
|
||||||
|
monkeypatch.setattr(svc, "import_skill_dir", fake_import_skill_dir)
|
||||||
|
|
||||||
|
results = await svc.install_remote_skills_batch(
|
||||||
|
None,
|
||||||
|
source="test/repo",
|
||||||
|
skills=["valid-skill", "Bad Name", "another-valid"],
|
||||||
|
created_by="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) == 3
|
||||||
|
assert results[0] == {"slug": "valid-skill", "success": True}
|
||||||
|
assert results[1]["success"] is False
|
||||||
|
assert "不合法" in results[1]["error"]
|
||||||
|
assert results[2] == {"slug": "another-valid", "success": False, "error": "skills CLI 未生成预期的技能目录"}
|
||||||
|
|
||||||
|
# Only valid skills passed to the CLI
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert "--skill" in str(calls[0][0])
|
||||||
|
assert "valid-skill" in str(calls[0][0])
|
||||||
|
assert "Bad" not in str(calls[0][0])
|
||||||
|
|||||||
@ -53,6 +53,7 @@
|
|||||||
- 合并智能体对话导航:移除 `AgentChatComponent` 内部聊天侧边栏,将新建对话入口和对话历史移动到 `AppLayout` 主侧边栏,并通过共享线程 store 统一管理历史列表、当前线程、重命名、删除、置顶和分页加载。
|
- 合并智能体对话导航:移除 `AgentChatComponent` 内部聊天侧边栏,将新建对话入口和对话历史移动到 `AppLayout` 主侧边栏,并通过共享线程 store 统一管理历史列表、当前线程、重命名、删除、置顶和分页加载。
|
||||||
- 新增独立模型配置模块:增加 `model_providers` 表、独立管理接口和”模型配置”页面,支持 provider 基础信息、可配置模型列表端点、远端候选模型、`enabled_models` 的早期配置验证;启动时会补齐内置 provider 模板,`provider_type` 暂统一默认为 `openai`,该模块暂不接入现有运行时模型选择逻辑。远端模型加载默认使用 `/models` 获取 chat/通用模型,provider 声明 `embedding` 能力时使用 `/embeddings/models` 获取 embedding 候选,rerank 模型列表端点按供应商文档显式配置后加载;修复路由请求模型未接收 `embedding_base_url`/`rerank_base_url` 导致前端已填写仍被后端校验拦截的问题。补充手动添加模型能力:`enabled_models[i]` 新增可选 `source: "manual"|"remote"` 字段(默认 `remote`),管理员可通过”+ 手动添加”入口录入远端清单未覆盖的模型(典型:自部署 embedding/rerank),手动模型在前端跳过”远端不存在”的 stale 警告并显示「手动」标签;type 选项受 `provider.capabilities` 约束,后端在 `_normalize_payload` 与 `update_provider_config` 双层一致性校验中拦截越权写入。
|
- 新增独立模型配置模块:增加 `model_providers` 表、独立管理接口和”模型配置”页面,支持 provider 基础信息、可配置模型列表端点、远端候选模型、`enabled_models` 的早期配置验证;启动时会补齐内置 provider 模板,`provider_type` 暂统一默认为 `openai`,该模块暂不接入现有运行时模型选择逻辑。远端模型加载默认使用 `/models` 获取 chat/通用模型,provider 声明 `embedding` 能力时使用 `/embeddings/models` 获取 embedding 候选,rerank 模型列表端点按供应商文档显式配置后加载;修复路由请求模型未接收 `embedding_base_url`/`rerank_base_url` 导致前端已填写仍被后端校验拦截的问题。补充手动添加模型能力:`enabled_models[i]` 新增可选 `source: "manual"|"remote"` 字段(默认 `remote`),管理员可通过”+ 手动添加”入口录入远端清单未覆盖的模型(典型:自部署 embedding/rerank),手动模型在前端跳过”远端不存在”的 stale 警告并显示「手动」标签;type 选项受 `provider.capabilities` 约束,后端在 `_normalize_payload` 与 `update_provider_config` 双层一致性校验中拦截越权写入。
|
||||||
- 统一前端 Markdown 预览渲染:新增共享 `MarkdownPreview` 组件与 `markdown_preview` 渲染工具,替换 Agent 消息、文件预览、知识库 chunk、任务工具结果、聊天导出等场景中的旧 `md-editor-v3/marked` 预览;支持 KaTeX、任务列表、frontmatter 卡片、Shiki 代码高亮、DOMPurify 清洗和浅层渲染缓存,并抽取 HTML 转义与代码语言归一化工具。Skill 详情页复用 `AgentFilePreview`,统一文件预览、编辑、保存和全屏交互。
|
- 统一前端 Markdown 预览渲染:新增共享 `MarkdownPreview` 组件与 `markdown_preview` 渲染工具,替换 Agent 消息、文件预览、知识库 chunk、任务工具结果、聊天导出等场景中的旧 `md-editor-v3/marked` 预览;支持 KaTeX、任务列表、frontmatter 卡片、Shiki 代码高亮、DOMPurify 清洗和浅层渲染缓存,并抽取 HTML 转义与代码语言归一化工具。Skill 详情页复用 `AgentFilePreview`,统一文件预览、编辑、保存和全屏交互。
|
||||||
|
- 优化远程 Skill 批量安装:`remote_skill_install_service.py` 新增 `install_remote_skills_batch()`,利用 `npx skills add --skill A --skill B --skill C` 原生多 skill 支持,将安装 N 个 skill 的仓库克隆次数从 2N 降至 1;配套新增路由 `POST /remote/install-batch`、前端 `installRemoteSkillsBatch()` API 方法和批处理 UI 逻辑
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,10 @@ export const installRemoteSkill = async (payload) => {
|
|||||||
return apiAdminPost(`${BASE_URL}/remote/install`, payload)
|
return apiAdminPost(`${BASE_URL}/remote/install`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const installRemoteSkillsBatch = async (payload) => {
|
||||||
|
return apiAdminPost(`${BASE_URL}/remote/install-batch`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
export const getSkillDependencyOptions = async () => {
|
export const getSkillDependencyOptions = async () => {
|
||||||
return apiAdminGet(`${BASE_URL}/dependency-options`)
|
return apiAdminGet(`${BASE_URL}/dependency-options`)
|
||||||
}
|
}
|
||||||
@ -77,6 +81,7 @@ export const skillApi = {
|
|||||||
importSkillZip,
|
importSkillZip,
|
||||||
listRemoteSkills,
|
listRemoteSkills,
|
||||||
installRemoteSkill,
|
installRemoteSkill,
|
||||||
|
installRemoteSkillsBatch,
|
||||||
getSkillDependencyOptions,
|
getSkillDependencyOptions,
|
||||||
listBuiltinSkills,
|
listBuiltinSkills,
|
||||||
installBuiltinSkill,
|
installBuiltinSkill,
|
||||||
|
|||||||
@ -379,23 +379,18 @@ const handleInstallRemoteSkill = async () => {
|
|||||||
remoteInstallProgress.visible = true
|
remoteInstallProgress.visible = true
|
||||||
remoteInstallProgress.total = skillsToInstall.length
|
remoteInstallProgress.total = skillsToInstall.length
|
||||||
try {
|
try {
|
||||||
for (const skill of skillsToInstall) {
|
const result = await skillApi.installRemoteSkillsBatch({
|
||||||
remoteInstallProgress.currentSkill = skill
|
source,
|
||||||
try {
|
skills: skillsToInstall
|
||||||
const result = await skillApi.installRemoteSkill({ source, skill })
|
})
|
||||||
const installedSlug = result?.data?.slug || skill
|
const results = result?.data || []
|
||||||
remoteInstallResults.success.push(installedSlug)
|
remoteInstallResults.success = results.filter((r) => r.success).map((r) => r.slug)
|
||||||
remoteInstallProgress.success += 1
|
remoteInstallResults.failed = results
|
||||||
} catch (error) {
|
.filter((r) => !r.success)
|
||||||
remoteInstallResults.failed.push({
|
.map((r) => ({ skill: r.slug, error: r.error || '安装失败' }))
|
||||||
skill,
|
remoteInstallProgress.success = remoteInstallResults.success.length
|
||||||
error: error?.response?.data?.detail || error.message || '远程 Skill 安装失败'
|
remoteInstallProgress.failed = remoteInstallResults.failed.length
|
||||||
})
|
remoteInstallProgress.completed = results.length
|
||||||
remoteInstallProgress.failed += 1
|
|
||||||
} finally {
|
|
||||||
remoteInstallProgress.completed += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remoteInstallProgress.currentSkill = ''
|
remoteInstallProgress.currentSkill = ''
|
||||||
await fetchSkills()
|
await fetchSkills()
|
||||||
if (remoteInstallResults.failed.length === 0) {
|
if (remoteInstallResults.failed.length === 0) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user