feat(skill): 新增推荐 Skill 列表与一键安装
- 管理端新增推荐 Skill 分组,未安装时可从卡片 + 直接拉取 ModelScope 来源并进入安装草稿确认 - 上传入口补充安装文档链接,文档补充 ModelScope 单个 Skill 与合集安装说明 - 同步更新 roadmap 与 REFACTOR 记录
This commit is contained in:
parent
8d3927629c
commit
83656a8eb5
@ -58,6 +58,7 @@
|
||||
- [x] 将 Qwen-Image 修改为 Skill
|
||||
- [x] 现在输入区域对于不同 mention 的渲染的 ICON 和 human-message 里面的渲染的 ICON;
|
||||
- [x] 节点的颜色按照 label 的分类来
|
||||
- [ ] 添加推荐 Skill 的列表,点击可以直接安装,实际上是替代了填入搜索的名称,并触发拉取,这个似乎只需要在前端修改就可以了
|
||||
|
||||
# bug
|
||||
|
||||
|
||||
@ -47,10 +47,11 @@ Skills 系统采用「文件系统存内容,数据库存索引」的分离架
|
||||
|
||||
系统提供以下方式创建或安装 Skills:
|
||||
|
||||
1. **ZIP / SKILL.md 上传(推荐)**:上传后先解析为安装草稿,确认共享范围后再写入正式 Skills 存储和数据库
|
||||
2. **远程仓库安装**:填写 skills 仓库地址和 skill 名称,后端调用 `npx skills` 下载并解析为安装草稿,确认后导入系统
|
||||
3. **在线编辑**:对已有且可管理的 Skill 在线创建目录、编辑文件和维护依赖
|
||||
4. **Agent 内安装**:主智能体可通过 `install_skill` 工具,从沙盒路径或 Git 来源安装当前用户私有 Skill;子智能体禁用该工具
|
||||
1. **推荐 Skill 安装**:在 Skills 管理页的推荐分组点击 `+`,系统会拉取对应远程来源并生成安装草稿
|
||||
2. **ZIP / SKILL.md 上传**:上传后先解析为安装草稿,确认共享范围后再写入正式 Skills 存储和数据库
|
||||
3. **远程仓库安装**:填写 skills 仓库地址、ModelScope Skill 地址或合集地址,后端调用 `npx skills` 下载并解析为安装草稿,确认后导入系统
|
||||
4. **在线编辑**:对已有且可管理的 Skill 在线创建目录、编辑文件和维护依赖
|
||||
5. **Agent 内安装**:主智能体可通过 `install_skill` 工具,从沙盒路径或 Git 来源安装当前用户私有 Skill;子智能体禁用该工具
|
||||
|
||||
不建议直接操作数据库或文件系统导入 Skill。直接写文件不会自动生成 `skills` 表记录,也无法参与权限、依赖和沙盒挂载。
|
||||
|
||||
@ -59,6 +60,7 @@ Skills 系统采用「文件系统存内容,数据库存索引」的分离架
|
||||
Skills 本质上是提示词和工具的封装,以下是一些可以参考的 Skills 实现:
|
||||
|
||||
- **Anthropic 官方 Tools**:https://github.com/anthropics/skills 可以参考其 skills 的组织方式和提示词设计
|
||||
- **ModelScope Skill 市场**:https://modelscope.cn/skills 支持单个 Skill 地址,也支持合集地址批量拉取
|
||||
- **MiniMax-AI CLI**:https://github.com/MiniMax-AI/cli 文本、图片、视频、语音和音乐生成 + Web 搜索(可通过 `MiniMax-AI/cli` 远程安装)
|
||||
- **社区 Skills**:各平台分享的 Agent 提示词模板
|
||||
- **自定义开发**:根据业务需求自行开发
|
||||
@ -111,12 +113,21 @@ description: 这是一个用于处理特定任务的技能
|
||||
|
||||
### 导入 Skill
|
||||
|
||||
有三种方式可以导入 Skill:
|
||||
可以通过以下方式导入或安装 Skill:
|
||||
|
||||
**方式一:通过 ZIP 包或 SKILL.md 上传(推荐)**
|
||||
**方式一:从推荐列表安装**
|
||||
|
||||
1. 在系统设置的「Skills 管理」页面查看「推荐」分组
|
||||
2. 未安装的推荐 Skill 会以普通 Skill 卡片样式展示,右侧显示 `+`
|
||||
3. 点击推荐卡片或 `+` 后,系统会使用该 Skill 的远程来源拉取内容
|
||||
4. 拉取成功后会弹出安装草稿,确认共享范围后完成安装
|
||||
|
||||
已安装的推荐 Skill 不会继续显示在「推荐」分组中。
|
||||
|
||||
**方式二:通过 ZIP 包或 SKILL.md 上传**
|
||||
|
||||
1. 将 Skill 目录打包成 ZIP 文件(注意:ZIP 的根目录就是 Skill 目录)
|
||||
2. 在系统设置的「Skills 管理」页面,点击「导入 Skill」
|
||||
2. 在系统设置的「Skills 管理」页面,点击「上传 Skill」
|
||||
3. 上传 ZIP 文件或单个 `SKILL.md`
|
||||
4. 系统解析上传内容并返回安装草稿
|
||||
5. 确认共享范围后完成安装;也可以放弃草稿
|
||||
@ -127,7 +138,30 @@ description: 这是一个用于处理特定任务的技能
|
||||
- 解析 SKILL.md 的 frontmatter 并存储到数据库
|
||||
- 按当前用户角色校验可选择的共享范围
|
||||
|
||||
**方式二:在线编辑已有 Skill**
|
||||
**方式三:从远程来源安装**
|
||||
|
||||
1. 在 Skills 管理页面点击「远程安装」
|
||||
2. 在“按仓库拉取”中填写来源,例如:
|
||||
- `anthropics/skills`
|
||||
- `https://github.com/anthropics/skills`
|
||||
- `https://modelscope.cn/skills/@anthropics/pdf`
|
||||
- `https://modelscope.cn/collections/MiniMax/MiniMax-Office-skills`
|
||||
3. 点击“拉取技能”获取该来源中可发现的 Skills 列表
|
||||
4. 单个 Skill 地址通常会自动选中;仓库或合集地址可在列表中勾选一个或多个 Skills
|
||||
5. 点击“解析并确认”,系统返回安装草稿,确认共享范围后正式安装
|
||||
|
||||
也可以切换到“全局搜索发现”,输入关键字检索 skills.sh 上的开源 Skills,再选择结果安装。
|
||||
|
||||
系统会在后端:
|
||||
- 调用 `npx skills add <source> --list` 校验来源并发现可安装的 skills
|
||||
- 使用隔离的临时 `HOME` 执行 `npx skills add <source> --skill <name> -g -y --copy`
|
||||
- 从临时目录中提取对应 skill,再按现有导入流程生成草稿;确认后写入 `/app/saves/skills` 与数据库
|
||||
|
||||
::: tip ModelScope 合集适合批量安装
|
||||
ModelScope 合集地址可以作为远程来源填写,例如 `https://modelscope.cn/collections/MiniMax/MiniMax-Office-skills`。拉取后在列表中勾选需要的 Skills,再统一解析为安装草稿。
|
||||
:::
|
||||
|
||||
**方式四:在线编辑已有 Skill**
|
||||
|
||||
在 Skills 管理页面,你可以:
|
||||
- 新建目录或文件
|
||||
@ -136,18 +170,6 @@ description: 这是一个用于处理特定任务的技能
|
||||
|
||||
只有具备 `can_manage` 权限的用户才能编辑文件、依赖、共享范围和启用状态。
|
||||
|
||||
**方式三:从远程 skills 仓库安装**
|
||||
|
||||
1. 在 Skills 管理页面的“远程安装”面板中填写仓库来源,例如 `anthropics/skills` 或完整 GitHub URL
|
||||
2. 点击“查看可安装 Skills”获取该仓库中可发现的 skills 列表
|
||||
3. 选择或输入目标 skill 名称后点击“解析”
|
||||
4. 系统返回安装草稿,确认共享范围后正式安装
|
||||
|
||||
系统会在后端:
|
||||
- 调用 `npx skills add <source> --list` 校验来源并发现可安装的 skills
|
||||
- 使用隔离的临时 `HOME` 执行 `npx skills add <source> --skill <name> -g -y --copy`
|
||||
- 从临时目录中提取对应 skill,再按现有导入流程生成草稿;确认后写入 `/app/saves/skills` 与数据库
|
||||
|
||||
::: tip 远程安装不会把 ~/.agents/skills 作为系统主存储
|
||||
远程安装只把 `skills.sh` CLI 作为“下载器”使用。Yuxi 仍然以 `/app/saves/skills + skills 表` 作为正式来源,这样才能与现有的权限、线程可见性和沙盒挂载机制保持一致。
|
||||
:::
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
- 新增 Milvus 图谱检索链路:Query 可召回图谱实体和三元组,结合 Chunk 命中实体构造 seed entity,读取 Neo4j 2-hop 子图后用 igraph 执行 PPR,最终以 Chunk 为产物并通过 RRF 与原 Chunk 召回融合;检索配置改为 dataclass 元数据生成,支持 `depend_on` 控制重排序和图检索参数展示。
|
||||
- 收紧用户管理部门隔离:普通管理员创建用户时固定归属本部门,用户列表、访问选项、详情、更新和删除接口均限制在本部门范围内。
|
||||
- 调整 Agent 资源默认选择与运行时上下文:未显式配置工具、知识库、MCP、Skills 时默认启用当前用户可访问/可用的全部资源,子智能体默认不启用,显式保存空列表仍表示不启用对应资源;Agent 创建前统一完成最终资源权限过滤、知识库 `db_id` 可见范围派生和 Skill prompt/readable 依赖闭包派生,聊天运行时与文件系统预览复用同一结果。
|
||||
- 重构 Skills 权限与安装流程:Skill 增加 `source_type/share_config/enabled`,内置 Skill 作为启动同步入库的全局资源,不再保留前端安装/更新状态,支持启停但不允许删除;上传和远程添加统一为解析草稿后确认生效范围,安装 slug 优先读取 `SKILL.md` 的 `slug` 字段并保留 `name` 展示名,压缩包名称不参与 slug 校验;管理端支持编辑生效范围与启停;Agent 运行时按当前用户可访问 Skills 派生 prompt/readable 依赖闭包并限制挂载/激活,Skills prompt 改为模型请求级注入以避免污染 runtime context;主智能体恢复 `install_skill` 工具,允许当前用户安装私有 Skill 并激活当前会话,子智能体配置和运行态均禁用该工具。
|
||||
- 重构 Skills 权限与安装流程:Skill 增加 `source_type/share_config/enabled`,内置 Skill 作为启动同步入库的全局资源,不再保留前端安装/更新状态,支持启停但不允许删除;上传和远程添加统一为解析草稿后确认生效范围,安装 slug 优先读取 `SKILL.md` 的 `slug` 字段并保留 `name` 展示名,压缩包名称不参与 slug 校验;管理端新增推荐 Skill 分组,未安装时可从卡片 `+` 直接拉取 ModelScope 来源并进入安装草稿确认,上传入口补充安装文档链接,文档补充 ModelScope 单个 Skill 与合集安装说明;管理端支持编辑生效范围与启停;Agent 运行时按当前用户可访问 Skills 派生 prompt/readable 依赖闭包并限制挂载/激活,Skills prompt 改为模型请求级注入以避免污染 runtime context;主智能体恢复 `install_skill` 工具,允许当前用户安装私有 Skill 并激活当前会话,子智能体配置和运行态均禁用该工具。
|
||||
- 精简历史兼容层:移除 sandbox provisioner `local` 后端别名、ask_user_question 单问题旧协议、JWT 历史默认密钥特殊判断、内置 Skill `SKILLS.md` 文件名回退、运行事件数字 seq 兼容和前端若干旧字段回退。
|
||||
- 重构知识库共享权限:`share_config` 改为全局共享、部门共享、指定人可访问三档,部门共享必须包含当前用户部门,指定人可访问必须包含当前用户,并补充权限过滤测试。
|
||||
- 移除知识库沙盒文件系统映射:不再通过 `/home/gem/kbs` 暴露知识库文件树,Agent 继续使用 `query_kb` 与 `open_kb_document` 访问知识库内容。
|
||||
|
||||
@ -30,6 +30,15 @@
|
||||
<span>上传 Skill</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button
|
||||
:href="SKILLS_DOC_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="lucide-icon-btn"
|
||||
>
|
||||
<ExternalLink :size="14" />
|
||||
<span>安装文档</span>
|
||||
</a-button>
|
||||
<a-tooltip title="刷新 Skills" placement="bottom">
|
||||
<a-button class="lucide-icon-btn" :disabled="loading" @click="fetchSkills">
|
||||
<RefreshCw :size="14" />
|
||||
@ -55,7 +64,7 @@
|
||||
</PageShoulder>
|
||||
|
||||
<div
|
||||
v-if="filteredInstalledSkills.length === 0"
|
||||
v-if="visibleSkillGroups.length === 0"
|
||||
class="extension-card-grid-empty-state skill-empty-state"
|
||||
>
|
||||
<div class="skill-empty-card">
|
||||
@ -76,21 +85,25 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<template v-for="group in skillGroups" :key="group.key">
|
||||
<template v-if="group.skills.length">
|
||||
<template v-for="group in visibleSkillGroups" :key="group.key">
|
||||
<div class="extension-section-header">{{ group.title }}</div>
|
||||
<ExtensionCardGrid :min-width="360">
|
||||
<div
|
||||
v-for="skill in group.skills"
|
||||
:key="skill.slug"
|
||||
:key="`${group.key}:${skill.slug}`"
|
||||
class="card-wrapper"
|
||||
:class="{
|
||||
selected: selectedCardSlugs.includes(skill.slug),
|
||||
'batch-mode': isBatchDeleteMode
|
||||
selected: !skill.isRecommendation && selectedCardSlugs.includes(skill.slug),
|
||||
'batch-mode': isBatchDeleteMode && !skill.isRecommendation
|
||||
}"
|
||||
>
|
||||
<a-checkbox
|
||||
v-if="isBatchDeleteMode && canManageSkill(skill) && skill.sourceType !== 'builtin'"
|
||||
v-if="
|
||||
!skill.isRecommendation &&
|
||||
isBatchDeleteMode &&
|
||||
canManageSkill(skill) &&
|
||||
skill.sourceType !== 'builtin'
|
||||
"
|
||||
:checked="selectedCardSlugs.includes(skill.slug)"
|
||||
@change="handleToggleCardSelect(skill.slug)"
|
||||
class="card-select-checkbox"
|
||||
@ -101,10 +114,27 @@
|
||||
:description="skill.description || '暂无描述'"
|
||||
:default-icon="BookMarkedIcon"
|
||||
@click="handleCardClick(skill)"
|
||||
:class="{ 'card-clickable-select': isBatchDeleteMode }"
|
||||
:class="{ 'card-clickable-select': isBatchDeleteMode && !skill.isRecommendation }"
|
||||
>
|
||||
<template #action>
|
||||
<button
|
||||
v-if="skill.isRecommendation"
|
||||
type="button"
|
||||
class="skill-enabled-action"
|
||||
:class="{ loading: isRecommendedSkillInstalling(skill.source) }"
|
||||
:disabled="isRecommendedSkillInstallDisabled(skill.source)"
|
||||
aria-label="安装推荐 Skill"
|
||||
@click.stop="handleRecommendedSkillInstall(skill)"
|
||||
>
|
||||
<LoaderCircle
|
||||
v-if="isRecommendedSkillInstalling(skill.source)"
|
||||
:size="15"
|
||||
class="action-icon action-icon-spin"
|
||||
/>
|
||||
<Plus v-else :size="15" class="action-icon" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="skill-enabled-action"
|
||||
:class="{ enabled: skill.enabled !== false }"
|
||||
@ -124,7 +154,6 @@
|
||||
</ExtensionCardGrid>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<a-modal
|
||||
v-model:open="skillPreviewVisible"
|
||||
@ -581,7 +610,9 @@ import {
|
||||
Trash2,
|
||||
Check,
|
||||
Plus,
|
||||
Minus
|
||||
Minus,
|
||||
ExternalLink,
|
||||
LoaderCircle
|
||||
} from 'lucide-vue-next'
|
||||
import { skillApi } from '@/apis/skill_api'
|
||||
import ExtensionCardGrid from './ExtensionCardGrid.vue'
|
||||
@ -592,6 +623,44 @@ import MarkdownPreview from '@/components/common/MarkdownPreview.vue'
|
||||
import { formatExtensionCardTitle } from '@/utils/extensionDisplayName'
|
||||
|
||||
const BookMarkedIcon = BookMarked
|
||||
const SKILLS_DOC_URL = 'https://xerrors.github.io/Yuxi/agents/skills-management.html'
|
||||
const RECOMMENDED_SKILLS = [
|
||||
{
|
||||
slug: 'skill-creator',
|
||||
name: 'skill-creator',
|
||||
description: '创建、维护和改进 Agent Skill。',
|
||||
source: 'https://modelscope.cn/skills/@anthropics/skill-creator',
|
||||
aliases: ['skill-creator', 'Skill Creator']
|
||||
},
|
||||
{
|
||||
slug: 'frontend-design',
|
||||
name: 'frontend-design',
|
||||
description: '面向前端界面与交互设计的实现建议。',
|
||||
source: 'https://modelscope.cn/skills/@anthropics/frontend-design',
|
||||
aliases: ['frontend-design', 'Frontend Design']
|
||||
},
|
||||
{
|
||||
slug: 'docx',
|
||||
name: 'docx',
|
||||
description: '读取、编辑和生成 Word DOCX 文档。',
|
||||
source: 'https://modelscope.cn/skills/@anthropics/docx',
|
||||
aliases: ['docx', 'DOCX']
|
||||
},
|
||||
{
|
||||
slug: 'xlsx',
|
||||
name: 'xlsx',
|
||||
description: '读取、编辑和生成 Excel XLSX 表格。',
|
||||
source: 'https://modelscope.cn/skills/@anthropics/xlsx',
|
||||
aliases: ['xlsx', 'XLSX']
|
||||
},
|
||||
{
|
||||
slug: 'pdf',
|
||||
name: 'pdf',
|
||||
description: '读取、分析和处理 PDF 文件。',
|
||||
source: 'https://modelscope.cn/skills/@anthropics/pdf',
|
||||
aliases: ['pdf', 'PDF']
|
||||
}
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@ -613,6 +682,7 @@ const skillPreviewLoading = ref(false)
|
||||
const skillPreviewError = ref('')
|
||||
const deletingPreviewSkill = ref(false)
|
||||
let previewRequestSeq = 0
|
||||
const installingRecommendedSources = ref([])
|
||||
|
||||
const remoteInstallModalVisible = ref(false)
|
||||
const activeTab = ref('repo') // 'repo' 或 'search'
|
||||
@ -645,7 +715,8 @@ const shareConfigFormRef = ref(null)
|
||||
const matchesSearch = (skill) => {
|
||||
if (!searchQuery.value) return true
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return skill.name.toLowerCase().includes(q) || skill.slug.toLowerCase().includes(q)
|
||||
const text = [skill.name, skill.slug, skill.description].filter(Boolean).join(' ').toLowerCase()
|
||||
return text.includes(q)
|
||||
}
|
||||
|
||||
const installedSkillCards = computed(() =>
|
||||
@ -655,8 +726,34 @@ const installedSkillCards = computed(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const installedSkillKeys = computed(() => {
|
||||
const keys = new Set()
|
||||
installedSkillCards.value.forEach((skill) => {
|
||||
const identifiers = [skill.slug, skill.name]
|
||||
identifiers.forEach((value) => {
|
||||
if (value) keys.add(String(value).toLowerCase())
|
||||
})
|
||||
})
|
||||
return keys
|
||||
})
|
||||
|
||||
const recommendedSkillCards = computed(() =>
|
||||
RECOMMENDED_SKILLS.filter(
|
||||
(skill) => !skill.aliases.some((alias) => installedSkillKeys.value.has(alias.toLowerCase()))
|
||||
).map((skill) => ({
|
||||
...skill,
|
||||
sourceType: 'recommended',
|
||||
isRecommendation: true
|
||||
}))
|
||||
)
|
||||
|
||||
const filteredInstalledSkills = computed(() => installedSkillCards.value.filter(matchesSearch))
|
||||
const skillGroups = computed(() => [
|
||||
{
|
||||
key: 'recommended',
|
||||
title: '推荐',
|
||||
skills: isBatchDeleteMode.value ? [] : recommendedSkillCards.value.filter(matchesSearch)
|
||||
},
|
||||
{
|
||||
key: 'builtin',
|
||||
title: '内置',
|
||||
@ -668,6 +765,7 @@ const skillGroups = computed(() => [
|
||||
skills: filteredInstalledSkills.value.filter((skill) => skill.sourceType !== 'builtin')
|
||||
}
|
||||
])
|
||||
const visibleSkillGroups = computed(() => skillGroups.value.filter((group) => group.skills.length))
|
||||
const filteredDeletableSkills = computed(() =>
|
||||
filteredInstalledSkills.value.filter(
|
||||
(skill) => canManageSkill(skill) && skill.sourceType !== 'builtin'
|
||||
@ -761,6 +859,11 @@ const sourceTypeLabel = (sourceType) => {
|
||||
|
||||
const canManageSkill = (skill) => skill?.can_manage !== false
|
||||
const isSkillToggling = (slug) => togglingSkillSlugs.value.includes(slug)
|
||||
const isRecommendedSkillInstalling = (source) => installingRecommendedSources.value.includes(source)
|
||||
const isRecommendedSkillInstallDisabled = (source) =>
|
||||
installingRecommendedSources.value.length > 0 ||
|
||||
draftConfirmVisible.value ||
|
||||
isRecommendedSkillInstalling(source)
|
||||
|
||||
const navigateToDetail = (skill) => {
|
||||
router.push({ path: `/extensions/skill/${encodeURIComponent(skill.slug)}` })
|
||||
@ -797,6 +900,10 @@ const goToPreviewSkillManagement = () => {
|
||||
}
|
||||
|
||||
const handleCardClick = (skill) => {
|
||||
if (skill?.isRecommendation) {
|
||||
if (!isBatchDeleteMode.value) handleRecommendedSkillInstall(skill)
|
||||
return
|
||||
}
|
||||
if (isBatchDeleteMode.value) {
|
||||
handleToggleCardSelect(skill.slug)
|
||||
} else {
|
||||
@ -1080,6 +1187,17 @@ const handleCancelInstall = () => {
|
||||
remoteInstallModalVisible.value = false
|
||||
}
|
||||
|
||||
const rememberRemoteSource = (source) => {
|
||||
let history = [...repoHistory.value]
|
||||
history = history.filter((item) => item !== source)
|
||||
history.unshift(source)
|
||||
if (history.length > 10) {
|
||||
history = history.slice(0, 10)
|
||||
}
|
||||
repoHistory.value = history
|
||||
localStorage.setItem('yuxi_remote_repo_history', JSON.stringify(history))
|
||||
}
|
||||
|
||||
const handleListRemoteSkills = async () => {
|
||||
const source = remoteInstallForm.source.trim()
|
||||
if (!source) {
|
||||
@ -1102,15 +1220,7 @@ const handleListRemoteSkills = async () => {
|
||||
message.success(`已发现 ${remoteSkillOptions.value.length} 个 Skills`)
|
||||
}
|
||||
|
||||
// 保存成功的拉取历史
|
||||
let history = [...repoHistory.value]
|
||||
history = history.filter((item) => item !== source)
|
||||
history.unshift(source)
|
||||
if (history.length > 10) {
|
||||
history = history.slice(0, 10)
|
||||
}
|
||||
repoHistory.value = history
|
||||
localStorage.setItem('yuxi_remote_repo_history', JSON.stringify(history))
|
||||
rememberRemoteSource(source)
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.detail || error.message || '获取远程 Skills 失败')
|
||||
} finally {
|
||||
@ -1118,6 +1228,54 @@ const handleListRemoteSkills = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecommendedSkillInstall = async (skill) => {
|
||||
if (!skill?.source || isRecommendedSkillInstallDisabled(skill.source)) {
|
||||
return
|
||||
}
|
||||
|
||||
installingRecommendedSources.value.push(skill.source)
|
||||
activeTab.value = 'repo'
|
||||
remoteInstallForm.source = skill.source
|
||||
remoteSkillOptions.value = []
|
||||
selectedRepoSkills.value = []
|
||||
repoFilterKeyword.value = ''
|
||||
|
||||
try {
|
||||
const listResult = await skillApi.listRemoteSkills(skill.source)
|
||||
const options = listResult?.data || []
|
||||
remoteSkillOptions.value = options
|
||||
const aliasSet = new Set(
|
||||
(skill.aliases || [skill.slug, skill.name]).map((item) => item.toLowerCase())
|
||||
)
|
||||
const matchedSkill =
|
||||
options.length === 1
|
||||
? options[0]
|
||||
: options.find((item) => aliasSet.has(String(item.name || '').toLowerCase()))
|
||||
|
||||
if (!matchedSkill?.name) {
|
||||
remoteInstallModalVisible.value = true
|
||||
message.warning('已拉取推荐来源,请选择要安装的 Skill')
|
||||
return
|
||||
}
|
||||
|
||||
selectedRepoSkills.value = [matchedSkill.name]
|
||||
rememberRemoteSource(skill.source)
|
||||
const prepareResult = await skillApi.prepareRemoteSkills({
|
||||
source: skill.source,
|
||||
skills: [matchedSkill.name]
|
||||
})
|
||||
if (await openDraftConfirmation(prepareResult?.data)) {
|
||||
message.success('解析完成,请确认 Skill 生效范围')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.detail || error.message || '解析推荐 Skill 失败')
|
||||
} finally {
|
||||
installingRecommendedSources.value = installingRecommendedSources.value.filter(
|
||||
(source) => source !== skill.source
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const loadHistory = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('yuxi_remote_repo_history')
|
||||
@ -1354,6 +1512,11 @@ defineExpose({
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
&.loading:disabled {
|
||||
cursor: wait;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
color: var(--color-success-700);
|
||||
|
||||
@ -1382,6 +1545,20 @@ defineExpose({
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-icon-spin {
|
||||
animation: skill-action-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes skill-action-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.skill-preview-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user