diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go new file mode 100644 index 000000000000..854a4b183663 --- /dev/null +++ b/agent/app/api/v2/agents.go @@ -0,0 +1,191 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/gin-gonic/gin" +) + +// @Tags AI +// @Summary Create Agent +// @Accept json +// @Param request body dto.AgentCreateReq true "request" +// @Success 200 {object} dto.AgentItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents [post] +func (b *BaseApi) CreateAgent(c *gin.Context) { + var req dto.AgentCreateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := agentService.Create(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags AI +// @Summary Page Agents +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/search [post] +func (b *BaseApi) PageAgents(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := agentService.Page(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags AI +// @Summary Delete Agent +// @Accept json +// @Param request body dto.AgentDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/delete [post] +func (b *BaseApi) DeleteAgent(c *gin.Context) { + var req dto.AgentDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.Delete(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + +// @Tags AI +// @Summary Get Providers +// @Success 200 {object} []dto.ProviderInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/providers [get] +func (b *BaseApi) GetAgentProviders(c *gin.Context) { + list, err := agentService.GetProviders() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags AI +// @Summary Create Agent account +// @Accept json +// @Param request body dto.AgentAccountCreateReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/accounts [post] +func (b *BaseApi) CreateAgentAccount(c *gin.Context) { + var req dto.AgentAccountCreateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.CreateAccount(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + +// @Tags AI +// @Summary Update Agent account +// @Accept json +// @Param request body dto.AgentAccountUpdateReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/accounts/update [post] +func (b *BaseApi) UpdateAgentAccount(c *gin.Context) { + var req dto.AgentAccountUpdateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.UpdateAccount(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + +// @Tags AI +// @Summary Page Agent accounts +// @Accept json +// @Param request body dto.AgentAccountSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/accounts/search [post] +func (b *BaseApi) PageAgentAccounts(c *gin.Context) { + var req dto.AgentAccountSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := agentService.PageAccounts(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags AI +// @Summary Verify Agent account +// @Accept json +// @Param request body dto.AgentAccountVerifyReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/accounts/verify [post] +func (b *BaseApi) VerifyAgentAccount(c *gin.Context) { + var req dto.AgentAccountVerifyReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.VerifyAccount(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + +// @Tags AI +// @Summary Delete Agent account +// @Accept json +// @Param request body dto.AgentAccountDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/accounts/delete [post] +func (b *BaseApi) DeleteAgentAccount(c *gin.Context) { + var req dto.AgentAccountDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.DeleteAccount(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} diff --git a/agent/app/api/v2/app.go b/agent/app/api/v2/app.go index 6b48e7d5af83..b78a14803ca7 100644 --- a/agent/app/api/v2/app.go +++ b/agent/app/api/v2/app.go @@ -164,7 +164,7 @@ func (b *BaseApi) InstallApp(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - install, err := appService.Install(req) + install, err := appService.Install(req, true) if err != nil { helper.InternalServer(c, err) return diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index ef1ee05dd589..cc44b346b2d2 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -20,6 +20,7 @@ var ( aiToolService = service.NewIAIToolService() mcpServerService = service.NewIMcpServerService() tensorrtLLMService = service.NewITensorRTLLMService() + agentService = service.NewIAgentService() containerService = service.NewIContainerService() composeTemplateService = service.NewIComposeTemplateService() diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go new file mode 100644 index 000000000000..d804fdb2646c --- /dev/null +++ b/agent/app/dto/agents.go @@ -0,0 +1,109 @@ +package dto + +import "time" + +type AgentCreateReq struct { + Name string `json:"name" validate:"required"` + AppVersion string `json:"appVersion" validate:"required"` + WebUIPort int `json:"webUIPort" validate:"required"` + BridgePort int `json:"bridgePort" validate:"required"` + Provider string `json:"provider" validate:"required"` + Model string `json:"model" validate:"required"` + AccountID uint `json:"accountId"` + APIKey string `json:"apiKey"` + BaseURL string `json:"baseURL"` + 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"` +} + +type AgentItem struct { + ID uint `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Model string `json:"model"` + BaseURL string `json:"baseUrl"` + APIKey string `json:"apiKey"` + Token string `json:"token"` + Status string `json:"status"` + Message string `json:"message"` + AppInstallID uint `json:"appInstallId"` + AppVersion string `json:"appVersion"` + Container string `json:"containerName"` + WebUIPort int `json:"webUIPort"` + BridgePort int `json:"bridgePort"` + Path string `json:"path"` + ConfigPath string `json:"configPath"` + CreatedAt time.Time `json:"createdAt"` +} + +type AgentDeleteReq struct { + ID uint `json:"id" validate:"required"` + TaskID string `json:"taskID"` + ForceDelete bool `json:"forceDelete"` +} + +type AgentAccountCreateReq struct { + Provider string `json:"provider" validate:"required"` + Name string `json:"name" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + BaseURL string `json:"baseURL"` + Remark string `json:"remark"` +} + +type AgentAccountUpdateReq struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + BaseURL string `json:"baseURL"` + Remark string `json:"remark"` + SyncAgents bool `json:"syncAgents"` +} + +type AgentAccountVerifyReq struct { + Provider string `json:"provider" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + BaseURL string `json:"baseURL"` +} + +type AgentAccountDeleteReq struct { + ID uint `json:"id" validate:"required"` +} + +type AgentAccountSearch struct { + PageInfo + Provider string `json:"provider"` + Name string `json:"name"` +} + +type AgentAccountInfo struct { + ID uint `json:"id"` + Provider string `json:"provider"` + Name string `json:"name"` + APIKey string `json:"apiKey"` + BaseURL string `json:"baseUrl"` + Verified bool `json:"verified"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"createdAt"` +} + +type ProviderModelInfo struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type ProviderInfo struct { + Provider string `json:"provider"` + BaseURL string `json:"baseUrl"` + Models []ProviderModelInfo `json:"models"` +} diff --git a/agent/app/model/agent.go b/agent/app/model/agent.go new file mode 100644 index 000000000000..c08c899c1ed0 --- /dev/null +++ b/agent/app/model/agent.go @@ -0,0 +1,16 @@ +package model + +type Agent struct { + BaseModel + Name string `json:"name" gorm:"not null;unique"` + Provider string `json:"provider"` + Model string `json:"model"` + BaseURL string `json:"baseUrl"` + APIKey string `json:"apiKey"` + Token string `json:"token"` + Status string `json:"status"` + Message string `json:"message"` + AppInstallID uint `json:"appInstallId"` + AccountID uint `json:"accountId"` + ConfigPath string `json:"configPath"` +} diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go new file mode 100644 index 000000000000..a10c0b5f2c3f --- /dev/null +++ b/agent/app/model/agent_account.go @@ -0,0 +1,15 @@ +package model + +type AgentAccount struct { + BaseModel + Provider string `json:"provider"` + Name string `json:"name"` + APIKey string `json:"apiKey"` + BaseURL string `json:"baseUrl"` + Verified bool `json:"verified"` + Remark string `json:"remark"` +} + +func (AgentAccount) TableName() string { + return "agent_provider_accounts" +} diff --git a/agent/app/repo/agent.go b/agent/app/repo/agent.go new file mode 100644 index 000000000000..1018549d04da --- /dev/null +++ b/agent/app/repo/agent.go @@ -0,0 +1,75 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type AgentRepo struct{} + +type IAgentRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.Agent, error) + GetFirst(opts ...DBOption) (*model.Agent, error) + Create(agent *model.Agent) error + Save(agent *model.Agent) error + DeleteByID(id uint) error + DeleteByAppInstallID(appInstallID uint) error + DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error + List(opts ...DBOption) ([]model.Agent, error) +} + +func NewIAgentRepo() IAgentRepo { + return &AgentRepo{} +} + +func (a AgentRepo) Page(page, size int, opts ...DBOption) (int64, []model.Agent, error) { + var agents []model.Agent + db := getDb(opts...).Model(&model.Agent{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&agents).Error + return count, agents, err +} + +func (a AgentRepo) GetFirst(opts ...DBOption) (*model.Agent, error) { + var agent model.Agent + if err := getDb(opts...).First(&agent).Error; err != nil { + return nil, err + } + return &agent, nil +} + +func (a AgentRepo) Create(agent *model.Agent) error { + return getDb().Create(agent).Error +} + +func (a AgentRepo) Save(agent *model.Agent) error { + return getDb().Save(agent).Error +} + +func (a AgentRepo) DeleteByID(id uint) error { + return getDb().Delete(&model.Agent{}, id).Error +} + +func (a AgentRepo) DeleteByAppInstallID(appInstallID uint) error { + if appInstallID == 0 { + return nil + } + return getDb().Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error +} + +func (a AgentRepo) DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error { + if appInstallID == 0 { + return nil + } + return getTx(ctx).Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error +} + +func (a AgentRepo) List(opts ...DBOption) ([]model.Agent, error) { + var agents []model.Agent + if err := getDb(opts...).Find(&agents).Error; err != nil { + return nil, err + } + return agents, nil +} diff --git a/agent/app/repo/agent_account.go b/agent/app/repo/agent_account.go new file mode 100644 index 000000000000..eab88e5669a2 --- /dev/null +++ b/agent/app/repo/agent_account.go @@ -0,0 +1,55 @@ +package repo + +import "github.com/1Panel-dev/1Panel/agent/app/model" + +type AgentAccountRepo struct{} + +type IAgentAccountRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error) + GetFirst(opts ...DBOption) (*model.AgentAccount, error) + Create(account *model.AgentAccount) error + Save(account *model.AgentAccount) error + DeleteByID(id uint) error + List(opts ...DBOption) ([]model.AgentAccount, error) +} + +func NewIAgentAccountRepo() IAgentAccountRepo { + return &AgentAccountRepo{} +} + +func (a AgentAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error) { + var accounts []model.AgentAccount + db := getDb(opts...).Model(&model.AgentAccount{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error + return count, accounts, err +} + +func (a AgentAccountRepo) GetFirst(opts ...DBOption) (*model.AgentAccount, error) { + var account model.AgentAccount + if err := getDb(opts...).First(&account).Error; err != nil { + return nil, err + } + return &account, nil +} + +func (a AgentAccountRepo) Create(account *model.AgentAccount) error { + return getDb().Create(account).Error +} + +func (a AgentAccountRepo) Save(account *model.AgentAccount) error { + return getDb().Save(account).Error +} + +func (a AgentAccountRepo) DeleteByID(id uint) error { + return getDb().Delete(&model.AgentAccount{}, id).Error +} + +func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) { + var accounts []model.AgentAccount + if err := getDb(opts...).Find(&accounts).Error; err != nil { + return nil, err + } + return accounts, nil +} diff --git a/agent/app/repo/common.go b/agent/app/repo/common.go index 36c2d4fe9d7a..3da6ce0cad46 100644 --- a/agent/app/repo/common.go +++ b/agent/app/repo/common.go @@ -72,6 +72,33 @@ func WithByDetailName(detailName string) DBOption { } } +func WithByProvider(provider string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(provider) == 0 { + return g + } + return g.Where("provider = ?", provider) + } +} + +func WithByModel(model string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(model) == 0 { + return g + } + return g.Where("model = ?", model) + } +} + +func WithByAccountID(accountID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + if accountID == 0 { + return g + } + return g.Where("account_id = ?", accountID) + } +} + func WithByType(tp string) DBOption { return func(g *gorm.DB) *gorm.DB { return g.Where("`type` = ?", tp) diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go new file mode 100644 index 000000000000..fceb5fe9778f --- /dev/null +++ b/agent/app/service/agents.go @@ -0,0 +1,820 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type AgentService struct{} + +type IAgentService interface { + Create(req dto.AgentCreateReq) (*dto.AgentItem, error) + Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) + Delete(req dto.AgentDeleteReq) error + GetProviders() ([]dto.ProviderInfo, error) + CreateAccount(req dto.AgentAccountCreateReq) error + UpdateAccount(req dto.AgentAccountUpdateReq) error + PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) + VerifyAccount(req dto.AgentAccountVerifyReq) error + DeleteAccount(req dto.AgentAccountDeleteReq) error +} + +func NewIAgentService() IAgentService { + return &AgentService{} +} + +func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { + provider := strings.ToLower(strings.TrimSpace(req.Provider)) + if !isSupportedAgentProvider(provider) { + return nil, buserr.WithDetail("ErrInvalidParams", "provider", nil) + } + if req.AccountID == 0 { + return nil, buserr.WithDetail("ErrInvalidParams", "accountId", nil) + } + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) + if err != nil { + return nil, err + } + if !account.Verified { + return nil, buserr.WithDetail("ErrInvalidParams", "account", nil) + } + if account.Provider != "" && provider != "" && account.Provider != provider { + return nil, buserr.WithDetail("ErrInvalidParams", "provider", nil) + } + provider = strings.ToLower(strings.TrimSpace(account.Provider)) + baseURL := strings.TrimSpace(account.BaseURL) + if baseURL == "" { + if defaultURL, ok := providerDefaultBaseURL(provider); ok { + baseURL = defaultURL + } + } + if provider == "ollama" && baseURL == "" { + return nil, buserr.WithDetail("ErrInvalidParams", "baseURL", nil) + } + if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" { + return nil, buserr.WithDetail("ErrInvalidParams", "apiKey", nil) + } + if err := checkPortExist(req.WebUIPort); err != nil { + return nil, err + } + if err := checkPortExist(req.BridgePort); err != nil { + return nil, err + } + if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 { + return nil, buserr.New("ErrNameIsExist") + } + if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { + return nil, buserr.New("ErrNameIsExist") + } + app, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenclaw)) + if err != nil || app.ID == 0 { + return nil, buserr.New("ErrRecordNotFound") + } + detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(req.AppVersion)) + if err != nil || detail.ID == 0 { + return nil, buserr.New("ErrRecordNotFound") + } + + token := strings.TrimSpace(req.Token) + if token == "" { + token = randomToken() + } + params := map[string]interface{}{ + "PROVIDER": provider, + "MODEL": req.Model, + "BASE_URL": baseURL, + "API_KEY": account.APIKey, + "OPENCLAW_GATEWAY_TOKEN": token, + "PANEL_APP_PORT_HTTP": req.WebUIPort, + "PANEL_APP_PORT_BRIDGE": req.BridgePort, + constant.CPUS: "0", + constant.MemoryLimit: "0", + constant.HostIP: "", + } + + if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { + return nil, buserr.WithDetail("ErrInvalidParams", "dockerCompose", nil) + } + installReq := request.AppInstallCreate{ + AppDetailId: detail.ID, + Name: req.Name, + Params: params, + TaskID: req.TaskID, + AppContainerConfig: request.AppContainerConfig{ + 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, + }, + } + appInstall, err := NewIAppService().Install(installReq, false) + if err != nil { + return nil, err + } + configPath := path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json") + agent := &model.Agent{ + Name: req.Name, + Provider: provider, + Model: req.Model, + BaseURL: baseURL, + APIKey: account.APIKey, + Token: token, + Status: appInstall.Status, + Message: appInstall.Message, + AppInstallID: appInstall.ID, + AccountID: account.ID, + ConfigPath: configPath, + } + if err := agentRepo.Create(agent); err != nil { + return nil, err + } + go a.writeConfigWithRetry(appInstall, provider, req.Model, baseURL, req.APIKey, token, agent.ID) + + item := buildAgentItem(agent, appInstall, nil) + return &item, nil +} + +func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) { + var opts []repo.DBOption + if strings.TrimSpace(req.Info) != "" { + opts = append(opts, repo.WithByLikeName(req.Info)) + } + count, list, err := agentRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + items := make([]dto.AgentItem, 0, len(list)) + for _, item := range list { + appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID)) + envMap := readInstallEnv(appInstall.Env) + items = append(items, buildAgentItem(&item, &appInstall, envMap)) + } + return count, items, nil +} + +func (a AgentService) Delete(req dto.AgentDeleteReq) error { + if req.ID == 0 { + return buserr.WithDetail("ErrInvalidParams", "id", nil) + } + agent, err := agentRepo.GetFirst(repo.WithByID(req.ID)) + if err != nil { + return err + } + if agent.AppInstallID == 0 { + return agentRepo.DeleteByID(agent.ID) + } + operate := request.AppInstalledOperate{ + InstallId: agent.AppInstallID, + Operate: constant.Delete, + TaskID: req.TaskID, + ForceDelete: req.ForceDelete, + } + if err := NewIAppInstalledService().Operate(operate); err != nil { + return err + } + go a.waitAndDeleteAgent(agent.ID, agent.AppInstallID) + return nil +} + +func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) { + definitions := providerDefinitions() + providers := make([]dto.ProviderInfo, 0, len(definitions)) + for key, def := range definitions { + providers = append(providers, dto.ProviderInfo{ + Provider: key, + BaseURL: def.BaseURL, + Models: def.Models, + }) + } + return providers, nil +} + +func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { + provider := strings.ToLower(strings.TrimSpace(req.Provider)) + if !isSupportedAgentProvider(provider) { + return buserr.WithDetail("ErrInvalidParams", "provider", nil) + } + apiKey := strings.TrimSpace(req.APIKey) + if apiKey == "" { + return buserr.WithDetail("ErrInvalidParams", "apiKey", nil) + } + baseURL := strings.TrimSpace(req.BaseURL) + if provider != "ollama" { + if defaultURL, ok := providerDefaultBaseURL(provider); ok { + baseURL = defaultURL + } + } + if provider == "ollama" && baseURL == "" { + return buserr.WithDetail("ErrInvalidParams", "baseURL", nil) + } + if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 { + return buserr.New("ErrRecordExist") + } + if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil { + return err + } + account := &model.AgentAccount{ + Provider: provider, + Name: req.Name, + APIKey: apiKey, + BaseURL: baseURL, + Verified: true, + Remark: req.Remark, + } + return agentAccountRepo.Create(account) +} + +func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) + if err != nil { + return err + } + provider := strings.ToLower(strings.TrimSpace(account.Provider)) + baseURL := strings.TrimSpace(req.BaseURL) + if provider != "ollama" { + if defaultURL, ok := providerDefaultBaseURL(provider); ok { + baseURL = defaultURL + } + } + if provider == "ollama" && baseURL == "" { + return buserr.WithDetail("ErrInvalidParams", "baseURL", nil) + } + if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil { + return err + } + account.Name = req.Name + account.APIKey = req.APIKey + account.BaseURL = baseURL + account.Remark = req.Remark + account.Verified = true + if err := agentAccountRepo.Save(account); err != nil { + return err + } + if req.SyncAgents { + if err := a.syncAgentsByAccount(account); err != nil { + return err + } + } + return nil +} + +func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) { + var opts []repo.DBOption + if strings.TrimSpace(req.Provider) != "" { + opts = append(opts, repo.WithByProvider(req.Provider)) + } + if strings.TrimSpace(req.Name) != "" { + opts = append(opts, repo.WithByLikeName(req.Name)) + } + count, list, err := agentAccountRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + items := make([]dto.AgentAccountInfo, 0, len(list)) + for _, item := range list { + items = append(items, dto.AgentAccountInfo{ + ID: item.ID, + Provider: item.Provider, + Name: item.Name, + APIKey: item.APIKey, + BaseURL: item.BaseURL, + Verified: item.Verified, + Remark: item.Remark, + CreatedAt: item.CreatedAt, + }) + } + return count, items, nil +} + +func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { + provider := strings.ToLower(strings.TrimSpace(req.Provider)) + if !isSupportedAgentProvider(provider) { + return buserr.WithDetail("ErrInvalidParams", "provider", nil) + } + apiKey := strings.TrimSpace(req.APIKey) + if apiKey == "" { + return buserr.WithDetail("ErrInvalidParams", "apiKey", nil) + } + baseURL := strings.TrimSpace(req.BaseURL) + if baseURL == "" { + if defaultURL, ok := providerDefaultBaseURL(provider); ok { + baseURL = defaultURL + } + } + if provider == "ollama" && baseURL == "" { + return buserr.WithDetail("ErrInvalidParams", "baseURL", nil) + } + if provider == "ollama" { + return nil + } + return verifyProvider(provider, baseURL, apiKey) +} + +func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { + if req.ID == 0 { + return buserr.WithDetail("ErrInvalidParams", "id", nil) + } + if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 { + return buserr.New("ErrRecordExist") + } + return agentAccountRepo.DeleteByID(req.ID) +} + +func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { + agents, err := agentRepo.List(repo.WithByAccountID(account.ID)) + if err != nil { + return err + } + baseURL := account.BaseURL + if account.Provider != "ollama" { + if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok { + baseURL = defaultURL + } + } + for _, agent := range agents { + confDir := "" + if agent.ConfigPath != "" { + confDir = path.Dir(agent.ConfigPath) + } else if agent.AppInstallID > 0 { + install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) + if err == nil { + confDir = path.Join(install.GetPath(), "data", "conf") + } + } + if confDir == "" { + continue + } + if err := writeOpenclawConfig(confDir, account.Provider, agent.Model, baseURL, account.APIKey, agent.Token); err != nil { + return err + } + agent.BaseURL = baseURL + agent.APIKey = account.APIKey + agent.Provider = account.Provider + _ = agentRepo.Save(&agent) + } + return nil +} + +func verifyProvider(provider, baseURL, apiKey string) error { + client := &http.Client{Timeout: 10 * time.Second} + reqURL, headers := buildVerifyRequest(provider, baseURL, apiKey) + request, err := http.NewRequest(http.MethodGet, reqURL, nil) + if err != nil { + return err + } + for key, value := range headers { + request.Header.Set(key, value) + } + resp, err := client.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("verify failed: %d", resp.StatusCode), nil) + } + return nil +} + +func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { + item := dto.AgentItem{ + ID: agent.ID, + Name: agent.Name, + Provider: agent.Provider, + Model: agent.Model, + BaseURL: agent.BaseURL, + APIKey: maskKey(agent.APIKey), + Token: agent.Token, + Status: agent.Status, + Message: agent.Message, + AppInstallID: agent.AppInstallID, + ConfigPath: agent.ConfigPath, + CreatedAt: agent.CreatedAt, + } + if appInstall != nil && appInstall.ID > 0 { + item.Container = appInstall.ContainerName + item.AppVersion = appInstall.Version + item.WebUIPort = appInstall.HttpPort + item.Path = appInstall.GetPath() + item.Status = appInstall.Status + item.Message = appInstall.Message + if envMap != nil { + if bridge, ok := envMap["PANEL_APP_PORT_BRIDGE"]; ok { + item.BridgePort = toInt(bridge) + } + } + } + return item +} + +func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) { + if appInstallID == 0 { + _ = agentRepo.DeleteByID(agentID) + return + } + for i := 0; i < 180; i++ { + _, err := appInstallRepo.GetFirst(repo.WithByID(appInstallID)) + if err != nil { + _ = agentRepo.DeleteByID(agentID) + return + } + time.Sleep(2 * time.Second) + } +} + +func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, baseURL, apiKey, token string, agentID uint) { + if appInstall == nil { + return + } + fileOp := files.NewFileOp() + composePath := appInstall.GetComposePath() + for i := 0; i < 60; i++ { + if fileOp.Stat(composePath) { + break + } + time.Sleep(time.Second) + } + confDir := path.Join(appInstall.GetPath(), "data", "conf") + if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token); err != nil { + global.LOG.Errorf("write openclaw config failed: %v", err) + agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID)) + if errGet == nil && agent != nil { + agent.Message = err.Error() + agent.Status = constant.StatusError + _ = agentRepo.Save(agent) + } + return + } + dataDir := path.Join(appInstall.GetPath(), "data") + for i := 0; i < 60; i++ { + if fileOp.Stat(dataDir) { + if err := fileOp.ChownR(dataDir, "1000", "1000", true); err != nil { + global.LOG.Errorf("chown data dir failed: %v", err) + agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID)) + if errGet == nil && agent != nil { + agent.Message = err.Error() + agent.Status = constant.StatusError + _ = agentRepo.Save(agent) + } + } + break + } + time.Sleep(time.Second) + } +} + +type openclawConfig struct { + Gateway gatewayConfig `json:"gateway"` + Agents agentsConfig `json:"agents"` + Models *modelsConfig `json:"models,omitempty"` +} + +type gatewayConfig struct { + Mode string `json:"mode"` + Bind string `json:"bind"` + Port int `json:"port"` + Auth gatewayAuth `json:"auth"` + ControlUi gatewayControlUi `json:"controlUi"` +} + +type gatewayControlUi struct { + AllowInsecureAuth bool `json:"allowInsecureAuth"` +} + +type gatewayAuth struct { + Mode string `json:"mode"` + Token string `json:"token"` +} + +type agentsConfig struct { + Defaults agentDefaults `json:"defaults"` +} + +type agentDefaults struct { + Model modelRef `json:"model"` +} + +type modelRef struct { + Primary string `json:"primary"` +} + +type modelsConfig struct { + Mode string `json:"mode,omitempty"` + Providers map[string]modelProvider `json:"providers,omitempty"` +} + +type modelProvider struct { + ApiKey string `json:"apiKey,omitempty"` + BaseUrl string `json:"baseUrl,omitempty"` + Api string `json:"api,omitempty"` + Models []modelEntry `json:"models,omitempty"` +} + +type modelEntry struct { + ID string `json:"id"` + Name string `json:"name"` + Reasoning bool `json:"reasoning"` + Input []string `json:"input"` + ContextWindow int `json:"contextWindow"` + MaxTokens int `json:"maxTokens"` +} + +func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token string) error { + if strings.TrimSpace(confDir) == "" { + return fmt.Errorf("config dir is required") + } + if strings.TrimSpace(modelName) == "" { + return fmt.Errorf("model is required") + } + if strings.TrimSpace(token) == "" { + return fmt.Errorf("gateway token is required") + } + fileOp := files.NewFileOp() + if !fileOp.Stat(confDir) { + if err := fileOp.CreateDir(confDir, constant.DirPerm); err != nil { + return err + } + } + + cfg := openclawConfig{ + Gateway: gatewayConfig{ + Mode: "local", + Bind: "lan", + Port: 18789, + Auth: gatewayAuth{ + Mode: "token", + Token: token, + }, + ControlUi: gatewayControlUi{ + AllowInsecureAuth: true, + }, + }, + Agents: agentsConfig{ + Defaults: agentDefaults{ + Model: modelRef{Primary: modelName}, + }, + }, + } + + provider = strings.ToLower(strings.TrimSpace(provider)) + if provider == "deepseek" { + base := baseURL + if base == "" { + base = "https://api.deepseek.com/v1" + } + cfg.Models = &modelsConfig{ + Mode: "merge", + Providers: map[string]modelProvider{ + "deepseek": { + ApiKey: "${DEEPSEEK_API_KEY}", + BaseUrl: base, + Api: "openai-completions", + Models: []modelEntry{ + { + ID: "deepseek-chat", + Name: "DeepSeek Chat", + Reasoning: false, + Input: []string{"text"}, + ContextWindow: 128000, + MaxTokens: 8192, + }, + }, + }, + }, + } + } else if provider == "ollama" { + modelID := modelName + if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 { + modelID = parts[1] + } + cfg.Models = &modelsConfig{ + Mode: "merge", + Providers: map[string]modelProvider{ + "ollama": { + ApiKey: "ollama", + BaseUrl: baseURL, + Api: "openai-responses", + Models: []modelEntry{ + { + ID: modelID, + Name: modelID, + Reasoning: true, + Input: []string{"text"}, + ContextWindow: 160000, + MaxTokens: 8192, + }, + }, + }, + }, + } + } + + payload, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + configPath := path.Join(confDir, "openclaw.json") + if err := fileOp.SaveFile(configPath, string(payload), 0600); err != nil { + return err + } + + envPath := path.Join(confDir, ".env") + lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} + if envKey := providerEnvKey(provider); envKey != "" && strings.TrimSpace(apiKey) != "" { + lines = append(lines, fmt.Sprintf("%s=%s", envKey, apiKey)) + } + content := strings.Join(lines, "\n") + "\n" + return fileOp.SaveFile(envPath, content, 0600) +} + +func providerEnvKey(provider string) string { + switch provider { + case "openai": + return "OPENAI_API_KEY" + case "anthropic": + return "ANTHROPIC_API_KEY" + case "gemini": + return "GEMINI_API_KEY" + case "minimax": + return "MINIMAX_API_KEY" + case "deepseek": + return "DEEPSEEK_API_KEY" + case "qwen": + return "QWEN_API_KEY" + case "ollama": + return "" + default: + return "" + } +} + +type providerDefinition struct { + BaseURL string + Models []dto.ProviderModelInfo +} + +func providerDefinitions() map[string]providerDefinition { + return map[string]providerDefinition{ + "openai": { + BaseURL: "https://api.openai.com/v1", + Models: []dto.ProviderModelInfo{ + {ID: "openai/codex-mini-latest", Name: "Codex Mini"}, + {ID: "openai/gpt-4.1", Name: "GPT-4.1"}, + {ID: "openai/gpt-4o", Name: "GPT-4o"}, + {ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"}, + {ID: "openai/gpt-5", Name: "GPT-5"}, + {ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"}, + }, + }, + "anthropic": { + BaseURL: "https://api.anthropic.com", + Models: []dto.ProviderModelInfo{ + {ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"}, + {ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"}, + {ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"}, + {ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"}, + {ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"}, + }, + }, + "gemini": { + BaseURL: "https://generativelanguage.googleapis.com", + Models: []dto.ProviderModelInfo{ + {ID: "google/gemini-1.5-flash", Name: "Gemini 1.5 Flash"}, + {ID: "google/gemini-1.5-pro", Name: "Gemini 1.5 Pro"}, + {ID: "google/gemini-2.0-flash", Name: "Gemini 2.0 Flash"}, + {ID: "google/gemini-2.5-flash", Name: "Gemini 2.5 Flash"}, + {ID: "google/gemini-2.5-pro", Name: "Gemini 2.5 Pro"}, + {ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, + }, + }, + "minimax": { + BaseURL: "https://api.minimax.chat/v1", + Models: []dto.ProviderModelInfo{ + {ID: "minimax/Minimax-M2.1", Name: "Minimax M2.1"}, + }, + }, + "deepseek": { + BaseURL: "https://api.deepseek.com/v1", + Models: []dto.ProviderModelInfo{ + {ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"}, + {ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"}, + {ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"}, + }, + }, + "ollama": { + BaseURL: "", + Models: []dto.ProviderModelInfo{}, + }, + } +} + +func providerDefaultBaseURL(provider string) (string, bool) { + defs := providerDefinitions() + if def, ok := defs[provider]; ok { + if def.BaseURL == "" { + return "", false + } + return def.BaseURL, true + } + return "", false +} + +func isSupportedAgentProvider(provider string) bool { + _, ok := providerDefinitions()[provider] + return ok +} + +func buildVerifyRequest(provider, baseURL, apiKey string) (string, map[string]string) { + headers := map[string]string{} + base := strings.TrimRight(baseURL, "/") + switch provider { + case "anthropic": + headers["x-api-key"] = apiKey + headers["anthropic-version"] = "2023-06-01" + if strings.Contains(base, "/v1") { + return base + "/models", headers + } + return base + "/v1/models", headers + case "gemini": + if strings.Contains(base, "/v1beta") { + return fmt.Sprintf("%s/models?key=%s", base, apiKey), headers + } + return fmt.Sprintf("%s/v1beta/models?key=%s", base, apiKey), headers + default: + headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) + if strings.Contains(base, "/v1") { + return base + "/models", headers + } + return base + "/v1/models", headers + } +} + +func readInstallEnv(envStr string) map[string]interface{} { + if strings.TrimSpace(envStr) == "" { + return nil + } + data := map[string]interface{}{} + if err := json.Unmarshal([]byte(envStr), &data); err != nil { + return nil + } + return data +} + +func maskKey(value string) string { + trim := strings.TrimSpace(value) + if len(trim) <= 6 { + return trim + } + return fmt.Sprintf("%s****%s", trim[:3], trim[len(trim)-3:]) +} + +func toInt(value interface{}) int { + switch v := value.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + case string: + if v == "" { + return 0 + } + parsed, _ := strconv.Atoi(v) + return parsed + default: + return 0 + } +} + +func randomToken() string { + bytes := make([]byte, 24) + if _, err := rand.Read(bytes); err != nil { + return "" + } + return hex.EncodeToString(bytes) +} diff --git a/agent/app/service/app.go b/agent/app/service/app.go index 7ab5332c4f1a..34ec4d85e1ca 100644 --- a/agent/app/service/app.go +++ b/agent/app/service/app.go @@ -42,7 +42,7 @@ type IAppService interface { GetAppTags(ctx *gin.Context) ([]response.TagDTO, error) GetApp(ctx *gin.Context, key string) (*response.AppDTO, error) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) - Install(req request.AppInstallCreate) (*model.AppInstall, error) + Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error) SyncAppListFromRemote(taskID string) error GetAppUpdate() (*response.AppUpdateRes, error) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) @@ -333,7 +333,7 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) { return res, nil } -func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.AppInstall, err error) { +func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) { if err = docker.CreateDefaultDockerNetwork(); err != nil { err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil) return @@ -527,8 +527,10 @@ func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.App if err = copyData(t, app, appDetail, appInstall, req); err != nil { return err } - if err = runScript(t, appInstall, "init"); err != nil { - return err + if executeScript { + if err = runScript(t, appInstall, "init"); err != nil { + return err + } } if app.Key == "openresty" { if err = handleSiteDir(app, appDetail, req, t); err != nil { diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index 0b55667ca758..ff5c528d43ea 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -392,6 +392,10 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error { if err = appInstallRepo.Delete(ctx, install); err != nil { return err } + appKey := install.App.Key + if appKey == constant.AppOpenclaw { + _ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID) + } resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) if len(resources) > 0 { diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 90213e7077a8..31015ce770c7 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -12,9 +12,11 @@ var ( appInstallResourceRepo = repo.NewIAppInstallResourceRpo() appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo() - aiRepo = repo.NewIAiRepo() - mcpServerRepo = repo.NewIMcpServerRepo() - tensorrtLLMRepo = repo.NewITensorRTLLMRepo() + aiRepo = repo.NewIAiRepo() + mcpServerRepo = repo.NewIMcpServerRepo() + tensorrtLLMRepo = repo.NewITensorRTLLMRepo() + agentRepo = repo.NewIAgentRepo() + agentAccountRepo = repo.NewIAgentAccountRepo() mysqlRepo = repo.NewIMysqlRepo() postgresqlRepo = repo.NewIPostgresqlRepo() diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 67d02c79b29e..ffe364e0c5bb 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -383,7 +383,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) req.AppDetailId = create.AppInstall.AppDetailId req.Params = create.AppInstall.Params req.AppContainerConfig = create.AppInstall.AppContainerConfig - install, err = NewIAppService().Install(req) + install, err = NewIAppService().Install(req, true) if err != nil { return err } diff --git a/agent/constant/app.go b/agent/constant/app.go index ee1efa62f2df..74e76612532f 100644 --- a/agent/constant/app.go +++ b/agent/constant/app.go @@ -7,6 +7,7 @@ const ( AppTakeDown = "TakeDown" AppOpenresty = "openresty" + AppOpenclaw = "openclaw" AppMysql = "mysql" AppMariaDB = "mariadb" AppPostgresql = "postgresql" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index c940244d59bf..490b72f9855a 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -63,6 +63,8 @@ func InitAgentDB() { migrations.UpdateApp, migrations.AddCronjobArgs, migrations.AddWebsiteAcmeAccountColumn, + migrations.AddAgentTables, + migrations.MigrateOpenclawAgents, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index de5a56684a6c..bf0cbd11b542 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -2,6 +2,7 @@ package migrations import ( "encoding/json" + "errors" "fmt" "os" "os/user" @@ -821,3 +822,135 @@ var AddWebsiteAcmeAccountColumn = &gormigrate.Migration{ return tx.AutoMigrate(&model.WebsiteAcmeAccount{}) }, } + +var AddAgentTables = &gormigrate.Migration{ + ID: "20260205-add-agent-tables", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + &model.Agent{}, + &model.AgentAccount{}, + ) + }, +} + +var MigrateOpenclawAgents = &gormigrate.Migration{ + ID: "20260207-migrate-openclaw-agents", + Migrate: func(tx *gorm.DB) error { + var installs []model.AppInstall + if err := tx.Preload("App").Find(&installs).Error; err != nil { + return err + } + for _, install := range installs { + appKey := install.App.Key + if appKey == "" || install.App.Resource == "" { + var app model.App + if err := tx.First(&app, install.AppId).Error; err == nil { + install.App = app + appKey = app.Key + } + } + if appKey != constant.AppOpenclaw { + continue + } + var count int64 + if err := tx.Model(&model.Agent{}).Where("app_install_id = ?", install.ID).Count(&count).Error; err != nil { + return err + } + if count > 0 { + continue + } + envMap := map[string]interface{}{} + if strings.TrimSpace(install.Env) != "" { + _ = json.Unmarshal([]byte(install.Env), &envMap) + } + provider := strings.ToLower(getEnvStr(envMap, "PROVIDER")) + if provider == "" { + continue + } + modelName := getEnvStr(envMap, "MODEL") + baseURL := getEnvStr(envMap, "BASE_URL") + apiKey := getEnvStr(envMap, "API_KEY") + token := getEnvStr(envMap, "OPENCLAW_GATEWAY_TOKEN") + if provider != "ollama" { + if baseURL == "" { + if defaultURL, ok := defaultBaseURL(provider); ok { + baseURL = defaultURL + } + } + } + if provider == "ollama" && baseURL == "" { + continue + } + var account model.AgentAccount + err := tx.Where("provider = ? AND api_key = ? AND base_url = ?", provider, apiKey, baseURL).First(&account).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + account = model.AgentAccount{ + Provider: provider, + Name: install.Name, + APIKey: apiKey, + BaseURL: baseURL, + Verified: apiKey != "" || provider == "ollama", + } + if err := tx.Create(&account).Error; err != nil { + return err + } + } else { + return err + } + } + configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json") + agent := model.Agent{ + Name: install.Name, + Provider: provider, + Model: modelName, + BaseURL: baseURL, + APIKey: apiKey, + Token: token, + Status: install.Status, + Message: install.Message, + AppInstallID: install.ID, + AccountID: account.ID, + ConfigPath: configPath, + } + if err := tx.Create(&agent).Error; err != nil { + return err + } + } + return nil + }, +} + +func getEnvStr(envMap map[string]interface{}, key string) string { + if envMap == nil { + return "" + } + if value, ok := envMap[key]; ok { + switch v := value.(type) { + case string: + return strings.TrimSpace(v) + default: + return strings.TrimSpace(fmt.Sprintf("%v", v)) + } + } + return "" +} + +func defaultBaseURL(provider string) (string, bool) { + switch provider { + case "openai": + return "https://api.openai.com/v1", true + case "anthropic": + return "https://api.anthropic.com", true + case "gemini": + return "https://generativelanguage.googleapis.com", true + case "minimax": + return "https://api.minimax.chat/v1", true + case "deepseek": + return "https://api.deepseek.com/v1", true + case "qwen": + return "https://dashscope.aliyuncs.com/compatible-mode/v1", true + default: + return "", false + } +} diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 47246c6aac5d..0f5649b735ff 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -39,5 +39,15 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/tensorrt/update", baseApi.UpdateTensorRTLLM) aiToolsRouter.POST("/tensorrt/delete", baseApi.DeleteTensorRTLLM) aiToolsRouter.POST("/tensorrt/operate", baseApi.OperateTensorRTLLM) + + aiToolsRouter.POST("/agents", baseApi.CreateAgent) + aiToolsRouter.POST("/agents/search", baseApi.PageAgents) + aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent) + aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders) + aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount) + aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount) + aiToolsRouter.POST("/agents/accounts/search", baseApi.PageAgentAccounts) + aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount) + aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount) } } diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index 1a6cee32aaa1..9c97da1b4508 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -22,6 +22,7 @@ func LoadMenus() string { }}, {ID: "4", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model", Sort: 400, Children: []dto.ShowMenu{ + {ID: "44", Disabled: false, Title: "aiTools.agents.agents", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50}, {ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "OllamaModel", Path: "/ai/model", Sort: 100}, {ID: "42", Disabled: false, Title: "menu.mcp", IsShow: true, Label: "MCPServer", Path: "/ai/mcp", Sort: 200}, {ID: "43", Disabled: false, Title: "aiTools.gpu.gpu", IsShow: true, Label: "GPU", Path: "/ai/gpu", Sort: 300}, @@ -68,6 +69,7 @@ func MenuSort() []dto.MenuLabelSort { {Label: "SSL", Sort: 200}, {Label: "PHP", Sort: 300}, {Label: "AI-Menu", Sort: 400}, + {Label: "Agents", Sort: 50}, {Label: "OllamaModel", Sort: 100}, {Label: "MCPServer", Sort: 200}, {Label: "GPU", Sort: 300}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 50be9260aec7..faa3708441a6 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -23,6 +23,7 @@ func Init() { migrations.DeleteXpackHideMenu, migrations.AddCronjobGroup, migrations.AddDiskMenu, + migrations.AddAgentsMenu, migrations.AddSimpleNodeGroup, migrations.AddUpgradeBackupCopies, migrations.AddScriptSync, diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index d2d6c23e1f82..f540219902fc 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -556,6 +556,20 @@ var AddDiskMenu = &gormigrate.Migration{ }, } +var AddAgentsMenu = &gormigrate.Migration{ + ID: "20260204-add-agents-menu", + Migrate: func(tx *gorm.DB) error { + return helper.AddMenu(dto.ShowMenu{ + ID: "44", + Disabled: false, + Title: "aiTools.agents.agents", + IsShow: true, + Label: "Agents", + Path: "/ai/agents/agent", + }, "4", tx) + }, +} + var AddSimpleNodeGroup = &gormigrate.Migration{ ID: "20250916-add-simple-node-group", Migrate: func(tx *gorm.DB) error { diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 3c10cdf13980..9fe265d41ff9 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -235,4 +235,112 @@ export namespace AI { id: number; operate: string; } + + export interface AgentCreateReq { + name: string; + appVersion: string; + webUIPort: number; + bridgePort: number; + provider: string; + model: string; + accountId: number; + apiKey: string; + baseURL: string; + token: string; + taskID: string; + advanced: boolean; + containerName: string; + allowPort: boolean; + specifyIP: string; + restartPolicy: string; + cpuQuota: number; + memoryLimit: number; + memoryUnit: string; + pullImage: boolean; + editCompose: boolean; + dockerCompose: string; + } + + export interface AgentItem { + id: number; + name: string; + provider: string; + model: string; + baseUrl: string; + apiKey: string; + token: string; + status: string; + message: string; + appInstallId: number; + appVersion: string; + containerName: string; + webUIPort: number; + bridgePort: number; + path: string; + configPath: string; + createdAt: string; + } + + + export interface AgentDeleteReq { + id: number; + taskID: string; + forceDelete: boolean; + } + + export interface ProviderModelInfo { + id: string; + name: string; + } + + export interface ProviderInfo { + provider: string; + baseUrl: string; + models: ProviderModelInfo[]; + } + + export interface AgentAccountCreateReq { + provider: string; + name: string; + apiKey: string; + baseURL: string; + remark: string; + } + + export interface AgentAccountUpdateReq { + id: number; + name: string; + apiKey: string; + baseURL: string; + remark: string; + syncAgents: boolean; + } + + export interface AgentAccountSearch { + page: number; + pageSize: number; + provider: string; + name: string; + } + + export interface AgentAccountItem { + id: number; + provider: string; + name: string; + apiKey: string; + baseUrl: string; + verified: boolean; + remark: string; + createdAt: string; + } + + export interface AgentAccountVerifyReq { + provider: string; + apiKey: string; + baseURL: string; + } + + export interface AgentAccountDeleteReq { + id: number; + } } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index fd2bfa4297c8..afb2f1014ba4 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -1,6 +1,6 @@ import { AI } from '@/api/interface/ai'; import http from '@/api'; -import { ResPage } from '../interface'; +import { ResPage, SearchWithPage } from '../interface'; export const createOllamaModel = (name: string, taskID: string) => { return http.post(`/ai/ollama/model`, { name: name, taskID: taskID }); @@ -91,3 +91,39 @@ export const deleteTensorRTLLM = (req: AI.TensorRTLLMDelete) => { export const operateTensorRTLLM = (req: AI.TensorRTLLMOperate) => { return http.post(`/ai/tensorrt/operate`, req); }; + +export const createAgent = (req: AI.AgentCreateReq) => { + return http.post(`/ai/agents`, req); +}; + +export const pageAgents = (req: SearchWithPage) => { + return http.post>(`/ai/agents/search`, req); +}; + +export const deleteAgent = (req: AI.AgentDeleteReq) => { + return http.post(`/ai/agents/delete`, req); +}; + +export const getAgentProviders = () => { + return http.get(`/ai/agents/providers`); +}; + +export const createAgentAccount = (req: AI.AgentAccountCreateReq) => { + return http.post(`/ai/agents/accounts`, req); +}; + +export const updateAgentAccount = (req: AI.AgentAccountUpdateReq) => { + return http.post(`/ai/agents/accounts/update`, req); +}; + +export const pageAgentAccounts = (req: AI.AgentAccountSearch) => { + return http.post>(`/ai/agents/accounts/search`, req); +}; + +export const verifyAgentAccount = (req: AI.AgentAccountVerifyReq) => { + return http.post(`/ai/agents/accounts/verify`, req); +}; + +export const deleteAgentAccount = (req: AI.AgentAccountDeleteReq) => { + return http.post(`/ai/agents/accounts/delete`, req); +}; diff --git a/frontend/src/components/advanced-setting/index.vue b/frontend/src/components/advanced-setting/index.vue new file mode 100644 index 000000000000..4b9ebd471044 --- /dev/null +++ b/frontend/src/components/advanced-setting/index.vue @@ -0,0 +1,128 @@ + + + diff --git a/frontend/src/components/port-jump/index.vue b/frontend/src/components/port-jump/index.vue index 50ec9d4e2248..6f22c26efd3d 100644 --- a/frontend/src/components/port-jump/index.vue +++ b/frontend/src/components/port-jump/index.vue @@ -31,6 +31,8 @@ interface DialogProps { port: any; ip: string; protocol: string; + path?: string; + query?: string; } const acceptParams = async (params: DialogProps): Promise => { @@ -48,18 +50,28 @@ const acceptParams = async (params: DialogProps): Promise => { return; } } + const buildUrl = (host: string) => { + let url = `${protocol}://${host}:${params.port}`; + if (params.path) { + url += params.path.startsWith('/') ? params.path : `/${params.path}`; + } + if (params.query) { + url += params.query.startsWith('?') ? params.query : `?${params.query}`; + } + return url; + }; if (res.data.systemIP.indexOf(':') === -1) { if (params.ip && params.ip === 'ipv6') { MsgWarning(i18n.global.t('setting.systemIPWarning1', ['IPv4'])); return; } - window.open(`${protocol}://${res.data.systemIP}:${params.port}`, '_blank'); + window.open(buildUrl(res.data.systemIP), '_blank'); } else { if (params.ip && params.ip === 'ipv4') { MsgWarning(i18n.global.t('setting.systemIPWarning1', ['IPv6'])); return; } - window.open(`${protocol}://[${res.data.systemIP}]:${params.port}`, '_blank'); + window.open(buildUrl(`[${res.data.systemIP}]`), '_blank'); } }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 6ee8e2441cac..0546a191ba95 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -676,6 +676,30 @@ const message = { localIP: 'Local IP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Model', create: 'Add Model', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index c4b7b4dd38e8..963e4aeef745 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -672,6 +672,30 @@ const message = { localIP: 'IP local', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Modelo', create: 'Agregar modelo', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index cfbd3ebc3a13..f583fb1a07e0 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -661,6 +661,30 @@ const message = { localIP: 'ローカルIP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'モデル', create: 'モデルを追加', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 4867841b7c03..4be6d75b5dc9 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -658,6 +658,30 @@ const message = { localIP: '로컬 IP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: '모델', create: '모델 추가', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 128c9a4ff5dd..0fd55ea2fa07 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -673,6 +673,30 @@ const message = { localIP: 'IP Tempatan', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Model', create: 'Tambah Model', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index b32ef47f2f09..14686f4f8fb0 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -670,6 +670,30 @@ const message = { localIP: 'IP local', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Modelo', create: 'Adicionar Modelo', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index fea549664da5..f304a42e81bc 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -666,6 +666,30 @@ const message = { localIP: 'Локальный IP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Модель', create: 'Добавить модель', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 2301a31036c8..4b51fa658ff3 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -680,6 +680,30 @@ const message = { localIP: 'Yerel IP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: 'Model', create: 'Model Ekle', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 387672e55cdd..fb764cd18e4a 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -648,6 +648,30 @@ const message = { localIP: '本機 IP', }, aiTools: { + agents: { + agents: 'Agents', + agent: 'Agent', + createAgent: 'Create Agent', + createModel: 'Create Model', + agentList: 'Agent List', + modelList: 'Model List', + account: 'Account', + accountName: 'Account Name', + accountList: 'Account List', + createAccount: 'Create Account', + syncAgents: 'Sync related agents', + syncAgentsHelper: 'Update openclaw.json for agents using this account', + appVersion: 'App Version', + webuiPort: 'WebUI Port', + bridgePort: 'Bridge Port', + provider: 'Provider', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: 'Model Name', + manualModel: 'Manual input', + verified: 'Verified', + }, model: { model: '模型', create: '新增模型', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index df9581a72e33..3b8093592861 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -650,6 +650,30 @@ const message = { localIP: '本机 IP', }, aiTools: { + agents: { + agents: '智能体', + agent: '智能体', + createAgent: '创建智能体', + createModel: '创建模型', + agentList: '智能体列表', + modelList: '模型列表', + account: '账号', + accountName: '账号名称', + accountList: '账号列表', + createAccount: '创建账号', + syncAgents: '同步关联智能体', + syncAgentsHelper: '更新使用该账号的智能体 openclaw.json', + appVersion: '应用版本', + webuiPort: 'WebUI 端口', + bridgePort: 'Bridge 端口', + provider: '模型供应商', + apiKey: 'API Key', + baseUrl: 'Base URL', + token: 'Token', + modelName: '模型名称', + manualModel: '手动输入模型', + verified: '验证状态', + }, model: { model: '模型', create: '添加模型', diff --git a/frontend/src/routers/modules/ai.ts b/frontend/src/routers/modules/ai.ts index e4bf716ec5d7..208dd5caa54c 100644 --- a/frontend/src/routers/modules/ai.ts +++ b/frontend/src/routers/modules/ai.ts @@ -11,6 +11,26 @@ const databaseRouter = { title: 'menu.aiTools', }, children: [ + { + path: '/ai/agents/agent', + name: 'Agents', + component: () => import('@/views/ai/agents/agent/index.vue'), + meta: { + icon: 'p-jiqiren2', + title: 'aiTools.agents.agents', + requiresAuth: true, + }, + }, + { + path: '/ai/agents/model', + name: 'AgentsModel', + component: () => import('@/views/ai/agents/model/index.vue'), + meta: { + title: 'aiTools.agents.account', + activeMenu: '/ai/agents/agent', + requiresAuth: true, + }, + }, { path: '/ai/model/ollama', name: 'OllamaModel', diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index d7edb173b953..0488f80f78fd 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -13,6 +13,9 @@ export const jumpToInstall = (type: string, key: string) => { return true; } switch (key) { + case 'openclaw': + jumpToPath(router, '/ai/agents/agent'); + return true; case 'mysql-cluster': jumpToPath(router, '/xpack/cluster/mysql'); return true; diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue new file mode 100644 index 000000000000..6d71db020647 --- /dev/null +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -0,0 +1,308 @@ + + + diff --git a/frontend/src/views/ai/agents/agent/delete/index.vue b/frontend/src/views/ai/agents/agent/delete/index.vue new file mode 100644 index 000000000000..e22947df5b2f --- /dev/null +++ b/frontend/src/views/ai/agents/agent/delete/index.vue @@ -0,0 +1,77 @@ + + + diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue new file mode 100644 index 000000000000..690f1adbff3c --- /dev/null +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -0,0 +1,195 @@ + + + diff --git a/frontend/src/views/ai/agents/index.vue b/frontend/src/views/ai/agents/index.vue new file mode 100644 index 000000000000..6905e0c69dd3 --- /dev/null +++ b/frontend/src/views/ai/agents/index.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/agents/model/add/index.vue new file mode 100644 index 000000000000..22035ce5679c --- /dev/null +++ b/frontend/src/views/ai/agents/model/add/index.vue @@ -0,0 +1,176 @@ + + + diff --git a/frontend/src/views/ai/agents/model/index.vue b/frontend/src/views/ai/agents/model/index.vue new file mode 100644 index 000000000000..e9358a9ef121 --- /dev/null +++ b/frontend/src/views/ai/agents/model/index.vue @@ -0,0 +1,131 @@ + + +