Add plugin management support to OpenClaw. (#13388)

This commit is contained in:
CityFun
2026-07-28 14:20:55 +08:00
committed by GitHub
parent cf37de66fc
commit 7506e709e2
33 changed files with 1234 additions and 0 deletions
+82
View File
@@ -1378,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary List OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginsReq true "request"
// @Success 200 {array} dto.AgentPluginItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/list [post]
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
var req dto.AgentPluginsReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.ListPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Search OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginSearchReq true "request"
// @Success 200 {array} dto.AgentPluginSearchItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/search [post]
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
var req dto.AgentPluginSearchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.SearchPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Install an OpenClaw marketplace plugin
// @Accept json
// @Param request body dto.AgentPluginMarketInstallReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/install [post]
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
var req dto.AgentPluginMarketInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.InstallMarketPlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Operate an OpenClaw plugin
// @Accept json
// @Param request body dto.AgentPluginOperateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/operate [post]
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
var req dto.AgentPluginOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.OperatePlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Login Agent Weixin channel
// @Accept json
+46
View File
@@ -604,6 +604,52 @@ type AgentPluginStatus struct {
Upgradable bool `json:"upgradable"`
}
type AgentPluginsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentPluginSearchReq struct {
AgentID uint `json:"agentId" validate:"required"`
Keyword string `json:"keyword" validate:"required,max=100"`
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
}
type AgentPluginMarketInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Package string `json:"package" validate:"required,max=200"`
Version string `json:"version" validate:"required,max=100"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginOperateReq struct {
AgentID uint `json:"agentId" validate:"required"`
PluginID string `json:"pluginId" validate:"required,max=200"`
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginItem struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type AgentPluginSearchItem struct {
Package string `json:"package"`
PluginID string `json:"pluginId"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Channel string `json:"channel"`
VerificationTier string `json:"verificationTier"`
Categories []string `json:"categories"`
Official bool `json:"official"`
Downloads int64 `json:"downloads"`
Score float64 `json:"score"`
}
type AgentDiscordConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
+4
View File
@@ -105,6 +105,10 @@ type IAgentService interface {
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
UninstallPlugin(req dto.AgentPluginUninstallReq) error
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
OperatePlugin(req dto.AgentPluginOperateReq) error
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
}
+315
View File
@@ -0,0 +1,315 @@
package service
import (
"database/sql"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
var (
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
)
type openclawPluginListOutput struct {
Plugins []struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
} `json:"plugins"`
}
type openclawPluginIndexItem struct {
PluginID string `json:"pluginId"`
PackageName string `json:"packageName"`
PackageVersion string `json:"packageVersion"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type openclawPluginSearchOutput struct {
Results []struct {
Score float64 `json:"score"`
Package struct {
Name string `json:"name"`
RuntimeID string `json:"runtimeId"`
DisplayName string `json:"displayName"`
Summary string `json:"summary"`
LatestVersion string `json:"latestVersion"`
Categories []string `json:"categories"`
Channel string `json:"channel"`
IsOfficial bool `json:"isOfficial"`
VerificationTier string `json:"verificationTier"`
Stats struct {
Downloads int64 `json:"downloads"`
} `json:"stats"`
} `json:"package"`
} `json:"results"`
}
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
return plugins, nil
}
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
if err != nil {
return nil, err
}
return parseOpenclawPluginList([]byte(output))
}
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
limit := req.Limit
if limit == 0 {
limit = 20
}
output, err := cmd.RunDockerExecWithStdout(
2*time.Minute,
install.ContainerName,
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
)
if err != nil {
return nil, err
}
return parseOpenclawPluginSearch([]byte(output))
}
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
if err != nil {
return err
}
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask(taskName, 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, "openclaw", "plugins", "install", spec)
}, nil)
addOpenclawPluginRestartTask(installTask, install)
go executeAgentPluginTask(installTask)
return nil
}
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
if !openclawPluginIDPattern.MatchString(req.PluginID) {
return buserr.New("ErrInvalidChar")
}
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if req.Operate == "update" || req.Operate == "uninstall" {
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
if err != nil {
return err
}
for _, plugin := range plugins {
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
return buserr.WithName("ErrNotSupportType", req.Operate)
}
}
}
taskType := map[string]string{
"enable": task.TaskUpdate,
"disable": task.TaskUpdate,
"update": task.TaskUpgrade,
"uninstall": task.TaskUninstall,
}[req.Operate]
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
"enable": "AgentPluginEnable",
"disable": "AgentPluginDisable",
"update": "AgentPluginUpdate",
"uninstall": "AgentPluginUninstall",
}[req.Operate]), req.PluginID)
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
operateTask.AddSubTask(taskName, func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Operate == "uninstall" {
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
return err
}
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
}
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
}, nil)
addOpenclawPluginRestartTask(operateTask, install)
go executeAgentPluginTask(operateTask)
return nil
}
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginItem{}, nil
}
var output openclawPluginListOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
for _, plugin := range output.Plugins {
items = append(items, dto.AgentPluginItem{
ID: plugin.ID,
Name: plugin.Name,
Version: plugin.Version,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
if err != nil {
return nil, err
}
defer db.Close()
var raw []byte
if err := db.QueryRow(
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
"installed-plugin-index",
).Scan(&raw); err != nil {
return nil, err
}
var plugins []openclawPluginIndexItem
if err := json.Unmarshal(raw, &plugins); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(plugins))
for _, plugin := range plugins {
name := plugin.PackageName
if name == "" {
name = plugin.PluginID
}
items = append(items, dto.AgentPluginItem{
ID: plugin.PluginID,
Name: name,
Version: plugin.PackageVersion,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginSearchItem{}, nil
}
var output openclawPluginSearchOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
for _, result := range output.Results {
items = append(items, dto.AgentPluginSearchItem{
Package: result.Package.Name,
PluginID: result.Package.RuntimeID,
Name: result.Package.DisplayName,
Description: result.Package.Summary,
Version: result.Package.LatestVersion,
Channel: result.Package.Channel,
VerificationTier: result.Package.VerificationTier,
Categories: append([]string{}, result.Package.Categories...),
Official: result.Package.IsOfficial,
Downloads: result.Package.Stats.Downloads,
Score: result.Score,
})
}
return items, nil
}
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
packageName = strings.TrimSpace(packageName)
version = strings.TrimSpace(version)
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
return "", buserr.New("ErrInvalidChar")
}
return "clawhub:" + packageName + "@" + version, nil
}
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
pluginType := map[string]string{
"openclaw-lark": "feishu",
"openclaw-qqbot": "qqbot",
"wecom-openclaw-plugin": "wecom",
"dingtalk-connector": "dingtalk",
"openclaw-weixin": "weixin",
}[pluginID]
if pluginType == "" {
return nil
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
cleanupOpenclawPluginConfig(conf, pluginType)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
output, err := compose.Restart(install.GetComposePath())
if output != "" {
t.Log(output)
}
return err
}, nil)
}
func executeAgentPluginTask(t *task.Task) {
if err := t.Execute(); err != nil {
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
}
}
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Create'
TaskDelete: 'Delete'
TaskUpgrade: 'Upgrade'
TaskUpdate: 'Update'
AgentPluginInstall: 'Install plugin'
AgentPluginEnable: 'Enable plugin'
AgentPluginDisable: 'Disable plugin'
AgentPluginUpdate: 'Update plugin'
AgentPluginUninstall: 'Uninstall plugin'
TaskRestart: 'Restart'
TaskProtect: "Protect"
TaskBackup: 'Backup'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Crear'
TaskDelete: 'Eliminar'
TaskUpgrade: 'Actualizar'
TaskUpdate: 'Actualizar'
AgentPluginInstall: 'Instalar complemento'
AgentPluginEnable: 'Activar complemento'
AgentPluginDisable: 'Desactivar complemento'
AgentPluginUpdate: 'Actualizar complemento'
AgentPluginUninstall: 'Desinstalar complemento'
TaskRestart: 'Reiniciar'
TaskProtect: 'Proteger'
TaskBackup: 'Respaldar'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'ایجاد'
TaskDelete: 'حذف'
TaskUpgrade: 'ارتقاء'
TaskUpdate: 'به‌روزرسانی'
AgentPluginInstall: 'نصب افزونه'
AgentPluginEnable: 'فعال‌کردن افزونه'
AgentPluginDisable: 'غیرفعال‌کردن افزونه'
AgentPluginUpdate: 'به‌روزرسانی افزونه'
AgentPluginUninstall: 'حذف افزونه'
TaskRestart: 'راه‌اندازی مجدد'
TaskProtect: "محافظت"
TaskBackup: 'پشتیبان‌گیری'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: '作成'
TaskDelete: '削除'
TaskUpgrade: 'アップグレード'
TaskUpdate: '更新'
AgentPluginInstall: 'プラグインをインストール'
AgentPluginEnable: 'プラグインを有効化'
AgentPluginDisable: 'プラグインを無効化'
AgentPluginUpdate: 'プラグインを更新'
AgentPluginUninstall: 'プラグインをアンインストール'
TaskRestart: '再起動'
TaskProtect: '保護'
TaskBackup: 'バックアップ'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: '생성'
TaskDelete: '삭제'
TaskUpgrade: '업그레이드'
TaskUpdate: '업데이트'
AgentPluginInstall: '플러그인 설치'
AgentPluginEnable: '플러그인 활성화'
AgentPluginDisable: '플러그인 비활성화'
AgentPluginUpdate: '플러그인 업데이트'
AgentPluginUninstall: '플러그인 제거'
TaskRestart: '다시 시작'
TaskProtect: '보호'
TaskBackup: '백업'
+5
View File
@@ -439,6 +439,11 @@ TaskCreate: 'ສ້າງ'
TaskDelete: 'ລຶບ'
TaskUpgrade: 'ອັບເກຣດ'
TaskUpdate: 'ອັບເດດ'
AgentPluginInstall: 'ຕິດຕັ້ງປລັກອິນ'
AgentPluginEnable: 'ເປີດໃຊ້ປລັກອິນ'
AgentPluginDisable: 'ປິດໃຊ້ປລັກອິນ'
AgentPluginUpdate: 'ອັບເດດປລັກອິນ'
AgentPluginUninstall: 'ຖອນການຕິດຕັ້ງປລັກອິນ'
TaskRestart: 'ເລີ່ມໃໝ່'
TaskProtect: "ປ້ອງກັນ"
TaskBackup: 'ສຳຮອງຂໍ້ມູນ'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Buat'
TaskDelete: 'Padam'
TaskUpgrade: 'Naik taraf'
TaskUpdate: 'Kemas kini'
AgentPluginInstall: 'Pasang pemalam'
AgentPluginEnable: 'Aktifkan pemalam'
AgentPluginDisable: 'Nyahaktifkan pemalam'
AgentPluginUpdate: 'Kemas kini pemalam'
AgentPluginUninstall: 'Nyahpasang pemalam'
TaskRestart: 'Mulakan semula'
TaskProtect: 'Lindungi'
TaskBackup: 'Sandaran'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Criar'
TaskDelete: 'Excluir'
TaskUpgrade: 'Atualizar'
TaskUpdate: 'Atualizar'
AgentPluginInstall: 'Instalar plugin'
AgentPluginEnable: 'Ativar plugin'
AgentPluginDisable: 'Desativar plugin'
AgentPluginUpdate: 'Atualizar plugin'
AgentPluginUninstall: 'Desinstalar plugin'
TaskRestart: 'Reiniciar'
TaskProtect: 'Proteger'
TaskBackup: 'Backup'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Создать'
TaskDelete: 'Удалить'
TaskUpgrade: 'Обновить'
TaskUpdate: 'Обновить'
AgentPluginInstall: 'Установить плагин'
AgentPluginEnable: 'Включить плагин'
AgentPluginDisable: 'Отключить плагин'
AgentPluginUpdate: 'Обновить плагин'
AgentPluginUninstall: 'Удалить плагин'
TaskRestart: 'Перезапуск'
TaskProtect: 'Защита'
TaskBackup: 'Резервное копирование'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: 'Oluştur'
TaskDelete: 'Sil'
TaskUpgrade: 'Yükselt'
TaskUpdate: 'Güncelle'
AgentPluginInstall: 'Eklenti yükle'
AgentPluginEnable: 'Eklentiyi etkinleştir'
AgentPluginDisable: 'Eklentiyi devre dışı bırak'
AgentPluginUpdate: 'Eklentiyi güncelle'
AgentPluginUninstall: 'Eklentiyi kaldır'
TaskRestart: 'Yeniden Başlat'
TaskProtect: 'Koru'
TaskBackup: 'Yedekle'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: '建立'
TaskDelete: '刪除'
TaskUpgrade: '升級'
TaskUpdate: '更新'
AgentPluginInstall: '安裝外掛程式'
AgentPluginEnable: '啟用外掛程式'
AgentPluginDisable: '停用外掛程式'
AgentPluginUpdate: '更新外掛程式'
AgentPluginUninstall: '解除安裝外掛程式'
TaskRestart: '重新啟動'
TaskProtect: '防護'
TaskBackup: '備份'
+5
View File
@@ -449,6 +449,11 @@ TaskCreate: "创建"
TaskDelete: "删除"
TaskUpgrade: "升级"
TaskUpdate: "更新"
AgentPluginInstall: "安装插件"
AgentPluginEnable: "启用插件"
AgentPluginDisable: "禁用插件"
AgentPluginUpdate: "更新插件"
AgentPluginUninstall: "卸载插件"
TaskRestart: "重启"
TaskProtect: "防护"
TaskBackup: "备份"
+4
View File
@@ -104,6 +104,10 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/plugin/upgrade", baseApi.UpgradeAgentPlugin)
aiToolsRouter.POST("/agents/plugin/uninstall", baseApi.UninstallAgentPlugin)
aiToolsRouter.POST("/agents/plugin/check", baseApi.CheckAgentPlugin)
aiToolsRouter.POST("/agents/plugins/list", baseApi.ListAgentPlugins)
aiToolsRouter.POST("/agents/plugins/search", baseApi.SearchAgentPlugins)
aiToolsRouter.POST("/agents/plugins/install", baseApi.InstallAgentMarketPlugin)
aiToolsRouter.POST("/agents/plugins/operate", baseApi.OperateAgentPlugin)
aiToolsRouter.POST("/agents/security/get", baseApi.GetAgentSecurityConfig)
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
+46
View File
@@ -853,6 +853,52 @@ export namespace AI {
upgradable: boolean;
}
export interface AgentPluginsReq {
agentId: number;
}
export interface AgentPluginSearchReq {
agentId: number;
keyword: string;
limit?: number;
}
export interface AgentPluginMarketInstallReq {
agentId: number;
package: string;
version: string;
taskID: string;
}
export interface AgentPluginOperateReq {
agentId: number;
pluginId: string;
operate: 'enable' | 'disable' | 'update' | 'uninstall';
taskID: string;
}
export interface AgentPluginItem {
id: string;
name: string;
version: string;
origin: string;
enabled: boolean;
}
export interface AgentPluginSearchItem {
package: string;
pluginId: string;
name: string;
description: string;
version: string;
channel: string;
verificationTier: string;
categories: string[];
official: boolean;
downloads: number;
score: number;
}
export interface AgentDiscordConfigReq {
agentId: number;
}
+16
View File
@@ -330,6 +330,22 @@ export const checkAgentPlugin = (req: AI.AgentPluginCheckReq) => {
return http.post<AI.AgentPluginStatus>(`/ai/agents/plugin/check`, req);
};
export const listAgentPlugins = (req: AI.AgentPluginsReq) => {
return http.post<AI.AgentPluginItem[]>(`/ai/agents/plugins/list`, req);
};
export const searchAgentPlugins = (req: AI.AgentPluginSearchReq) => {
return http.post<AI.AgentPluginSearchItem[]>(`/ai/agents/plugins/search`, req);
};
export const installAgentMarketPlugin = (req: AI.AgentPluginMarketInstallReq) => {
return http.post(`/ai/agents/plugins/install`, req);
};
export const operateAgentPlugin = (req: AI.AgentPluginOperateReq) => {
return http.post(`/ai/agents/plugins/operate`, req);
};
export const getAgentSecurityConfig = (req: AI.AgentSecurityConfigReq) => {
return http.post<AI.AgentSecurityConfig>(`/ai/agents/security/get`, req);
};
+24
View File
@@ -753,6 +753,30 @@ const message = {
verified: 'Verified',
verifySkipped: 'No verification',
skillsTab: 'Skills',
pluginsTab: 'Plugins',
pluginsInstalled: 'Installed',
pluginsMarket: 'Plugin Market',
pluginsMarketHint: 'Enter a keyword to search for plugins',
pluginSearchPlaceholder: 'Search plugins...',
pluginOrigin: 'Source',
pluginOriginAll: 'All Sources',
pluginOriginBundled: 'Bundled',
pluginOriginExternal: 'External',
pluginStatusAll: 'All Statuses',
pluginEnabled: 'Enabled',
pluginDisabled: 'Disabled',
pluginCategory: 'Category',
pluginVerification: 'Verification',
pluginOfficial: 'Official',
pluginCommunity: 'Community',
pluginDownloads: 'Downloads',
pluginEnable: 'Enable',
pluginDisable: 'Disable',
pluginListEmpty: 'No plugins',
pluginSearchEmpty: 'No matching plugins found',
pluginInstallConfirm: 'Install plugin {0}?',
pluginDisableConfirm: 'Disable plugin {0}?',
pluginUninstallConfirm: 'Uninstall plugin {0}?',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Time Zone',
+24
View File
@@ -754,6 +754,30 @@ const message = {
verified: 'Verificado',
verifySkipped: 'Sin verificación',
skillsTab: 'Habilidades',
pluginsTab: 'Complementos',
pluginsInstalled: 'Instalados',
pluginsMarket: 'Mercado de complementos',
pluginsMarketHint: 'Introduce una palabra clave para buscar complementos',
pluginSearchPlaceholder: 'Buscar complementos...',
pluginOrigin: 'Origen',
pluginOriginAll: 'Todos los orígenes',
pluginOriginBundled: 'Integrado',
pluginOriginExternal: 'Externo',
pluginStatusAll: 'Todos los estados',
pluginEnabled: 'Activado',
pluginDisabled: 'Desactivado',
pluginCategory: 'Categoría',
pluginVerification: 'Verificación',
pluginOfficial: 'Oficial',
pluginCommunity: 'Comunidad',
pluginDownloads: 'Descargas',
pluginEnable: 'Activar',
pluginDisable: 'Desactivar',
pluginListEmpty: 'No hay complementos',
pluginSearchEmpty: 'No se encontraron complementos coincidentes',
pluginInstallConfirm: '¿Instalar el complemento {0}?',
pluginDisableConfirm: '¿Desactivar el complemento {0}?',
pluginUninstallConfirm: '¿Desinstalar el complemento {0}?',
securityTab: 'Seguridad',
otherTab: 'Otros',
timeZone: 'Zona horaria',
+24
View File
@@ -741,6 +741,30 @@ const message = {
verified: 'تأیید شده',
verifySkipped: 'بدون تأیید',
skillsTab: 'مهارت‌ها',
pluginsTab: 'افزونه‌ها',
pluginsInstalled: 'نصب‌شده',
pluginsMarket: 'بازار افزونه‌ها',
pluginsMarketHint: 'برای جستجوی افزونه یک کلیدواژه وارد کنید',
pluginSearchPlaceholder: 'جستجوی افزونه‌ها...',
pluginOrigin: 'منبع',
pluginOriginAll: 'همه منابع',
pluginOriginBundled: 'داخلی',
pluginOriginExternal: 'خارجی',
pluginStatusAll: 'همه وضعیت‌ها',
pluginEnabled: 'فعال',
pluginDisabled: 'غیرفعال',
pluginCategory: 'دسته‌بندی',
pluginVerification: 'اعتبارسنجی',
pluginOfficial: 'رسمی',
pluginCommunity: 'جامعه',
pluginDownloads: 'دانلودها',
pluginEnable: 'فعال‌کردن',
pluginDisable: 'غیرفعال‌کردن',
pluginListEmpty: 'افزونه‌ای وجود ندارد',
pluginSearchEmpty: 'افزونه مرتبطی پیدا نشد',
pluginInstallConfirm: 'افزونه {0} نصب شود؟',
pluginDisableConfirm: 'افزونه {0} غیرفعال شود؟',
pluginUninstallConfirm: 'افزونه {0} حذف شود؟',
securityTab: 'امنیت',
otherTab: 'سایر',
timeZone: 'منطقه زمانی',
+24
View File
@@ -747,6 +747,30 @@ const message = {
verified: '検証済み',
verifySkipped: '検証なし',
skillsTab: '技能',
pluginsTab: 'プラグイン',
pluginsInstalled: 'インストール済み',
pluginsMarket: 'プラグイン市場',
pluginsMarketHint: 'キーワードを入力してプラグインを検索してください',
pluginSearchPlaceholder: 'プラグインを検索...',
pluginOrigin: '提供元',
pluginOriginAll: 'すべての提供元',
pluginOriginBundled: '組み込み',
pluginOriginExternal: '外部',
pluginStatusAll: 'すべての状態',
pluginEnabled: '有効',
pluginDisabled: '無効',
pluginCategory: 'カテゴリ',
pluginVerification: '検証',
pluginOfficial: '公式',
pluginCommunity: 'コミュニティ',
pluginDownloads: 'ダウンロード数',
pluginEnable: '有効化',
pluginDisable: '無効化',
pluginListEmpty: 'プラグインがありません',
pluginSearchEmpty: '一致するプラグインが見つかりません',
pluginInstallConfirm: 'プラグイン {0} をインストールしますか',
pluginDisableConfirm: 'プラグイン {0} を無効にしますか',
pluginUninstallConfirm: 'プラグイン {0} をアンインストールしますか',
securityTab: 'セキュリティ',
otherTab: 'その他',
timeZone: 'タイムゾーン',
+24
View File
@@ -738,6 +738,30 @@ const message = {
verified: '검증됨',
verifySkipped: '검증 ',
skillsTab: '기술',
pluginsTab: '플러그인',
pluginsInstalled: '설치됨',
pluginsMarket: '플러그인 마켓',
pluginsMarketHint: '키워드를 입력하여 플러그인을 검색하세요',
pluginSearchPlaceholder: '플러그인 검색...',
pluginOrigin: '출처',
pluginOriginAll: '모든 출처',
pluginOriginBundled: '내장',
pluginOriginExternal: '외부',
pluginStatusAll: '모든 상태',
pluginEnabled: '활성화됨',
pluginDisabled: '비활성화됨',
pluginCategory: '분류',
pluginVerification: '검증',
pluginOfficial: '공식',
pluginCommunity: '커뮤니티',
pluginDownloads: '다운로드',
pluginEnable: '활성화',
pluginDisable: '비활성화',
pluginListEmpty: '플러그인이 없습니다',
pluginSearchEmpty: '일치하는 플러그인을 찾을 없습니다',
pluginInstallConfirm: '플러그인 {0}() 설치하시겠습니까?',
pluginDisableConfirm: '플러그인 {0}() 비활성화하시겠습니까?',
pluginUninstallConfirm: '플러그인 {0}() 제거하시겠습니까?',
securityTab: '보안',
otherTab: '기타',
timeZone: '시간대',
+24
View File
@@ -747,6 +747,30 @@ const message = {
verified: 'ກວດສອບແລ້ວ',
verifySkipped: 'ບໍ່ມີການກວດສອບ',
skillsTab: 'ທັກສະ',
pluginsTab: 'ປລັກອິນ',
pluginsInstalled: 'ຕິດຕັ້ງແລ້ວ',
pluginsMarket: 'ຕະຫຼາດປລັກອິນ',
pluginsMarketHint: 'ປ້ອນຄຳຄົ້ນເພື່ອຊອກຫາປລັກອິນ',
pluginSearchPlaceholder: 'ຊອກຫາປລັກອິນ...',
pluginOrigin: 'ແຫຼ່ງທີ່ມາ',
pluginOriginAll: 'ທຸກແຫຼ່ງທີ່ມາ',
pluginOriginBundled: 'ມາພ້ອມລະບົບ',
pluginOriginExternal: 'ພາຍນອກ',
pluginStatusAll: 'ທຸກສະຖານະ',
pluginEnabled: 'ເປີດໃຊ້ແລ້ວ',
pluginDisabled: 'ປິດໃຊ້ແລ້ວ',
pluginCategory: 'ໝວດໝູ່',
pluginVerification: 'ການຢືນຢັນ',
pluginOfficial: 'ທາງການ',
pluginCommunity: 'ຊຸມຊົນ',
pluginDownloads: 'ດາວໂຫຼດ',
pluginEnable: 'ເປີດໃຊ້',
pluginDisable: 'ປິດໃຊ້',
pluginListEmpty: 'ບໍ່ມີປລັກອິນ',
pluginSearchEmpty: 'ບໍ່ພົບປລັກອິນທີ່ກົງກັນ',
pluginInstallConfirm: 'ຢືນຢັນຕິດຕັ້ງປລັກອິນ {0} ບໍ?',
pluginDisableConfirm: 'ຢືນຢັນປິດໃຊ້ປລັກອິນ {0} ບໍ?',
pluginUninstallConfirm: 'ຢືນຢັນຖອນການຕິດຕັ້ງປລັກອິນ {0} ບໍ?',
securityTab: 'ຄວາມປອດໄພ',
otherTab: 'ອື່ນໆ',
timeZone: 'ເຂດເວລາ',
+24
View File
@@ -755,6 +755,30 @@ const message = {
verified: 'Disahkan',
verifySkipped: 'Tanpa pengesahan',
skillsTab: 'Kemahiran',
pluginsTab: 'Pemalam',
pluginsInstalled: 'Dipasang',
pluginsMarket: 'Pasaran pemalam',
pluginsMarketHint: 'Masukkan kata kunci untuk mencari pemalam',
pluginSearchPlaceholder: 'Cari pemalam...',
pluginOrigin: 'Sumber',
pluginOriginAll: 'Semua sumber',
pluginOriginBundled: 'Terbina dalam',
pluginOriginExternal: 'Luaran',
pluginStatusAll: 'Semua status',
pluginEnabled: 'Diaktifkan',
pluginDisabled: 'Dinyahaktifkan',
pluginCategory: 'Kategori',
pluginVerification: 'Pengesahan',
pluginOfficial: 'Rasmi',
pluginCommunity: 'Komuniti',
pluginDownloads: 'Muat turun',
pluginEnable: 'Aktifkan',
pluginDisable: 'Nyahaktifkan',
pluginListEmpty: 'Tiada pemalam',
pluginSearchEmpty: 'Tiada pemalam sepadan ditemui',
pluginInstallConfirm: 'Pasang pemalam {0}?',
pluginDisableConfirm: 'Nyahaktifkan pemalam {0}?',
pluginUninstallConfirm: 'Nyahpasang pemalam {0}?',
securityTab: 'Keselamatan',
otherTab: 'Lain-lain',
timeZone: 'Zon Waktu',
+24
View File
@@ -749,6 +749,30 @@ const message = {
verified: 'Verificado',
verifySkipped: 'Sem verificação',
skillsTab: 'Habilidades',
pluginsTab: 'Plugins',
pluginsInstalled: 'Instalados',
pluginsMarket: 'Mercado de plugins',
pluginsMarketHint: 'Digite uma palavra-chave para pesquisar plugins',
pluginSearchPlaceholder: 'Pesquisar plugins...',
pluginOrigin: 'Origem',
pluginOriginAll: 'Todas as origens',
pluginOriginBundled: 'Integrado',
pluginOriginExternal: 'Externo',
pluginStatusAll: 'Todos os estados',
pluginEnabled: 'Ativado',
pluginDisabled: 'Desativado',
pluginCategory: 'Categoria',
pluginVerification: 'Verificação',
pluginOfficial: 'Oficial',
pluginCommunity: 'Comunidade',
pluginDownloads: 'Downloads',
pluginEnable: 'Ativar',
pluginDisable: 'Desativar',
pluginListEmpty: 'Nenhum plugin',
pluginSearchEmpty: 'Nenhum plugin correspondente encontrado',
pluginInstallConfirm: 'Instalar o plugin {0}?',
pluginDisableConfirm: 'Desativar o plugin {0}?',
pluginUninstallConfirm: 'Desinstalar o plugin {0}?',
securityTab: 'Segurança',
otherTab: 'Outros',
timeZone: 'Fuso horário',
+24
View File
@@ -745,6 +745,30 @@ const message = {
verified: 'Проверено',
verifySkipped: 'Без проверки',
skillsTab: 'Навыки',
pluginsTab: 'Плагины',
pluginsInstalled: 'Установленные',
pluginsMarket: 'Каталог плагинов',
pluginsMarketHint: 'Введите ключевое слово для поиска плагинов',
pluginSearchPlaceholder: 'Поиск плагинов...',
pluginOrigin: 'Источник',
pluginOriginAll: 'Все источники',
pluginOriginBundled: 'Встроенный',
pluginOriginExternal: 'Внешний',
pluginStatusAll: 'Все статусы',
pluginEnabled: 'Включён',
pluginDisabled: 'Отключён',
pluginCategory: 'Категория',
pluginVerification: 'Проверка',
pluginOfficial: 'Официальный',
pluginCommunity: 'Сообщество',
pluginDownloads: 'Загрузки',
pluginEnable: 'Включить',
pluginDisable: 'Отключить',
pluginListEmpty: 'Плагины отсутствуют',
pluginSearchEmpty: 'Подходящие плагины не найдены',
pluginInstallConfirm: 'Установить плагин {0}?',
pluginDisableConfirm: 'Отключить плагин {0}?',
pluginUninstallConfirm: 'Удалить плагин {0}?',
securityTab: 'Безопасность',
otherTab: 'Другое',
timeZone: 'Часовой пояс',
+24
View File
@@ -751,6 +751,30 @@ const message = {
verified: 'Doğrulandı',
verifySkipped: 'Doğrulama yok',
skillsTab: 'Yetenekler',
pluginsTab: 'Eklentiler',
pluginsInstalled: 'Yüklü',
pluginsMarket: 'Eklenti mağazası',
pluginsMarketHint: 'Eklenti aramak için bir anahtar kelime girin',
pluginSearchPlaceholder: 'Eklenti ara...',
pluginOrigin: 'Kaynak',
pluginOriginAll: 'Tüm kaynaklar',
pluginOriginBundled: 'Yerleşik',
pluginOriginExternal: 'Harici',
pluginStatusAll: 'Tüm durumlar',
pluginEnabled: 'Etkin',
pluginDisabled: 'Devre dışı',
pluginCategory: 'Kategori',
pluginVerification: 'Doğrulama',
pluginOfficial: 'Resmî',
pluginCommunity: 'Topluluk',
pluginDownloads: 'İndirmeler',
pluginEnable: 'Etkinleştir',
pluginDisable: 'Devre dışı bırak',
pluginListEmpty: 'Eklenti yok',
pluginSearchEmpty: 'Eşleşen eklenti bulunamadı',
pluginInstallConfirm: '{0} eklentisi yüklensin mi?',
pluginDisableConfirm: '{0} eklentisi devre dışı bırakılsın mı?',
pluginUninstallConfirm: '{0} eklentisi kaldırılsın mı?',
securityTab: 'Güvenlik',
otherTab: 'Diğer',
timeZone: 'Saat Dilimi',
+24
View File
@@ -714,6 +714,30 @@ const message = {
verified: '驗證狀態',
verifySkipped: '不驗證',
skillsTab: '技能',
pluginsTab: '外掛程式',
pluginsInstalled: '已安裝',
pluginsMarket: '外掛程式市集',
pluginsMarketHint: '請輸入關鍵字搜尋外掛程式',
pluginSearchPlaceholder: '搜尋外掛程式...',
pluginOrigin: '來源',
pluginOriginAll: '全部來源',
pluginOriginBundled: '內建',
pluginOriginExternal: '外部',
pluginStatusAll: '全部狀態',
pluginEnabled: '已啟用',
pluginDisabled: '已停用',
pluginCategory: '分類',
pluginVerification: '驗證',
pluginOfficial: '官方',
pluginCommunity: '社群',
pluginDownloads: '下載量',
pluginEnable: '啟用',
pluginDisable: '停用',
pluginListEmpty: '暫無外掛程式',
pluginSearchEmpty: '找不到相關外掛程式',
pluginInstallConfirm: '確認安裝外掛程式 {0}',
pluginDisableConfirm: '確認停用外掛程式 {0}',
pluginUninstallConfirm: '確認解除安裝外掛程式 {0}',
securityTab: '安全',
otherTab: '其他',
timeZone: '時區',
+24
View File
@@ -717,6 +717,30 @@ const message = {
verified: '验证状态',
verifySkipped: '不验证',
skillsTab: '技能',
pluginsTab: '插件',
pluginsInstalled: '已安装',
pluginsMarket: '插件市场',
pluginsMarketHint: '请输入关键词搜索插件',
pluginSearchPlaceholder: '搜索插件...',
pluginOrigin: '来源',
pluginOriginAll: '全部来源',
pluginOriginBundled: '内置',
pluginOriginExternal: '外部',
pluginStatusAll: '全部状态',
pluginEnabled: '已启用',
pluginDisabled: '已禁用',
pluginCategory: '分类',
pluginVerification: '验证',
pluginOfficial: '官方',
pluginCommunity: '社区',
pluginDownloads: '下载量',
pluginEnable: '启用',
pluginDisable: '禁用',
pluginListEmpty: '暂无插件',
pluginSearchEmpty: '未找到相关插件',
pluginInstallConfirm: '确认安装插件 {0}',
pluginDisableConfirm: '确认禁用插件 {0}',
pluginUninstallConfirm: '确认卸载插件 {0}',
securityTab: '安全',
otherTab: '其他',
timeZone: '时区',
@@ -22,6 +22,9 @@
>
<SkillsTab ref="skillsRef" :app-version="appVersion" :agent-type="agentType" />
</el-tab-pane>
<el-tab-pane v-if="agentType === 'openclaw'" :label="t('aiTools.agents.pluginsTab')" name="plugins">
<PluginsTab ref="pluginsRef" />
</el-tab-pane>
<el-tab-pane
v-if="agentType === 'openclaw' || agentType === 'hermes-agent'"
:label="t('file.setting')"
@@ -43,6 +46,7 @@ import ChannelsTab from './tabs/channels.vue';
import ModelTab from './tabs/model.vue';
import AgentTab from './tabs/agents/index.vue';
import SkillsTab from './tabs/skills.vue';
import PluginsTab from './tabs/plugins.vue';
import SettingsTab from './tabs/settings.vue';
const { t } = useI18n();
@@ -60,6 +64,7 @@ const channelsRef = ref();
const modelRef = ref();
const agentRef = ref();
const skillsRef = ref();
const pluginsRef = ref();
const settingsRef = ref();
const loadSettings = async () => {
@@ -115,6 +120,14 @@ const loadSkills = async () => {
await skillsRef.value?.load(agentId.value);
};
const loadPlugins = async () => {
if (agentId.value <= 0) {
return;
}
await nextTick();
await pluginsRef.value?.load(agentId.value);
};
const handleClose = () => {
activeTab.value = 'channels';
};
@@ -129,6 +142,9 @@ const handleTabClick = async (pane: TabsPaneContext) => {
if (pane.paneName === 'skills') {
await loadSkills();
}
if (pane.paneName === 'plugins') {
await loadPlugins();
}
if (pane.paneName === 'agent') {
await loadAgent();
}
@@ -0,0 +1,357 @@
<template>
<el-radio-group v-model="mode" class="view-switch">
<el-radio-button label="installed">{{ t('aiTools.agents.pluginsInstalled') }}</el-radio-button>
<el-radio-button label="market">{{ t('aiTools.agents.pluginsMarket') }}</el-radio-button>
</el-radio-group>
<div v-loading="loading" class="plugin-content">
<div class="toolbar">
<template v-if="mode === 'installed'">
<el-input
v-model="keyword"
:placeholder="t('aiTools.agents.pluginSearchPlaceholder')"
clearable
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select v-model="origin" class="filter-select">
<el-option :label="t('aiTools.agents.pluginOriginAll')" value="" />
<el-option :label="t('aiTools.agents.pluginOriginBundled')" value="bundled" />
<el-option :label="t('aiTools.agents.pluginOriginExternal')" value="external" />
</el-select>
<el-select v-model="status" class="filter-select">
<el-option :label="t('aiTools.agents.pluginStatusAll')" value="" />
<el-option :label="t('aiTools.agents.pluginEnabled')" value="enabled" />
<el-option :label="t('aiTools.agents.pluginDisabled')" value="disabled" />
</el-select>
<el-button :loading="loading" @click="loadPlugins">
<el-icon><Refresh /></el-icon>
</el-button>
</template>
<template v-else>
<el-input
v-model="marketKeyword"
:placeholder="t('aiTools.agents.pluginSearchPlaceholder')"
clearable
class="search-input"
@keyup.enter="searchMarket"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" :loading="searching" @click="searchMarket">
{{ t('commons.button.search') }}
</el-button>
</template>
</div>
<el-table v-if="mode === 'installed'" :data="pagedPlugins">
<el-table-column :label="t('commons.table.name')" min-width="210">
<template #default="{ row }">
<div class="plugin-name">{{ row.name || row.id }}</div>
<div class="secondary">{{ row.id }}</div>
</template>
</el-table-column>
<el-table-column :label="t('app.version')" min-width="110">
<template #default="{ row }">{{ row.version || '-' }}</template>
</el-table-column>
<el-table-column :label="t('aiTools.agents.pluginOrigin')" min-width="110">
<template #default="{ row }">
<el-tag size="small" effect="plain">
{{
row.origin === 'bundled'
? t('aiTools.agents.pluginOriginBundled')
: t('aiTools.agents.pluginOriginExternal')
}}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('commons.table.status')" min-width="120">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
{{ row.enabled ? t('aiTools.agents.pluginEnabled') : t('aiTools.agents.pluginDisabled') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('commons.table.operate')" fixed="right" min-width="210">
<template #default="{ row }">
<el-button
v-permission
type="primary"
link
:disabled="operating !== ''"
@click="operate(row, row.enabled ? 'disable' : 'enable')"
>
{{ row.enabled ? t('aiTools.agents.pluginDisable') : t('aiTools.agents.pluginEnable') }}
</el-button>
<el-button
v-if="row.origin !== 'bundled'"
v-permission
type="primary"
link
:disabled="operating !== ''"
@click="operate(row, 'update')"
>
{{ t('commons.button.upgrade') }}
</el-button>
<el-button
v-if="row.origin !== 'bundled'"
v-permission
type="danger"
link
:disabled="operating !== ''"
@click="operate(row, 'uninstall')"
>
{{ t('commons.button.uninstall') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-table v-else :data="marketResults">
<el-table-column :label="t('commons.table.name')" min-width="260">
<template #default="{ row }">
<div class="plugin-name">{{ row.name || row.package }}</div>
<div class="secondary">{{ row.package }}</div>
</template>
</el-table-column>
<el-table-column :label="t('app.version')" prop="version" min-width="110" />
<el-table-column :label="t('aiTools.agents.pluginCategory')" min-width="150">
<template #default="{ row }">
<el-space wrap>
<el-tag v-for="item in row.categories" :key="item" size="small" type="info">
{{ item }}
</el-tag>
</el-space>
</template>
</el-table-column>
<el-table-column :label="t('aiTools.agents.pluginVerification')" min-width="130">
<template #default="{ row }">
<el-tag :type="row.official ? 'success' : 'info'" size="small" effect="plain">
{{ row.official ? t('aiTools.agents.pluginOfficial') : t('aiTools.agents.pluginCommunity') }}
</el-tag>
<div class="secondary">{{ row.verificationTier || '-' }}</div>
</template>
</el-table-column>
<el-table-column :label="t('aiTools.agents.pluginDownloads')" prop="downloads" min-width="110" />
<el-table-column :label="t('commons.table.operate')" fixed="right" width="100">
<template #default="{ row }">
<el-button v-permission type="primary" link :disabled="operating !== ''" @click="install(row)">
{{ t('commons.button.install') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="mode === 'installed' && !loading && filteredPlugins.length === 0"
:description="t('aiTools.agents.pluginListEmpty')"
/>
<el-empty
v-if="mode === 'market' && !searching && marketResults.length === 0"
:description="
marketSearched ? t('aiTools.agents.pluginSearchEmpty') : t('aiTools.agents.pluginsMarketHint')
"
/>
<div v-if="mode === 'installed' && filteredPlugins.length > pageSize" class="pagination">
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="filteredPlugins.length"
layout="total, prev, pager, next"
/>
</div>
</div>
<TaskLog ref="taskLogRef" @close="loadPlugins" />
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { installAgentMarketPlugin, listAgentPlugins, operateAgentPlugin, searchAgentPlugins } from '@/api/modules/ai';
import { newUUID } from '@/utils/id';
import TaskLog from '@/components/log/task/index.vue';
type PluginMode = 'installed' | 'market';
type PluginOperate = AI.AgentPluginOperateReq['operate'];
const { t } = useI18n();
const mode = ref<PluginMode>('installed');
const agentId = ref(0);
const loading = ref(false);
const searching = ref(false);
const operating = ref('');
const keyword = ref('');
const marketKeyword = ref('');
const origin = ref('');
const status = ref('');
const page = ref(1);
const pageSize = 10;
const plugins = ref<AI.AgentPluginItem[]>([]);
const marketResults = ref<AI.AgentPluginSearchItem[]>([]);
const marketSearched = ref(false);
const taskLogRef = ref<InstanceType<typeof TaskLog>>();
const filteredPlugins = computed(() => {
const search = keyword.value.trim().toLowerCase();
return plugins.value.filter((plugin) => {
const matchesKeyword =
!search || plugin.name.toLowerCase().includes(search) || plugin.id.toLowerCase().includes(search);
const matchesOrigin =
!origin.value ||
(origin.value === 'external' ? plugin.origin !== 'bundled' : plugin.origin === origin.value);
const matchesStatus = !status.value || (status.value === 'enabled' ? plugin.enabled : !plugin.enabled);
return matchesKeyword && matchesOrigin && matchesStatus;
});
});
const pagedPlugins = computed(() => {
const start = (page.value - 1) * pageSize;
return filteredPlugins.value.slice(start, start + pageSize);
});
watch([keyword, origin, status], () => {
page.value = 1;
});
async function loadPlugins() {
if (!agentId.value) {
return;
}
loading.value = true;
try {
const res = await listAgentPlugins({ agentId: agentId.value });
plugins.value = res.data || [];
} finally {
loading.value = false;
operating.value = '';
}
}
async function searchMarket() {
const search = marketKeyword.value.trim();
if (!search) {
return;
}
searching.value = true;
try {
const res = await searchAgentPlugins({ agentId: agentId.value, keyword: search, limit: 20 });
marketResults.value = res.data || [];
marketSearched.value = true;
} finally {
searching.value = false;
}
}
async function install(plugin: AI.AgentPluginSearchItem) {
await ElMessageBox.confirm(
t('aiTools.agents.pluginInstallConfirm', [plugin.name || plugin.package]),
t('commons.button.install'),
{ type: 'warning' },
);
const taskID = newUUID();
operating.value = plugin.package;
try {
await installAgentMarketPlugin({
agentId: agentId.value,
package: plugin.package,
version: plugin.version,
taskID,
});
taskLogRef.value?.openWithTaskID(taskID);
} finally {
operating.value = '';
}
}
async function operate(plugin: AI.AgentPluginItem, action: PluginOperate) {
if (action === 'disable' || action === 'uninstall') {
const key =
action === 'disable' ? 'aiTools.agents.pluginDisableConfirm' : 'aiTools.agents.pluginUninstallConfirm';
await ElMessageBox.confirm(t(key, [plugin.name || plugin.id]), t('commons.button.confirm'), {
type: 'warning',
});
}
const taskID = newUUID();
operating.value = plugin.id;
try {
await operateAgentPlugin({
agentId: agentId.value,
pluginId: plugin.id,
operate: action,
taskID,
});
taskLogRef.value?.openWithTaskID(taskID);
} finally {
operating.value = '';
}
}
async function load(id: number) {
agentId.value = id;
mode.value = 'installed';
keyword.value = '';
marketKeyword.value = '';
origin.value = '';
status.value = '';
page.value = 1;
marketResults.value = [];
marketSearched.value = false;
await loadPlugins();
}
defineExpose({ load });
</script>
<style scoped lang="scss">
.view-switch {
padding-top: 1px;
margin-bottom: 16px;
}
.plugin-content {
min-height: 390px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.search-input {
width: 280px;
max-width: 100%;
}
.filter-select {
width: 150px;
}
.plugin-name {
color: var(--el-text-color-primary);
font-weight: 500;
}
.secondary {
margin-top: 2px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>