diff --git a/REFACTOR.md b/REFACTOR.md index a87fe73a..ea5108ad 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -58,6 +58,7 @@ - [x] 将 Qwen-Image 修改为 Skill - [x] 现在输入区域对于不同 mention 的渲染的 ICON 和 human-message 里面的渲染的 ICON; - [x] 节点的颜色按照 label 的分类来 +- [ ] 添加推荐 Skill 的列表,点击可以直接安装,实际上是替代了填入搜索的名称,并触发拉取,这个似乎只需要在前端修改就可以了 # bug diff --git a/docs/agents/skills-management.md b/docs/agents/skills-management.md index bc2f42e1..0f41d905 100644 --- a/docs/agents/skills-management.md +++ b/docs/agents/skills-management.md @@ -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 --list` 校验来源并发现可安装的 skills +- 使用隔离的临时 `HOME` 执行 `npx skills add --skill -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 --list` 校验来源并发现可安装的 skills -- 使用隔离的临时 `HOME` 执行 `npx skills add --skill -g -y --copy` -- 从临时目录中提取对应 skill,再按现有导入流程生成草稿;确认后写入 `/app/saves/skills` 与数据库 - ::: tip 远程安装不会把 ~/.agents/skills 作为系统主存储 远程安装只把 `skills.sh` CLI 作为“下载器”使用。Yuxi 仍然以 `/app/saves/skills + skills 表` 作为正式来源,这样才能与现有的权限、线程可见性和沙盒挂载机制保持一致。 ::: diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 4ee240ec..1b89ee15 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -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` 访问知识库内容。 diff --git a/web/src/components/extensions/SkillCardList.vue b/web/src/components/extensions/SkillCardList.vue index 9a5fd390..61f131c2 100644 --- a/web/src/components/extensions/SkillCardList.vue +++ b/web/src/components/extensions/SkillCardList.vue @@ -30,6 +30,15 @@ 上传 Skill + + + 安装文档 + @@ -55,7 +64,7 @@
@@ -76,53 +85,73 @@
@@ -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;