mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
feat: Add website binding support for the agent (#12413)
This commit is contained in:
@@ -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
|
||||
|
||||
+33
-25
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Composeが必要です'
|
||||
ErrAgentIDRequired: 'エージェントIDが必要です'
|
||||
ErrAgentAccountIDRequired: 'アカウントIDが必要です'
|
||||
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Pro Edition にアップグレードすると無制限で利用できます'
|
||||
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
|
||||
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイト、静的サイト、またはデプロイメントサイトのみです'
|
||||
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
|
||||
Localhost: 'ローカルマシン'
|
||||
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
|
||||
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
|
||||
|
||||
@@ -54,6 +54,9 @@ ErrAgentComposeRequired: 'Compose 내용 필요'
|
||||
ErrAgentIDRequired: '에이전트 ID 필요'
|
||||
ErrAgentAccountIDRequired: '계정 ID 필요'
|
||||
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 만들 수 있습니다. Pro Edition으로 업그레이드하면 제한 없이 사용할 수 있습니다'
|
||||
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
|
||||
ErrAgentWebsiteTypeUnsupported: '프록시, 정적 또는 배포 웹사이트만 연결할 수 있습니다'
|
||||
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
|
||||
Localhost: '로컬 머신'
|
||||
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
|
||||
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -54,6 +54,9 @@ ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
|
||||
ErrAgentIDRequired: '智能體 ID 不可為空,請重試'
|
||||
ErrAgentAccountIDRequired: '帳號 ID 不可為空,請重試'
|
||||
ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體,升級至 Pro 版即可無限制使用'
|
||||
ErrAgentWebsiteBound: '該智能體已關聯網站'
|
||||
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理、靜態網站或一鍵部署網站'
|
||||
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
|
||||
Localhost: '本機'
|
||||
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
|
||||
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentComposeRequired: "自定义编排内容不能为空"
|
||||
ErrAgentIDRequired: "智能体 ID 不能为空"
|
||||
ErrAgentAccountIDRequired: "账号 ID 不能为空"
|
||||
ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至专业版无数量限制"
|
||||
ErrAgentWebsiteBound: "该智能体已关联网站"
|
||||
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理、静态网站或一键部署网站"
|
||||
ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
|
||||
|
||||
#backup
|
||||
Localhost: '本机'
|
||||
|
||||
@@ -78,6 +78,7 @@ func InitAgentDB() {
|
||||
migrations.UpdateAgentQuickJumpTitle,
|
||||
migrations.FixOpenclaw20260323HTTPPort,
|
||||
migrations.AddAgentRemarkColumn,
|
||||
migrations.AddAgentWebsiteBinding,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.AgentModelConfig>(`/ai/agents/model/get`, req);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const getWebsite = (id: number) => {
|
||||
};
|
||||
|
||||
export const getWebsiteOptions = (req: Website.OptionReq) => {
|
||||
return http.post<any>(`/websites/options`, req);
|
||||
return http.post<Website.WebsiteOption[]>(`/websites/options`, req);
|
||||
};
|
||||
|
||||
export const getWebsiteConfig = (id: number, type: string) => {
|
||||
|
||||
@@ -101,6 +101,43 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('menu.website')" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.websiteId > 0" class="website-link-cell">
|
||||
<el-text type="primary" class="cursor-pointer" @click="openWebsite(row)">
|
||||
{{ getWebsiteDisplayName(row) }}
|
||||
</el-text>
|
||||
<el-popover
|
||||
placement="right"
|
||||
trigger="hover"
|
||||
:width="420"
|
||||
@before-enter="loadWebsiteDomains(row.websiteId)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button link icon="Promotion" class="ml-2.5"></el-button>
|
||||
</template>
|
||||
<table v-if="getWebsiteBaseUrls(row).length > 0">
|
||||
<tbody>
|
||||
<tr v-for="url in getWebsiteBaseUrls(row)" :key="url">
|
||||
<td>
|
||||
<el-button type="primary" link @click="openWebsiteUrl(url, row)">
|
||||
{{ url }}
|
||||
</el-button>
|
||||
</td>
|
||||
<td>
|
||||
<CopyButton :content="url" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<el-empty v-else :image-size="48" :description="$t('commons.msg.noneData')" />
|
||||
</el-popover>
|
||||
</div>
|
||||
<el-button v-else link type="primary" @click="openBindWebsite(row)">
|
||||
{{ $t('commons.button.bind') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('website.remark')" prop="remark" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<fu-read-write-switch>
|
||||
@@ -155,6 +192,7 @@
|
||||
<DeleteDialog ref="deleteRef" @close="search" />
|
||||
<ConfigDrawer ref="configRef" @updated="search" />
|
||||
<OverviewDrawer ref="overviewRef" />
|
||||
<BindWebsiteDialog ref="bindWebsiteRef" @success="search" />
|
||||
<AppUpgrade ref="upgradeRef" @close="search" />
|
||||
<ComposeLogs ref="composeLogRef" />
|
||||
<AgentTerminalDialog ref="dialogTerminalRef" />
|
||||
@@ -166,9 +204,10 @@
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { pageAgents, resetAgentToken, updateAgentRemark } from '@/api/modules/ai';
|
||||
import { installedOp, searchApp, searchAppInstalled } from '@/api/modules/app';
|
||||
import { checkAppInstalled, installedOp, searchApp, searchAppInstalled } from '@/api/modules/app';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { App } from '@/api/interface/app';
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { SearchWithPage } from '@/api/interface';
|
||||
import { dateFormat, newUUID } from '@/utils/util';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
@@ -177,6 +216,7 @@ import AddDialog from '@/views/ai/agents/agent/add/index.vue';
|
||||
import DeleteDialog from '@/views/ai/agents/agent/delete/index.vue';
|
||||
import ConfigDrawer from '@/views/ai/agents/agent/config/index.vue';
|
||||
import OverviewDrawer from '@/views/ai/agents/agent/components/overview.vue';
|
||||
import BindWebsiteDialog from '@/views/ai/agents/agent/website/index.vue';
|
||||
import AppUpgrade from '@/views/app-store/installed/upgrade/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import ComposeLogs from '@/components/log/compose/index.vue';
|
||||
@@ -186,6 +226,7 @@ import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import { getAgentProviderDisplayName, getOpenclawAccessScheme } from '@/utils/agent';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
import { listDomains } from '@/api/modules/website';
|
||||
import NoApp from '@/views/app-store/apps/no-app/index.vue';
|
||||
import openclawIcon from '@/assets/images/ai-agent-openclaw.svg';
|
||||
import copawIcon from '@/assets/images/ai-agent-copaw.svg';
|
||||
@@ -197,6 +238,7 @@ const taskLogRef = ref();
|
||||
const deleteRef = ref();
|
||||
const configRef = ref();
|
||||
const overviewRef = ref();
|
||||
const bindWebsiteRef = ref();
|
||||
const upgradeRef = ref();
|
||||
const composeLogRef = ref();
|
||||
const dialogTerminalRef = ref();
|
||||
@@ -207,6 +249,9 @@ const isActive = ref(false);
|
||||
const isExist = ref(false);
|
||||
const noApp = ref(false);
|
||||
const searchName = ref('');
|
||||
const defaultHttpsPort = ref(443);
|
||||
const openrestyPortLoaded = ref(false);
|
||||
const websiteDomainsMap = ref<Record<number, Website.Domain[]>>({});
|
||||
|
||||
const headerButtons = [
|
||||
{
|
||||
@@ -290,9 +335,10 @@ const search = async () => {
|
||||
pageSize: paginationConfig.pageSize,
|
||||
info: searchName.value || '',
|
||||
};
|
||||
const [res] = await Promise.all([pageAgents(req), checkNoApp()]);
|
||||
const [res] = await Promise.all([pageAgents(req), checkNoApp(), loadOpenrestyHttpsPort()]);
|
||||
items.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total || 0;
|
||||
await preloadWebsiteDomains(items.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -429,6 +475,109 @@ const openOverview = (row: AI.AgentItem) => {
|
||||
overviewRef.value?.acceptParams(row);
|
||||
};
|
||||
|
||||
const openBindWebsite = (row: AI.AgentItem) => {
|
||||
bindWebsiteRef.value?.acceptParams(row);
|
||||
};
|
||||
|
||||
const loadOpenrestyHttpsPort = async () => {
|
||||
if (openrestyPortLoaded.value) {
|
||||
return;
|
||||
}
|
||||
openrestyPortLoaded.value = true;
|
||||
try {
|
||||
const res = await checkAppInstalled('openresty', '');
|
||||
defaultHttpsPort.value = res.data.httpsPort || 443;
|
||||
} catch {
|
||||
defaultHttpsPort.value = 443;
|
||||
}
|
||||
};
|
||||
|
||||
const sortWebsiteDomains = (domains: Website.Domain[]) => {
|
||||
return [...domains].sort((a, b) => a.id - b.id);
|
||||
};
|
||||
|
||||
const loadWebsiteDomains = async (websiteId: number) => {
|
||||
if (!websiteId) {
|
||||
return [];
|
||||
}
|
||||
if (websiteDomainsMap.value[websiteId]) {
|
||||
return websiteDomainsMap.value[websiteId];
|
||||
}
|
||||
const res = await listDomains(websiteId);
|
||||
const domains = sortWebsiteDomains(res.data || []);
|
||||
websiteDomainsMap.value = {
|
||||
...websiteDomainsMap.value,
|
||||
[websiteId]: domains,
|
||||
};
|
||||
return domains;
|
||||
};
|
||||
|
||||
const preloadWebsiteDomains = async (rows: AI.AgentItem[]) => {
|
||||
const websiteIDs = [...new Set(rows.map((row) => row.websiteId).filter((websiteId) => websiteId > 0))];
|
||||
await Promise.allSettled(websiteIDs.map((websiteId) => loadWebsiteDomains(websiteId)));
|
||||
};
|
||||
|
||||
const formatWebsiteHost = (domain: string) => {
|
||||
const host = String(domain || '')
|
||||
.trim()
|
||||
.replace(/^\[|\]$/g, '');
|
||||
return host.includes(':') ? `[${host}]` : host;
|
||||
};
|
||||
|
||||
const buildWebsiteBaseUrl = (domain: Website.Domain, row: AI.AgentItem) => {
|
||||
const protocol = (row.websiteProtocol || 'http').toLowerCase();
|
||||
let url = `${protocol}://${formatWebsiteHost(domain.domain)}`;
|
||||
if (protocol === 'http') {
|
||||
if (domain.port && domain.port !== 80) {
|
||||
url = `${url}:${domain.port}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
let port = domain.port;
|
||||
if (!domain.ssl) {
|
||||
port = defaultHttpsPort.value || 443;
|
||||
}
|
||||
if (port && port !== 443) {
|
||||
url = `${url}:${port}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const appendWebsiteAgentToken = (url: string, row: AI.AgentItem) => {
|
||||
if (row.agentType !== 'copaw' && row.token) {
|
||||
const target = new URL(url);
|
||||
target.hash = `token=${row.token}`;
|
||||
return target.toString();
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const getWebsiteBaseUrls = (row: AI.AgentItem) => {
|
||||
const domains = websiteDomainsMap.value[row.websiteId] || [];
|
||||
return domains.map((domain) => buildWebsiteBaseUrl(domain, row));
|
||||
};
|
||||
|
||||
const getWebsiteDisplayName = (row: AI.AgentItem) => {
|
||||
const domains = websiteDomainsMap.value[row.websiteId] || [];
|
||||
return domains[0]?.domain || row.websitePrimaryDomain || '-';
|
||||
};
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
const openWebsiteUrl = (url: string, row: AI.AgentItem) => {
|
||||
openUrl(appendWebsiteAgentToken(url, row));
|
||||
};
|
||||
|
||||
const openWebsite = async (row: AI.AgentItem) => {
|
||||
const domains = await loadWebsiteDomains(row.websiteId);
|
||||
if (domains.length === 0) {
|
||||
return;
|
||||
}
|
||||
openWebsiteUrl(buildWebsiteBaseUrl(domains[0], row), row);
|
||||
};
|
||||
|
||||
const openUpgrade = async (row: AI.AgentItem) => {
|
||||
const res = await searchAppInstalled({ page: 1, pageSize: 200, name: row.name, update: true });
|
||||
const appInstall = (res.data.items || []).find((item: App.AppInstallDto) => item.id === row.appInstallId);
|
||||
@@ -463,4 +612,10 @@ onMounted(async () => {
|
||||
flex: 0 0 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.website-link-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="open"
|
||||
:header="$t('commons.button.bind') + $t('menu.website')"
|
||||
:resource="agentName"
|
||||
size="736"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" v-loading="loading" label-position="top" :model="form" :rules="rules" @submit.prevent>
|
||||
<el-form-item :label="$t('menu.website')" prop="websiteId">
|
||||
<el-select v-model="form.websiteId" clearable filterable>
|
||||
<el-option
|
||||
v-for="website in websites"
|
||||
:key="website.id"
|
||||
:label="website.primaryDomain"
|
||||
:value="website.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="open = false" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, FormRules } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { bindAgentWebsite } from '@/api/modules/ai';
|
||||
import { getWebsiteOptions } from '@/api/modules/website';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const agentName = ref('');
|
||||
const websites = ref<Website.WebsiteOption[]>([]);
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive<AI.AgentWebsiteBindReq>({
|
||||
agentId: 0,
|
||||
websiteId: 0,
|
||||
});
|
||||
const rules = reactive<FormRules>({
|
||||
websiteId: [Rules.requiredSelectBusiness],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ (e: 'success'): void }>();
|
||||
|
||||
const acceptParams = async (row: AI.AgentItem) => {
|
||||
form.agentId = row.id;
|
||||
form.websiteId = 0;
|
||||
agentName.value = row.name;
|
||||
open.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getWebsiteOptions({ types: ['proxy', 'deployment', 'static'] });
|
||||
websites.value = res.data || [];
|
||||
form.websiteId = websites.value[0]?.id || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
websites.value = [];
|
||||
form.agentId = 0;
|
||||
form.websiteId = 0;
|
||||
agentName.value = '';
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
await bindAgentWebsite(form);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
open.value = false;
|
||||
emit('success');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user