From f1159e4c68b764fd8bb0f299c03168ab37dae124 Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Fri, 3 Apr 2026 16:20:58 +0800 Subject: [PATCH] feat: Add website binding support for the agent --- agent/app/api/v2/agents.go | 20 ++ agent/app/dto/agents.go | 58 ++-- agent/app/model/agent.go | 1 + agent/app/repo/agent.go | 8 + agent/app/repo/common.go | 18 ++ agent/app/repo/website.go | 10 + agent/app/repo/website_domain.go | 10 + agent/app/service/agents.go | 4 + agent/app/service/agents_utils.go | 1 + agent/app/service/agents_website.go | 301 ++++++++++++++++++ agent/app/service/website.go | 11 +- agent/i18n/lang/en.yaml | 3 + agent/i18n/lang/es-ES.yaml | 3 + agent/i18n/lang/ja.yaml | 3 + agent/i18n/lang/ko.yaml | 3 + agent/i18n/lang/ms.yaml | 3 + agent/i18n/lang/pt-BR.yaml | 3 + agent/i18n/lang/ru.yaml | 3 + agent/i18n/lang/tr.yaml | 3 + agent/i18n/lang/zh-Hant.yaml | 3 + agent/i18n/lang/zh.yaml | 3 + agent/init/migration/migrate.go | 1 + agent/init/migration/migrations/init.go | 36 +++ agent/router/ro_ai.go | 1 + frontend/src/api/interface/ai.ts | 8 + frontend/src/api/interface/website.ts | 6 + frontend/src/api/modules/ai.ts | 4 + frontend/src/api/modules/website.ts | 2 +- frontend/src/views/ai/agents/agent/index.vue | 159 ++++++++- .../views/ai/agents/agent/website/index.vue | 99 ++++++ 30 files changed, 759 insertions(+), 29 deletions(-) create mode 100644 agent/app/service/agents_website.go create mode 100644 frontend/src/views/ai/agents/agent/website/index.vue diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index a282036da8ee..dd2bd075e6a2 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -111,6 +111,26 @@ func (b *BaseApi) UpdateAgentRemark(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Bind Agent website +// @Accept json +// @Param request body dto.AgentWebsiteBindReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/website/bind [post] +func (b *BaseApi) BindAgentWebsite(c *gin.Context) { + var req dto.AgentWebsiteBindReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.BindWebsite(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Get Agent model config // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 8295a5bd5514..f48ae7800034 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -28,31 +28,34 @@ type AgentCreateReq struct { } type AgentItem struct { - ID uint `json:"id"` - Name string `json:"name"` - Remark string `json:"remark"` - AgentType string `json:"agentType"` - Provider string `json:"provider"` - ProviderName string `json:"providerName"` - Model string `json:"model"` - APIType string `json:"apiType"` - MaxTokens int `json:"maxTokens"` - ContextWindow int `json:"contextWindow"` - 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"` - AppVersion string `json:"appVersion"` - Container string `json:"containerName"` - WebUIPort int `json:"webUIPort"` - BridgePort int `json:"bridgePort"` - Path string `json:"path"` - ConfigPath string `json:"configPath"` - Upgradable bool `json:"upgradable"` - CreatedAt time.Time `json:"createdAt"` + ID uint `json:"id"` + Name string `json:"name"` + Remark string `json:"remark"` + AgentType string `json:"agentType"` + Provider string `json:"provider"` + ProviderName string `json:"providerName"` + Model string `json:"model"` + APIType string `json:"apiType"` + MaxTokens int `json:"maxTokens"` + ContextWindow int `json:"contextWindow"` + BaseURL string `json:"baseUrl"` + APIKey string `json:"apiKey"` + Token string `json:"token"` + Status string `json:"status"` + Message string `json:"message"` + AppInstallID uint `json:"appInstallId"` + WebsiteID uint `json:"websiteId"` + WebsitePrimaryDomain string `json:"websitePrimaryDomain"` + WebsiteProtocol string `json:"websiteProtocol"` + AccountID uint `json:"accountId"` + AppVersion string `json:"appVersion"` + Container string `json:"containerName"` + WebUIPort int `json:"webUIPort"` + BridgePort int `json:"bridgePort"` + Path string `json:"path"` + ConfigPath string `json:"configPath"` + Upgradable bool `json:"upgradable"` + CreatedAt time.Time `json:"createdAt"` } type AgentDeleteReq struct { @@ -70,6 +73,11 @@ type AgentRemarkUpdateReq struct { Remark string `json:"remark"` } +type AgentWebsiteBindReq struct { + AgentID uint `json:"agentId" validate:"required"` + WebsiteID uint `json:"websiteId" validate:"required"` +} + type AgentModelConfigUpdateReq struct { AgentID uint `json:"agentId" validate:"required"` AccountID uint `json:"accountId" validate:"required"` diff --git a/agent/app/model/agent.go b/agent/app/model/agent.go index f0aa923c1a40..08dcdbe5a3ca 100644 --- a/agent/app/model/agent.go +++ b/agent/app/model/agent.go @@ -16,6 +16,7 @@ type Agent struct { Status string `json:"status"` Message string `json:"message"` AppInstallID uint `json:"appInstallId"` + WebsiteID uint `json:"websiteId"` AccountID uint `json:"accountId"` ConfigPath string `json:"configPath"` } diff --git a/agent/app/repo/agent.go b/agent/app/repo/agent.go index 1018549d04da..7b8bd51ecd47 100644 --- a/agent/app/repo/agent.go +++ b/agent/app/repo/agent.go @@ -16,6 +16,7 @@ type IAgentRepo interface { DeleteByID(id uint) error DeleteByAppInstallID(appInstallID uint) error DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error + ClearWebsiteIDByWebsiteIDWithCtx(ctx context.Context, websiteID uint) error List(opts ...DBOption) ([]model.Agent, error) } @@ -66,6 +67,13 @@ func (a AgentRepo) DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID return getTx(ctx).Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error } +func (a AgentRepo) ClearWebsiteIDByWebsiteIDWithCtx(ctx context.Context, websiteID uint) error { + if websiteID == 0 { + return nil + } + return getTx(ctx).Model(&model.Agent{}).Where("website_id = ?", websiteID).Update("website_id", 0).Error +} + func (a AgentRepo) List(opts ...DBOption) ([]model.Agent, error) { var agents []model.Agent if err := getDb(opts...).Find(&agents).Error; err != nil { diff --git a/agent/app/repo/common.go b/agent/app/repo/common.go index 57873450123a..df24bd5b8a36 100644 --- a/agent/app/repo/common.go +++ b/agent/app/repo/common.go @@ -112,6 +112,24 @@ func WithByAccountID(accountID uint) DBOption { } } +func WithByWebsiteID(websiteID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + if websiteID == 0 { + return g + } + return g.Where("website_id = ?", websiteID) + } +} + +func WithByAppInstallID(appInstallID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + if appInstallID == 0 { + return g + } + return g.Where("app_install_id = ?", appInstallID) + } +} + func WithByType(tp string) DBOption { return func(g *gorm.DB) *gorm.DB { return g.Where("`type` = ?", tp) diff --git a/agent/app/repo/website.go b/agent/app/repo/website.go index 3f498176ff6a..114170d9bf85 100644 --- a/agent/app/repo/website.go +++ b/agent/app/repo/website.go @@ -10,6 +10,7 @@ import ( type IWebsiteRepo interface { WithAppInstallId(appInstallId uint) DBOption + WithAppInstallIds(appInstallIds []uint) DBOption WithDomain(domain string) DBOption WithAlias(alias string) DBOption WithWebsiteSSLID(sslId uint) DBOption @@ -48,6 +49,15 @@ func (w *WebsiteRepo) WithAppInstallId(appInstallID uint) DBOption { } } +func (w *WebsiteRepo) WithAppInstallIds(appInstallIDs []uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + if len(appInstallIDs) == 0 { + return db + } + return db.Where("app_install_id in (?)", appInstallIDs) + } +} + func (w *WebsiteRepo) WithRuntimeID(runtimeID uint) DBOption { return func(db *gorm.DB) *gorm.DB { return db.Where("runtime_id = ?", runtimeID) diff --git a/agent/app/repo/website_domain.go b/agent/app/repo/website_domain.go index 40258aace01e..36c871f4ed2d 100644 --- a/agent/app/repo/website_domain.go +++ b/agent/app/repo/website_domain.go @@ -13,6 +13,7 @@ type WebsiteDomainRepo struct { type IWebsiteDomainRepo interface { WithWebsiteId(websiteId uint) DBOption + WithWebsiteIds(websiteIds []uint) DBOption WithPort(port int) DBOption WithDomain(domain string) DBOption WithDomainLike(domain string) DBOption @@ -36,6 +37,15 @@ func (w WebsiteDomainRepo) WithWebsiteId(websiteId uint) DBOption { } } +func (w WebsiteDomainRepo) WithWebsiteIds(websiteIds []uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + if len(websiteIds) == 0 { + return db + } + return db.Where("website_id in (?)", websiteIds) + } +} + func (w WebsiteDomainRepo) WithPort(port int) DBOption { return func(db *gorm.DB) *gorm.DB { return db.Where("port = ?", port) diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index aeb490fcc051..cc1bdef17fea 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -31,6 +31,7 @@ type IAgentService interface { Delete(req dto.AgentDeleteReq) error ResetToken(req dto.AgentTokenResetReq) error UpdateRemark(req dto.AgentRemarkUpdateReq) error + BindWebsite(req dto.AgentWebsiteBindReq) error GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error) @@ -289,6 +290,9 @@ func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, erro agentItem.Upgradable = checkAgentUpgradable(appInstall) items = append(items, agentItem) } + if err := hydrateAgentWebsiteItems(items); err != nil { + return 0, nil, err + } return count, items, nil } diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 4981c5b2f582..ec1c673cb6fd 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -357,6 +357,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map Status: agent.Status, Message: agent.Message, AppInstallID: agent.AppInstallID, + WebsiteID: agent.WebsiteID, AccountID: agent.AccountID, ConfigPath: agent.ConfigPath, CreatedAt: agent.CreatedAt, diff --git a/agent/app/service/agents_website.go b/agent/app/service/agents_website.go new file mode 100644 index 000000000000..0d81d8f95b98 --- /dev/null +++ b/agent/app/service/agents_website.go @@ -0,0 +1,301 @@ +package service + +import ( + "context" + "errors" + "fmt" + "net" + "sort" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "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" + "gorm.io/gorm" +) + +func (a AgentService) BindWebsite(req dto.AgentWebsiteBindReq) error { + agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID)) + if err != nil { + return err + } + if agent.WebsiteID != 0 { + return buserr.New("ErrAgentWebsiteBound") + } + + website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + if !isBindableAgentWebsiteType(website.Type) { + return buserr.New("ErrAgentWebsiteTypeUnsupported") + } + + boundAgent, err := agentRepo.GetFirst(repo.WithByWebsiteID(req.WebsiteID)) + if err == nil && boundAgent.ID > 0 { + return buserr.New("ErrAgentWebsiteInUse") + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + agent.WebsiteID = req.WebsiteID + if err := agentRepo.Save(agent); err != nil { + return err + } + return ensureOpenclawWebsiteAllowedOrigin(agent, &website) +} + +func hydrateAgentWebsiteItems(items []dto.AgentItem) error { + explicitWebsiteMap, err := loadAgentWebsiteMapByID(items) + if err != nil { + return err + } + websiteDomainMap, err := loadAgentWebsiteDomainMapByWebsiteID(items) + if err != nil { + return err + } + fillAgentWebsiteItems(items, explicitWebsiteMap, websiteDomainMap) + return nil +} + +func loadAgentWebsiteMapByID(items []dto.AgentItem) (map[uint]model.Website, error) { + websiteIDs := make([]uint, 0, len(items)) + for _, item := range items { + if item.WebsiteID > 0 { + websiteIDs = append(websiteIDs, item.WebsiteID) + } + } + if len(websiteIDs) == 0 { + return map[uint]model.Website{}, nil + } + websites, err := websiteRepo.GetBy(repo.WithByIDs(uniqueUintList(websiteIDs))) + if err != nil { + return nil, err + } + websiteMap := make(map[uint]model.Website, len(websites)) + for _, website := range websites { + websiteMap[website.ID] = website + } + return websiteMap, nil +} + +func loadAgentWebsiteDomainMapByWebsiteID(items []dto.AgentItem) (map[uint][]model.WebsiteDomain, error) { + websiteIDs := make([]uint, 0, len(items)) + for _, item := range items { + if item.WebsiteID > 0 { + websiteIDs = append(websiteIDs, item.WebsiteID) + } + } + if len(websiteIDs) == 0 { + return map[uint][]model.WebsiteDomain{}, nil + } + websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteIds(uniqueUintList(websiteIDs)), repo.WithOrderAsc("id")) + if err != nil { + return nil, err + } + websiteDomainMap := make(map[uint][]model.WebsiteDomain, len(websiteDomains)) + for _, websiteDomain := range websiteDomains { + websiteDomainMap[websiteDomain.WebsiteID] = append(websiteDomainMap[websiteDomain.WebsiteID], websiteDomain) + } + return websiteDomainMap, nil +} + +func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]model.Website, websiteDomainMap map[uint][]model.WebsiteDomain) { + for index := range items { + if items[index].WebsiteID == 0 { + continue + } + website, ok := explicitWebsiteMap[items[index].WebsiteID] + if !ok { + items[index].WebsiteID = 0 + items[index].WebsitePrimaryDomain = "" + items[index].WebsiteProtocol = "" + continue + } + items[index].WebsiteProtocol = website.Protocol + websiteDomains := websiteDomainMap[items[index].WebsiteID] + if len(websiteDomains) == 0 { + items[index].WebsitePrimaryDomain = "" + continue + } + items[index].WebsitePrimaryDomain = websiteDomains[0].Domain + } +} + +func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { + websiteMap := make(map[uint]model.Website) + duplicateAppInstallIDs := make(map[uint]struct{}) + for _, website := range websites { + if website.AppInstallID == 0 { + continue + } + if _, duplicated := duplicateAppInstallIDs[website.AppInstallID]; duplicated { + continue + } + if _, exists := websiteMap[website.AppInstallID]; exists { + delete(websiteMap, website.AppInstallID) + duplicateAppInstallIDs[website.AppInstallID] = struct{}{} + continue + } + websiteMap[website.AppInstallID] = website + } + return websiteMap +} + +func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website { + return uniqueDeploymentWebsiteMapByAppInstall(websites) +} + +func uniqueUintList(items []uint) []uint { + itemMap := make(map[uint]struct{}, len(items)) + uniq := make([]uint, 0, len(items)) + for _, item := range items { + if item == 0 { + continue + } + if _, exists := itemMap[item]; exists { + continue + } + itemMap[item] = struct{}{} + uniq = append(uniq, item) + } + return uniq +} + +func isBindableAgentWebsiteType(websiteType string) bool { + return websiteType == constant.Proxy || websiteType == constant.Deployment || websiteType == constant.Static +} + +func bindDeploymentWebsiteToAgentByAppInstall(website *model.Website) error { + if website.ID == 0 || website.Type != constant.Deployment || website.AppInstallID == 0 { + return nil + } + + appInstall, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if appInstall.App.Key != constant.AppOpenclaw && appInstall.App.Key != constant.AppCopaw { + return nil + } + + agent, err := agentRepo.GetFirst(repo.WithByAppInstallID(website.AppInstallID)) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + if err != nil { + return err + } + if agent.WebsiteID != 0 { + return nil + } + + agent.WebsiteID = website.ID + if err := agentRepo.Save(agent); err != nil { + return err + } + return ensureOpenclawWebsiteAllowedOrigin(agent, website) +} + +func ensureOpenclawWebsiteAllowedOrigin(agent *model.Agent, website *model.Website) error { + if agent == nil || website == nil || agent.AgentType == constant.AppCopaw { + return nil + } + + origins, err := buildWebsiteAllowedOrigins(website) + if err != nil { + return err + } + if len(origins) == 0 { + return nil + } + + install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) + if err != nil { + return err + } + conf, err := readOpenclawConfig(agent.ConfigPath) + if err != nil { + return err + } + + allowedOrigins := extractSecurityConfig(conf).AllowedOrigins + allowedOrigins, err = normalizeAllowedOrigins(append(allowedOrigins, origins...)) + if err != nil { + return err + } + setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins}) + if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { + return err + } + if err := syncOpenclawAllowedOriginEnv(&install, allowedOrigins); err != nil { + return err + } + return appInstallRepo.Save(context.Background(), &install) +} + +func buildWebsiteAllowedOrigins(website *model.Website) ([]string, error) { + websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID), repo.WithOrderAsc("id")) + if err != nil { + return nil, err + } + if len(websiteDomains) == 0 { + return nil, nil + } + defaultHTTPSPort := 443 + if strings.EqualFold(website.Protocol, "https") { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err == nil && nginxInstall.HttpsPort > 0 { + defaultHTTPSPort = nginxInstall.HttpsPort + } + } + return buildWebsiteAllowedOriginsFromDomains(website, websiteDomains, defaultHTTPSPort) +} + +func buildWebsiteAllowedOriginsFromDomains(website *model.Website, websiteDomains []model.WebsiteDomain, defaultHTTPSPort int) ([]string, error) { + if len(websiteDomains) == 0 { + return nil, nil + } + sort.Slice(websiteDomains, func(i, j int) bool { + return websiteDomains[i].ID < websiteDomains[j].ID + }) + origins := make([]string, 0, len(websiteDomains)) + for _, websiteDomain := range websiteDomains { + origin, err := buildWebsiteDomainOrigin(website.Protocol, websiteDomain, defaultHTTPSPort) + if err != nil { + return nil, err + } + origins = append(origins, origin) + } + return origins, nil +} + +func buildWebsiteDomainOrigin(protocol string, websiteDomain model.WebsiteDomain, defaultHTTPSPort int) (string, error) { + origin := strings.ToLower(strings.TrimSpace(protocol)) + "://" + formatWebsiteDomainHost(websiteDomain.Domain) + switch strings.ToLower(strings.TrimSpace(protocol)) { + case "http": + if websiteDomain.Port > 0 && websiteDomain.Port != 80 { + origin = fmt.Sprintf("%s:%d", origin, websiteDomain.Port) + } + case "https": + port := websiteDomain.Port + if !websiteDomain.SSL { + port = defaultHTTPSPort + } + if port > 0 && port != 443 { + origin = fmt.Sprintf("%s:%d", origin, port) + } + } + return normalizeAllowedOrigin(origin) +} + +func formatWebsiteDomainHost(domain string) string { + host := strings.Trim(strings.TrimSpace(domain), "[]") + if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") { + return "[" + host + "]" + } + return host +} diff --git a/agent/app/service/website.go b/agent/app/service/website.go index fffd5d4a55d3..3be6051002eb 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -538,7 +538,13 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) createTask.AddSubTaskWithIgnoreErr(i18n.GetWithName("ConfigFTP", create.FtpUser), createFtpUser) } - return createTask.Execute() + if err := createTask.Execute(); err != nil { + return err + } + if err := bindDeploymentWebsiteToAgentByAppInstall(website); err != nil { + global.LOG.Errorf("bind deployment website to agent failed: %v", err) + } + return nil } func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { @@ -733,6 +739,9 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error { if err := websiteRepo.DeleteBy(ctx, repo.WithByID(req.ID)); err != nil { return err } + if err := agentRepo.ClearWebsiteIDByWebsiteIDWithCtx(ctx, req.ID); err != nil { + return err + } if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil { return err } diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 091b26275093..36d8482afa22 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -59,6 +59,9 @@ ErrAgentComposeRequired: 'Compose content is required' ErrAgentIDRequired: 'Agent ID is required' ErrAgentAccountIDRequired: 'Account ID is required' ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Pro Edition for unlimited agents' +ErrAgentWebsiteBound: 'This agent is already bound to a website' +ErrAgentWebsiteTypeUnsupported: 'Only proxy, static, or deployment websites can be bound' +ErrAgentWebsiteInUse: 'This website is already bound to another agent' #backup Localhost: 'Local' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index eb841f611035..ebdea368d56f 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Contenido compose requerido' ErrAgentIDRequired: 'ID de agente requerido' ErrAgentAccountIDRequired: 'ID de cuenta requerido' ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualiza a la edición Pro para usar agentes ilimitados' +ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web' +ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy, estáticos o deployment' +ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente' Localhost: 'Máquina local' ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada' ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index bd9c18fd7026..142e916c9f51 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Composeが必要です' ErrAgentIDRequired: 'エージェントIDが必要です' ErrAgentAccountIDRequired: 'アカウントIDが必要です' ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Pro Edition にアップグレードすると無制限で利用できます' +ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています' +ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイト、静的サイト、またはデプロイメントサイトのみです' +ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています' Localhost: 'ローカルマシン' ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中' ErrBackupCheck: '接続テストに失敗しました: {{ .err }}' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 8bb439b74ed0..cf7390f498d1 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Compose 내용 필요' ErrAgentIDRequired: '에이전트 ID 필요' ErrAgentAccountIDRequired: '계정 ID 필요' ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 만들 수 있습니다. Pro Edition으로 업그레이드하면 제한 없이 사용할 수 있습니다' +ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다' +ErrAgentWebsiteTypeUnsupported: '프록시, 정적 또는 배포 웹사이트만 연결할 수 있습니다' +ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다' Localhost: '로컬 머신' ErrBackupInUsed: '백업 계정이 예약에 사용 중' ErrBackupCheck: '연결 테스트 실패: {{ .err }}' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index f3ed47a8642c..be8bb2620d60 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Kandungan compose diperlukan' ErrAgentIDRequired: 'ID ejen diperlukan' ErrAgentAccountIDRequired: 'ID akaun diperlukan' ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke Edisi Pro untuk penggunaan tanpa had' +ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web' +ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy, statik atau deployment boleh dipautkan' +ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain' Localhost: 'Mesin Tempatan' ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas' ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 5f5f3ff05f5a..72eec070b584 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Conteúdo de compose obrigatório' ErrAgentIDRequired: 'ID do agente obrigatório' ErrAgentAccountIDRequired: 'ID da conta obrigatório' ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Faça upgrade para a edição Pro para usar agentes ilimitados' +ErrAgentWebsiteBound: 'Este agente já está vinculado a um site' +ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy, estáticos ou deployment podem ser vinculados' +ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente' Localhost: 'Máquina Local' ErrBackupInUsed: 'Conta de backup em uso por tarefa' ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index bdfb416e440d..5107c13f7a2c 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Нужен compose' ErrAgentIDRequired: 'Нужен ID агента' ErrAgentAccountIDRequired: 'Нужен ID аккаунта' ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Обновитесь до Pro Edition, чтобы использовать их без ограничений' +ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом' +ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy-, static- или deployment-сайты' +ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом' Localhost: 'Локальная машина' ErrBackupInUsed: 'Аккаунт бэкапа занят задачей' ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index f987a21bbef0..c548f5b65184 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Compose içeriği gerekli' ErrAgentIDRequired: 'Ajans ID gerekli' ErrAgentAccountIDRequired: 'Hesap ID gerekli' ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırsız kullanım için Pro Sürüme yükseltin' +ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı' +ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy, statik veya deployment web siteleri bağlanabilir' +ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı' Localhost: 'Yerel Makine' ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor' ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 7b724ed56a03..80dac06b41bd 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -54,6 +54,9 @@ ErrAgentComposeRequired: '自訂編排內容不可為空,請重試' ErrAgentIDRequired: '智能體 ID 不可為空,請重試' ErrAgentAccountIDRequired: '帳號 ID 不可為空,請重試' ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體,升級至 Pro 版即可無限制使用' +ErrAgentWebsiteBound: '該智能體已關聯網站' +ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理、靜態網站或一鍵部署網站' +ErrAgentWebsiteInUse: '該網站已被其他智能體關聯' Localhost: '本機' ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除' ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 465d798a573a..ef14aa1d7e1d 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -59,6 +59,9 @@ ErrAgentComposeRequired: "自定义编排内容不能为空" ErrAgentIDRequired: "智能体 ID 不能为空" ErrAgentAccountIDRequired: "账号 ID 不能为空" ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至专业版无数量限制" +ErrAgentWebsiteBound: "该智能体已关联网站" +ErrAgentWebsiteTypeUnsupported: "只能关联反向代理、静态网站或一键部署网站" +ErrAgentWebsiteInUse: "该网站已被其他智能体关联" #backup Localhost: '本机' diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 920f58a9ca0b..fc2b493792ce 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -78,6 +78,7 @@ func InitAgentDB() { migrations.UpdateAgentQuickJumpTitle, migrations.FixOpenclaw20260323HTTPPort, migrations.AddAgentRemarkColumn, + migrations.AddAgentWebsiteBinding, }) 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 4ee55a676540..e807f3e667f4 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -1158,3 +1158,39 @@ var AddAgentRemarkColumn = &gormigrate.Migration{ return tx.AutoMigrate(&model.Agent{}) }, } + +var AddAgentWebsiteBinding = &gormigrate.Migration{ + ID: "20260403-add-agent-website-binding", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.Agent{}); err != nil { + return err + } + + var agents []model.Agent + if err := tx.Find(&agents).Error; err != nil { + return err + } + if len(agents) == 0 { + return nil + } + + var websites []model.Website + if err := tx.Where("type = ? AND app_install_id > 0", constant.Deployment).Find(&websites).Error; err != nil { + return err + } + websiteMap := service.UniqueDeploymentWebsiteMapForMigration(websites) + for _, agent := range agents { + if agent.WebsiteID != 0 || agent.AppInstallID == 0 { + continue + } + website, ok := websiteMap[agent.AppInstallID] + if !ok { + continue + } + if err := tx.Model(&model.Agent{}).Where("id = ?", agent.ID).Update("website_id", website.ID).Error; err != nil { + return err + } + } + return nil + }, +} diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index c23b104ca505..59c12879f948 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -45,6 +45,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent) aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken) aiToolsRouter.POST("/agents/remark", baseApi.UpdateAgentRemark) + aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite) aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig) aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig) aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview) diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 902e4ebc9030..7b2e536aeb66 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -278,6 +278,9 @@ export namespace AI { status: string; message: string; appInstallId: number; + websiteId: number; + websitePrimaryDomain: string; + websiteProtocol: string; accountId: number; appVersion: string; containerName: string; @@ -304,6 +307,11 @@ export namespace AI { remark: string; } + export interface AgentWebsiteBindReq { + agentId: number; + websiteId: number; + } + export interface AgentModelConfigUpdateReq { agentId: number; accountId: number; diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 3ee30ffebb89..79be407603f3 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -138,6 +138,12 @@ export namespace Website { types?: string[]; } + export interface WebsiteOption { + id: number; + primaryDomain: string; + alias: string; + } + export interface WebSiteLog { enable: boolean; content: string; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 114421115506..36b518bb836e 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -113,6 +113,10 @@ export const updateAgentRemark = (req: AI.AgentRemarkUpdateReq) => { return http.post(`/ai/agents/remark`, req); }; +export const bindAgentWebsite = (req: AI.AgentWebsiteBindReq) => { + return http.post(`/ai/agents/website/bind`, req); +}; + export const getAgentModelConfig = (req: AI.AgentIDReq) => { return http.post(`/ai/agents/model/get`, req); }; diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 67d852268681..860ec860d1e9 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -41,7 +41,7 @@ export const getWebsite = (id: number) => { }; export const getWebsiteOptions = (req: Website.OptionReq) => { - return http.post(`/websites/options`, req); + return http.post(`/websites/options`, req); }; export const getWebsiteConfig = (id: number, type: string) => { diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index b89a3821cb7a..e4be657e7e69 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -101,6 +101,43 @@ + + +