diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index ab4ca8078..25a974588 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -763,6 +763,27 @@ func (b *BaseApi) ListAgentSkills(c *gin.Context) { helper.SuccessWithData(c, data) } +// @Tags AI +// @Summary Search Agent skills +// @Accept json +// @Param request body dto.AgentSkillSearchReq true "request" +// @Success 200 {array} dto.AgentSkillSearchItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/skills/search [post] +func (b *BaseApi) SearchAgentSkills(c *gin.Context) { + var req dto.AgentSkillSearchReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.SearchSkills(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + // @Tags AI // @Summary Update Agent skill status // @Accept json @@ -783,6 +804,26 @@ func (b *BaseApi) UpdateAgentSkill(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Install Agent skill +// @Accept json +// @Param request body dto.AgentSkillInstallReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/skills/install [post] +func (b *BaseApi) InstallAgentSkill(c *gin.Context) { + var req dto.AgentSkillInstallReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.InstallSkill(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Login Agent Weixin channel // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index b951c32b0..c382b7abb 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -389,6 +389,12 @@ type AgentSkillsReq struct { AgentID uint `json:"agentId" validate:"required"` } +type AgentSkillSearchReq struct { + AgentID uint `json:"agentId" validate:"required"` + Source string `json:"source" validate:"required,oneof=clawhub skillhub"` + Keyword string `json:"keyword" validate:"required"` +} + type AgentSkillItem struct { Name string `json:"name"` Description string `json:"description"` @@ -397,8 +403,25 @@ type AgentSkillItem struct { Disabled bool `json:"disabled"` } +type AgentSkillSearchItem struct { + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Summary string `json:"summary"` + Version string `json:"version"` + Source string `json:"source"` + Score string `json:"score"` +} + type AgentSkillUpdateReq struct { AgentID uint `json:"agentId" validate:"required"` Name string `json:"name" validate:"required"` Enabled bool `json:"enabled"` } + +type AgentSkillInstallReq struct { + AgentID uint `json:"agentId" validate:"required"` + Source string `json:"source" validate:"required,oneof=clawhub skillhub"` + Slug string `json:"slug" validate:"required"` + TaskID string `json:"taskID" validate:"required"` +} diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 9f5bd3f6a..4865511a5 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -39,7 +39,9 @@ type IAgentService interface { GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, error) + SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) UpdateSkill(req dto.AgentSkillUpdateReq) error + InstallSkill(req dto.AgentSkillInstallReq) error CreateAccount(req dto.AgentAccountCreateReq) error UpdateAccount(req dto.AgentAccountUpdateReq) error @@ -79,6 +81,7 @@ const ( maxCommunityAIAgents = int64(5) openclawPluginBaseDir = "/home/node/.openclaw/extensions" openclawPluginPackageTmpDir = "/tmp/openclaw-plugin" + openclawManagedSkillsDir = "/home/node/.openclaw/skills" openclawGatewayPort = 18789 openclawAllowedOriginHost = "127.0.0.1" openclawHTTPSVersion = "2026.3.13" diff --git a/agent/app/service/agents_skills.go b/agent/app/service/agents_skills.go index 13d7d5488..a4f710328 100644 --- a/agent/app/service/agents_skills.go +++ b/agent/app/service/agents_skills.go @@ -3,11 +3,14 @@ package service import ( "encoding/json" "fmt" + "regexp" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" ) @@ -27,6 +30,13 @@ type openclawSkillInfo struct { SkillKey string `json:"skillKey"` } +type skillhubSearchPayload struct { + Skills []dto.AgentSkillSearchItem `json:"skills"` + Results []dto.AgentSkillSearchItem `json:"results"` +} + +var clawhubSearchLinePattern = regexp.MustCompile(`^(\S+)\s+(.+?)\s+\(([\d.]+)\)$`) + func (a AgentService) ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, error) { agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { @@ -56,6 +66,36 @@ func (a AgentService) ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, return parseOpenclawSkillsList(output) } +func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) { + agent, install, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return nil, err + } + if agent.AgentType != constant.AppOpenclaw { + return nil, fmt.Errorf("copaw does not support skills") + } + status, err := checkContainerStatus(install.ContainerName) + if err != nil { + return nil, err + } + if status != "running" { + return nil, fmt.Errorf("container %s is not running, please check and retry", install.ContainerName) + } + output, err := loadOpenclawSkillSearchOutput(install.ContainerName, req.Source, req.Keyword) + if err != nil { + return nil, err + } + if len(output) == 0 { + return nil, nil + } + switch req.Source { + case "skillhub": + return parseSkillhubSearchResult(output) + default: + return parseClawhubSearchResult(output), nil + } +} + func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error { agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { @@ -83,6 +123,37 @@ func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error { return writeOpenclawConfigRaw(agent.ConfigPath, conf) } +func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error { + agent, install, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return err + } + if agent.AgentType != constant.AppOpenclaw { + return fmt.Errorf("copaw does not support skills") + } + status, err := checkContainerStatus(install.ContainerName) + if err != nil { + return err + } + if status != "running" { + return fmt.Errorf("container %s is not running, please check and retry", install.ContainerName) + } + installTask, err := task.NewTaskWithOps(req.Slug, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID) + if err != nil { + return err + } + installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error { + mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) + return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug)) + }, nil) + go func() { + if err := installTask.Execute(); err != nil { + global.LOG.Errorf("install openclaw skill failed: %v", err) + } + }() + return nil +} + func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) { var payload openclawSkillsList if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil { @@ -101,6 +172,87 @@ func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) { return items, nil } +func loadOpenclawSkillSearchOutput(containerName, source, keyword string) (string, error) { + switch source { + case "skillhub": + return cmd.RunDefaultWithStdoutBashCfAndTimeOut( + "docker exec %s skillhub search %q --json 2>&1", + 30*time.Second, + containerName, + keyword, + ) + default: + return cmd.RunDefaultWithStdoutBashCfAndTimeOut( + "docker exec %s clawhub search %q 2>&1", + 30*time.Second, + containerName, + keyword, + ) + } +} + +func parseSkillhubSearchResult(output string) ([]dto.AgentSkillSearchItem, error) { + trimmed := strings.TrimSpace(output) + if trimmed == "" { + return nil, nil + } + var list []dto.AgentSkillSearchItem + if err := json.Unmarshal([]byte(trimmed), &list); err == nil { + for i := range list { + list[i].Source = "skillhub" + } + return list, nil + } + var payload skillhubSearchPayload + if err := json.Unmarshal([]byte(trimmed), &payload); err != nil { + return nil, err + } + items := payload.Skills + if len(items) == 0 { + items = payload.Results + } + for i := range items { + items[i].Source = "skillhub" + } + return items, nil +} + +func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem { + lines := strings.Split(strings.TrimSpace(output), "\n") + items := make([]dto.AgentSkillSearchItem, 0, len(lines)) + for _, line := range lines { + matches := clawhubSearchLinePattern.FindStringSubmatch(strings.TrimSpace(line)) + if len(matches) != 4 { + continue + } + items = append(items, dto.AgentSkillSearchItem{ + Slug: matches[1], + Name: matches[2], + Score: matches[3], + Source: "clawhub", + }) + } + return items +} + +func buildOpenclawSkillInstallCommand(source, slug string) string { + switch source { + case "clawhub": + return fmt.Sprintf( + "mkdir -p %s && clawhub --workdir /home/node/.openclaw --dir skills install %q", + openclawManagedSkillsDir, + slug, + ) + default: + return fmt.Sprintf( + "mkdir -p %s && skillhub --dir %s install %q", + openclawManagedSkillsDir, + openclawManagedSkillsDir, + slug, + ) + } +} + func getOpenclawSkillKey(containerName, name string) (string, error) { output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut( "docker exec %s openclaw skills info %q --json 2>&1", diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 86be8104a..5aade3056 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -78,7 +78,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/config-file/get", baseApi.GetAgentConfigFile) aiToolsRouter.POST("/agents/config-file/update", baseApi.UpdateAgentConfigFile) aiToolsRouter.POST("/agents/skills/list", baseApi.ListAgentSkills) + aiToolsRouter.POST("/agents/skills/search", baseApi.SearchAgentSkills) aiToolsRouter.POST("/agents/skills/update", baseApi.UpdateAgentSkill) + aiToolsRouter.POST("/agents/skills/install", baseApi.InstallAgentSkill) aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing) } } diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 8404d1db3..aafaf4bc8 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -623,6 +623,12 @@ export namespace AI { agentId: number; } + export interface AgentSkillSearchReq { + agentId: number; + source: 'clawhub' | 'skillhub'; + keyword: string; + } + export interface AgentSkillItem { name: string; description: string; @@ -631,9 +637,26 @@ export namespace AI { disabled: boolean; } + export interface AgentSkillSearchItem { + slug: string; + name: string; + description: string; + summary: string; + version: string; + source: string; + score: string; + } + export interface AgentSkillUpdateReq { agentId: number; name: string; enabled: boolean; } + + export interface AgentSkillInstallReq { + agentId: number; + source: 'clawhub' | 'skillhub'; + slug: string; + taskID: string; + } } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index e50b3337e..5c8c37125 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -245,10 +245,18 @@ export const listAgentSkills = (req: AI.AgentSkillsReq) => { return http.post(`/ai/agents/skills/list`, req); }; +export const searchAgentSkills = (req: AI.AgentSkillSearchReq) => { + return http.post(`/ai/agents/skills/search`, req); +}; + export const updateAgentSkill = (req: AI.AgentSkillUpdateReq) => { return http.post(`/ai/agents/skills/update`, req); }; +export const installAgentSkill = (req: AI.AgentSkillInstallReq) => { + return http.post(`/ai/agents/skills/install`, req); +}; + export const approveAgentChannelPairing = (req: AI.AgentChannelPairingApproveReq) => { return http.post(`/ai/agents/channel/pairing/approve`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index aa24fb7eb..e025e07b3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -706,6 +706,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index ff8d101da..45ce5fa39 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -714,6 +714,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 373272650..bae8f599a 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -707,6 +707,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub(公式)', + skillsMarketSourceSkillhub: 'SkillHub(Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index f151e1299..3f3fce38f 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -699,6 +699,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 2a9b7d964..7bc44bd29 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -714,6 +714,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index b743e38cb..b0baa95f0 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -709,6 +709,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 09865c897..f3ae850ad 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -706,6 +706,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 1c8150d41..39f68b483 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -710,6 +710,12 @@ const message = { 'Go to Settings -> Other to configure the NPM registry and speed up plugin installation', skillsSearchPlaceholder: 'Search skills...', skillsEmpty: 'No skills', + skillsMarket: 'Skill Market', + skillsMarketHint: 'Select a source and search for skills', + skillsMarketEmpty: 'No matching skills found', + skillsMarketSourceClawhub: 'ClawHub (Official)', + skillsMarketSourceSkillhub: 'SkillHub (Tencent)', + skillsScore: 'Score', versionUnsupportedTitle: 'This feature is not supported in the current version', versionUnsupportedHelper: 'Please upgrade OpenClaw to version {0} or later.', skillsStatusDisabled: 'Disabled', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index f0876f178..061d0fdc7 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -672,6 +672,12 @@ const message = { pluginInstallNPMRegistryHelper: '可前往 設定 -> 其他 設定 NPM 源,以加速外掛安裝', skillsSearchPlaceholder: '搜尋技能...', skillsEmpty: '暫無技能', + skillsMarket: '技能市場', + skillsMarketHint: '請選擇來源並搜尋技能', + skillsMarketEmpty: '未找到相關技能', + skillsMarketSourceClawhub: 'ClawHub(官方)', + skillsMarketSourceSkillhub: 'SkillHub(騰訊)', + skillsScore: '評分', versionUnsupportedTitle: '當前版本暫不支援該功能', versionUnsupportedHelper: '請升級 OpenClaw 到 {0} 或以上版本後使用。', skillsStatusDisabled: '已禁用', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index b03e6ce86..f2f69cfb9 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -671,6 +671,12 @@ const message = { pluginInstallNPMRegistryHelper: '可前往 设置 -> 其他 配置 NPM 源,以加速插件安装', skillsSearchPlaceholder: '搜索技能...', skillsEmpty: '暂无技能', + skillsMarket: '技能市场', + skillsMarketHint: '请选择来源并搜索技能', + skillsMarketEmpty: '未找到相关技能', + skillsMarketSourceClawhub: 'ClawHub(官方)', + skillsMarketSourceSkillhub: 'SkillHub(腾讯)', + skillsScore: '评分', versionUnsupportedTitle: '当前版本暂不支持该功能', versionUnsupportedHelper: '请升级 OpenClaw 到 {0} 或以上版本后使用。', skillsStatusDisabled: '已禁用', diff --git a/frontend/src/views/ai/agents/agent/config/tabs/skills.vue b/frontend/src/views/ai/agents/agent/config/tabs/skills.vue index 714ad454e..9a1a714dd 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/skills.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/skills.vue @@ -1,56 +1,129 @@ @@ -59,12 +132,17 @@ import { computed, ref } from 'vue'; import { Refresh, Search } from '@element-plus/icons-vue'; import { useI18n } from 'vue-i18n'; import { AI } from '@/api/interface/ai'; -import { listAgentSkills, updateAgentSkill } from '@/api/modules/ai'; +import { installAgentSkill, listAgentSkills, searchAgentSkills, updateAgentSkill } from '@/api/modules/ai'; +import { useGlobalStore } from '@/composables/useGlobalStore'; import { MsgSuccess } from '@/utils/message'; +import { newUUID } from '@/utils/util'; import { isOpenclawCurrentHTTPVersion } from '@/utils/agent'; +import TaskLog from '@/components/log/task/index.vue'; import VersionSupport from './components/version-support.vue'; type SkillGroupKey = 'builtIn' | 'external' | 'workspace' | 'extra' | 'other'; +type SkillViewMode = 'installed' | 'market'; +type SkillMarketSource = 'clawhub' | 'skillhub'; const openclawMinSupportedVersion = '2026.3.23'; const props = defineProps<{ @@ -72,11 +150,20 @@ const props = defineProps<{ }>(); const { t } = useI18n(); +const { isIntl } = useGlobalStore(); const loading = ref(false); -const keyword = ref(''); +const searching = ref(false); +const mode = ref('installed'); +const installedKeyword = ref(''); +const marketKeyword = ref(''); +const marketSource = ref(isIntl.value ? 'clawhub' : 'skillhub'); +const marketSearched = ref(false); const agentId = ref(0); const skills = ref([]); +const marketResults = ref([]); const updatingSkill = ref(''); +const installingSkill = ref(''); +const taskLogRef = ref>(); const supported = computed(() => isOpenclawCurrentHTTPVersion(props.appVersion)); const groupTagLabels = computed>(() => ({ @@ -101,7 +188,7 @@ const groupLabels = computed>(() => ({ })); const filteredSkills = computed(() => { - const value = keyword.value.trim().toLowerCase(); + const value = installedKeyword.value.trim().toLowerCase(); if (!value) { return skills.value; } @@ -121,7 +208,7 @@ const groupedSkills = computed(() => { for (const skill of filteredSkills.value) { groups[resolveGroupKey(skill)].push(skill); } - const order: SkillGroupKey[] = ['external', 'extra', 'builtIn', 'workspace', 'other']; + const order: SkillGroupKey[] = ['external', 'workspace', 'extra', 'other', 'builtIn']; return order .filter((key) => groups[key].length > 0) .map((key) => ({ @@ -161,8 +248,35 @@ const loadSkills = async () => { } }; +const searchMarketSkills = async () => { + if (!supported.value || !agentId.value || !marketKeyword.value.trim()) { + return; + } + searching.value = true; + try { + const res = await searchAgentSkills({ + agentId: agentId.value, + source: marketSource.value, + keyword: marketKeyword.value.trim(), + }); + marketResults.value = res.data || []; + marketSearched.value = true; + } finally { + searching.value = false; + } +}; + +const handleMarketSourceChange = () => { + marketResults.value = []; + marketSearched.value = false; +}; + const load = async (id: number) => { agentId.value = id; + mode.value = 'installed'; + marketSource.value = isIntl.value ? 'clawhub' : 'skillhub'; + marketResults.value = []; + marketSearched.value = false; await loadSkills(); }; @@ -184,12 +298,39 @@ const toggleSkill = async (skill: AI.AgentSkillItem, enabled: boolean) => { } }; +const installSkill = async (skill: AI.AgentSkillSearchItem) => { + if (!supported.value || !agentId.value) { + return; + } + const taskID = newUUID(); + installingSkill.value = skill.slug; + try { + await installAgentSkill({ + agentId: agentId.value, + source: skill.source as SkillMarketSource, + slug: skill.slug, + taskID, + }); + taskLogRef.value?.openWithTaskID(taskID); + } finally { + installingSkill.value = ''; + } +}; + +const handleTaskClose = async () => { + await loadSkills(); +}; + defineExpose({ load, });