diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index c491cc9ff..77cc48878 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -27,6 +27,69 @@ func (b *BaseApi) CreateAgent(c *gin.Context) { helper.SuccessWithData(c, res) } +// @Tags AI +// @Summary Batch install Agent +// @Accept json +// @Param request body dto.AgentBatchInstallReq true "request" +// @Success 200 {object} dto.AgentItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/batch/install [post] +func (b *BaseApi) BatchInstallAgent(c *gin.Context) { + var req dto.AgentBatchInstallReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := agentService.BatchInstall(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags AI +// @Summary Batch upgrade Agent +// @Accept json +// @Param request body dto.AgentBatchUpgradeReq true "request" +// @Success 200 {array} dto.AgentBatchUpgradeResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/batch/upgrade [post] +func (b *BaseApi) BatchUpgradeAgent(c *gin.Context) { + var req dto.AgentBatchUpgradeReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := agentService.BatchUpgrade(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags AI +// @Summary Batch install Agent Skill +// @Accept json +// @Param request body dto.AgentBatchSkillInstallReq true "request" +// @Success 200 {array} dto.AgentBatchSkillInstallResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/batch/skill/install [post] +func (b *BaseApi) BatchInstallAgentSkill(c *gin.Context) { + var req dto.AgentBatchSkillInstallReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := agentService.BatchInstallSkill(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + // @Tags AI // @Summary Page Agents // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index c5c263c7a..23ec5b573 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -27,6 +27,68 @@ type AgentCreateReq struct { DockerCompose string `json:"dockerCompose"` } +type AgentBatchInstallReq struct { + Name string `json:"name"` + Remark string `json:"remark"` + AppVersion string `json:"appVersion" validate:"required"` + WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` + BridgePort int `json:"bridgePort"` + AllowedOrigins []string `json:"allowedOrigins"` + AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` + Model string `json:"model"` + AccountID uint `json:"accountId"` + Token string `json:"token"` + TaskID string `json:"taskID"` + Advanced bool `json:"advanced"` + ContainerName string `json:"containerName"` + AllowPort bool `json:"allowPort"` + SpecifyIP string `json:"specifyIP"` + RestartPolicy string `json:"restartPolicy"` + CpuQuota float64 `json:"cpuQuota"` + MemoryLimit float64 `json:"memoryLimit"` + MemoryUnit string `json:"memoryUnit"` + PullImage bool `json:"pullImage"` + EditCompose bool `json:"editCompose"` + DockerCompose string `json:"dockerCompose"` + MasterAccountID uint `json:"masterAccountId"` + FallbackAccessHost string `json:"fallbackAccessHost"` + AccountSnapshot AgentAccountInfo `json:"accountSnapshot"` + AccountModels []AgentAccountModel `json:"accountModels"` +} + +type AgentBatchUpgradeReq struct { + AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` + TargetVersion string `json:"targetVersion" validate:"required"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + TaskID string `json:"taskID"` +} + +type AgentBatchUpgradeResult struct { + AgentID uint `json:"agentID"` + AgentName string `json:"agentName"` + AppInstallID uint `json:"appInstallID"` + Success bool `json:"success"` + Skipped bool `json:"skipped"` + Message string `json:"message"` +} + +type AgentBatchSkillInstallReq struct { + AgentType string `json:"agentType" validate:"required,oneof=openclaw hermes-agent"` + SkillName string `json:"skillName" validate:"required"` + PackagePath string `json:"packagePath" validate:"required"` + TaskID string `json:"taskID"` +} + +type AgentBatchSkillInstallResult struct { + AgentID uint `json:"agentID"` + AgentName string `json:"agentName"` + AppInstallID uint `json:"appInstallID"` + Success bool `json:"success"` + Skipped bool `json:"skipped"` + Message string `json:"message"` +} + type AgentItem struct { ID uint `json:"id"` Name string `json:"name"` @@ -279,18 +341,19 @@ type AgentAccountProviderCountReq struct { } type AgentAccountInfo struct { - ID uint `json:"id"` - Provider string `json:"provider"` - ProviderName string `json:"providerName"` - Name string `json:"name"` - APIKey string `json:"apiKey"` - RememberAPIKey bool `json:"rememberApiKey"` - BaseURL string `json:"baseUrl"` - Models []AgentAccountModel `json:"models"` - APIType string `json:"apiType"` - Verified bool `json:"verified"` - Remark string `json:"remark"` - CreatedAt time.Time `json:"createdAt"` + ID uint `json:"id"` + MasterAccountID uint `json:"masterAccountId"` + Provider string `json:"provider"` + ProviderName string `json:"providerName"` + Name string `json:"name"` + APIKey string `json:"apiKey"` + RememberAPIKey bool `json:"rememberApiKey"` + BaseURL string `json:"baseUrl"` + Models []AgentAccountModel `json:"models"` + APIType string `json:"apiType"` + Verified bool `json:"verified"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"createdAt"` } type ProviderModelInfo struct { diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go index 463e76edd..2f499f7a4 100644 --- a/agent/app/model/agent_account.go +++ b/agent/app/model/agent_account.go @@ -2,14 +2,15 @@ package model type AgentAccount struct { BaseModel - Provider string `json:"provider"` - Name string `json:"name"` - APIKey string `json:"apiKey"` - BaseURL string `json:"baseUrl"` - APIType string `json:"apiType"` - RememberAPIKey bool `json:"rememberApiKey"` - Verified bool `json:"verified"` - Remark string `json:"remark"` + Provider string `json:"provider"` + Name string `json:"name"` + APIKey string `json:"apiKey"` + BaseURL string `json:"baseUrl"` + APIType string `json:"apiType"` + RememberAPIKey bool `json:"rememberApiKey"` + Verified bool `json:"verified"` + Remark string `json:"remark"` + MasterAccountID uint `json:"masterAccountId" gorm:"index"` } func (AgentAccount) TableName() string { diff --git a/agent/app/repo/agent_account.go b/agent/app/repo/agent_account.go index 609a3947b..f691f58ea 100644 --- a/agent/app/repo/agent_account.go +++ b/agent/app/repo/agent_account.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" ) type AgentAccountRepo struct{} @@ -11,6 +12,7 @@ type AgentAccountRepo struct{} type IAgentAccountRepo interface { Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error) GetFirst(opts ...DBOption) (*model.AgentAccount, error) + WithByMasterAccountID(masterID uint) DBOption Create(account *model.AgentAccount) error Save(account *model.AgentAccount) error DeleteByID(id uint) error @@ -39,6 +41,12 @@ func (a AgentAccountRepo) GetFirst(opts ...DBOption) (*model.AgentAccount, error return &account, nil } +func (a AgentAccountRepo) WithByMasterAccountID(masterID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("master_account_id = ?", masterID) + } +} + func (a AgentAccountRepo) Create(account *model.AgentAccount) error { return getDb().Create(account).Error } diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index fb63d2005..18dc5f2e6 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "os" "path" @@ -16,6 +17,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/model" providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" @@ -30,6 +32,9 @@ import ( type IAgentService interface { Create(req dto.AgentCreateReq) (*dto.AgentItem, error) + BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) + BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) + BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) Delete(req dto.AgentDeleteReq) error @@ -99,6 +104,11 @@ type IAgentService interface { ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error } +type batchUpgradePlan struct { + agent model.Agent + req request.AppInstallUpgrade +} + const ( defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser" defaultBrowserProfile = "openclaw" @@ -291,6 +301,343 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { return &item, nil } +func (a AgentService) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) { + accountID, err := a.ensureBatchInstallAccount(req) + if err != nil { + return nil, err + } + createReq := buildCreateReqFromBatchInstallReq(req) + createReq.AccountID = accountID + if strings.TrimSpace(createReq.Name) == "" { + name, err := buildBatchInstallAgentName(createReq.AgentType) + if err != nil { + return nil, err + } + createReq.Name = name + } + if createReq.AgentType == constant.AppOpenclaw && len(createReq.AllowedOrigins) == 0 { + allowedOrigin, err := buildBatchInstallOpenclawAllowedOrigin(req) + if err != nil { + return nil, err + } + createReq.AllowedOrigins = []string{allowedOrigin} + } + return a.Create(createReq) +} + +func (a AgentService) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) { + plans, results, err := buildBatchUpgradePlans(req) + if err != nil { + return nil, err + } + for _, plan := range plans { + result := dto.AgentBatchUpgradeResult{ + AgentID: plan.agent.ID, + AgentName: plan.agent.Name, + AppInstallID: plan.agent.AppInstallID, + } + if err := upgradeInstall(plan.req); err != nil { + result.Message = err.Error() + } else { + result.Success = true + } + results = append(results, result) + } + return results, nil +} + +func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) { + if req.AgentType != constant.AppOpenclaw && req.AgentType != constant.AppHermesAgent { + return nil, fmt.Errorf("%s does not support skill batch install", req.AgentType) + } + packagePath, err := validateLocalSkillPackagePath(req.PackagePath) + if err != nil { + return nil, err + } + if format, err := localSkillPackageFormat(packagePath); err != nil { + return nil, err + } else if format != "zip" { + return nil, fmt.Errorf("only .zip skill packages can be installed") + } + skillName := sanitizeLocalSkillDirName(req.SkillName, packagePath, req.SkillName) + agents, err := agentRepo.List(func(db *gorm.DB) *gorm.DB { + return db.Where("agent_type = ?", req.AgentType).Order("id ASC") + }) + if err != nil { + return nil, err + } + results := make([]dto.AgentBatchSkillInstallResult, 0, len(agents)) + for _, agent := range agents { + result := dto.AgentBatchSkillInstallResult{ + AgentID: agent.ID, + AgentName: agent.Name, + AppInstallID: agent.AppInstallID, + } + if agent.AppInstallID == 0 { + result.Message = "agent app install id is empty" + results = append(results, result) + continue + } + install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) + if err != nil { + result.Message = err.Error() + results = append(results, result) + continue + } + result.AppInstallID = install.ID + if install.App.Key != req.AgentType { + result.Message = fmt.Sprintf("app key %s does not match agent type %s", install.App.Key, req.AgentType) + results = append(results, result) + continue + } + if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading { + result.Message = fmt.Sprintf("agent status is %s", install.Status) + results = append(results, result) + continue + } + if err := ensureContainerRunning(install.ContainerName); err != nil { + result.Message = err.Error() + results = append(results, result) + continue + } + installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID) + if err != nil { + result.Message = err.Error() + results = append(results, result) + continue + } + currentAgent := agent + currentInstall := install + installTask.AddSubTask("Install local skill", func(t *task.Task) error { + mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute)) + return installLocalSkillPackage(mgr, currentInstall.ContainerName, currentAgent.AgentType, currentAgent.ConfigPath, packagePath, skillName) + }, nil) + go func() { + if err := installTask.Execute(); err != nil { + global.LOG.Errorf("batch install local skill failed: %v", err) + } + }() + result.Success = true + results = append(results, result) + } + return results, nil +} + +func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, []dto.AgentBatchUpgradeResult, error) { + agents, err := agentRepo.List(func(db *gorm.DB) *gorm.DB { + return db.Where("agent_type = ?", req.AgentType).Order("id ASC") + }) + if err != nil { + return nil, nil, err + } + plans := make([]batchUpgradePlan, 0, len(agents)) + results := make([]dto.AgentBatchUpgradeResult, 0) + for _, agent := range agents { + result := dto.AgentBatchUpgradeResult{ + AgentID: agent.ID, + AgentName: agent.Name, + AppInstallID: agent.AppInstallID, + } + if agent.AppInstallID == 0 { + result.Message = "agent app install id is empty" + results = append(results, result) + continue + } + install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) + if err != nil { + result.Message = err.Error() + results = append(results, result) + continue + } + result.AppInstallID = install.ID + if install.App.Key != req.AgentType { + result.Message = fmt.Sprintf("app key %s does not match agent type %s", install.App.Key, req.AgentType) + results = append(results, result) + continue + } + if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading { + result.Message = fmt.Sprintf("agent status is %s", install.Status) + results = append(results, result) + continue + } + if install.Version == req.TargetVersion { + result.Success = true + result.Skipped = true + result.Message = "already target version" + results = append(results, result) + continue + } + detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(install.AppId), appDetailRepo.WithVersion(req.TargetVersion)) + if err != nil { + result.Message = err.Error() + results = append(results, result) + continue + } + if detail.ID == 0 { + result.Message = fmt.Sprintf("target version %s not found", req.TargetVersion) + results = append(results, result) + continue + } + plans = append(plans, batchUpgradePlan{ + agent: agent, + req: request.AppInstallUpgrade{ + InstallID: install.ID, + DetailID: detail.ID, + Backup: req.Backup, + PullImage: req.PullImage, + TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID), + }, + }) + } + return plans, results, nil +} + +func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string { + taskID = strings.TrimSpace(taskID) + if taskID == "" { + taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano()) + } + return fmt.Sprintf("%s-%d", taskID, appInstallID) +} + +func buildBatchSkillInstallTaskID(taskID string, agentID uint) string { + taskID = strings.TrimSpace(taskID) + if taskID == "" { + taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano()) + } + return fmt.Sprintf("%s-%d", taskID, agentID) +} + +func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq { + return dto.AgentCreateReq{ + Name: req.Name, + Remark: req.Remark, + AppVersion: req.AppVersion, + WebUIPort: req.WebUIPort, + BridgePort: req.BridgePort, + AllowedOrigins: req.AllowedOrigins, + AgentType: req.AgentType, + Model: req.Model, + AccountID: req.AccountID, + Token: req.Token, + TaskID: req.TaskID, + Advanced: req.Advanced, + ContainerName: req.ContainerName, + AllowPort: req.AllowPort, + SpecifyIP: req.SpecifyIP, + RestartPolicy: req.RestartPolicy, + CpuQuota: req.CpuQuota, + MemoryLimit: req.MemoryLimit, + MemoryUnit: req.MemoryUnit, + PullImage: req.PullImage, + EditCompose: req.EditCompose, + DockerCompose: req.DockerCompose, + } +} + +func buildBatchInstallAgentName(agentType string) (string, error) { + prefix := strings.ToLower(strings.TrimSpace(agentType)) + if prefix == "" { + prefix = "agent" + } + for i := 1; i <= 10000; i++ { + name := fmt.Sprintf("%s-%d", prefix, i) + exists, err := agentNameExists(name) + if err != nil { + return "", err + } + if !exists { + return name, nil + } + } + return "", fmt.Errorf("no available agent name") +} + +func agentNameExists(name string) (bool, error) { + if exist, err := agentRepo.GetFirst(repo.WithByLowerName(name)); err == nil && exist != nil && exist.ID > 0 { + return true, nil + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return false, err + } + installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(name)) + if err != nil { + return false, err + } + return len(installs) > 0, nil +} + +func buildBatchInstallOpenclawAllowedOrigin(req dto.AgentBatchInstallReq) (string, error) { + host := "" + if value, err := settingRepo.GetValueByKey("SystemIP"); err == nil { + host = strings.TrimSpace(value) + } + if host == "" { + host = strings.TrimSpace(req.FallbackAccessHost) + } + if host == "" { + host = "127.0.0.1" + } + return buildOpenclawAllowedOrigin(openclawAllowedOriginScheme(req.AppVersion), host, req.WebUIPort) +} + +func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (uint, error) { + if !requiresAgentAccount(req.AgentType) { + return 0, nil + } + if req.MasterAccountID == 0 { + return 0, buserr.New("ErrInvalidParams") + } + snapshot := req.AccountSnapshot + account, err := agentAccountRepo.GetFirst(agentAccountRepo.WithByMasterAccountID(req.MasterAccountID)) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return 0, err + } + if account == nil && global.IsMaster { + account, err = agentAccountRepo.GetFirst(repo.WithByID(req.MasterAccountID)) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return 0, err + } + } + if account == nil { + account = &model.AgentAccount{ + MasterAccountID: req.MasterAccountID, + } + } + account.MasterAccountID = req.MasterAccountID + account.Provider = snapshot.Provider + account.Name = snapshot.Name + account.APIKey = snapshot.APIKey + account.RememberAPIKey = snapshot.RememberAPIKey + account.BaseURL = snapshot.BaseURL + account.APIType = snapshot.APIType + account.Remark = snapshot.Remark + account.Verified = true + + initialModels, err := buildInitialAgentAccountModels(account, req.AccountModels) + if err != nil { + return 0, err + } + if err := global.DB.Transaction(func(tx *gorm.DB) error { + if account.ID == 0 { + if err := tx.Create(account).Error; err != nil { + return err + } + } else { + if err := tx.Save(account).Error; err != nil { + return err + } + } + return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels) + }); err != nil { + return 0, err + } + return account.ID, nil +} + +func requiresAgentAccount(agentType string) bool { + return agentType != constant.AppCopaw +} + func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) { if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) { params["PANEL_APP_PORT_HTTPS"] = webUIPort @@ -653,18 +1000,19 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age apiKey = item.APIKey } items = append(items, dto.AgentAccountInfo{ - ID: item.ID, - Provider: item.Provider, - ProviderName: providercatalog.DisplayName(item.Provider), - Name: item.Name, - APIKey: apiKey, - RememberAPIKey: item.RememberAPIKey, - BaseURL: item.BaseURL, - Models: nil, - APIType: item.APIType, - Verified: item.Verified, - Remark: item.Remark, - CreatedAt: item.CreatedAt, + ID: item.ID, + MasterAccountID: item.MasterAccountID, + Provider: item.Provider, + ProviderName: providercatalog.DisplayName(item.Provider), + Name: item.Name, + APIKey: apiKey, + RememberAPIKey: item.RememberAPIKey, + BaseURL: item.BaseURL, + Models: nil, + APIType: item.APIType, + Verified: item.Verified, + Remark: item.Remark, + CreatedAt: item.CreatedAt, }) } for i := range items { diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index 7cfc60264..905afc190 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -912,7 +912,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error { } } - upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskScopeApp, task.TaskUpgrade), upgradeApp, rollBackApp, 0, 1*time.Hour) + upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour) go func() { err = upgradeTask.Execute() diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 45344140b..305da3181 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -75,6 +75,7 @@ func InitAgentDB() { migrations.NormalizeAgentAccountVerifiedStatus, migrations.NormalizeOllamaAccountAPIType, migrations.InitAgentAccountModelPool, + migrations.AddAgentAccountMasterID, migrations.AddHostTable, migrations.AddAITerminalSettings, migrations.UpdateAgentQuickJumpTitle, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index dac29c8dd..8119a9817 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -1118,6 +1118,13 @@ var InitAgentAccountModelPool = &gormigrate.Migration{ }, } +var AddAgentAccountMasterID = &gormigrate.Migration{ + ID: "20260401-add-agent-account-master-id", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate(&model.AgentAccount{}) + }, +} + var AddHostTable = &gormigrate.Migration{ ID: "20260318-add-host-table", Migrate: func(tx *gorm.DB) error { diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 68a27b252..912291d50 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -56,6 +56,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/accounts/delete", baseApi.DeleteAgentAccount) aiToolsRouter.POST("/agents", baseApi.CreateAgent) + aiToolsRouter.POST("/agents/batch/install", baseApi.BatchInstallAgent) + aiToolsRouter.POST("/agents/batch/upgrade", baseApi.BatchUpgradeAgent) + aiToolsRouter.POST("/agents/batch/skill/install", baseApi.BatchInstallAgentSkill) aiToolsRouter.POST("/agents/search", baseApi.PageAgents) aiToolsRouter.POST("/agents/delete/check", baseApi.DeleteCheckAgent) aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent) diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index 77caee78e..46605de13 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -135,6 +135,12 @@ TaskUpgrade: "Upgrade" TaskSync: 'Sync' TaskSyncForNode: "Sync node data" TaskBackup: "Backup" +BatchInstallAgent: "Batch install agent" +DispatchAgentInstallTasks: "Dispatch agent install tasks" +BatchUpgradeAgent: "Batch upgrade agent" +DispatchAgentUpgradeTasks: "Dispatch agent upgrade tasks" +BatchInstallAgentSkill: "Batch distribute Skill" +DispatchAgentSkillInstallTasks: "Dispatch Skill install tasks" AIBenchmarkRun: "Run AI benchmark" SuccessStatus: "{{ .name }} succeeded" FailedStatus: "{{ .name }} failed {{ .err }}" diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 0d6a454cc..e2c16997e 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -135,6 +135,12 @@ TaskUpgrade: "Actualizar" TaskSync: 'Sincronizar' TaskSyncForNode: "Sincronizar Datos del Nodo" TaskBackup: "Respaldar" +BatchInstallAgent: "Instalación masiva de agentes" +DispatchAgentInstallTasks: "Enviar tareas de instalación de agentes" +BatchUpgradeAgent: "Actualización masiva de agentes" +DispatchAgentUpgradeTasks: "Enviar tareas de actualización de agentes" +BatchInstallAgentSkill: "Distribuir Skill por lotes" +DispatchAgentSkillInstallTasks: "Enviar tareas de instalación de Skill" SuccessStatus: "{{ .name }} correcta" FailedStatus: "{{ .name }} fallida {{ .err }}" Start: "Iniciar" diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 1a988d335..944685680 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -130,6 +130,12 @@ TaskUpgrade: "アップグレード" TaskSync: '同期' TaskSyncForNode: "ノードデータを同期" TaskBackup: "バックアップ" +BatchInstallAgent: "エージェントの一括インストール" +DispatchAgentInstallTasks: "エージェントインストールタスクを配信" +BatchUpgradeAgent: "エージェントの一括アップグレード" +DispatchAgentUpgradeTasks: "エージェントアップグレードタスクを配信" +BatchInstallAgentSkill: "Skill 一括配布" +DispatchAgentSkillInstallTasks: "Skill インストールタスクを配信" SuccessStatus: "{{ .name }} 成功" FailedStatus: "{{ .name }} 失敗 {{ .err }}" Start: "開始" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 682499a66..831755a84 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -129,6 +129,12 @@ TaskUpgrade: "업그레이드" TaskSync: '동기화' TaskSyncForNode: "노드 데이터 동기화" TaskBackup: "백업" +BatchInstallAgent: "에이전트 일괄 설치" +DispatchAgentInstallTasks: "에이전트 설치 작업 배포" +BatchUpgradeAgent: "에이전트 일괄 업그레이드" +DispatchAgentUpgradeTasks: "에이전트 업그레이드 작업 배포" +BatchInstallAgentSkill: "Skill 일괄 배포" +DispatchAgentSkillInstallTasks: "Skill 설치 작업 배포" SuccessStatus: "{{ .name }} 성공" FailedStatus: "{{ .name }} 실패 {{ .err }}" Start: "시작" diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index 53d1ca5c7..be8d48777 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -124,6 +124,12 @@ TaskUpgrade: "Kemas kini" TaskSync: 'Selaraskan' TaskSyncForNode: "Segerakan Data Nod" TaskBackup: "Sandaran" +BatchInstallAgent: "Pasang ejen secara pukal" +DispatchAgentInstallTasks: "Hantar tugas pemasangan ejen" +BatchUpgradeAgent: "Naik taraf ejen secara pukal" +DispatchAgentUpgradeTasks: "Hantar tugas naik taraf ejen" +BatchInstallAgentSkill: "Hantar Skill secara pukal" +DispatchAgentSkillInstallTasks: "Hantar tugas pemasangan Skill" SuccessStatus: "{{ .name }} berjaya" FailedStatus: "{{ .name }} gagal {{ .err }}" Start: "Mula" diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index f16bbd661..47d153b18 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -129,6 +129,12 @@ TaskUpgrade: "Atualizar" TaskSync: 'Sincronizar' TaskSyncForNode: "Sincronizar Dados do Nó" TaskBackup: "Backup" +BatchInstallAgent: "Instalar agentes em lote" +DispatchAgentInstallTasks: "Distribuir tarefas de instalação de agentes" +BatchUpgradeAgent: "Atualizar agentes em lote" +DispatchAgentUpgradeTasks: "Distribuir tarefas de atualização de agentes" +BatchInstallAgentSkill: "Distribuir Skill em lote" +DispatchAgentSkillInstallTasks: "Distribuir tarefas de instalação de Skill" SuccessStatus: "{{ .name }} bem-sucedido" FailedStatus: "{{ .name }} falhou {{ .err }}" Start: "Iniciar" diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index a960aaa7c..9546c34dd 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -129,6 +129,12 @@ TaskUpgrade: "Обновить" TaskSync: 'Синхронизация' TaskSyncForNode: "Синхронизировать Данные Узла" TaskBackup: "Резервная копия" +BatchInstallAgent: "Пакетная установка агентов" +DispatchAgentInstallTasks: "Отправить задачи установки агентов" +BatchUpgradeAgent: "Пакетное обновление агентов" +DispatchAgentUpgradeTasks: "Отправить задачи обновления агентов" +BatchInstallAgentSkill: "Пакетная отправка Skill" +DispatchAgentSkillInstallTasks: "Отправить задачи установки Skill" SuccessStatus: "{{ .name }} успешно" FailedStatus: "{{ .name }} не удалось {{ .err }}" Start: "Начать" diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index 1b3733cd4..2534b6cac 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -128,6 +128,12 @@ TaskUpgrade: "Yükseltme" TaskSync: 'Senkronize' TaskSyncForNode: "Düğüm Verilerini Senkronize Et" TaskBackup: "Yedekleme" +BatchInstallAgent: "Aracıları toplu kur" +DispatchAgentInstallTasks: "Aracı kurulum görevlerini gönder" +BatchUpgradeAgent: "Aracıları toplu yükselt" +DispatchAgentUpgradeTasks: "Aracı yükseltme görevlerini gönder" +BatchInstallAgentSkill: "Skill toplu dağıt" +DispatchAgentSkillInstallTasks: "Skill kurulum görevlerini gönder" SuccessStatus: "{{ .name }} başarılı" FailedStatus: "{{ .name }} başarısız {{ .err }}" Start: "Başla" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 0a702c868..6812f0ff2 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -129,6 +129,12 @@ TaskUpgrade: "升級" TaskSync: "同步" TaskSyncForNode: "同步節點資料" TaskBackup: "備份" +BatchInstallAgent: "批量安裝智能體" +DispatchAgentInstallTasks: "下發智能體安裝任務" +BatchUpgradeAgent: "批量升級智能體" +DispatchAgentUpgradeTasks: "下發智能體升級任務" +BatchInstallAgentSkill: "批量下發 Skill" +DispatchAgentSkillInstallTasks: "下發 Skill 安裝任務" AIBenchmarkRun: "執行 AI 基準測試" SuccessStatus: "{{ .name }} 成功" FailedStatus: "{{ .name }} 失敗 {{ .err }}" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 5f30d0586..3d8439538 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -135,6 +135,12 @@ TaskUpgrade: "升级" TaskSync: "同步" TaskSyncForNode: "同步节点数据" TaskBackup: "备份" +BatchInstallAgent: "批量安装智能体" +DispatchAgentInstallTasks: "下发智能体安装任务" +BatchUpgradeAgent: "批量升级智能体" +DispatchAgentUpgradeTasks: "下发智能体升级任务" +BatchInstallAgentSkill: "批量下发 Skill" +DispatchAgentSkillInstallTasks: "下发 Skill 安装任务" AIBenchmarkRun: "执行 AI 基准测试" SuccessStatus: "{{ .name }} 成功" FailedStatus: "{{ .name }} 失败 {{ .err }}" diff --git a/frontend/src/components/log/file/index.vue b/frontend/src/components/log/file/index.vue index aa10c9d1a..10f7ccb74 100644 --- a/frontend/src/components/log/file/index.vue +++ b/frontend/src/components/log/file/index.vue @@ -56,6 +56,7 @@ import { readTaskLogByLine } from '@/api/modules/log'; import { useGlobalStore } from '@/composables/useGlobalStore'; import bus from '@/global/bus'; import Highlight from '@/components/log/custom-highlight/index.vue'; +import { translateTaskText } from '@/utils/task'; const { currentNode } = useGlobalStore(); interface LogProps { @@ -304,7 +305,7 @@ const getContent = async (pre: boolean) => { return String.fromCharCode(parseInt(grp, 16)); }), ); - const newLogs = res.data.lines; + const newLogs = res.data.lines.map((line) => translateTaskText(line)); if (tailLog.value && newLogs.length === readReq.pageSize && readReq.page < res.data.total) { readReq.page++; } diff --git a/frontend/src/components/log/task/index.vue b/frontend/src/components/log/task/index.vue index 4fdc8f439..11aacb846 100644 --- a/frontend/src/components/log/task/index.vue +++ b/frontend/src/components/log/task/index.vue @@ -46,6 +46,10 @@ const showTail = ref(true); const openWithTaskID = (id: string, tail?: boolean, operateNode?: string) => { config.taskID = id; + config.type = 'task'; + config.taskType = ''; + config.taskOperate = ''; + config.resourceID = 0; if (tail === undefined) { config.tail = true; } else { @@ -57,6 +61,8 @@ const openWithTaskID = (id: string, tail?: boolean, operateNode?: string) => { }; const openWithResourceID = (taskType: string, taskOperate: string, resourceID: number, operateNode?: string) => { + config.taskID = ''; + config.type = 'task'; config.taskType = taskType; config.resourceID = resourceID; config.taskOperate = taskOperate; diff --git a/frontend/src/components/task-list/index.vue b/frontend/src/components/task-list/index.vue index ba81dafa2..5ae13b1f9 100644 --- a/frontend/src/components/task-list/index.vue +++ b/frontend/src/components/task-list/index.vue @@ -18,7 +18,11 @@