mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: Add skill hub for openclaw (#12333)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,10 +245,18 @@ export const listAgentSkills = (req: AI.AgentSkillsReq) => {
|
||||
return http.post<AI.AgentSkillItem[]>(`/ai/agents/skills/list`, req);
|
||||
};
|
||||
|
||||
export const searchAgentSkills = (req: AI.AgentSkillSearchReq) => {
|
||||
return http.post<AI.AgentSkillSearchItem[]>(`/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);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -672,6 +672,12 @@ const message = {
|
||||
pluginInstallNPMRegistryHelper: '可前往 設定 -> 其他 設定 NPM 源,以加速外掛安裝',
|
||||
skillsSearchPlaceholder: '搜尋技能...',
|
||||
skillsEmpty: '暫無技能',
|
||||
skillsMarket: '技能市場',
|
||||
skillsMarketHint: '請選擇來源並搜尋技能',
|
||||
skillsMarketEmpty: '未找到相關技能',
|
||||
skillsMarketSourceClawhub: 'ClawHub(官方)',
|
||||
skillsMarketSourceSkillhub: 'SkillHub(騰訊)',
|
||||
skillsScore: '評分',
|
||||
versionUnsupportedTitle: '當前版本暫不支援該功能',
|
||||
versionUnsupportedHelper: '請升級 OpenClaw 到 {0} 或以上版本後使用。',
|
||||
skillsStatusDisabled: '已禁用',
|
||||
|
||||
@@ -671,6 +671,12 @@ const message = {
|
||||
pluginInstallNPMRegistryHelper: '可前往 设置 -> 其他 配置 NPM 源,以加速插件安装',
|
||||
skillsSearchPlaceholder: '搜索技能...',
|
||||
skillsEmpty: '暂无技能',
|
||||
skillsMarket: '技能市场',
|
||||
skillsMarketHint: '请选择来源并搜索技能',
|
||||
skillsMarketEmpty: '未找到相关技能',
|
||||
skillsMarketSourceClawhub: 'ClawHub(官方)',
|
||||
skillsMarketSourceSkillhub: 'SkillHub(腾讯)',
|
||||
skillsScore: '评分',
|
||||
versionUnsupportedTitle: '当前版本暂不支持该功能',
|
||||
versionUnsupportedHelper: '请升级 OpenClaw 到 {0} 或以上版本后使用。',
|
||||
skillsStatusDisabled: '已禁用',
|
||||
|
||||
@@ -1,56 +1,129 @@
|
||||
<template>
|
||||
<VersionSupport v-if="!supported" :min-version="openclawMinSupportedVersion" />
|
||||
<div v-else v-loading="loading">
|
||||
<el-radio-group v-model="mode" class="view-switch">
|
||||
<el-radio-button label="installed">{{ t('app.installed') }}</el-radio-button>
|
||||
<el-radio-button label="market">{{ t('aiTools.agents.skillsMarket') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
:placeholder="t('aiTools.agents.skillsSearchPlaceholder')"
|
||||
clearable
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button :loading="loading" @click="loadSkills">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
<template v-if="mode === 'installed'">
|
||||
<el-input
|
||||
v-model="installedKeyword"
|
||||
:placeholder="t('aiTools.agents.skillsSearchPlaceholder')"
|
||||
clearable
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button :loading="loading" @click="loadSkills">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-select v-model="marketSource" class="p-w-200" @change="handleMarketSourceChange">
|
||||
<el-option :value="'clawhub'" :label="t('aiTools.agents.skillsMarketSourceClawhub')" />
|
||||
<el-option :value="'skillhub'" :label="t('aiTools.agents.skillsMarketSourceSkillhub')" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="marketKeyword"
|
||||
:placeholder="t('aiTools.agents.skillsSearchPlaceholder')"
|
||||
clearable
|
||||
class="search-input"
|
||||
@keyup.enter="searchMarketSkills"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button :loading="searching" @click="searchMarketSkills">{{ t('commons.button.search') }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="groupedSkills.length" class="group-list">
|
||||
<section v-for="group in groupedSkills" :key="group.key" class="group-section">
|
||||
<div class="group-header">
|
||||
<span class="group-title">{{ group.label }}</span>
|
||||
<span class="group-count">{{ group.items.length }}</span>
|
||||
</div>
|
||||
<div class="skills-grid">
|
||||
<el-card v-for="skill in group.items" :key="skill.name" class="skill-card">
|
||||
<div class="skill-head">
|
||||
<div class="skill-name">{{ skill.name }}</div>
|
||||
<el-switch
|
||||
:model-value="!skill.disabled"
|
||||
:loading="updatingSkill === skill.name"
|
||||
@change="(value) => toggleSkill(skill, Boolean(value))"
|
||||
/>
|
||||
</div>
|
||||
<el-tooltip placement="top-start" :show-after="200" popper-class="skill-desc-tooltip">
|
||||
<template #content>
|
||||
<div class="skill-desc-tooltip-content">{{ skill.description }}</div>
|
||||
</template>
|
||||
<div class="skill-desc">
|
||||
{{ skill.description }}
|
||||
<template v-if="mode === 'installed'">
|
||||
<div v-if="groupedSkills.length" class="group-list">
|
||||
<section v-for="group in groupedSkills" :key="group.key" class="group-section">
|
||||
<div class="group-header">
|
||||
<span class="group-title">{{ group.label }}</span>
|
||||
<span class="group-count">{{ group.items.length }}</span>
|
||||
</div>
|
||||
<div class="skills-grid">
|
||||
<el-card v-for="skill in group.items" :key="skill.name" class="skill-card">
|
||||
<div class="skill-head">
|
||||
<div class="skill-name">{{ skill.name }}</div>
|
||||
<el-switch
|
||||
:model-value="!skill.disabled"
|
||||
:loading="updatingSkill === skill.name"
|
||||
@change="(value) => toggleSkill(skill, Boolean(value))"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div class="skill-tags">
|
||||
<el-tag size="small" effect="plain">
|
||||
{{ group.tagLabel }}
|
||||
</el-tag>
|
||||
<el-tooltip placement="top-start" :show-after="200" popper-class="skill-desc-tooltip">
|
||||
<template #content>
|
||||
<div class="skill-desc-tooltip-content">{{ skill.description }}</div>
|
||||
</template>
|
||||
<div class="skill-desc">
|
||||
{{ skill.description }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div class="skill-tags">
|
||||
<el-tag size="small" type="primary" effect="plain">
|
||||
{{ group.tagLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<el-empty v-else :description="t('aiTools.agents.skillsEmpty')" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="marketResults.length" class="skills-grid">
|
||||
<el-card v-for="skill in marketResults" :key="`${marketSource}-${skill.slug}`" class="skill-card">
|
||||
<div class="skill-head">
|
||||
<div>
|
||||
<div class="skill-name">{{ skill.name || skill.slug }}</div>
|
||||
<div class="skill-slug">{{ skill.slug }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<el-empty v-else :description="t('aiTools.agents.skillsEmpty')" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:loading="installingSkill === skill.slug"
|
||||
@click="installSkill(skill)"
|
||||
>
|
||||
{{ t('commons.button.install') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-tooltip
|
||||
v-if="skill.description || skill.summary"
|
||||
placement="top-start"
|
||||
:show-after="200"
|
||||
popper-class="skill-desc-tooltip"
|
||||
>
|
||||
<template #content>
|
||||
<div class="skill-desc-tooltip-content">{{ skill.description || skill.summary }}</div>
|
||||
</template>
|
||||
<div class="skill-desc">
|
||||
{{ skill.description || skill.summary }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div class="skill-meta">
|
||||
<span v-if="skill.version">{{ `${t('app.version')}: ${skill.version}` }}</span>
|
||||
<span v-if="skill.score">{{ `${t('aiTools.agents.skillsScore')}: ${skill.score}` }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-empty
|
||||
v-else
|
||||
:description="
|
||||
marketSearched ? t('aiTools.agents.skillsMarketEmpty') : t('aiTools.agents.skillsMarketHint')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<TaskLog ref="taskLogRef" @close="handleTaskClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<SkillViewMode>('installed');
|
||||
const installedKeyword = ref('');
|
||||
const marketKeyword = ref('');
|
||||
const marketSource = ref<SkillMarketSource>(isIntl.value ? 'clawhub' : 'skillhub');
|
||||
const marketSearched = ref(false);
|
||||
const agentId = ref(0);
|
||||
const skills = ref<AI.AgentSkillItem[]>([]);
|
||||
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
|
||||
const updatingSkill = ref('');
|
||||
const installingSkill = ref('');
|
||||
const taskLogRef = ref<InstanceType<typeof TaskLog>>();
|
||||
const supported = computed(() => isOpenclawCurrentHTTPVersion(props.appVersion));
|
||||
|
||||
const groupTagLabels = computed<Record<SkillGroupKey, string>>(() => ({
|
||||
@@ -101,7 +188,7 @@ const groupLabels = computed<Record<SkillGroupKey, string>>(() => ({
|
||||
}));
|
||||
|
||||
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,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.view-switch {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -271,6 +412,20 @@ defineExpose({
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.skill-slug {
|
||||
margin-top: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.skill-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:global(.skill-desc-tooltip) {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user