diff --git a/agent/app/api/v2/helper/helper.go b/agent/app/api/v2/helper/helper.go index 56f02ab91..a44c11e98 100644 --- a/agent/app/api/v2/helper/helper.go +++ b/agent/app/api/v2/helper/helper.go @@ -12,7 +12,6 @@ import ( "sync" "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/mcpapi" "gorm.io/gorm" "github.com/1Panel-dev/1Panel/agent/app/dto" @@ -23,7 +22,7 @@ import ( ) func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { - mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) + applyErrorMetadata(ctx, code, msgKey, err) res := dto.Response{ Code: code, Message: "", @@ -34,7 +33,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { } func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) { - mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) + applyErrorMetadata(ctx, code, msgKey, err) res := dto.Response{ Code: code, ErrorCode: businessCode, @@ -45,7 +44,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri } func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) { - mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) + applyErrorMetadata(ctx, code, msgKey, err) res := dto.Response{ Code: code, Data: data, @@ -209,3 +208,26 @@ func SetOperationLogValues(c *gin.Context, values map[string]string) { c.Header(header, base64.RawURLEncoding.EncodeToString(data)) } } + +const errorMetadataKey = "error_response_metadata_policies" + +// ErrorMetadataPolicy adds transport metadata before the existing error response is written. +type ErrorMetadataPolicy func(*gin.Context, int, string, error) + +func WithErrorMetadata(policies ...ErrorMetadataPolicy) gin.HandlerFunc { + configured := append([]ErrorMetadataPolicy(nil), policies...) + return func(c *gin.Context) { + c.Set(errorMetadataKey, configured) + c.Next() + } +} + +func applyErrorMetadata(c *gin.Context, code int, messageKey string, err error) { + value, _ := c.Get(errorMetadataKey) + policies, _ := value.([]ErrorMetadataPolicy) + for _, policy := range policies { + if policy != nil { + policy(c, code, messageKey, err) + } + } +} diff --git a/agent/app/dto/request/mcp_website.go b/agent/app/dto/request/mcp_website.go index 1e2e78a38..e6462439f 100644 --- a/agent/app/dto/request/mcp_website.go +++ b/agent/app/dto/request/mcp_website.go @@ -1,14 +1,15 @@ package request type MCPWebsiteBinding struct { - Operation string `json:"operation"` - RequestID string `json:"requestId"` - BindingID string `json:"bindingId"` - WebsiteID uint `json:"websiteId"` - Domain string `json:"domain"` - EnableSSL bool `json:"enableSSL"` - SSLID uint `json:"sslID"` - Upstream string `json:"upstream"` - CoreHost string `json:"coreHost"` - ProxyKey string `json:"proxyKey"` + Operation string `json:"operation"` + RequestID string `json:"requestId"` + BindingID string `json:"bindingId"` + WebsiteID uint `json:"websiteId"` + Domain string `json:"domain"` + EnableSSL bool `json:"enableSSL"` + SSLID uint `json:"sslID"` + Upstream string `json:"upstream"` + CoreHost string `json:"coreHost"` + ProxyKey string `json:"proxyKey"` + DeleteWebsite bool `json:"deleteWebsite,omitempty"` } diff --git a/agent/app/dto/response/mcp_website.go b/agent/app/dto/response/mcp_website.go index e89e7c11e..35cdddded 100644 --- a/agent/app/dto/response/mcp_website.go +++ b/agent/app/dto/response/mcp_website.go @@ -1,17 +1,18 @@ package response type MCPWebsiteBinding struct { - State string `json:"state"` - WebsiteID uint `json:"websiteId"` - BindingID string `json:"bindingId"` - RequestID string `json:"requestId"` - Domain string `json:"domain"` - Endpoint string `json:"endpoint"` - EnableSSL bool `json:"enableSSL"` - SSLID uint `json:"sslID"` - WebsiteStatus string `json:"websiteStatus"` - Reason string `json:"reason"` - Code string `json:"code,omitempty"` - Error string `json:"error,omitempty"` - Definitive bool `json:"definitive,omitempty"` + State string `json:"state"` + WebsiteID uint `json:"websiteId"` + BindingID string `json:"bindingId"` + RequestID string `json:"requestId"` + Domain string `json:"domain"` + Endpoint string `json:"endpoint"` + EnableSSL bool `json:"enableSSL"` + SSLID uint `json:"sslID"` + WebsiteStatus string `json:"websiteStatus"` + Reason string `json:"reason"` + Code string `json:"code,omitempty"` + Error string `json:"error,omitempty"` + Definitive bool `json:"definitive,omitempty"` + WebsiteDeleted bool `json:"websiteDeleted,omitempty"` } diff --git a/agent/app/service/mcp_website_binding.go b/agent/app/service/mcp_website_binding.go index 4b945cfe9..c750bc98c 100644 --- a/agent/app/service/mcp_website_binding.go +++ b/agent/app/service/mcp_website_binding.go @@ -17,14 +17,17 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/response" "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/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/nginx" "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" "github.com/docker/docker/api/types/container" @@ -52,6 +55,7 @@ func init() { type MCPWebsiteBindingService struct { createWebsite func(request.WebsiteCreate) error + deleteWebsite func(request.WebsiteDelete) error updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error applyInclude func(model.Website) error removeInclude func(model.Website) error @@ -63,6 +67,7 @@ type MCPWebsiteBindingService struct { func NewMCPWebsiteBindingService() *MCPWebsiteBindingService { s := &MCPWebsiteBindingService{ createWebsite: func(req request.WebsiteCreate) error { return NewIWebsiteService().CreateWebsite(req) }, + deleteWebsite: func(req request.WebsiteDelete) error { return NewIWebsiteService().DeleteWebsite(req) }, updateHTTPS: func(ctx context.Context, req request.WebsiteHTTPSOp) error { if req.Enable { current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID) @@ -140,14 +145,22 @@ func mcpWebsiteHasWWWMount(mounts []container.MountPoint, root string) bool { } type mcpWebsiteOwner struct { - BindingID string `json:"bindingId"` - WebsiteID uint `json:"websiteId"` - KeyHash string `json:"keyHash"` - BindHash string `json:"bindHash"` - ProxyHash string `json:"proxyHash"` - RequestID string `json:"requestId"` - RequestHash string `json:"requestHash"` - Operation string `json:"operation"` + BindingID string `json:"bindingId"` + WebsiteID uint `json:"websiteId"` + KeyHash string `json:"keyHash"` + BindHash string `json:"bindHash"` + ProxyHash string `json:"proxyHash"` + RequestID string `json:"requestId"` + RequestHash string `json:"requestHash"` + Operation string `json:"operation"` + DeleteRoot string `json:"deleteRoot,omitempty"` + DeletePaths []mcpWebsiteDeletePath `json:"deletePaths,omitempty"` +} + +type mcpWebsiteDeletePath struct { + Kind string `json:"kind"` + Device uint64 `json:"device"` + Inode uint64 `json:"inode"` } func mcpWebsiteHash(value string) string { @@ -236,6 +249,9 @@ func mcpWebsiteFailure(reason string, definitive, conflict bool) response.MCPWeb if conflict { code, message = "MCP_WEBSITE_CONFLICT", "MCP website configuration changed" } + if definitive && reason == "http_port_requires_default" { + code, message = "MCP_WEBSITE_HTTP_PORT_REQUIRED", "Use the OpenResty HTTP port before enabling HTTPS" + } return response.MCPWebsiteBinding{State: "unavailable", Reason: reason, Code: code, Error: message, Definitive: definitive} } @@ -461,13 +477,190 @@ func mcpWebsiteReadProxy(path string) ([]byte, error) { } func mcpWebsitePayloadHash(req request.MCPWebsiteBinding) string { - return mcpWebsiteHash(fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID)) + payload := fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID) + if req.DeleteWebsite { + payload += "\ndeleteWebsite=true" + } + return mcpWebsiteHash(payload) } func mcpWebsiteBindHash(req request.MCPWebsiteBinding) string { return mcpWebsiteHash(fmt.Sprintf("%s\n%t\n%d", req.Domain, req.EnableSSL, req.SSLID)) } +func mcpWebsiteDeleteLocations(site model.Website) map[string]string { + return map[string]string{"site": GetSitePath(site, SiteDir), "config": GetSitePath(site, SiteConf)} +} + +func mcpWebsiteDeleteParents() error { + root := filepath.Clean(GetWebSiteRootDir()) + if !filepath.IsAbs(root) { + return errors.New("invalid managed website root") + } + for _, directory := range []string{root, filepath.Join(root, "sites"), filepath.Join(root, "conf.d")} { + info, err := os.Lstat(directory) + if os.IsNotExist(err) { + continue + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return errors.New("managed website directory changed") + } + } + return nil +} + +func mcpWebsiteDeleteIdentity(kind, location string) (mcpWebsiteDeletePath, bool, error) { + info, err := os.Lstat(location) + if os.IsNotExist(err) { + return mcpWebsiteDeletePath{}, false, nil + } + if err != nil || info.Mode()&os.ModeSymlink != 0 || kind == "site" && !info.IsDir() || kind == "config" && !info.Mode().IsRegular() { + return mcpWebsiteDeletePath{}, false, errors.New("managed website path changed") + } + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return mcpWebsiteDeletePath{}, false, errors.New("managed website path identity is unavailable") + } + return mcpWebsiteDeletePath{Kind: kind, Device: uint64(stat.Dev), Inode: uint64(stat.Ino)}, true, nil +} + +func mcpWebsiteRecordDeletePaths(site model.Website, owner *mcpWebsiteOwner) error { + if err := mcpWebsiteDeleteParents(); err != nil { + return err + } + owner.DeleteRoot, owner.DeletePaths = filepath.Clean(GetWebSiteRootDir()), nil + for kind, location := range mcpWebsiteDeleteLocations(site) { + identity, exists, err := mcpWebsiteDeleteIdentity(kind, location) + if err != nil { + return err + } + if exists { + owner.DeletePaths = append(owner.DeletePaths, identity) + } + } + return nil +} + +func mcpWebsiteVerifyDeletePaths(site model.Website, owner mcpWebsiteOwner) (map[string]string, error) { + if owner.DeleteRoot == "" || owner.DeleteRoot != filepath.Clean(GetWebSiteRootDir()) { + return nil, errors.New("managed website root changed") + } + if err := mcpWebsiteDeleteParents(); err != nil { + return nil, err + } + expected := make(map[string]mcpWebsiteDeletePath) + for _, identity := range owner.DeletePaths { + if identity.Kind != "site" && identity.Kind != "config" || expected[identity.Kind].Kind != "" { + return nil, errors.New("invalid managed website deletion journal") + } + expected[identity.Kind] = identity + } + remaining := make(map[string]string) + for kind, location := range mcpWebsiteDeleteLocations(site) { + actual, exists, err := mcpWebsiteDeleteIdentity(kind, location) + if err != nil { + return nil, err + } + if exists { + if expected[kind] != actual { + return nil, errors.New("managed website path was replaced") + } + remaining[kind] = location + } + } + return remaining, nil +} + +func mcpWebsiteCleanupDeletedPrivate(site model.Website) error { + if err := mcpWebsitePrivateDirectory(site, false); err != nil { + if _, statErr := os.Lstat(filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName))); os.IsNotExist(statErr) { + return nil + } + return err + } + if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) { + return errors.New("managed proxy remains after website deletion") + } + if err := os.Remove(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); err != nil && !os.IsNotExist(err) { + return err + } + directory := filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)) + entries, err := os.ReadDir(directory) + if err != nil { + return err + } + if len(entries) == 0 { + return os.Remove(directory) + } + return nil +} + +func (s *MCPWebsiteBindingService) deletedWebsite(req request.MCPWebsiteBinding, site model.Website, owner mcpWebsiteOwner, recoverFiles bool) response.MCPWebsiteBinding { + if replacement, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) { + if err == nil && replacement.ID == site.ID { + return mcpWebsiteFailure("website_delete_unconfirmed", false, false) + } + return mcpWebsiteFailure("website_ownership_changed", false, true) + } + remaining, err := mcpWebsiteVerifyDeletePaths(site, owner) + if err != nil { + return mcpWebsiteFailure("website_directory_changed", false, true) + } + if len(remaining) > 0 && !recoverFiles { + return mcpWebsiteFailure("website_delete_unconfirmed", false, false) + } + for kind, location := range remaining { + fileOp := files.NewFileOp() + if kind == "site" { + err = fileOp.DeleteDir(location) + } else { + err = fileOp.DeleteFile(location) + } + if err != nil { + return mcpWebsiteFailure("website_delete_failed", false, false) + } + } + remaining, err = mcpWebsiteVerifyDeletePaths(site, owner) + if err != nil || len(remaining) != 0 { + return mcpWebsiteFailure("website_delete_unconfirmed", false, false) + } + if err = mcpWebsiteCleanupDeletedPrivate(site); err != nil { + return mcpWebsiteFailure("ownership_cleanup_failed", false, false) + } + return response.MCPWebsiteBinding{State: "unbound", WebsiteID: site.ID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true} +} + +func (s *MCPWebsiteBindingService) missingDeletedWebsite(req request.MCPWebsiteBinding) response.MCPWebsiteBinding { + site := model.Website{BaseModel: model.BaseModel{ID: req.WebsiteID}, Alias: mcpWebsiteAlias(req.BindingID)} + if _, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) { + return mcpWebsiteFailure("website_ownership_changed", false, true) + } + owner, ownerErr := mcpWebsiteReadOwner(site) + if ownerErr == nil && owner.BindingID == req.BindingID && (req.WebsiteID == 0 || owner.WebsiteID == req.WebsiteID) && owner.KeyHash == mcpWebsiteHash(req.ProxyKey) && + owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == mcpWebsitePayloadHash(req) { + site.ID = owner.WebsiteID + if _, err := websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) { + return mcpWebsiteFailure("website_ownership_changed", false, true) + } + return s.deletedWebsite(req, site, owner, true) + } + if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); !os.IsNotExist(err) { + return mcpWebsiteFailure("website_ownership_changed", false, true) + } + if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) { + return mcpWebsiteFailure("proxy_modified", false, true) + } + if err := mcpWebsiteDeleteParents(); err != nil { + return mcpWebsiteFailure("website_directory_changed", false, true) + } + for _, location := range mcpWebsiteDeleteLocations(site) { + if _, err := os.Lstat(location); !os.IsNotExist(err) { + return mcpWebsiteFailure("website_delete_unconfirmed", false, false) + } + } + return response.MCPWebsiteBinding{State: "unbound", WebsiteID: req.WebsiteID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true} +} + func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBinding, site model.Website) bool { host, port, err := mcpWebsiteDomain(req.Domain) if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID { @@ -479,12 +672,9 @@ func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBind return false } port = installed.HttpPort - if req.EnableSSL { - port = installed.HttpsPort - } } for _, domain := range site.Domains { - if domain.Domain == host && domain.Port == port { + if domain.Domain == host && domain.Port == port && !domain.SSL { return true } } @@ -617,6 +807,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe if req.Operation != "status" && req.Operation != "bind" && req.Operation != "update" && req.Operation != "unbind" && req.Operation != "sync" { return mcpWebsiteFailure("invalid_operation", true, false) } + if req.DeleteWebsite && req.Operation != "unbind" { + return mcpWebsiteFailure("invalid_operation", true, false) + } if req.BindingID == "" && req.WebsiteID == 0 && req.Operation == "status" { return response.MCPWebsiteBinding{State: "unbound"} } @@ -639,6 +832,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe site, err := s.load(req) if errors.Is(err, gorm.ErrRecordNotFound) { if req.Operation == "status" || req.Operation == "unbind" { + if req.DeleteWebsite { + return s.missingDeletedWebsite(req) + } if req.WebsiteID == 0 || req.Operation == "unbind" { return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID} } @@ -658,19 +854,31 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe domain, port, _ := mcpWebsiteDomain(req.Domain) if port == 0 { port = installed.HttpPort - if req.EnableSSL { - port = installed.HttpsPort - } } - if existing, conflictErr := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain), websiteDomainRepo.WithPort(port)); conflictErr == nil && existing.ID > 0 { - return mcpWebsiteFailure("domain_conflict", true, true) - } else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) { + if req.EnableSSL && port != installed.HttpPort { + return mcpWebsiteFailure("http_port_requires_default", true, false) + } + candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port}}} + if req.EnableSSL { + candidate.Protocol = constant.ProtocolHTTPS + } + if conflictErr := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); conflictErr != nil { + var business buserr.BusinessError + if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" { + return mcpWebsiteFailure("domain_conflict", true, true) + } return mcpWebsiteFailure("website_unavailable", false, false) } create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID, - Domains: []request.WebsiteDomain{{Domain: domain, Port: port, SSL: req.EnableSSL}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}} + Domains: []request.WebsiteDomain{{Domain: domain, Port: port}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}} + previousMutation := mutationStarted mutationStarted = true if createErr := s.createWebsite(create); createErr != nil { + var business buserr.BusinessError + if errors.As(createErr, &business) && business.Msg == "ErrDomainIsUsed" { + mutationStarted = previousMutation + return mcpWebsiteFailure("domain_conflict", true, true) + } return mcpWebsiteFailure("website_create_failed", false, false) } site, err = s.load(req) @@ -701,6 +909,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe return mcpWebsiteFailure("website_unavailable", false, false) } if ownerErr != nil { + if req.DeleteWebsite { + return mcpWebsiteFailure("website_ownership_changed", false, true) + } if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) { return mcpWebsiteFailure("website_ownership_changed", true, true) } @@ -711,15 +922,44 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe } else if owner.BindingID != req.BindingID || owner.WebsiteID != site.ID || owner.KeyHash != mcpWebsiteHash(req.ProxyKey) { return mcpWebsiteFailure("website_ownership_changed", true, true) } + if req.DeleteWebsite { + if site.Type != constant.Static || site.AppInstallID != 0 || site.RuntimeID != 0 || site.DbID != 0 || site.DbType != "" || site.ParentWebsiteID != 0 { + return mcpWebsiteFailure("website_modified", true, true) + } + if err = mcpWebsiteDeleteParents(); err != nil { + return mcpWebsiteFailure("website_directory_changed", true, true) + } + for kind, location := range mcpWebsiteDeleteLocations(site) { + if _, _, err = mcpWebsiteDeleteIdentity(kind, location); err != nil { + return mcpWebsiteFailure("website_directory_changed", true, true) + } + } + children, childErr := websiteRepo.GetBy(websiteRepo.WithParentID(site.ID)) + if childErr != nil { + return mcpWebsiteFailure("website_unavailable", false, false) + } + if len(children) != 0 { + return mcpWebsiteFailure("website_modified", true, true) + } + } if req.Operation == "status" { return s.status(req, site, owner) } operationHash := mcpWebsitePayloadHash(req) + deleteRetry := req.DeleteWebsite && owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == operationHash && owner.DeleteRoot != "" + if owner.DeleteRoot != "" && req.Operation != "status" && (owner.RequestID != req.RequestID || owner.RequestHash != operationHash) { + return mcpWebsiteFailure("operation_request_changed", false, true) + } + if deleteRetry { + if _, err = mcpWebsiteVerifyDeletePaths(site, owner); err != nil { + return mcpWebsiteFailure("website_directory_changed", false, true) + } + } if owner.RequestID == req.RequestID && req.RequestID != "" { if owner.RequestHash != operationHash { return mcpWebsiteFailure("operation_request_changed", true, true) } - if owner.Operation == "unbind" || owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) { + if owner.Operation == "unbind" && !req.DeleteWebsite || owner.Operation != "unbind" && owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) { return s.status(req, site, owner) } } @@ -774,17 +1014,48 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe return mcpWebsiteFailure("proxy_remove_failed", false, false) } } + if req.DeleteWebsite && !deleteRetry { + if err = mcpWebsiteRecordDeletePaths(site, &owner); err != nil { + return mcpWebsiteFailure("website_directory_changed", false, true) + } + } owner.Operation, owner.RequestID, owner.RequestHash = "unbind", req.RequestID, operationHash mutationStarted = true if err = mcpWebsiteWriteOwner(site, owner); err != nil { return mcpWebsiteFailure("ownership_write_failed", false, false) } + if req.DeleteWebsite { + if err = s.deleteWebsite(request.WebsiteDelete{ID: site.ID}); err != nil { + return mcpWebsiteFailure("website_delete_failed", false, false) + } + if _, err = websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) { + return mcpWebsiteFailure("website_delete_unconfirmed", false, false) + } + return s.deletedWebsite(req, site, owner, false) + } return s.status(req, site, owner) } if req.Operation == "update" { if req.Domain != site.PrimaryDomain { return mcpWebsiteFailure("domain_changed", true, true) } + if req.EnableSSL { + installed, installErr := s.openresty() + if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning { + return mcpWebsiteFailure("openresty_unavailable", true, false) + } + host, _, _ := mcpWebsiteDomain(site.PrimaryDomain) + usesHTTPPort := false + for _, domain := range site.Domains { + if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL { + usesHTTPPort = true + break + } + } + if !usesHTTPPort { + return mcpWebsiteFailure("http_port_requires_default", true, false) + } + } if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID { mutationStarted = true if err = s.updateHTTPS(context.WithoutCancel(ctx), request.WebsiteHTTPSOp{WebsiteID: site.ID, Enable: req.EnableSSL, Type: constant.SSLExisted, WebsiteSSLID: req.SSLID, HttpConfig: constant.HTTPSOnly}); err != nil { diff --git a/agent/app/service/website.go b/agent/app/service/website.go index b228ddfde..bb011b619 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -280,6 +280,8 @@ func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp { } func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) { + websiteListenMutation.Lock() + defer websiteListenMutation.Unlock() alias := create.Alias if alias == "default" { return buserr.New("ErrDefaultAlias") @@ -377,6 +379,11 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) website.HttpConfig = sslReq.HttpConfig initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq} } + candidate := *website + candidate.Domains = domains + if err = checkWebsiteListenConflict(candidate, defaultHttpPort, nginxInstall.HttpsPort); err != nil { + return err + } } createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0) @@ -1004,6 +1011,8 @@ func applyWebsiteHTTPSParams(res *response.WebsiteHTTPS, params []response.Nginx } func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) { + websiteListenMutation.Lock() + defer websiteListenMutation.Unlock() website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID)) if err != nil { return nil, err @@ -1012,6 +1021,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH res response.WebsiteHTTPS websiteSSL model.WebsiteSSL ) + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + candidate := website + candidate.Protocol, candidate.HttpConfig = constant.ProtocolHTTP, req.HttpConfig + if req.Enable { + candidate.Protocol = constant.ProtocolHTTPS + } + if err = checkWebsiteListenConflict(candidate, nginxInstall.HttpPort, nginxInstall.HttpsPort); err != nil { + return nil, err + } if err = ChangeHSTSConfig(req.Hsts, req.HstsIncludeSubDomains, req.Http3, website); err != nil { return nil, err } diff --git a/agent/app/service/website_domain.go b/agent/app/service/website_domain.go index 6dc5b6194..fcc63a2cb 100644 --- a/agent/app/service/website_domain.go +++ b/agent/app/service/website_domain.go @@ -14,6 +14,8 @@ import ( ) func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) { + websiteListenMutation.Lock() + defer websiteListenMutation.Unlock() var ( domainModels []model.WebsiteDomain addPorts []int @@ -31,6 +33,15 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) if err != nil { return nil, err } + ports := websiteListenPorts(website, httpPort, httpsPort) + candidate := website + candidate.Domains = append(append([]model.WebsiteDomain(nil), website.Domains...), domainModels...) + for _, domain := range domainModels { + ports[domain.Port] = struct{}{} + } + if err = checkWebsiteListenPorts(candidate, ports, httpPort, httpsPort); err != nil { + return nil, err + } go func() { if err := ensureFirewallPorts(addPorts); err != nil { global.LOG.Errorf("allow website firewall ports failed: %v", err) diff --git a/agent/app/service/website_listen.go b/agent/app/service/website_listen.go new file mode 100644 index 000000000..340c2fff6 --- /dev/null +++ b/agent/app/service/website_listen.go @@ -0,0 +1,74 @@ +package service + +import ( + "strings" + "sync" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +var websiteListenMutation sync.Mutex + +func websiteListenPorts(website model.Website, httpPort, httpsPort int) map[int]struct{} { + ports := make(map[int]struct{}) + for _, domain := range website.Domains { + if domain.Port > 0 { + ports[domain.Port] = struct{}{} + } + } + if website.Protocol == constant.ProtocolHTTPS { + plan := buildWebsiteTLSPlan(website.Domains, httpPort, httpsPort) + for _, port := range plan.httpsPorts { + if port > 0 { + ports[port] = struct{}{} + } + } + } + return ports +} + +func checkWebsiteListenConflict(website model.Website, httpPort, httpsPort int) error { + return checkWebsiteListenPorts(website, websiteListenPorts(website, httpPort, httpsPort), httpPort, httpsPort) +} + +func checkWebsiteListenPorts(website model.Website, ports map[int]struct{}, httpPort, httpsPort int) error { + if len(website.Domains) == 0 || len(ports) == 0 { + return nil + } + names := make(map[string]struct{}, len(website.Domains)) + for _, domain := range website.Domains { + names[strings.ToLower(domain.Domain)] = struct{}{} + } + existing, err := websiteRepo.GetBy() + if err != nil { + return err + } + domains, err := websiteDomainRepo.GetBy() + if err != nil { + return err + } + byWebsite := make(map[uint][]model.WebsiteDomain) + for _, domain := range domains { + byWebsite[domain.WebsiteID] = append(byWebsite[domain.WebsiteID], domain) + } + for _, other := range existing { + if website.ID != 0 && other.ID == website.ID || other.Type == constant.Stream { + continue + } + other.Domains = byWebsite[other.ID] + for _, domain := range other.Domains { + if _, match := names[strings.ToLower(domain.Domain)]; !match { + continue + } + for port := range websiteListenPorts(other, httpPort, httpsPort) { + if _, occupied := ports[port]; occupied { + return buserr.WithName("ErrDomainIsUsed", other.PrimaryDomain) + } + } + break + } + } + return nil +} diff --git a/agent/init/router/router.go b/agent/init/router/router.go index 791ddf196..45956c261 100644 --- a/agent/init/router/router.go +++ b/agent/init/router/router.go @@ -20,6 +20,7 @@ func Routers() *gin.Engine { } Router = gin.Default() Router.Use(i18n.UseI18n()) + Router.Use(middleware.MCPErrorMetadata()) registerMCPRuntime(Router) PrivateGroup := Router.Group("/api/v2") diff --git a/agent/middleware/mcp_error_metadata.go b/agent/middleware/mcp_error_metadata.go new file mode 100644 index 000000000..4eb027f99 --- /dev/null +++ b/agent/middleware/mcp_error_metadata.go @@ -0,0 +1,13 @@ +package middleware + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/utils/mcpapi" + "github.com/gin-gonic/gin" +) + +func MCPErrorMetadata() gin.HandlerFunc { + return helper.WithErrorMetadata(func(c *gin.Context, _ int, _ string, err error) { + mcpapi.SetBusinessErrorHeader(c.Request, c.Writer.Header(), err) + }) +} diff --git a/agent/utils/mcpruntime/docker.go b/agent/utils/mcpruntime/docker.go index 0e852d3e1..5efd4586b 100644 --- a/agent/utils/mcpruntime/docker.go +++ b/agent/utils/mcpruntime/docker.go @@ -51,13 +51,27 @@ var digestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`) var ErrUnsupported = errors.New("MCP Docker runtime requires a local rootful Linux Docker daemon without user namespace remapping") type Request struct { - Operation string `json:"operation"` - Image string `json:"image"` - Port int `json:"port"` - Enabled bool `json:"enabled"` - Restart bool `json:"restart"` - ImageID string `json:"imageID,omitempty"` - StreamProgress bool `json:"streamProgress,omitempty"` + Operation string `json:"operation"` + Image string `json:"image"` + Port int `json:"port"` + Enabled bool `json:"enabled"` + Restart bool `json:"restart"` + ImageID string `json:"imageID,omitempty"` + StreamProgress bool `json:"streamProgress,omitempty"` + Cleanup *CleanupReference `json:"cleanup,omitempty"` + DeleteOldImage bool `json:"deleteOldImage,omitempty"` +} + +type CleanupReference struct { + ContainerID string `json:"containerID"` + PreviousContainerID string `json:"previousContainerID"` + PreviousImageID string `json:"previousImageID"` +} + +type CleanupResult struct { + ContainerRemoved bool `json:"containerRemoved,omitempty"` + ImageRemoved bool `json:"imageRemoved,omitempty"` + ImageRetainedReason string `json:"imageRetainedReason,omitempty"` } type State struct { @@ -71,11 +85,13 @@ type State struct { } type Response struct { - State *State `json:"state,omitempty"` - Catalog json.RawMessage `json:"catalog,omitempty"` - Logs string `json:"logs,omitempty"` - Error string `json:"error,omitempty"` - ImageID string `json:"imageID,omitempty"` + State *State `json:"state,omitempty"` + Catalog json.RawMessage `json:"catalog,omitempty"` + Logs string `json:"logs,omitempty"` + Error string `json:"error,omitempty"` + ImageID string `json:"imageID,omitempty"` + Cleanup *CleanupReference `json:"cleanup,omitempty"` + CleanupResult *CleanupResult `json:"cleanupResult,omitempty"` } type DockerClient interface { @@ -84,6 +100,7 @@ type DockerClient interface { Info(context.Context) (system.Info, error) ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error) ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error) + ImageRemove(context.Context, string, image.RemoveOptions) ([]image.DeleteResponse, error) ContainerInspect(context.Context, string) (container.InspectResponse, error) ContainerCreate(context.Context, *container.Config, *container.HostConfig, *network.NetworkingConfig, *ocispec.Platform, string) (container.CreateResponse, error) ContainerStart(context.Context, string, container.StartOptions) error @@ -156,10 +173,13 @@ func (c *Controller) HandleWithProgress(ctx context.Context, req Request, progre return Response{}, errors.New("MCP Docker progress is only available when pulling an image") } switch req.Operation { - case "prepare", "catalog", "apply_catalog", "candidate_catalog", "status", "apply", "install", "rollback", "rollback_absent", "pull", "logs": + case "prepare", "catalog", "apply_catalog", "candidate_catalog", "status", "apply", "install", "rollback", "rollback_absent", "pull", "logs", "finalize": default: return Response{}, errors.New("invalid MCP Docker operation") } + if req.Operation != "finalize" && (req.Cleanup != nil || req.DeleteOldImage) { + return Response{}, errors.New("MCP cleanup is only available after installation") + } if err := ValidateImage(req.Image); err != nil { return Response{}, err } @@ -178,7 +198,7 @@ func (c *Controller) HandleWithProgress(ctx context.Context, req Request, progre } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - if req.Operation == "apply" || req.Operation == "install" || req.Operation == "rollback" || req.Operation == "rollback_absent" || req.Operation == "pull" { + if req.Operation == "apply" || req.Operation == "install" || req.Operation == "rollback" || req.Operation == "rollback_absent" || req.Operation == "pull" || req.Operation == "finalize" { select { case c.mutation <- struct{}{}: defer func() { <-c.mutation }() @@ -222,7 +242,17 @@ func (c *Controller) HandleWithProgress(ctx context.Context, req Request, progre case "apply": err = c.apply(ctx, cli, req) case "install": - err = c.installRuntime(ctx, cli, req) + var cleanup *CleanupReference + cleanup, err = c.installRuntime(ctx, cli, req) + if err == nil { + return Response{Cleanup: cleanup}, nil + } + case "finalize": + var result *CleanupResult + result, err = c.finalize(ctx, cli, req) + if err == nil { + return Response{CleanupResult: result}, nil + } case "rollback": err = c.rollback(ctx, cli, req) case "rollback_absent": diff --git a/agent/utils/mcpruntime/finalize.go b/agent/utils/mcpruntime/finalize.go new file mode 100644 index 000000000..55541d3ad --- /dev/null +++ b/agent/utils/mcpruntime/finalize.go @@ -0,0 +1,105 @@ +package mcpruntime + +import ( + "context" + "errors" + "strconv" + + "github.com/containerd/errdefs" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" +) + +func (c *Controller) installationCleanup(ctx context.Context, cli DockerClient, before *container.InspectResponse) (*CleanupReference, error) { + if before == nil { + return nil, nil + } + current, err := c.owned(ctx, cli, ContainerName) + if err != nil || current == nil { + return nil, errors.New("installed MCP container is unavailable") + } + if current.ID == before.ID { + return nil, nil + } + previous, err := c.owned(ctx, cli, previousName) + if err != nil || previous == nil || previous.ID != before.ID || previous.Image != before.Image || isRunning(previous) { + return nil, errors.New("MCP previous container changed during installation") + } + return &CleanupReference{ContainerID: current.ID, PreviousContainerID: previous.ID, PreviousImageID: previous.Image}, nil +} + +// finalize runs only after Core commits the installed settings. The reference +// binds cleanup to this replacement; fixed names alone cannot authorize it. +func (c *Controller) finalize(ctx context.Context, cli DockerClient, req Request) (*CleanupResult, error) { + reference := req.Cleanup + if reference == nil || reference.ContainerID == "" || reference.PreviousContainerID == "" || + reference.ContainerID == reference.PreviousContainerID || !digestPattern.MatchString(reference.PreviousImageID) || !digestPattern.MatchString(req.ImageID) { + return nil, errors.New("invalid MCP installation cleanup reference") + } + current, err := c.owned(ctx, cli, ContainerName) + if err != nil || current == nil || current.ID != reference.ContainerID || current.Image != req.ImageID { + return nil, errors.New("MCP installed container changed before cleanup") + } + verified, err := c.verify(ctx, cli, current.Image) + if err != nil || !matchesRuntimeWithCapabilities(ctx, cli, current, req.ImageID, req, verified.metadata.AdapterVersion) { + return nil, errors.New("MCP installed container is unsafe or incompatible for cleanup") + } + if req.Enabled { + if err = c.waitHealthy(ctx, cli, current.ID, req, verified.metadata.AdapterVersion); err != nil { + return nil, errors.New("MCP installed container is not healthy; previous container was retained") + } + } else if current.State == nil || isRunning(current) { + return nil, errors.New("MCP installed container is not in its expected stopped state") + } + previous, err := c.owned(ctx, cli, previousName) + if err != nil { + return nil, err + } + if previous == nil { + // A retry after deletion has no inspected image provenance. Never use + // a caller-provided image ID as a substitute for that missing instance. + return &CleanupResult{ImageRetainedReason: "previous_missing"}, nil + } + if previous.ID != reference.PreviousContainerID || previous.Image != reference.PreviousImageID || previous.State == nil || isRunning(previous) { + return nil, errors.New("MCP previous container changed before cleanup") + } + oldPort, err := strconv.Atoi(previous.Config.Labels[portLabel]) + oldRequest := Request{Image: previous.Config.Labels[imageLabel], ImageID: previous.Image, Port: oldPort} + if err != nil || oldPort < 1024 || oldPort > 65535 || ValidateImage(oldRequest.Image) != nil { + return nil, errors.New("MCP previous container configuration is invalid") + } + oldImage, err := c.resolve(ctx, cli, previous.Image) + if err != nil || oldImage.ID != previous.Image { + return nil, errors.New("MCP previous image is unavailable or changed") + } + oldVerified, err := c.verify(ctx, cli, previous.Image) + if err != nil || !matchesRuntimeWithCapabilities(ctx, cli, previous, previous.Image, oldRequest, oldVerified.metadata.AdapterVersion) { + return nil, errors.New("MCP previous container was modified; cleanup was rejected") + } + if err = cli.ContainerRemove(ctx, previous.ID, container.RemoveOptions{}); err != nil { + return nil, errors.New("MCP previous container could not be removed") + } + result := &CleanupResult{ContainerRemoved: true} + if !req.DeleteOldImage { + result.ImageRetainedReason = "not_requested" + return result, nil + } + if previous.Image == current.Image { + result.ImageRetainedReason = "same_image" + return result, nil + } + if len(oldImage.RepoTags) > 1 { + result.ImageRetainedReason = "multiple_tags" + return result, nil + } + _, err = cli.ImageRemove(ctx, previous.Image, image.RemoveOptions{}) + if errdefs.IsConflict(err) { + result.ImageRetainedReason = "in_use" + return result, nil + } + if err != nil && !errdefs.IsNotFound(err) { + return nil, errors.New("MCP previous image could not be removed") + } + result.ImageRemoved = true + return result, nil +} diff --git a/agent/utils/mcpruntime/lifecycle.go b/agent/utils/mcpruntime/lifecycle.go index 7fae857b7..cda3714ca 100644 --- a/agent/utils/mcpruntime/lifecycle.go +++ b/agent/utils/mcpruntime/lifecycle.go @@ -96,20 +96,20 @@ func (c *Controller) pullImage(ctx context.Context, cli DockerClient, req Reques return reportPullProgress(ctx, progress, PullProgress{Status: "verified_image"}) } -func (c *Controller) installRuntime(ctx context.Context, cli DockerClient, req Request) error { +func (c *Controller) installRuntime(ctx context.Context, cli DockerClient, req Request) (*CleanupReference, error) { before, err := c.owned(ctx, cli, ContainerName) if err != nil { - return err + return nil, err } wasRunning := isRunning(before) activate := req activate.Enabled = true activate.Restart = true if err = c.apply(ctx, cli, activate); err != nil { - return err + return nil, err } if req.Enabled { - return nil + return c.installationCleanup(ctx, cli, before) } current, err := c.owned(ctx, cli, ContainerName) if err == nil && current == nil { @@ -119,7 +119,7 @@ func (c *Controller) installRuntime(ctx context.Context, cli DockerClient, req R err = stopContainer(ctx, cli, current.ID) } if err == nil { - return nil + return c.installationCleanup(ctx, cli, before) } rollbackCtx, cancel := context.WithTimeout(context.Background(), 40*time.Second) defer cancel() @@ -136,7 +136,7 @@ func (c *Controller) installRuntime(ctx context.Context, cli DockerClient, req R } else if current != nil { restoreErr = cli.ContainerRemove(rollbackCtx, current.ID, container.RemoveOptions{Force: true}) } - return errors.Join(errors.New("installed MCP container could not be left stopped"), restoreErr) + return nil, errors.Join(errors.New("installed MCP container could not be left stopped"), restoreErr) } func runtimeConfig(id string, req Request, version string) (*container.Config, *container.HostConfig) { diff --git a/core/app/api/v2/helper/helper.go b/core/app/api/v2/helper/helper.go index 5247b4815..fdabb1a1d 100644 --- a/core/app/api/v2/helper/helper.go +++ b/core/app/api/v2/helper/helper.go @@ -3,21 +3,19 @@ package helper import ( "errors" "fmt" - "github.com/1Panel-dev/1Panel/core/cmd/server/res" "net/http" "strconv" + "github.com/1Panel-dev/1Panel/core/cmd/server/res" + "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/i18n" - mcpmeta "github.com/1Panel-dev/1Panel/core/utils/mcpapi/metadata" "github.com/gin-gonic/gin" ) func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { - if msgKey == "ErrProxy" && ctx.GetBool("MCP_API_AUTH") && ctx.Request != nil && mcpmeta.TrustedRequest(ctx.Request) { - ctx.Header(mcpmeta.BusinessCodeHeader, "ErrProxy") - } + applyErrorMetadata(ctx, code, msgKey, err) res := dto.Response{ Code: code, Message: "", @@ -113,3 +111,25 @@ func GetParamID(c *gin.Context) (uint, error) { intNum, _ := strconv.Atoi(idParam) return uint(intNum), nil } + +const errorMetadataKey = "error_response_metadata_policies" + +type ErrorMetadataPolicy func(*gin.Context, int, string, error) + +func WithErrorMetadata(policies ...ErrorMetadataPolicy) gin.HandlerFunc { + configured := append([]ErrorMetadataPolicy(nil), policies...) + return func(c *gin.Context) { + c.Set(errorMetadataKey, configured) + c.Next() + } +} + +func applyErrorMetadata(c *gin.Context, code int, messageKey string, err error) { + value, _ := c.Get(errorMetadataKey) + policies, _ := value.([]ErrorMetadataPolicy) + for _, policy := range policies { + if policy != nil { + policy(c, code, messageKey, err) + } + } +} diff --git a/core/app/api/v2/mcp.go b/core/app/api/v2/mcp.go index cc980ce3d..844139dce 100644 --- a/core/app/api/v2/mcp.go +++ b/core/app/api/v2/mcp.go @@ -80,21 +80,23 @@ func mcpManagementError(c *gin.Context, err error) { code := http.StatusInternalServerError errorCode := "MCP_OPERATION_FAILED" invocationID := "" + reason := "" var known *contract.Error if errors.As(err, &known) { code = known.Status errorCode = known.Code invocationID = known.InvocationID - } else if errors.Is(err, service.ErrMCPSettingsRevision) { - code = 409 - errorCode = "MCP_CONFLICT" + if errorCode == "MCP_CONFLICT" && mcpConflictMessageKey(known.ManagementReason) != "" { + reason = known.ManagementReason + } } c.Header("Cache-Control", "no-store") c.JSON(200, struct { dto.Response ErrorCode string `json:"errorCode"` InvocationID string `json:"invocationId,omitempty"` - }{Response: dto.Response{Code: code, Message: mcpManagementMessage(errorCode)}, ErrorCode: errorCode, InvocationID: invocationID}) + Reason string `json:"reason,omitempty"` + }{Response: dto.Response{Code: code, Message: mcpManagementMessage(errorCode, reason)}, ErrorCode: errorCode, InvocationID: invocationID, Reason: reason}) c.Abort() } func mcpManagement(c *gin.Context, write bool, admin bool) (*service.MCPService, auth.APIKeyOwner, bool) { diff --git a/core/app/api/v2/mcp_errors.go b/core/app/api/v2/mcp_errors.go index 6f40b971c..052f7ad29 100644 --- a/core/app/api/v2/mcp_errors.go +++ b/core/app/api/v2/mcp_errors.go @@ -2,9 +2,58 @@ package v2 import "github.com/1Panel-dev/1Panel/core/i18n" -// Management responses use panel localization while runtime errors retain their -// stable machine contract. Never render raw errors or credential data here. -func mcpManagementMessage(code string) string { +func mcpConflictMessageKey(reason string) string { + switch reason { + case "settings_revision_changed": + return "ErrMCPSettingsChanged" + case "credential_revision_changed": + return "ErrMCPCredentialChanged" + case "credential_revoked": + return "ErrMCPCredentialRevoked" + case "credential_name_exists": + return "ErrMCPCredentialNameExists" + case "request_reused": + return "ErrMCPRequestReused" + case "installation_running": + return "ErrMCPInstallationRunning" + case "service_operation_running": + return "ErrMCPServiceOperationRunning" + case "endpoint_managed_by_website": + return "ErrMCPEndpointManagedByWebsite" + case "website_configuration_exists": + return "ErrMCPWebsiteConfigurationExists" + case "website_already_bound": + return "ErrMCPWebsiteAlreadyBound" + case "website_not_bound": + return "ErrMCPWebsiteNotBound" + case "website_missing": + return "ErrMCPWebsiteMissing" + case "website_operation_pending": + return "ErrMCPWebsiteOperationPending" + case "website_domain_conflict": + return "ErrMCPWebsiteDomainConflict" + case "website_ownership_changed": + return "ErrMCPWebsiteOwnershipChanged" + case "website_domain_changed": + return "ErrMCPWebsiteDomainChanged" + case "website_directory_changed": + return "ErrMCPWebsiteDirectoryChanged" + case "website_proxy_modified": + return "ErrMCPWebsiteProxyModified" + case "website_modified": + return "ErrMCPWebsiteModified" + case "website_request_changed": + return "ErrMCPWebsiteRequestChanged" + case "website_proxy_not_loaded": + return "ErrMCPWebsiteProxyNotLoaded" + case "website_state_unconfirmed": + return "ErrMCPWebsiteStateUnconfirmed" + default: + return "" + } +} + +func mcpManagementMessage(code string, reasons ...string) string { key := "ErrInternalServer" switch code { case "MCP_INVALID_ARGUMENTS": @@ -15,6 +64,11 @@ func mcpManagementMessage(code string) string { key = "ErrRBAC" case "MCP_CONFLICT": key = "ErrMCPConflict" + if len(reasons) > 0 { + if specific := mcpConflictMessageKey(reasons[0]); specific != "" { + key = specific + } + } case "MCP_POLICY_CHANGED", "MCP_SCHEMA_MISMATCH": key = "ErrMCPPolicyChanged" case "MCP_CATALOG_UNAVAILABLE": @@ -37,6 +91,8 @@ func mcpManagementMessage(code string) string { key = "ErrMCPWebsiteUnavailable" case "MCP_WEBSITE_SERVICE_REQUIRED": key = "ErrMCPWebsiteServiceRequired" + case "MCP_WEBSITE_HTTP_PORT_REQUIRED": + key = "ErrMCPWebsiteHTTPPortRequired" } return i18n.GetMsgWithMap(key, nil) } diff --git a/core/app/dto/mcp_runtime.go b/core/app/dto/mcp_runtime.go index c669a1075..e54d2eb36 100644 --- a/core/app/dto/mcp_runtime.go +++ b/core/app/dto/mcp_runtime.go @@ -14,13 +14,14 @@ type MCPRuntimeSettings struct { type MCPRuntimeSettingsUpdate = MCPRuntimeSettings type MCPRuntimeInstall struct { - DockerImage string `json:"dockerImage"` - ListenPort int `json:"listenPort"` - ExternalURL string `json:"externalUrl"` - Revision uint64 `json:"revision"` - AccessPolicy string `json:"accessPolicy"` - AllowedIPs []string `json:"allowedIPs"` - Website *MCPRuntimeInstallWebsite `json:"website,omitempty"` + DockerImage string `json:"dockerImage"` + DeleteOldImage bool `json:"deleteOldImage"` + ListenPort int `json:"listenPort"` + ExternalURL string `json:"externalUrl"` + Revision uint64 `json:"revision"` + AccessPolicy string `json:"accessPolicy"` + AllowedIPs []string `json:"allowedIPs"` + Website *MCPRuntimeInstallWebsite `json:"website,omitempty"` } type MCPRuntimeInstallWebsite struct { diff --git a/core/app/dto/mcp_website.go b/core/app/dto/mcp_website.go index f95fefbb5..ee22ffcf5 100644 --- a/core/app/dto/mcp_website.go +++ b/core/app/dto/mcp_website.go @@ -16,16 +16,18 @@ type MCPWebsiteUpdate struct { } type MCPWebsiteUnbind struct { - RequestID string `json:"requestId"` - Revision uint64 `json:"revision"` + RequestID string `json:"requestId"` + Revision uint64 `json:"revision"` + DeleteWebsite bool `json:"deleteWebsite,omitempty"` } type MCPWebsitePendingRequest struct { - Operation string `json:"operation"` - RequestID string `json:"requestId"` - Domain string `json:"domain"` - EnableSSL bool `json:"enableSSL"` - SSLID uint `json:"sslID"` + Operation string `json:"operation"` + RequestID string `json:"requestId"` + Domain string `json:"domain"` + EnableSSL bool `json:"enableSSL"` + SSLID uint `json:"sslID"` + DeleteWebsite bool `json:"deleteWebsite,omitempty"` } type MCPWebsiteStatus struct { @@ -40,4 +42,5 @@ type MCPWebsiteStatus struct { Revision uint64 `json:"revision"` Reason string `json:"reason,omitempty"` PendingRequest *MCPWebsitePendingRequest `json:"pendingRequest,omitempty"` + WebsiteDeleted bool `json:"websiteDeleted,omitempty"` } diff --git a/core/app/service/mcp.go b/core/app/service/mcp.go index ebab9c00d..659b02369 100644 --- a/core/app/service/mcp.go +++ b/core/app/service/mcp.go @@ -337,7 +337,7 @@ func (s *MCPService) Create(owner auth.APIKeyOwner, req dto.MCPCredentialCreate) key, err = store.CredentialByRequest(owner.Type, owner.ID, req.RequestID) if err == nil { if key.RequestHash != requestHash { - return contract.Fail("MCP_CONFLICT", 409, "Creation request already used") + return contract.Conflict("request_reused", "Creation request already used") } repeated = true return nil @@ -350,7 +350,7 @@ func (s *MCPService) Create(owner auth.APIKeyOwner, req dto.MCPCredentialCreate) return err } if count > 0 { - return contract.Fail("MCP_CONFLICT", 409, "Credential name already exists") + return contract.Conflict("credential_name_exists", "Credential name already exists") } secret := make([]byte, 32) if _, err = rand.Read(secret); err != nil { @@ -395,8 +395,11 @@ func (s *MCPService) mutate(owner auth.APIKeyOwner, req dto.MCPCredentialMutatio } return err } - if key.Revision != req.Revision || key.Status == "Revoked" { - return contract.Fail("MCP_CONFLICT", 409, "Credential changed; refresh and retry") + if key.Revision != req.Revision { + return contract.Conflict("credential_revision_changed", "Credential changed; refresh and retry") + } + if key.Status == "Revoked" { + return contract.Conflict("credential_revoked", "Credential was revoked") } if err := change(tx, &key); err != nil { return err @@ -411,7 +414,7 @@ func (s *MCPService) mutate(owner auth.APIKeyOwner, req dto.MCPCredentialMutatio return err } if affected != 1 { - return contract.Fail("MCP_CONFLICT", 409, "Credential changed; refresh and retry") + return contract.Conflict("credential_revision_changed", "Credential changed; refresh and retry") } return nil }) @@ -430,7 +433,7 @@ func (s *MCPService) Update(owner auth.APIKeyOwner, req dto.MCPCredentialUpdate) return err } if count > 0 { - return contract.Fail("MCP_CONFLICT", 409, "Credential name already exists") + return contract.Conflict("credential_name_exists", "Credential name already exists") } key.Name = req.Name key.ActiveName = &req.Name diff --git a/core/app/service/mcp_credential_delete.go b/core/app/service/mcp_credential_delete.go index 914740fc8..b28b04cd6 100644 --- a/core/app/service/mcp_credential_delete.go +++ b/core/app/service/mcp_credential_delete.go @@ -40,7 +40,7 @@ func (s *MCPService) Delete(owner auth.APIKeyOwner, req dto.MCPCredentialMutatio return err } if key.Revision != req.Revision { - return contract.Fail("MCP_CONFLICT", 409, "Credential changed; refresh and retry") + return contract.Conflict("credential_revision_changed", "Credential changed; refresh and retry") } if err := data.DeleteGrants(key.ID); err != nil { return err @@ -50,7 +50,7 @@ func (s *MCPService) Delete(owner auth.APIKeyOwner, req dto.MCPCredentialMutatio return err } if deleted != 1 { - return contract.Fail("MCP_CONFLICT", 409, "Credential changed; refresh and retry") + return contract.Conflict("credential_revision_changed", "Credential changed; refresh and retry") } return s.auditEnrollment(tx, owner, "delete", req, nil) }) diff --git a/core/app/service/mcp_enrollment.go b/core/app/service/mcp_enrollment.go index c23ce2174..ef532568c 100644 --- a/core/app/service/mcp_enrollment.go +++ b/core/app/service/mcp_enrollment.go @@ -11,7 +11,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "strings" "github.com/1Panel-dev/1Panel/core/app/auth" @@ -102,7 +101,7 @@ func (s *MCPService) enrollmentKey(owner auth.APIKeyOwner, req dto.MCPCredential return key, err } if key.Revision != req.Revision { - return key, contract.Fail("MCP_CONFLICT", 409, "Credential changed; refresh and retry") + return key, contract.Conflict("credential_revision_changed", "Credential changed; refresh and retry") } if err := s.enrollmentActive(key, false); err != nil { return key, err @@ -184,13 +183,6 @@ func (s *MCPService) Rotate(owner auth.APIKeyOwner, req dto.MCPCredentialMutatio return &dto.MCPCredentialCreated{Item: item, Token: token}, nil } -func mcpDocumentEndpoint(endpoint string) bool { - u, err := url.Parse(endpoint) - return err == nil && len(endpoint) <= 2048 && !strings.ContainsAny(endpoint, "\r\n\x00") && - (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" && u.User == nil && u.Path == "/mcp" && - u.RawQuery == "" && u.Fragment == "" && u.Opaque == "" -} - func mcpDocumentJSON(value any) string { data, _ := json.MarshalIndent(value, "", " ") return strings.ReplaceAll(string(data), "`", "\\u0060") @@ -220,7 +212,7 @@ func mcpEnrollmentDocument(locale, client, syntax, config, endpoint string) (str "MCPDocumentTitle", "MCPDocumentIntro", "MCPDocumentTaskTitle", "MCPDocumentTask", clientKey, "MCPDocumentSteps", "MCPDocumentConfigTitle", "MCPDocumentNotes", } - if strings.HasPrefix(endpoint, "http://") { + if strings.HasPrefix(strings.ToLower(endpoint), "http://") { keys = append(keys, "MCPDocumentHTTPWarning") } texts := make([]string, len(keys)) @@ -247,7 +239,7 @@ func (s *MCPService) ExportMarkdown(owner auth.APIKeyOwner, req dto.MCPEnrollmen if req.Client != "generic" && req.Client != "codex" && req.Client != "claude" { return nil, contract.Invalid() } - if !mcpDocumentEndpoint(endpoint) { + if !validMCPExternalEndpoint(endpoint) { return nil, contract.Fail("MCP_ENDPOINT_UNAVAILABLE", 409, "Configure an MCP external endpoint before exporting") } mcpMutationMu.Lock() diff --git a/core/app/service/mcp_install_task.go b/core/app/service/mcp_install_task.go index 2f4b8ae03..4292418a8 100644 --- a/core/app/service/mcp_install_task.go +++ b/core/app/service/mcp_install_task.go @@ -24,11 +24,11 @@ import ( func (s *MCPRuntimeService) SubmitInstall(ctx context.Context, req dto.MCPRuntimeInstall) (dto.MCPRuntimeInstallTask, error) { if !mcpInstalling.CompareAndSwap(false, true) { - return dto.MCPRuntimeInstallTask{}, contract.Fail("MCP_CONFLICT", 409, "An MCP installation is already running") + return dto.MCPRuntimeInstallTask{}, contract.Conflict("installation_running", "An MCP installation is already running") } if !mcpRuntimeMutation.TryLock() { mcpInstalling.Store(false) - return dto.MCPRuntimeInstallTask{}, contract.Fail("MCP_CONFLICT", 409, "An MCP service operation is already running") + return dto.MCPRuntimeInstallTask{}, contract.Conflict("service_operation_running", "An MCP service operation is already running") } accepted := false defer func() { @@ -58,8 +58,19 @@ func (s *MCPRuntimeService) SubmitInstall(ctx context.Context, req dto.MCPRuntim if err != nil { return dto.MCPRuntimeInstallTask{}, err } - if row.WebsiteID != 0 || row.WebsiteBindingID != "" || row.WebsiteOperation != "" { - return dto.MCPRuntimeInstallTask{}, contract.Fail("MCP_CONFLICT", 409, "Review the existing MCP website configuration before installation") + operation, err := decodeMCPWebsiteOperation(row.WebsiteOperation) + if err != nil { + return dto.MCPRuntimeInstallTask{}, err + } + // Completed operations remain for request replay after a binding is gone. + unbound := operation == nil || + operation.Operation == "unbind" && operation.State == "succeeded" || + operation.Operation == "bind" && operation.State == "failed" + if row.WebsiteID != 0 || row.WebsiteBindingID != "" || operation != nil && operation.State == "pending" { + return dto.MCPRuntimeInstallTask{}, contract.Conflict("website_configuration_exists", "Review the existing MCP website configuration before installation") + } + if !unbound { + return dto.MCPRuntimeInstallTask{}, contract.Conflict("website_state_unconfirmed", "MCP website state could not be confirmed") } website.Domain = domain req.Website = &website @@ -107,7 +118,7 @@ func (s *MCPRuntimeService) executeInstallTask(ctx context.Context, req dto.MCPR report := mcpInstallProgressLogger(t) ctx = mcpruntime.WithProgress(ctx, report) installCtx, cancel := context.WithTimeout(ctx, 6*time.Minute) - _, err := s.install(installCtx, req) + err := s.install(installCtx, req) cancel() if err != nil { return safeMCPInstallTaskError(err) @@ -165,6 +176,10 @@ func mcpInstallProgressLogger(t *task.Task) func(mcpruntime.Progress) { "local_image": "MCPInstallLocalImage", "verifying_image": "MCPInstallVerifyImage", "verified_image": "MCPInstallVerifiedImage", "catalog": "MCPInstallCatalog", "starting": "MCPInstallStarting", "saving": "MCPInstallSaving", "installed": "MCPInstallCompleted", "rollback": "MCPInstallRollback", + "cleanup": "MCPInstallCleanup", "cleanup_complete": "MCPInstallOldContainerRemoved", + "old_image_removed": "MCPInstallOldImageRemoved", "old_image_kept": "MCPInstallOldImageKept", + "old_image_retained": "MCPInstallOldImageRetained", "cleanup_skipped": "MCPInstallCleanupSkipped", + "cleanup_failed": "MCPInstallCleanupFailed", "recovery_failed": "MCPInstallRecoveryFailed", }[progress.Status] if stage != "" { diff --git a/core/app/service/mcp_runtime.go b/core/app/service/mcp_runtime.go index 29bde1dd9..7d5527761 100644 --- a/core/app/service/mcp_runtime.go +++ b/core/app/service/mcp_runtime.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "reflect" + "strconv" "strings" "sync" "sync/atomic" @@ -22,10 +23,12 @@ import ( var mcpRuntimeMutation sync.Mutex var mcpInstalling atomic.Bool -var ErrMCPSettingsRevision = errors.New("MCP settings changed; reload the page before saving") +var ErrMCPSettingsRevision = contract.Conflict("settings_revision_changed", "MCP settings changed; reload the page before saving") func lockMCPRuntimeMutation() error { - conflict := func() error { return contract.Fail("MCP_CONFLICT", 409, "An MCP service operation is already running") } + conflict := func() error { + return contract.Conflict("installation_running", "An MCP installation is already running") + } if mcpInstalling.Load() { return conflict() } @@ -43,6 +46,7 @@ type mcpRuntimeManager interface { Status(context.Context, bool) mcpruntime.State Apply(context.Context, bool, bool) error InstallImage(context.Context, bool) error + Finalize(context.Context, bool) error Restore(context.Context, string, bool) error RestoreAbsent(context.Context) error Logs(context.Context) (string, error) @@ -165,7 +169,7 @@ func (s *MCPRuntimeService) update(ctx context.Context, req dto.MCPRuntimeSettin if row, loadErr := s.websiteSetting(ctx); loadErr != nil { return old, loadErr } else if row.WebsiteID != 0 || row.WebsiteBindingID != "" { - return old, contract.Fail("MCP_CONFLICT", 409, "The website controls the MCP endpoint") + return old, contract.Conflict("endpoint_managed_by_website", "The website controls the MCP endpoint") } } if err := validateMCPRuntimeSettings(req); err != nil { @@ -290,7 +294,10 @@ func (s *MCPRuntimeService) Install(ctx context.Context, req dto.MCPRuntimeInsta return dto.MCPRuntimeStatus{}, err } defer mcpRuntimeMutation.Unlock() - return s.install(ctx, req) + if err := s.install(ctx, req); err != nil { + return dto.MCPRuntimeStatus{}, err + } + return s.Status(ctx) } func (s *MCPRuntimeService) installSettings(ctx context.Context, req dto.MCPRuntimeInstall) (old, next dto.MCPRuntimeSettings, err error) { @@ -308,7 +315,7 @@ func (s *MCPRuntimeService) installSettings(ctx context.Context, req dto.MCPRunt if row, loadErr := s.websiteSetting(ctx); loadErr != nil { return old, next, loadErr } else if row.WebsiteID != 0 || row.WebsiteBindingID != "" { - return old, next, contract.Fail("MCP_CONFLICT", 409, "The website controls the MCP endpoint") + return old, next, contract.Conflict("endpoint_managed_by_website", "The website controls the MCP endpoint") } } if err := validateMCPRuntimeSettings(next); err != nil { @@ -320,15 +327,15 @@ func (s *MCPRuntimeService) installSettings(ctx context.Context, req dto.MCPRunt return old, next, nil } -func (s *MCPRuntimeService) install(ctx context.Context, req dto.MCPRuntimeInstall) (dto.MCPRuntimeStatus, error) { +func (s *MCPRuntimeService) install(ctx context.Context, req dto.MCPRuntimeInstall) error { old, next, err := s.installSettings(ctx, req) if err != nil { - return dto.MCPRuntimeStatus{}, err + return err } mcpruntime.ReportProgress(ctx, mcpruntime.Progress{Status: "snapshot"}) snapshot, err := s.snapshot(ctx, old) if err != nil { - return dto.MCPRuntimeStatus{}, err + return err } if !snapshot.installed { next.Enabled = true @@ -338,7 +345,7 @@ func (s *MCPRuntimeService) install(ctx context.Context, req dto.MCPRuntimeInsta if policyPrepared { prepared.AccessPolicy, prepared.AllowedIPs = next.AccessPolicy, next.AllowedIPs if err := s.persist(prepared, old.Revision); err != nil { - return dto.MCPRuntimeStatus{}, err + return err } prepared.Revision++ } @@ -376,14 +383,28 @@ func (s *MCPRuntimeService) install(ctx context.Context, req dto.MCPRuntimeInsta return operationErr } if err := installMCPRuntimeImage(ctx, manager, next.Enabled); err != nil { - return dto.MCPRuntimeStatus{}, recoverFailure(err) + return recoverFailure(err) } mcpruntime.ReportProgress(ctx, mcpruntime.Progress{Status: "saving"}) if err := s.persist(next, prepared.Revision); err != nil { - return dto.MCPRuntimeStatus{}, recoverFailure(err) + return recoverFailure(err) } + cleanupCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + if cleanupErr := finalizeMCPRuntime(cleanupCtx, manager, req.DeleteOldImage); cleanupErr != nil { + mcpruntime.ReportProgress(ctx, mcpruntime.Progress{Status: "cleanup_failed"}) + } + cancel() mcpruntime.ReportProgress(ctx, mcpruntime.Progress{Status: "installed"}) - return s.Status(ctx) + return nil +} + +func finalizeMCPRuntime(ctx context.Context, manager mcpRuntimeManager, deleteOldImage bool) (err error) { + defer func() { + if recover() != nil { + err = errors.New("MCP old resources could not be cleaned up") + } + }() + return manager.Finalize(ctx, deleteOldImage) } func installMCPRuntimeImage(ctx context.Context, manager mcpRuntimeManager, enabled bool) (err error) { @@ -423,12 +444,36 @@ func validateMCPRuntimeSettings(req dto.MCPRuntimeSettings) error { if req.ExternalURL == "" { return nil } - if len(req.ExternalURL) > 2048 || strings.ContainsAny(req.ExternalURL, "\r\n") { - return errors.New("invalid MCP external URL") - } - u, err := url.Parse(req.ExternalURL) - if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/mcp" { - return errors.New("MCP external URL must be an HTTP or HTTPS URL ending in /mcp without credentials or query parameters") + if !validMCPExternalEndpoint(req.ExternalURL) { + return errors.New("MCP external URL must be a valid HTTP or HTTPS URL without credentials, query parameters or fragments") } return nil } + +func validMCPExternalEndpoint(endpoint string) bool { + if endpoint == "" || len(endpoint) > 2048 || strings.TrimSpace(endpoint) != endpoint || strings.ContainsAny(endpoint, "\\?#") { + return false + } + for _, char := range endpoint { + if char <= 0x20 || char == 0x7f { + return false + } + } + u, err := url.Parse(endpoint) + if err != nil || (!strings.EqualFold(u.Scheme, "http") && !strings.EqualFold(u.Scheme, "https")) || + u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.Opaque != "" { + return false + } + for _, char := range u.Path { + if char < 0x20 || char == 0x7f || char == '\\' { + return false + } + } + if port := u.Port(); port != "" { + value, err := strconv.Atoi(port) + if err != nil || value < 0 || value > 65535 { + return false + } + } + return true +} diff --git a/core/app/service/mcp_website.go b/core/app/service/mcp_website.go index 954729a96..95b50a7b1 100644 --- a/core/app/service/mcp_website.go +++ b/core/app/service/mcp_website.go @@ -28,32 +28,34 @@ import ( ) type mcpWebsiteAgentRequest struct { - Operation string `json:"operation"` - WebsiteID uint `json:"websiteId"` - BindingID string `json:"bindingId"` - RequestID string `json:"requestId"` - Domain string `json:"domain"` - EnableSSL bool `json:"enableSSL"` - SSLID uint `json:"sslID"` - Upstream string `json:"upstream"` - CoreHost string `json:"coreHost"` - ProxyKey string `json:"proxyKey"` -} - -type mcpWebsiteAgentResponse struct { - State string `json:"state"` + Operation string `json:"operation"` WebsiteID uint `json:"websiteId"` BindingID string `json:"bindingId"` RequestID string `json:"requestId"` Domain string `json:"domain"` - Endpoint string `json:"endpoint"` EnableSSL bool `json:"enableSSL"` SSLID uint `json:"sslID"` - WebsiteStatus string `json:"websiteStatus"` - Reason string `json:"reason,omitempty"` - Error string `json:"error,omitempty"` - Code string `json:"code,omitempty"` - Definitive bool `json:"definitive,omitempty"` + Upstream string `json:"upstream"` + CoreHost string `json:"coreHost"` + ProxyKey string `json:"proxyKey"` + DeleteWebsite bool `json:"deleteWebsite,omitempty"` +} + +type mcpWebsiteAgentResponse struct { + State string `json:"state"` + WebsiteID uint `json:"websiteId"` + BindingID string `json:"bindingId"` + RequestID string `json:"requestId"` + Domain string `json:"domain"` + Endpoint string `json:"endpoint"` + EnableSSL bool `json:"enableSSL"` + SSLID uint `json:"sslID"` + WebsiteStatus string `json:"websiteStatus"` + Reason string `json:"reason,omitempty"` + Error string `json:"error,omitempty"` + Code string `json:"code,omitempty"` + Definitive bool `json:"definitive,omitempty"` + WebsiteDeleted bool `json:"websiteDeleted,omitempty"` } type mcpWebsiteTransport interface { @@ -97,10 +99,11 @@ func (mcpWebsiteHTTPTransport) Call(ctx context.Context, request mcpWebsiteAgent type mcpWebsiteOperation struct { dto.MCPWebsitePendingRequest - Hash string `json:"hash"` - BindingID string `json:"bindingId"` - State string `json:"state"` - ErrorCode string `json:"errorCode,omitempty"` + Hash string `json:"hash"` + BindingID string `json:"bindingId"` + State string `json:"state"` + ErrorCode string `json:"errorCode,omitempty"` + ErrorReason string `json:"errorReason,omitempty"` } func (s *MCPRuntimeService) websiteAgent() mcpWebsiteTransport { @@ -126,7 +129,8 @@ func decodeMCPWebsiteOperation(value string) (*mcpWebsiteOperation, error) { if json.Unmarshal([]byte(value), &operation) != nil || !validMCPWebsiteRequestID(operation.RequestID) || !validMCPWebsiteRequestID(operation.BindingID) || len(operation.Hash) != 64 || (operation.State != "pending" && operation.State != "succeeded" && operation.State != "failed") || - (operation.Operation != "bind" && operation.Operation != "update" && operation.Operation != "unbind") { + (operation.Operation != "bind" && operation.Operation != "update" && operation.Operation != "unbind") || + (operation.DeleteWebsite && operation.Operation != "unbind") { return nil, contract.Fail("MCP_WEBSITE_UNAVAILABLE", 503, "MCP website state is unavailable") } return &operation, nil @@ -181,7 +185,7 @@ func normalizeMCPWebsiteDomain(domain string) (string, error) { } func validMCPWebsiteMetadata(response mcpWebsiteAgentResponse) bool { - if !mcpDocumentEndpoint(response.Endpoint) || response.EnableSSL && response.SSLID == 0 || !response.EnableSSL && response.SSLID != 0 { + if !validMCPExternalEndpoint(response.Endpoint) || response.EnableSSL && response.SSLID == 0 || !response.EnableSSL && response.SSLID != 0 { return false } domain, err := normalizeMCPWebsiteDomain(response.Domain) @@ -190,7 +194,7 @@ func validMCPWebsiteMetadata(response mcpWebsiteAgentResponse) bool { } domainURL, _ := url.Parse("http://" + domain) endpoint, _ := url.Parse(response.Endpoint) - return strings.EqualFold(domainURL.Hostname(), endpoint.Hostname()) && (endpoint.Scheme == "https") == response.EnableSSL + return endpoint.Path == "/mcp" && strings.EqualFold(domainURL.Hostname(), endpoint.Hostname()) && (endpoint.Scheme == "https") == response.EnableSSL } func (s *MCPRuntimeService) websiteAgentRequest(ctx context.Context, row model.MCPSetting, operation string, operationID string) (mcpWebsiteAgentRequest, error) { @@ -249,6 +253,7 @@ func (s *MCPRuntimeService) websiteStatus(ctx context.Context, row model.MCPSett } if operation != nil { result.RequestID = operation.RequestID + result.WebsiteDeleted = operation.Operation == "unbind" && operation.State == "succeeded" && operation.DeleteWebsite } if row.WebsiteID == 0 && row.WebsiteBindingID == "" && (operation == nil || operation.State != "pending") { return result, nil @@ -301,18 +306,45 @@ func (s *MCPRuntimeService) UpdateWebsite(ctx context.Context, req dto.MCPWebsit } func (s *MCPRuntimeService) UnbindWebsite(ctx context.Context, req dto.MCPWebsiteUnbind) (dto.MCPWebsiteStatus, error) { - return s.mutateWebsite(ctx, dto.MCPWebsitePendingRequest{Operation: "unbind", RequestID: req.RequestID}, req.Revision) + return s.mutateWebsite(ctx, dto.MCPWebsitePendingRequest{Operation: "unbind", RequestID: req.RequestID, DeleteWebsite: req.DeleteWebsite}, req.Revision) } -func mcpWebsiteOperationError(code string, uncertain bool) error { +func mcpWebsiteConflictReason(reason string) string { + switch reason { + case "domain_conflict": + return "website_domain_conflict" + case "website_ownership_changed": + return "website_ownership_changed" + case "domain_changed": + return "website_domain_changed" + case "website_directory_changed", "proxy_directory_changed": + return "website_directory_changed" + case "proxy_modified", "proxy_already_exists": + return "website_proxy_modified" + case "website_modified": + return "website_modified" + case "binding_request_changed", "operation_request_changed": + return "website_request_changed" + case "proxy_not_loaded": + return "website_proxy_not_loaded" + case "website_missing": + return "website_missing" + default: + return "" + } +} + +func mcpWebsiteOperationError(code, reason string, uncertain bool) error { if uncertain { return contract.Fail("MCP_WEBSITE_PENDING", 503, "MCP website operation could not be confirmed") } switch code { case "MCP_INVALID_ARGUMENTS", "MCP_WEBSITE_INVALID", "MCP_WEBSITE_INVALID_ARGUMENTS": return contract.Invalid() + case "MCP_WEBSITE_HTTP_PORT_REQUIRED": + return contract.Fail("MCP_WEBSITE_HTTP_PORT_REQUIRED", 400, "Use the OpenResty HTTP port as the website base port before enabling HTTPS") case "MCP_CONFLICT", "MCP_WEBSITE_CONFLICT": - return contract.Fail("MCP_CONFLICT", 409, "MCP website configuration changed") + return contract.Conflict(reason, "MCP website configuration changed") case "MCP_FORBIDDEN": return contract.Forbidden() default: @@ -330,6 +362,7 @@ func (s *MCPRuntimeService) mutateWebsite(ctx context.Context, pending dto.MCPWe func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto.MCPWebsitePendingRequest, revision uint64) (dto.MCPWebsiteStatus, error) { if !validMCPWebsiteRequestID(pending.RequestID) || revision == 0 || + (pending.DeleteWebsite && pending.Operation != "unbind") || (pending.Operation != "unbind" && (pending.EnableSSL && pending.SSLID == 0 || !pending.EnableSSL && pending.SSLID != 0)) { return dto.MCPWebsiteStatus{}, contract.Invalid() } @@ -347,24 +380,40 @@ func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto operation := mcpWebsiteOperation{MCPWebsitePendingRequest: pending, Hash: hash, State: "pending", BindingID: row.WebsiteBindingID} if previous != nil && previous.RequestID == pending.RequestID { if previous.Hash != hash { - return dto.MCPWebsiteStatus{}, contract.Fail("MCP_CONFLICT", 409, "MCP website request was already used") + return dto.MCPWebsiteStatus{}, contract.Conflict("request_reused", "MCP website request was already used") } if previous.State == "succeeded" { return s.websiteStatus(ctx, row) } if previous.State == "failed" { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError(previous.ErrorCode, false) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError(previous.ErrorCode, previous.ErrorReason, false) } operation = *previous } else { cancelPendingChange := previous != nil && previous.State == "pending" && (previous.Operation == "bind" || previous.Operation == "update") && pending.Operation == "unbind" - if previous != nil && previous.State == "pending" && !cancelPendingChange || row.Revision != revision { + if row.Revision != revision { return dto.MCPWebsiteStatus{}, ErrMCPSettingsRevision } + if previous != nil && previous.State == "pending" && !cancelPendingChange { + return dto.MCPWebsiteStatus{}, contract.Conflict("website_operation_pending", "An MCP website operation is still pending") + } + if cancelPendingChange && previous.Operation == "bind" && row.WebsiteID == 0 && pending.DeleteWebsite { + // Keep the original bind retryable until the Agent can identify its website. + probe, probeErr := s.websiteAgentRequest(ctx, row, "status", previous.RequestID) + if probeErr != nil { + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("MCP_CONFLICT", "website_state_unconfirmed", false) + } + status, statusErr := s.websiteAgent().Call(ctx, probe) + if statusErr != nil || status.Error != "" || status.BindingID != row.WebsiteBindingID || + (status.State != "unbound" && status.State != "bound" && status.State != "unavailable") || + (status.WebsiteID == 0 && status.State != "unbound") { + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("MCP_CONFLICT", "website_state_unconfirmed", false) + } + } if pending.Operation == "bind" { if row.WebsiteID != 0 { - return dto.MCPWebsiteStatus{}, contract.Fail("MCP_CONFLICT", 409, "MCP already has a bound website") + return dto.MCPWebsiteStatus{}, contract.Conflict("website_already_bound", "MCP already has a bound website") } settings, err := s.settings(ctx) if err != nil { @@ -376,7 +425,7 @@ func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto operation.BindingID = pending.RequestID row.WebsiteBindingID = operation.BindingID } else if row.WebsiteID == 0 && !cancelPendingChange || !validMCPWebsiteRequestID(row.WebsiteBindingID) { - return dto.MCPWebsiteStatus{}, contract.Fail("MCP_CONFLICT", 409, "MCP has no bound website") + return dto.MCPWebsiteStatus{}, contract.Conflict("website_not_bound", "MCP has no bound website") } if _, err := s.websiteAgentRequest(ctx, row, pending.Operation, pending.RequestID); err != nil { return dto.MCPWebsiteStatus{}, err @@ -387,33 +436,36 @@ func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto } request, err := s.websiteAgentRequest(ctx, row, pending.Operation, pending.RequestID) if err != nil { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } request.Domain, request.EnableSSL, request.SSLID = pending.Domain, pending.EnableSSL, pending.SSLID + request.DeleteWebsite = pending.DeleteWebsite response, callErr := s.websiteAgent().Call(ctx, request) if callErr != nil { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } if response.Error != "" { if !response.Definitive || pending.Operation == "unbind" && row.WebsiteID == 0 && row.WebsiteBindingID != "" { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } operation.State, operation.ErrorCode = "failed", response.Code + operation.ErrorReason = mcpWebsiteConflictReason(response.Reason) bindingID := row.WebsiteBindingID if pending.Operation == "bind" && row.WebsiteID == 0 { bindingID = "" } if err := s.saveWebsiteOperation(ctx, &row, operation, row.WebsiteID, bindingID); err != nil { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError(response.Code, false) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError(response.Code, operation.ErrorReason, false) } if response.BindingID != operation.BindingID || response.RequestID != pending.RequestID || (pending.Operation != "unbind" && (response.State != "bound" || response.WebsiteID == 0 || !validMCPWebsiteMetadata(response))) || (pending.Operation != "unbind" && (response.EnableSSL != pending.EnableSSL || response.SSLID != pending.SSLID)) || (pending.Operation == "update" && response.WebsiteID != row.WebsiteID) || - (pending.Operation == "unbind" && response.State != "unbound") { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + (pending.Operation == "unbind" && response.State != "unbound") || + (pending.Operation == "unbind" && pending.DeleteWebsite && (!response.WebsiteDeleted || (row.WebsiteID != 0 && response.WebsiteID != row.WebsiteID))) { + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } websiteID, bindingID := response.WebsiteID, operation.BindingID if pending.Operation == "unbind" { @@ -421,10 +473,10 @@ func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto } operation.State = "succeeded" if err := s.saveWebsiteOperation(ctx, &row, operation, websiteID, bindingID); err != nil { - return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", true) + return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("", "", true) } if pending.Operation == "unbind" { - return dto.MCPWebsiteStatus{State: "unbound", Revision: row.Revision, RequestID: pending.RequestID}, nil + return dto.MCPWebsiteStatus{State: "unbound", Revision: row.Revision, RequestID: pending.RequestID, WebsiteDeleted: pending.DeleteWebsite}, nil } return dto.MCPWebsiteStatus{State: "bound", Revision: row.Revision, WebsiteID: response.WebsiteID, Domain: response.Domain, Endpoint: response.Endpoint, EnableSSL: response.EnableSSL, SSLID: response.SSLID, WebsiteStatus: response.WebsiteStatus, RequestID: pending.RequestID}, nil diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index 6147d3205..8e9268614 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "The panel server checks tool, node and current account permis MCPDocumentHTTPWarning: "This address uses unencrypted HTTP. Configure HTTPS before using it on an untrusted network." # MCP management -ErrMCPConflict: "MCP data changed or conflicts with existing data. Refresh and review it." -ErrMCPPolicyChanged: "The MCP tool catalog or authorization changed. Refresh, review, and save the authorization." -ErrMCPCatalogUnavailable: "The MCP tool catalog is unavailable. Check the service status." -ErrMCPDisabled: "The MCP service is not enabled." -ErrMCPSecretKeyUnavailable: "MCP credential encryption is unavailable. Try again later." -ErrMCPSecretInvalid: "The stored MCP Token cannot be decrypted. Regenerate the Token." -ErrMCPCredentialInactive: "The MCP credential is disabled, expired, or revoked." -ErrMCPAuditUnavailable: "MCP operation auditing is unavailable. Verify the operation result before continuing." -ErrMCPEndpointUnavailable: "Configure a valid external MCP endpoint first." -ErrMCPWebsitePending: "The website operation result is uncertain. Refresh the binding status before retrying the original operation." -ErrMCPWebsiteUnavailable: "The website binding is unavailable. Check the website status and OpenResty configuration." +ErrMCPConflict: "MCP data conflict. Refresh and review." +ErrMCPSettingsChanged: "Service settings changed. Refresh and review before saving." +ErrMCPCredentialChanged: "Credential changed. Refresh and review before submitting." +ErrMCPCredentialRevoked: "Credential revoked. Create a new one." +ErrMCPCredentialNameExists: "Credential name already exists. Choose another name." +ErrMCPRequestReused: "Request details conflict. Close and reopen this window." +ErrMCPInstallationRunning: "Installation in progress. Check the installation log." +ErrMCPServiceOperationRunning: "Service operation in progress. Refresh when it finishes." +ErrMCPEndpointManagedByWebsite: "The website manages this endpoint. Change it in website settings." +ErrMCPWebsiteConfigurationExists: "A binding or pending operation exists. Clear “Bind a website after installation”." +ErrMCPWebsiteAlreadyBound: "Website already bound. Manage or remove the existing binding." +ErrMCPWebsiteNotBound: "No website bound. Refresh before binding." +ErrMCPWebsiteMissing: "The original website is missing. Remove the stale binding and bind again." +ErrMCPWebsiteOperationPending: "Website operation unconfirmed. Refresh, then retry the original operation." +ErrMCPWebsiteDomainConflict: "This domain and port are in use. Change the domain or port." +ErrMCPWebsiteOwnershipChanged: "Website ownership changed. Review the website and binding settings." +ErrMCPWebsiteDomainChanged: "Website domain changed. Review it and refresh the binding status." +ErrMCPWebsiteDirectoryChanged: "Website directory or permissions changed. Restore them before retrying." +ErrMCPWebsiteProxyModified: "Proxy configuration conflict. Restore the MCP proxy configuration." +ErrMCPWebsiteModified: "Website protocol or settings changed. Review and refresh." +ErrMCPWebsiteRequestChanged: "Website operation record mismatch. Refresh and verify before starting again." +ErrMCPWebsiteProxyNotLoaded: "MCP proxy not loaded. Check website settings and OpenResty." +ErrMCPWebsiteStateUnconfirmed: "Website state unconfirmed. Check the website and OpenResty, then refresh." +ErrMCPPolicyChanged: "Tool catalog or grants changed. Refresh and review before saving." +ErrMCPCatalogUnavailable: "Tool catalog unavailable. Check service status." +ErrMCPDisabled: "MCP service is not enabled." +ErrMCPSecretKeyUnavailable: "Credential encryption unavailable. Try again later." +ErrMCPSecretInvalid: "Token cannot be decrypted. Regenerate it." +ErrMCPCredentialInactive: "Credential is disabled, expired, or revoked." +ErrMCPAuditUnavailable: "Operation audit unavailable. Verify the operation result first." +ErrMCPEndpointUnavailable: "Configure a valid MCP endpoint first." +ErrMCPWebsitePending: "Website outcome unconfirmed. Refresh, then retry the original operation." +ErrMCPWebsiteUnavailable: "Website binding unavailable. Check the website and OpenResty." ErrMCPWebsiteServiceRequired: "Install the 1Panel MCP service first." +ErrMCPWebsiteHTTPPortRequired: "Use the OpenResty HTTP port for HTTPS binding." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "Check the current MCP service" @@ -556,10 +580,17 @@ MCPInstallSaving: "Save the service configuration" MCPInstallCompleted: "MCP service installation completed" MCPInstallRollback: "Installation failed; restore the previous MCP service" MCPInstallBindWebsite: "Create the bound website" -MCPInstallWebsiteFailed: "The MCP service is installed, but website binding failed. Resolve the website configuration in the service settings." -MCPInstallInterrupted: "Installation was interrupted by a panel restart. Check the service status before continuing." -MCPInstallFailed: "MCP installation failed. Check Docker, the image, and the panel service logs." -MCPInstallRecoveryFailed: "Installation failed and the previous service could not be fully restored. Check the MCP service status before continuing." +MCPInstallWebsiteFailed: "Service installed; website binding failed. Resolve it in service settings." +MCPInstallInterrupted: "Panel restart interrupted installation. Verify service status." +MCPInstallFailed: "Installation failed. Check Docker, the image, and panel logs." +MCPInstallRecoveryFailed: "Installation failed and service recovery is incomplete. Verify service status." MCPInstallLocalImage: "The image exists locally; skip downloading" MCPInstallVerifyImage: "Validate the image version and managed service contract" MCPInstallVerifiedImage: "Image validation passed" +MCPInstallCleanup: "Clean up the previous container" +MCPInstallOldContainerRemoved: "Previous container removed" +MCPInstallOldImageRemoved: "Previous image removed" +MCPInstallOldImageKept: "Previous image retained" +MCPInstallOldImageRetained: "The previous image is referenced or has other tags; it was retained." +MCPInstallCleanupSkipped: "The previous container is absent; skip image cleanup." +MCPInstallCleanupFailed: "MCP service upgraded, but old resources could not be fully cleaned up. Check them manually." diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 1858892f6..86348c140 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "El servidor del panel comprueba los permisos de herramientas, MCPDocumentHTTPWarning: "Esta dirección utiliza HTTP sin cifrar. Configure HTTPS antes de usarla en una red no fiable." # MCP management -ErrMCPConflict: "Los datos MCP han cambiado o entran en conflicto. Actualice la página y revíselos." -ErrMCPPolicyChanged: "El catálogo de herramientas MCP o la autorización han cambiado. Actualice, revise y guarde la autorización." -ErrMCPCatalogUnavailable: "El catálogo de herramientas MCP no está disponible. Compruebe el estado del servicio." +ErrMCPConflict: "Conflicto de datos MCP. Actualice y revise." +ErrMCPSettingsChanged: "La configuración cambió. Actualice y revise antes de guardar." +ErrMCPCredentialChanged: "La credencial cambió. Actualice y revise antes de enviar." +ErrMCPCredentialRevoked: "Credencial revocada. Cree otra." +ErrMCPCredentialNameExists: "El nombre de credencial ya existe. Elija otro." +ErrMCPRequestReused: "Conflicto en la solicitud. Cierre y vuelva a abrir esta ventana." +ErrMCPInstallationRunning: "Instalación en curso. Consulte el registro de instalación." +ErrMCPServiceOperationRunning: "Operación del servicio en curso. Actualice cuando termine." +ErrMCPEndpointManagedByWebsite: "El sitio gestiona esta dirección. Modifíquela en sus ajustes." +ErrMCPWebsiteConfigurationExists: "Existe una vinculación u operación pendiente. Desmarque «Vincular sitio después de la instalación»." +ErrMCPWebsiteAlreadyBound: "Sitio ya vinculado. Gestione o elimine la vinculación actual." +ErrMCPWebsiteNotBound: "No hay sitio vinculado. Actualice antes de vincular." +ErrMCPWebsiteMissing: "El sitio original ya no existe. Elimine la vinculación obsoleta y vuelva a vincular." +ErrMCPWebsiteOperationPending: "Operación del sitio sin confirmar. Actualice y reintente la operación original." +ErrMCPWebsiteDomainConflict: "La combinación de dominio y puerto está en uso. Cambie el dominio o el puerto." +ErrMCPWebsiteOwnershipChanged: "La propiedad del sitio cambió. Revise el sitio y la vinculación." +ErrMCPWebsiteDomainChanged: "El dominio cambió. Revíselo y actualice el estado de vinculación." +ErrMCPWebsiteDirectoryChanged: "El directorio o sus permisos cambiaron. Restáurelos antes de reintentar." +ErrMCPWebsiteProxyModified: "Conflicto de proxy. Restaure la configuración del proxy MCP." +ErrMCPWebsiteModified: "El protocolo o los ajustes del sitio cambiaron. Revise y actualice." +ErrMCPWebsiteRequestChanged: "El registro no coincide. Actualice y verifique antes de iniciar otra operación." +ErrMCPWebsiteProxyNotLoaded: "Proxy MCP no cargado. Revise los ajustes del sitio y OpenResty." +ErrMCPWebsiteStateUnconfirmed: "Estado del sitio sin confirmar. Revise el sitio y OpenResty y actualice." +ErrMCPPolicyChanged: "El catálogo o los permisos cambiaron. Actualice y revise antes de guardar." +ErrMCPCatalogUnavailable: "Catálogo no disponible. Revise el estado del servicio." ErrMCPDisabled: "El servicio MCP no está habilitado." -ErrMCPSecretKeyUnavailable: "El cifrado de credenciales MCP no está disponible. Inténtelo más tarde." -ErrMCPSecretInvalid: "No se puede descifrar el Token MCP almacenado. Genere un Token nuevo." -ErrMCPCredentialInactive: "La credencial MCP está deshabilitada, ha caducado o se ha revocado." -ErrMCPAuditUnavailable: "La auditoría de operaciones MCP no está disponible. Verifique el resultado antes de continuar." -ErrMCPEndpointUnavailable: "Configure primero una dirección externa MCP válida." -ErrMCPWebsitePending: "El resultado de la operación del sitio es incierto. Actualice el estado de la vinculación antes de reintentar la operación original." -ErrMCPWebsiteUnavailable: "La vinculación del sitio no está disponible. Revise el estado del sitio y la configuración de OpenResty." +ErrMCPSecretKeyUnavailable: "Cifrado de credenciales no disponible. Inténtelo más tarde." +ErrMCPSecretInvalid: "No se puede descifrar el Token. Genérelo de nuevo." +ErrMCPCredentialInactive: "La credencial está deshabilitada, caducada o revocada." +ErrMCPAuditUnavailable: "Auditoría no disponible. Verifique primero el resultado de la operación." +ErrMCPEndpointUnavailable: "Configure primero una dirección MCP válida." +ErrMCPWebsitePending: "Resultado del sitio sin confirmar. Actualice y reintente la operación original." +ErrMCPWebsiteUnavailable: "Vinculación no disponible. Revise el sitio y OpenResty." ErrMCPWebsiteServiceRequired: "Instale primero el servicio 1Panel MCP." +ErrMCPWebsiteHTTPPortRequired: "Use el puerto HTTP de OpenResty para vincular con HTTPS." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "Comprobar el servicio MCP actual" @@ -556,10 +580,17 @@ MCPInstallSaving: "Guardar la configuración del servicio" MCPInstallCompleted: "Instalación del servicio MCP completada" MCPInstallRollback: "La instalación falló; restaurar el servicio MCP anterior" MCPInstallBindWebsite: "Crear el sitio web vinculado" -MCPInstallWebsiteFailed: "El servicio MCP está instalado, pero la vinculación del sitio web falló. Revise la configuración del sitio web en los ajustes del servicio." -MCPInstallInterrupted: "Un reinicio del panel interrumpió la instalación. Compruebe el estado del servicio antes de continuar." -MCPInstallFailed: "La instalación de MCP falló. Revise Docker, la imagen y los registros del servicio del panel." -MCPInstallRecoveryFailed: "La instalación falló y el servicio anterior no se pudo restaurar por completo. Compruebe el estado del servicio MCP antes de continuar." +MCPInstallWebsiteFailed: "Servicio instalado; falló la vinculación. Resuélvalo en los ajustes del servicio." +MCPInstallInterrupted: "El reinicio del panel interrumpió la instalación. Verifique el estado del servicio." +MCPInstallFailed: "Instalación fallida. Revise Docker, la imagen y los registros del panel." +MCPInstallRecoveryFailed: "Instalación fallida y recuperación incompleta. Verifique el estado del servicio." MCPInstallLocalImage: "La imagen ya existe localmente; omitir la descarga" MCPInstallVerifyImage: "Validar la versión de la imagen y el contrato del servicio administrado" MCPInstallVerifiedImage: "La imagen superó la validación" +MCPInstallCleanup: "Limpiar el contenedor anterior" +MCPInstallOldContainerRemoved: "Contenedor anterior eliminado" +MCPInstallOldImageRemoved: "Imagen anterior eliminada" +MCPInstallOldImageKept: "Imagen anterior conservada" +MCPInstallOldImageRetained: "La imagen anterior tiene referencias u otras etiquetas; se ha conservado." +MCPInstallCleanupSkipped: "El contenedor anterior no existe; se omite la limpieza de la imagen." +MCPInstallCleanupFailed: "MCP se ha actualizado, pero no se han limpiado todos los recursos anteriores. Revísalos manualmente." diff --git a/core/i18n/lang/fa.yaml b/core/i18n/lang/fa.yaml index 92a4cabb4..5d0d1f4df 100644 --- a/core/i18n/lang/fa.yaml +++ b/core/i18n/lang/fa.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "سرور پنل در هر درخواست مجوزهای اب MCPDocumentHTTPWarning: "این نشانی از HTTP بدون رمزنگاری استفاده می‌کند. پیش از استفاده در شبکه نامطمئن، HTTPS را پیکربندی کنید." # MCP management -ErrMCPConflict: "داده‌های MCP تغییر کرده‌اند یا با داده‌های موجود تداخل دارند. صفحه را تازه‌سازی و داده‌ها را بررسی کنید." -ErrMCPPolicyChanged: "فهرست ابزارهای MCP یا مجوزها تغییر کرده است. صفحه را تازه‌سازی کنید، مجوزها را بررسی و ذخیره کنید." -ErrMCPCatalogUnavailable: "فهرست ابزارهای MCP در دسترس نیست. وضعیت سرویس را بررسی کنید." +ErrMCPConflict: "تداخل داده‌های MCP. تازه‌سازی و بررسی کنید." +ErrMCPSettingsChanged: "تنظیمات سرویس تغییر کرده است. پیش از ذخیره تازه‌سازی و بررسی کنید." +ErrMCPCredentialChanged: "اطلاعات ورود تغییر کرده است. پیش از ارسال تازه‌سازی و بررسی کنید." +ErrMCPCredentialRevoked: "اطلاعات ورود لغو شده است. مورد جدیدی بسازید." +ErrMCPCredentialNameExists: "نام اطلاعات ورود تکراری است. نام دیگری انتخاب کنید." +ErrMCPRequestReused: "جزئیات درخواست تداخل دارد. پنجره را ببندید و دوباره باز کنید." +ErrMCPInstallationRunning: "نصب در حال انجام است. گزارش نصب را بررسی کنید." +ErrMCPServiceOperationRunning: "عملیات سرویس در حال انجام است. پس از پایان تازه‌سازی کنید." +ErrMCPEndpointManagedByWebsite: "این آدرس توسط وب‌سایت مدیریت می‌شود. آن را در تنظیمات سایت تغییر دهید." +ErrMCPWebsiteConfigurationExists: "اتصال یا عملیات معلق وجود دارد. گزینه «اتصال وب‌سایت پس از نصب» را غیرفعال کنید." +ErrMCPWebsiteAlreadyBound: "وب‌سایت متصل است. اتصال فعلی را مدیریت یا حذف کنید." +ErrMCPWebsiteNotBound: "وب‌سایتی متصل نیست. پیش از اتصال تازه‌سازی کنید." +ErrMCPWebsiteMissing: "وب‌سایت قبلی وجود ندارد. اتصال نامعتبر را حذف و دوباره متصل کنید." +ErrMCPWebsiteOperationPending: "عملیات سایت تأیید نشده است. تازه‌سازی و همان عملیات را دوباره امتحان کنید." +ErrMCPWebsiteDomainConflict: "ترکیب این دامنه و پورت در حال استفاده است. دامنه یا پورت را تغییر دهید." +ErrMCPWebsiteOwnershipChanged: "مالکیت سایت تغییر کرده است. سایت و تنظیمات اتصال را بررسی کنید." +ErrMCPWebsiteDomainChanged: "دامنه تغییر کرده است. آن را بررسی و وضعیت اتصال را تازه‌سازی کنید." +ErrMCPWebsiteDirectoryChanged: "پوشه یا مجوزهای سایت تغییر کرده است. پیش از تلاش دوباره بازیابی کنید." +ErrMCPWebsiteProxyModified: "تداخل تنظیمات پروکسی. تنظیمات پروکسی MCP را بازیابی کنید." +ErrMCPWebsiteModified: "پروتکل یا تنظیمات سایت تغییر کرده است. بررسی و تازه‌سازی کنید." +ErrMCPWebsiteRequestChanged: "رکورد عملیات سایت مطابقت ندارد. پیش از عملیات جدید تازه‌سازی و نتیجه را بررسی کنید." +ErrMCPWebsiteProxyNotLoaded: "پروکسی MCP بارگیری نشده است. تنظیمات سایت و OpenResty را بررسی کنید." +ErrMCPWebsiteStateUnconfirmed: "وضعیت سایت تأیید نشده است. سایت و OpenResty را بررسی و تازه‌سازی کنید." +ErrMCPPolicyChanged: "فهرست ابزارها یا مجوزها تغییر کرده است. پیش از ذخیره تازه‌سازی و بررسی کنید." +ErrMCPCatalogUnavailable: "فهرست ابزارها در دسترس نیست. وضعیت سرویس را بررسی کنید." ErrMCPDisabled: "سرویس MCP فعال نیست." -ErrMCPSecretKeyUnavailable: "رمزنگاری اطلاعات اعتبار MCP در دسترس نیست. بعداً دوباره تلاش کنید." -ErrMCPSecretInvalid: "رمزگشایی Token ذخیره‌شده MCP ممکن نیست. Token را دوباره ایجاد کنید." -ErrMCPCredentialInactive: "اعتبار MCP غیرفعال، منقضی یا لغو شده است." -ErrMCPAuditUnavailable: "ثبت حسابرسی عملیات MCP در دسترس نیست. پیش از ادامه، نتیجه عملیات را بررسی کنید." -ErrMCPEndpointUnavailable: "ابتدا یک نشانی خارجی معتبر برای MCP تنظیم کنید." -ErrMCPWebsitePending: "نتیجه عملیات وب‌سایت تأیید نشده است. پیش از تلاش دوباره برای عملیات اصلی، وضعیت اتصال را تازه‌سازی کنید." -ErrMCPWebsiteUnavailable: "اتصال وب‌سایت در دسترس نیست. وضعیت وب‌سایت و تنظیمات OpenResty را بررسی کنید." +ErrMCPSecretKeyUnavailable: "رمزگذاری اطلاعات ورود در دسترس نیست. بعداً تلاش کنید." +ErrMCPSecretInvalid: "رمزگشایی Token ممکن نیست. آن را دوباره بسازید." +ErrMCPCredentialInactive: "اطلاعات ورود غیرفعال، منقضی یا لغو شده است." +ErrMCPAuditUnavailable: "ممیزی عملیات در دسترس نیست. ابتدا نتیجه عملیات را بررسی کنید." +ErrMCPEndpointUnavailable: "ابتدا یک آدرس معتبر MCP تنظیم کنید." +ErrMCPWebsitePending: "نتیجه عملیات سایت تأیید نشده است. تازه‌سازی و همان عملیات را دوباره امتحان کنید." +ErrMCPWebsiteUnavailable: "اتصال سایت در دسترس نیست. سایت و OpenResty را بررسی کنید." ErrMCPWebsiteServiceRequired: "ابتدا سرویس 1Panel MCP را نصب کنید." +ErrMCPWebsiteHTTPPortRequired: "برای اتصال HTTPS از پورت HTTP در OpenResty استفاده کنید." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "بررسی سرویس فعلی MCP" @@ -556,10 +580,17 @@ MCPInstallSaving: "ذخیره پیکربندی سرویس" MCPInstallCompleted: "نصب سرویس MCP تکمیل شد" MCPInstallRollback: "نصب ناموفق بود؛ بازیابی سرویس قبلی MCP" MCPInstallBindWebsite: "ایجاد وب‌سایت متصل" -MCPInstallWebsiteFailed: "سرویس MCP نصب شده است، اما اتصال وب‌سایت ناموفق بود. پیکربندی وب‌سایت را در تنظیمات سرویس اصلاح کنید." -MCPInstallInterrupted: "راه‌اندازی مجدد پنل باعث قطع نصب شد. پیش از ادامه، وضعیت سرویس را بررسی کنید." -MCPInstallFailed: "نصب MCP ناموفق بود. Docker، تصویر و گزارش‌های سرویس پنل را بررسی کنید." -MCPInstallRecoveryFailed: "نصب ناموفق بود و سرویس قبلی به‌طور کامل بازیابی نشد. پیش از ادامه، وضعیت سرویس MCP را بررسی کنید." +MCPInstallWebsiteFailed: "سرویس نصب شد؛ اتصال سایت ناموفق بود. در تنظیمات سرویس آن را برطرف کنید." +MCPInstallInterrupted: "راه‌اندازی مجدد پنل نصب را قطع کرد. وضعیت سرویس را بررسی کنید." +MCPInstallFailed: "نصب ناموفق بود. Docker، تصویر و گزارش‌های پنل را بررسی کنید." +MCPInstallRecoveryFailed: "نصب ناموفق و بازیابی سرویس ناقص است. وضعیت سرویس را بررسی کنید." MCPInstallLocalImage: "تصویر به‌صورت محلی موجود است؛ دریافت انجام نمی‌شود" MCPInstallVerifyImage: "اعتبارسنجی نسخه تصویر و قرارداد سرویس مدیریت‌شده" MCPInstallVerifiedImage: "اعتبارسنجی تصویر موفق بود" +MCPInstallCleanup: "پاک‌سازی کانتینر قبلی" +MCPInstallOldContainerRemoved: "کانتینر قبلی حذف شد" +MCPInstallOldImageRemoved: "ایمیج قبلی حذف شد" +MCPInstallOldImageKept: "ایمیج قبلی حفظ شد" +MCPInstallOldImageRetained: "ایمیج قبلی ارجاع یا برچسب دیگری دارد و حفظ شد." +MCPInstallCleanupSkipped: "کانتینر قبلی وجود ندارد؛ پاک‌سازی ایمیج انجام نمی‌شود." +MCPInstallCleanupFailed: "سرویس MCP ارتقا یافت، اما پاک‌سازی منابع قبلی کامل نشد. آن‌ها را دستی بررسی کنید." diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 6da636238..39c0de83f 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "パネルのサーバーは、ツール、ノード、所属 MCPDocumentHTTPWarning: "現在のアドレスは暗号化されていない HTTP を使用しています。信頼できないネットワークで使用する前に HTTPS を設定してください。" # MCP management -ErrMCPConflict: "MCP データが変更されたか、競合しています。再読み込みして確認してください。" -ErrMCPPolicyChanged: "MCP ツールカタログまたは認可設定が変更されました。再読み込みして確認し、認可設定を保存してください。" -ErrMCPCatalogUnavailable: "MCP ツールカタログを利用できません。サービスの状態を確認してください。" -ErrMCPDisabled: "MCP サービスが有効になっていません。" -ErrMCPSecretKeyUnavailable: "MCP 認証情報を暗号化できません。しばらくしてから再試行してください。" -ErrMCPSecretInvalid: "保存済みの MCP Token を復号できません。Token を再生成してください。" -ErrMCPCredentialInactive: "MCP 認証情報が無効、期限切れ、または取り消し済みです。" -ErrMCPAuditUnavailable: "MCP 操作の監査記録を利用できません。操作結果を確認してから続行してください。" -ErrMCPEndpointUnavailable: "先に有効な MCP 外部接続先を設定してください。" -ErrMCPWebsitePending: "サイト操作の結果を確認できません。連携状態を更新してから元の操作を再試行してください。" -ErrMCPWebsiteUnavailable: "サイト連携を利用できません。サイトの状態と OpenResty の設定を確認してください。" +ErrMCPConflict: "MCP データが競合しています。再読み込みして確認してください。" +ErrMCPSettingsChanged: "サービス設定が変更されました。再読み込みして確認後、保存してください。" +ErrMCPCredentialChanged: "認証情報が変更されました。再読み込みして確認後、送信してください。" +ErrMCPCredentialRevoked: "認証情報は失効しています。新しく作成してください。" +ErrMCPCredentialNameExists: "同名の認証情報があります。別の名前にしてください。" +ErrMCPRequestReused: "リクエスト内容が競合しています。操作画面を開き直してください。" +ErrMCPInstallationRunning: "インストール中です。インストールログを確認してください。" +ErrMCPServiceOperationRunning: "サービスを操作中です。完了後に再読み込みしてください。" +ErrMCPEndpointManagedByWebsite: "接続先はサイトで管理されています。サイト設定で変更してください。" +ErrMCPWebsiteConfigurationExists: "サイト連携または未完了の操作があります。「インストール後にサイトを連携」をオフにしてください。" +ErrMCPWebsiteAlreadyBound: "サイトは連携済みです。現在の連携を管理または解除してください。" +ErrMCPWebsiteNotBound: "サイトは未連携です。再読み込みしてから連携してください。" +ErrMCPWebsiteMissing: "元のサイトがありません。無効な連携を解除して再連携してください。" +ErrMCPWebsiteOperationPending: "サイト操作は未確認です。再読み込み後、元の操作を再試行してください。" +ErrMCPWebsiteDomainConflict: "このドメインとポートの組み合わせは使用中です。ドメインまたはポートを変更してください。" +ErrMCPWebsiteOwnershipChanged: "サイトの所有情報が変わりました。サイトと連携設定を確認してください。" +ErrMCPWebsiteDomainChanged: "ドメインが変わりました。確認後、連携状態を再読み込みしてください。" +ErrMCPWebsiteDirectoryChanged: "サイトのディレクトリまたは権限が変わりました。復元してから再試行してください。" +ErrMCPWebsiteProxyModified: "プロキシ設定が競合しています。MCP プロキシ設定を復元してください。" +ErrMCPWebsiteModified: "サイトのプロトコルまたは設定が変わりました。確認して再読み込みしてください。" +ErrMCPWebsiteRequestChanged: "サイト操作の記録が一致しません。再読み込みして結果を確認後、再操作してください。" +ErrMCPWebsiteProxyNotLoaded: "MCP プロキシが読み込まれていません。サイト設定と OpenResty を確認してください。" +ErrMCPWebsiteStateUnconfirmed: "サイト状態は未確認です。サイトと OpenResty を確認後、再読み込みしてください。" +ErrMCPPolicyChanged: "ツール一覧または権限が変わりました。再読み込みして確認後、保存してください。" +ErrMCPCatalogUnavailable: "ツール一覧を取得できません。サービス状態を確認してください。" +ErrMCPDisabled: "MCP サービスは無効です。" +ErrMCPSecretKeyUnavailable: "認証情報を暗号化できません。後でもう一度お試しください。" +ErrMCPSecretInvalid: "Token を復号できません。再生成してください。" +ErrMCPCredentialInactive: "認証情報は無効、期限切れ、または失効しています。" +ErrMCPAuditUnavailable: "操作監査を利用できません。先に操作結果を確認してください。" +ErrMCPEndpointUnavailable: "有効な MCP 接続先を先に設定してください。" +ErrMCPWebsitePending: "サイト操作の結果は未確認です。再読み込み後、元の操作を再試行してください。" +ErrMCPWebsiteUnavailable: "サイト連携を利用できません。サイトと OpenResty を確認してください。" ErrMCPWebsiteServiceRequired: "先に 1Panel MCP サービスをインストールしてください。" +ErrMCPWebsiteHTTPPortRequired: "HTTPS 連携には OpenResty の HTTP ポートを使用してください。" + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "現在の MCP サービスを確認" @@ -556,10 +580,17 @@ MCPInstallSaving: "サービス設定を保存" MCPInstallCompleted: "MCP サービスのインストールが完了しました" MCPInstallRollback: "インストールに失敗したため、以前の MCP サービスを復元" MCPInstallBindWebsite: "連携する Web サイトを作成" -MCPInstallWebsiteFailed: "MCP サービスはインストールされましたが、Web サイトの連携に失敗しました。サービス設定で Web サイトの設定を確認してください。" -MCPInstallInterrupted: "パネルの再起動によりインストールが中断されました。操作を続ける前にサービスの状態を確認してください。" -MCPInstallFailed: "MCP のインストールに失敗しました。Docker、イメージ、パネルサービスのログを確認してください。" -MCPInstallRecoveryFailed: "インストールに失敗し、以前のサービスを完全に復元できませんでした。操作を続ける前に MCP サービスの状態を確認してください。" +MCPInstallWebsiteFailed: "サービスはインストール済みですが、サイト連携に失敗しました。サービス設定で修正してください。" +MCPInstallInterrupted: "パネルの再起動でインストールが中断されました。サービス状態を確認してください。" +MCPInstallFailed: "インストールに失敗しました。Docker、イメージ、パネルログを確認してください。" +MCPInstallRecoveryFailed: "インストールに失敗し、サービスの復元が不完全です。サービス状態を確認してください。" MCPInstallLocalImage: "ローカルにイメージが存在するため、ダウンロードをスキップ" MCPInstallVerifyImage: "イメージのバージョンと管理対象サービスの契約を検証" MCPInstallVerifiedImage: "イメージの検証に成功しました" +MCPInstallCleanup: "以前のコンテナーをクリーンアップ" +MCPInstallOldContainerRemoved: "以前のコンテナーを削除しました" +MCPInstallOldImageRemoved: "以前のイメージを削除しました" +MCPInstallOldImageKept: "以前のイメージを保持しました" +MCPInstallOldImageRetained: "以前のイメージは参照中か別のタグがあるため、保持しました。" +MCPInstallCleanupSkipped: "以前のコンテナーが存在しないため、イメージの削除をスキップします。" +MCPInstallCleanupFailed: "MCP サービスは更新されましたが、旧リソースの削除は完了していません。手動で確認してください。" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 31bb72dd7..448da47db 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "패널 서버는 요청마다 도구, 노드 및 소유 계 MCPDocumentHTTPWarning: "현재 주소는 암호화되지 않은 HTTP를 사용합니다. 신뢰할 수 없는 네트워크에서 사용하기 전에 HTTPS를 설정하세요." # MCP management -ErrMCPConflict: "MCP 데이터가 변경되었거나 충돌합니다. 새로 고친 후 확인하세요." -ErrMCPPolicyChanged: "MCP 도구 카탈로그 또는 권한이 변경되었습니다. 새로 고친 후 권한을 확인하고 저장하세요." -ErrMCPCatalogUnavailable: "MCP 도구 카탈로그를 사용할 수 없습니다. 서비스 상태를 확인하세요." +ErrMCPConflict: "MCP 데이터 충돌입니다. 새로고침 후 확인하세요." +ErrMCPSettingsChanged: "서비스 설정이 변경되었습니다. 새로고침 후 확인하고 저장하세요." +ErrMCPCredentialChanged: "자격 증명이 변경되었습니다. 새로고침 후 확인하고 제출하세요." +ErrMCPCredentialRevoked: "자격 증명이 해지되었습니다. 새로 생성하세요." +ErrMCPCredentialNameExists: "자격 증명 이름이 이미 있습니다. 다른 이름을 사용하세요." +ErrMCPRequestReused: "요청 내용이 충돌합니다. 작업 창을 닫고 다시 여세요." +ErrMCPInstallationRunning: "설치 중입니다. 설치 로그를 확인하세요." +ErrMCPServiceOperationRunning: "서비스 작업 중입니다. 완료 후 새로고침하세요." +ErrMCPEndpointManagedByWebsite: "이 주소는 웹사이트에서 관리합니다. 웹사이트 설정에서 변경하세요." +ErrMCPWebsiteConfigurationExists: "연결 또는 대기 중인 작업이 있습니다. “설치 후 웹사이트 연결”을 해제하세요." +ErrMCPWebsiteAlreadyBound: "웹사이트가 이미 연결되었습니다. 기존 연결을 관리하거나 해제하세요." +ErrMCPWebsiteNotBound: "연결된 웹사이트가 없습니다. 새로고침 후 연결하세요." +ErrMCPWebsiteMissing: "기존 웹사이트가 없습니다. 유효하지 않은 연결을 해제한 뒤 다시 연결하세요." +ErrMCPWebsiteOperationPending: "웹사이트 작업이 확인되지 않았습니다. 새로고침 후 원래 작업을 재시도하세요." +ErrMCPWebsiteDomainConflict: "이 도메인과 포트 조합은 사용 중입니다. 도메인 또는 포트를 변경하세요." +ErrMCPWebsiteOwnershipChanged: "웹사이트 소유 정보가 변경되었습니다. 웹사이트와 연결 설정을 확인하세요." +ErrMCPWebsiteDomainChanged: "도메인이 변경되었습니다. 확인 후 연결 상태를 새로고침하세요." +ErrMCPWebsiteDirectoryChanged: "웹사이트 디렉터리 또는 권한이 변경되었습니다. 복원 후 재시도하세요." +ErrMCPWebsiteProxyModified: "프록시 설정 충돌입니다. MCP 프록시 설정을 복원하세요." +ErrMCPWebsiteModified: "웹사이트 프로토콜 또는 설정이 변경되었습니다. 확인 후 새로고침하세요." +ErrMCPWebsiteRequestChanged: "웹사이트 작업 기록이 일치하지 않습니다. 새로고침하여 결과를 확인한 뒤 다시 진행하세요." +ErrMCPWebsiteProxyNotLoaded: "MCP 프록시가 로드되지 않았습니다. 웹사이트 설정과 OpenResty를 확인하세요." +ErrMCPWebsiteStateUnconfirmed: "웹사이트 상태가 확인되지 않았습니다. 웹사이트와 OpenResty를 확인한 뒤 새로고침하세요." +ErrMCPPolicyChanged: "도구 목록 또는 권한이 변경되었습니다. 새로고침 후 확인하고 저장하세요." +ErrMCPCatalogUnavailable: "도구 목록을 사용할 수 없습니다. 서비스 상태를 확인하세요." ErrMCPDisabled: "MCP 서비스가 활성화되지 않았습니다." -ErrMCPSecretKeyUnavailable: "MCP 자격 증명을 암호화할 수 없습니다. 나중에 다시 시도하세요." -ErrMCPSecretInvalid: "저장된 MCP Token을 복호화할 수 없습니다. Token을 다시 생성하세요." -ErrMCPCredentialInactive: "MCP 자격 증명이 비활성화되었거나 만료 또는 취소되었습니다." -ErrMCPAuditUnavailable: "MCP 작업 감사 기록을 사용할 수 없습니다. 작업 결과를 확인한 후 계속하세요." -ErrMCPEndpointUnavailable: "먼저 유효한 MCP 외부 접속 주소를 설정하세요." -ErrMCPWebsitePending: "웹사이트 작업 결과를 확인할 수 없습니다. 연결 상태를 새로 고친 후 원래 작업을 다시 시도하세요." -ErrMCPWebsiteUnavailable: "웹사이트 연결을 사용할 수 없습니다. 웹사이트 상태와 OpenResty 설정을 확인하세요." +ErrMCPSecretKeyUnavailable: "자격 증명 암호화를 사용할 수 없습니다. 나중에 재시도하세요." +ErrMCPSecretInvalid: "Token을 복호화할 수 없습니다. 다시 생성하세요." +ErrMCPCredentialInactive: "자격 증명이 비활성화, 만료 또는 해지되었습니다." +ErrMCPAuditUnavailable: "작업 감사를 사용할 수 없습니다. 먼저 작업 결과를 확인하세요." +ErrMCPEndpointUnavailable: "먼저 유효한 MCP 접속 주소를 설정하세요." +ErrMCPWebsitePending: "웹사이트 작업 결과가 확인되지 않았습니다. 새로고침 후 원래 작업을 재시도하세요." +ErrMCPWebsiteUnavailable: "웹사이트 연결을 사용할 수 없습니다. 웹사이트와 OpenResty를 확인하세요." ErrMCPWebsiteServiceRequired: "먼저 1Panel MCP 서비스를 설치하세요." +ErrMCPWebsiteHTTPPortRequired: "HTTPS 연결에는 OpenResty의 HTTP 포트를 사용하세요." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "현재 MCP 서비스 확인" @@ -556,10 +580,17 @@ MCPInstallSaving: "서비스 구성 저장" MCPInstallCompleted: "MCP 서비스 설치 완료" MCPInstallRollback: "설치 실패로 이전 MCP 서비스 복원" MCPInstallBindWebsite: "연결할 웹사이트 생성" -MCPInstallWebsiteFailed: "MCP 서비스는 설치되었지만 웹사이트 연결에 실패했습니다. 서비스 설정에서 웹사이트 구성을 확인하세요." -MCPInstallInterrupted: "패널이 다시 시작되어 설치가 중단되었습니다. 계속하기 전에 서비스 상태를 확인하세요." -MCPInstallFailed: "MCP 설치에 실패했습니다. Docker, 이미지 및 패널 서비스 로그를 확인하세요." -MCPInstallRecoveryFailed: "설치에 실패했으며 이전 서비스를 완전히 복원하지 못했습니다. 계속하기 전에 MCP 서비스 상태를 확인하세요." +MCPInstallWebsiteFailed: "서비스는 설치되었지만 웹사이트 연결에 실패했습니다. 서비스 설정에서 처리하세요." +MCPInstallInterrupted: "패널 재시작으로 설치가 중단되었습니다. 서비스 상태를 확인하세요." +MCPInstallFailed: "설치에 실패했습니다. Docker, 이미지, 패널 로그를 확인하세요." +MCPInstallRecoveryFailed: "설치에 실패했고 서비스 복원이 불완전합니다. 서비스 상태를 확인하세요." MCPInstallLocalImage: "로컬 이미지가 있어 다운로드를 건너뜁니다" MCPInstallVerifyImage: "이미지 버전 및 관리형 서비스 계약 검증" MCPInstallVerifiedImage: "이미지 검증 통과" +MCPInstallCleanup: "이전 컨테이너 정리" +MCPInstallOldContainerRemoved: "이전 컨테이너가 삭제되었습니다" +MCPInstallOldImageRemoved: "이전 이미지가 삭제되었습니다" +MCPInstallOldImageKept: "이전 이미지가 유지되었습니다" +MCPInstallOldImageRetained: "이전 이미지가 참조 중이거나 다른 태그가 있어 유지되었습니다." +MCPInstallCleanupSkipped: "이전 컨테이너가 없어 이미지 정리를 건너뜁니다." +MCPInstallCleanupFailed: "MCP 서비스는 업그레이드되었지만 이전 리소스 정리가 완료되지 않았습니다. 직접 확인하세요." diff --git a/core/i18n/lang/lo.yaml b/core/i18n/lang/lo.yaml index 9371d3057..cff4834f8 100644 --- a/core/i18n/lang/lo.yaml +++ b/core/i18n/lang/lo.yaml @@ -533,18 +533,42 @@ MCPDocumentNotes: "ເຊີບເວີແຜງຄວບຄຸມກວດສ MCPDocumentHTTPWarning: "ທີ່ຢູ່ນີ້ໃຊ້ HTTP ທີ່ບໍ່ເຂົ້າລະຫັດ. ກະລຸນາຕັ້ງຄ່າ HTTPS ກ່ອນໃຊ້ໃນເຄືອຂ່າຍທີ່ບໍ່ເຊື່ອຖື." # MCP management -ErrMCPConflict: "ຂໍ້ມູນ MCP ຖືກປ່ຽນແປງ ຫຼື ຂັດແຍ່ງກັບຂໍ້ມູນເດີມ. ກະລຸນາໂຫຼດໜ້າໃໝ່ແລະກວດສອບ." -ErrMCPPolicyChanged: "ລາຍການເຄື່ອງມື MCP ຫຼື ການອະນຸຍາດຖືກປ່ຽນແປງ. ກະລຸນາໂຫຼດໜ້າໃໝ່, ກວດສອບ ແລະ ບັນທຶກການອະນຸຍາດ." -ErrMCPCatalogUnavailable: "ລາຍການເຄື່ອງມື MCP ບໍ່ພ້ອມໃຊ້ງານ. ກະລຸນາກວດສອບສະຖານະບໍລິການ." -ErrMCPDisabled: "ບໍລິການ MCP ຍັງບໍ່ຖືກເປີດໃຊ້ງານ." -ErrMCPSecretKeyUnavailable: "ການເຂົ້າລະຫັດຂໍ້ມູນຢັ້ງຢືນ MCP ບໍ່ພ້ອມໃຊ້ງານ. ກະລຸນາລອງໃໝ່ພາຍຫຼັງ." -ErrMCPSecretInvalid: "ບໍ່ສາມາດຖອດລະຫັດ MCP Token ທີ່ບັນທຶກໄວ້. ກະລຸນາສ້າງ Token ໃໝ່." -ErrMCPCredentialInactive: "ຂໍ້ມູນຢັ້ງຢືນ MCP ຖືກປິດໃຊ້ງານ, ໝົດອາຍຸ ຫຼື ຖືກຖອນ." -ErrMCPAuditUnavailable: "ການບັນທຶກກວດສອບການດຳເນີນງານ MCP ບໍ່ພ້ອມໃຊ້ງານ. ກະລຸນາກວດສອບຜົນການດຳເນີນງານກ່ອນດຳເນີນການຕໍ່." -ErrMCPEndpointUnavailable: "ກະລຸນາຕັ້ງຄ່າທີ່ຢູ່ MCP ພາຍນອກທີ່ຖືກຕ້ອງກ່ອນ." -ErrMCPWebsitePending: "ຍັງບໍ່ສາມາດຢືນຢັນຜົນການດຳເນີນການເວັບໄຊ. ໂຫຼດສະຖານະການຜູກມັດໃໝ່ກ່ອນລອງການດຳເນີນການເດີມອີກຄັ້ງ." -ErrMCPWebsiteUnavailable: "ການຜູກມັດເວັບໄຊບໍ່ພ້ອມໃຊ້ງານ. ກວດສອບສະຖານະເວັບໄຊ ແລະ ການຕັ້ງຄ່າ OpenResty." -ErrMCPWebsiteServiceRequired: "ກະລຸນາຕິດຕັ້ງບໍລິການ 1Panel MCP ກ່ອນ." +ErrMCPConflict: "ຂໍ້ມູນ MCP ຂັດແຍ່ງ. ໂຫຼດໃໝ່ແລ້ວກວດສອບ." +ErrMCPSettingsChanged: "ການຕັ້ງຄ່າບໍລິການປ່ຽນແລ້ວ. ໂຫຼດໃໝ່ແລະກວດກ່ອນບັນທຶກ." +ErrMCPCredentialChanged: "ຂໍ້ມູນຢັ້ງຢືນປ່ຽນແລ້ວ. ໂຫຼດໃໝ່ແລະກວດກ່ອນສົ່ງ." +ErrMCPCredentialRevoked: "ຂໍ້ມູນຢັ້ງຢືນຖືກຖອນແລ້ວ. ກະລຸນາສ້າງໃໝ່." +ErrMCPCredentialNameExists: "ຊື່ຂໍ້ມູນຢັ້ງຢືນມີແລ້ວ. ກະລຸນາໃຊ້ຊື່ອື່ນ." +ErrMCPRequestReused: "ເນື້ອຫາຄຳຂໍຂັດແຍ່ງ. ປິດແລ້ວເປີດໜ້າຕ່າງນີ້ໃໝ່." +ErrMCPInstallationRunning: "ກຳລັງຕິດຕັ້ງ. ກະລຸນາກວດບັນທຶກການຕິດຕັ້ງ." +ErrMCPServiceOperationRunning: "ກຳລັງດຳເນີນການບໍລິການ. ໂຫຼດໃໝ່ເມື່ອສຳເລັດ." +ErrMCPEndpointManagedByWebsite: "ເວັບໄຊຈັດການທີ່ຢູ່ນີ້. ປ່ຽນໃນການຕັ້ງຄ່າເວັບໄຊ." +ErrMCPWebsiteConfigurationExists: "ມີການຜູກຫຼືວຽກຄ້າງຢູ່. ຍົກເລີກ “ຜູກເວັບໄຊຫຼັງຕິດຕັ້ງ”." +ErrMCPWebsiteAlreadyBound: "ຜູກເວັບໄຊແລ້ວ. ຈັດການຫຼືຍົກເລີກການຜູກເດີມ." +ErrMCPWebsiteNotBound: "ຍັງບໍ່ຜູກເວັບໄຊ. ໂຫຼດໃໝ່ກ່ອນຜູກ." +ErrMCPWebsiteMissing: "ເວັບໄຊເດີມບໍ່ມີແລ້ວ. ລຶບການຜູກທີ່ໃຊ້ບໍ່ໄດ້ແລ້ວຜູກໃໝ່." +ErrMCPWebsiteOperationPending: "ການດຳເນີນການເວັບໄຊຍັງບໍ່ຢືນຢັນ. ໂຫຼດໃໝ່ແລ້ວລອງວຽກເດີມອີກຄັ້ງ." +ErrMCPWebsiteDomainConflict: "ຄູ່ໂດເມນແລະພອດນີ້ຖືກໃຊ້ແລ້ວ. ກະລຸນາປ່ຽນໂດເມນຫຼືພອດ." +ErrMCPWebsiteOwnershipChanged: "ຂໍ້ມູນເຈົ້າຂອງເວັບໄຊປ່ຽນແລ້ວ. ກວດເວັບໄຊແລະການຜູກ." +ErrMCPWebsiteDomainChanged: "ໂດເມນປ່ຽນແລ້ວ. ກວດແລ້ວໂຫຼດສະຖານະການຜູກໃໝ່." +ErrMCPWebsiteDirectoryChanged: "ໄດເລກະທໍຣີຫຼືສິດເວັບໄຊປ່ຽນແລ້ວ. ກູ້ຄືນກ່ອນລອງອີກຄັ້ງ." +ErrMCPWebsiteProxyModified: "ການຕັ້ງຄ່າພຣັອກຊີຂັດແຍ່ງ. ກູ້ຄືນການຕັ້ງຄ່າພຣັອກຊີ MCP." +ErrMCPWebsiteModified: "ໂປຣໂຕຄອນຫຼືການຕັ້ງຄ່າເວັບໄຊປ່ຽນແລ້ວ. ກວດແລ້ວໂຫຼດໃໝ່." +ErrMCPWebsiteRequestChanged: "ບັນທຶກວຽກເວັບໄຊບໍ່ກົງກັນ. ໂຫຼດໃໝ່ແລະກວດຜົນກ່ອນເລີ່ມວຽກໃໝ່." +ErrMCPWebsiteProxyNotLoaded: "ພຣັອກຊີ MCP ຍັງບໍ່ໂຫຼດ. ກວດການຕັ້ງຄ່າເວັບໄຊແລະ OpenResty." +ErrMCPWebsiteStateUnconfirmed: "ສະຖານະເວັບໄຊຍັງບໍ່ຢືນຢັນ. ກວດເວັບໄຊແລະ OpenResty ແລ້ວໂຫຼດໃໝ່." +ErrMCPPolicyChanged: "ລາຍການເຄື່ອງມືຫຼືສິດປ່ຽນແລ້ວ. ໂຫຼດໃໝ່ແລະກວດກ່ອນບັນທຶກ." +ErrMCPCatalogUnavailable: "ລາຍການເຄື່ອງມືໃຊ້ບໍ່ໄດ້. ກວດສະຖານະບໍລິການ." +ErrMCPDisabled: "ບໍລິການ MCP ຍັງບໍ່ເປີດໃຊ້." +ErrMCPSecretKeyUnavailable: "ການເຂົ້າລະຫັດຂໍ້ມູນຢັ້ງຢືນໃຊ້ບໍ່ໄດ້. ລອງອີກພາຍຫຼັງ." +ErrMCPSecretInvalid: "ຖອດລະຫັດ Token ບໍ່ໄດ້. ກະລຸນາສ້າງໃໝ່." +ErrMCPCredentialInactive: "ຂໍ້ມູນຢັ້ງຢືນຖືກປິດໃຊ້, ໝົດອາຍຸ ຫຼືຖອນແລ້ວ." +ErrMCPAuditUnavailable: "ການກວດສອບວຽກໃຊ້ບໍ່ໄດ້. ກວດຜົນການດຳເນີນການກ່ອນ." +ErrMCPEndpointUnavailable: "ກຳນົດທີ່ຢູ່ MCP ທີ່ຖືກຕ້ອງກ່ອນ." +ErrMCPWebsitePending: "ຜົນການດຳເນີນການເວັບໄຊຍັງບໍ່ຢືນຢັນ. ໂຫຼດໃໝ່ແລ້ວລອງວຽກເດີມອີກຄັ້ງ." +ErrMCPWebsiteUnavailable: "ການຜູກເວັບໄຊໃຊ້ບໍ່ໄດ້. ກວດເວັບໄຊແລະ OpenResty." +ErrMCPWebsiteServiceRequired: "ຕິດຕັ້ງບໍລິການ 1Panel MCP ກ່ອນ." + +ErrMCPWebsiteHTTPPortRequired: "ໃຊ້ພອດ HTTP ຂອງ OpenResty ເພື່ອຜູກ HTTPS." # MCP installation tasks PanelMCP: "1Panel MCP" @@ -556,10 +580,17 @@ MCPInstallSaving: "ບັນທຶກການຕັ້ງຄ່າບໍລິ MCPInstallCompleted: "ຕິດຕັ້ງບໍລິການ MCP ສຳເລັດແລ້ວ" MCPInstallRollback: "ການຕິດຕັ້ງລົ້ມເຫຼວ; ກູ້ຄືນບໍລິການ MCP ກ່ອນໜ້າ" MCPInstallBindWebsite: "ສ້າງເວັບໄຊທີ່ເຊື່ອມໂຍງ" -MCPInstallWebsiteFailed: "ຕິດຕັ້ງບໍລິການ MCP ແລ້ວ ແຕ່ການເຊື່ອມໂຍງເວັບໄຊລົ້ມເຫຼວ. ກະລຸນາແກ້ໄຂການຕັ້ງຄ່າເວັບໄຊໃນການຕັ້ງຄ່າບໍລິການ." -MCPInstallInterrupted: "ການເລີ່ມແຜງຄວບຄຸມໃໝ່ເຮັດໃຫ້ການຕິດຕັ້ງຢຸດຊະງັກ. ກະລຸນາກວດສອບສະຖານະບໍລິການກ່ອນດຳເນີນການຕໍ່." -MCPInstallFailed: "ການຕິດຕັ້ງ MCP ລົ້ມເຫຼວ. ກະລຸນາກວດສອບ Docker, ອິມເມດ ແລະ ບັນທຶກບໍລິການແຜງຄວບຄຸມ." -MCPInstallRecoveryFailed: "ການຕິດຕັ້ງລົ້ມເຫຼວ ແລະ ບໍລິການເດີມບໍ່ສາມາດກູ້ຄືນໄດ້ຄົບຖ້ວນ. ກະລຸນາກວດສອບສະຖານະບໍລິການ MCP ກ່ອນດຳເນີນການຕໍ່." +MCPInstallWebsiteFailed: "ຕິດຕັ້ງບໍລິການແລ້ວ ແຕ່ຜູກເວັບໄຊບໍ່ສຳເລັດ. ແກ້ໄຂໃນການຕັ້ງຄ່າບໍລິການ." +MCPInstallInterrupted: "ການເລີ່ມແຜງໃໝ່ຂັດຈັງຫວະການຕິດຕັ້ງ. ກວດສະຖານະບໍລິການ." +MCPInstallFailed: "ຕິດຕັ້ງບໍ່ສຳເລັດ. ກວດ Docker, ອິມເມຈ ແລະບັນທຶກແຜງ." +MCPInstallRecoveryFailed: "ຕິດຕັ້ງບໍ່ສຳເລັດແລະກູ້ຄືນບໍລິການບໍ່ຄົບ. ກວດສະຖານະບໍລິການ." MCPInstallLocalImage: "ມີອິມເມດຢູ່ໃນເຄື່ອງແລ້ວ; ຂ້າມການດາວໂຫຼດ" MCPInstallVerifyImage: "ກວດສອບເວີຊັນອິມເມດ ແລະ ສັນຍາບໍລິການທີ່ຈັດການ" MCPInstallVerifiedImage: "ກວດສອບອິມເມດຜ່ານແລ້ວ" +MCPInstallCleanup: "ກຳລັງລ້າງຄອນເທນເນີເກົ່າ" +MCPInstallOldContainerRemoved: "ລຶບຄອນເທນເນີເກົ່າແລ້ວ" +MCPInstallOldImageRemoved: "ລຶບອິມເມຈເກົ່າແລ້ວ" +MCPInstallOldImageKept: "ເກັບອິມເມຈເກົ່າໄວ້" +MCPInstallOldImageRetained: "ອິມເມຈເກົ່າຍັງຖືກອ້າງອີງ ຫຼືມີແທັກອື່ນ ຈຶ່ງເກັບໄວ້." +MCPInstallCleanupSkipped: "ບໍ່ມີຄອນເທນເນີເກົ່າ; ຂ້າມການລ້າງອິມເມຈ." +MCPInstallCleanupFailed: "ອັບເກຣດ MCP ແລ້ວ ແຕ່ການລ້າງຊັບພະຍາກອນເກົ່າຍັງບໍ່ສຳເລັດ. ກະລຸນາກວດສອບເອງ." diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index 86902d445..e678b5484 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -533,18 +533,42 @@ MCPDocumentNotes: "Pelayan panel menyemak kebenaran alat, nod dan akaun pemilik MCPDocumentHTTPWarning: "Alamat ini menggunakan HTTP tanpa penyulitan. Konfigurasikan HTTPS sebelum menggunakannya pada rangkaian yang tidak dipercayai." # MCP management -ErrMCPConflict: "Data MCP telah berubah atau bercanggah dengan data sedia ada. Muat semula dan semak data tersebut." -ErrMCPPolicyChanged: "Katalog alat MCP atau kebenaran telah berubah. Muat semula, semak dan simpan kebenaran." -ErrMCPCatalogUnavailable: "Katalog alat MCP tidak tersedia. Semak status perkhidmatan." -ErrMCPDisabled: "Perkhidmatan MCP tidak diaktifkan." -ErrMCPSecretKeyUnavailable: "Penyulitan kelayakan MCP tidak tersedia. Cuba lagi kemudian." -ErrMCPSecretInvalid: "Token MCP yang disimpan tidak dapat dinyahsulit. Jana semula Token." -ErrMCPCredentialInactive: "Kelayakan MCP telah dinyahaktifkan, tamat tempoh atau dibatalkan." -ErrMCPAuditUnavailable: "Audit operasi MCP tidak tersedia. Sahkan hasil operasi sebelum meneruskan." -ErrMCPEndpointUnavailable: "Konfigurasikan alamat luaran MCP yang sah terlebih dahulu." -ErrMCPWebsitePending: "Hasil operasi laman web belum dapat dipastikan. Muat semula status pautan sebelum mencuba semula operasi asal." -ErrMCPWebsiteUnavailable: "Pautan laman web tidak tersedia. Semak status laman web dan konfigurasi OpenResty." -ErrMCPWebsiteServiceRequired: "Pasang perkhidmatan 1Panel MCP terlebih dahulu." +ErrMCPConflict: "Konflik data MCP. Muat semula dan semak." +ErrMCPSettingsChanged: "Tetapan perkhidmatan berubah. Muat semula dan semak sebelum menyimpan." +ErrMCPCredentialChanged: "Bukti kelayakan berubah. Muat semula dan semak sebelum menghantar." +ErrMCPCredentialRevoked: "Bukti kelayakan dibatalkan. Cipta yang baharu." +ErrMCPCredentialNameExists: "Nama bukti kelayakan sudah wujud. Pilih nama lain." +ErrMCPRequestReused: "Butiran permintaan bercanggah. Tutup dan buka semula tetingkap ini." +ErrMCPInstallationRunning: "Pemasangan sedang berjalan. Semak log pemasangan." +ErrMCPServiceOperationRunning: "Operasi perkhidmatan sedang berjalan. Muat semula selepas selesai." +ErrMCPEndpointManagedByWebsite: "Alamat ini diurus oleh laman web. Ubah dalam tetapan laman web." +ErrMCPWebsiteConfigurationExists: "Pautan atau operasi tertunda wujud. Nyahpilih “Pautkan laman web selepas pemasangan”." +ErrMCPWebsiteAlreadyBound: "Laman web sudah dipautkan. Urus atau buang pautan sedia ada." +ErrMCPWebsiteNotBound: "Tiada laman web dipautkan. Muat semula sebelum memautkan." +ErrMCPWebsiteMissing: "Laman web asal tiada. Buang pautan tidak sah dan pautkan semula." +ErrMCPWebsiteOperationPending: "Operasi laman web belum disahkan. Muat semula dan cuba semula operasi asal." +ErrMCPWebsiteDomainConflict: "Gabungan domain dan port ini sedang digunakan. Tukar domain atau port." +ErrMCPWebsiteOwnershipChanged: "Pemilikan laman web berubah. Semak laman web dan tetapan pautan." +ErrMCPWebsiteDomainChanged: "Domain berubah. Semak dan muat semula status pautan." +ErrMCPWebsiteDirectoryChanged: "Direktori atau keizinan laman web berubah. Pulihkan sebelum mencuba semula." +ErrMCPWebsiteProxyModified: "Konflik konfigurasi proksi. Pulihkan konfigurasi proksi MCP." +ErrMCPWebsiteModified: "Protokol atau tetapan laman web berubah. Semak dan muat semula." +ErrMCPWebsiteRequestChanged: "Rekod operasi tidak sepadan. Muat semula dan sahkan hasil sebelum operasi baharu." +ErrMCPWebsiteProxyNotLoaded: "Proksi MCP belum dimuatkan. Semak tetapan laman web dan OpenResty." +ErrMCPWebsiteStateUnconfirmed: "Status laman web belum disahkan. Semak laman web dan OpenResty, kemudian muat semula." +ErrMCPPolicyChanged: "Katalog alat atau kebenaran berubah. Muat semula dan semak sebelum menyimpan." +ErrMCPCatalogUnavailable: "Katalog alat tidak tersedia. Semak status perkhidmatan." +ErrMCPDisabled: "Perkhidmatan MCP belum didayakan." +ErrMCPSecretKeyUnavailable: "Penyulitan bukti kelayakan tidak tersedia. Cuba lagi kemudian." +ErrMCPSecretInvalid: "Token tidak dapat dinyahsulit. Jana semula." +ErrMCPCredentialInactive: "Bukti kelayakan dinyahdayakan, tamat tempoh atau dibatalkan." +ErrMCPAuditUnavailable: "Audit operasi tidak tersedia. Sahkan hasil operasi dahulu." +ErrMCPEndpointUnavailable: "Tetapkan alamat MCP yang sah dahulu." +ErrMCPWebsitePending: "Hasil operasi laman web belum disahkan. Muat semula dan cuba semula operasi asal." +ErrMCPWebsiteUnavailable: "Pautan laman web tidak tersedia. Semak laman web dan OpenResty." +ErrMCPWebsiteServiceRequired: "Pasang perkhidmatan 1Panel MCP dahulu." + +ErrMCPWebsiteHTTPPortRequired: "Gunakan port HTTP OpenResty untuk pautan HTTPS." # MCP installation tasks PanelMCP: "1Panel MCP" @@ -556,10 +580,17 @@ MCPInstallSaving: "Simpan konfigurasi perkhidmatan" MCPInstallCompleted: "Pemasangan perkhidmatan MCP selesai" MCPInstallRollback: "Pemasangan gagal; pulihkan perkhidmatan MCP sebelumnya" MCPInstallBindWebsite: "Cipta laman web yang dipautkan" -MCPInstallWebsiteFailed: "Perkhidmatan MCP telah dipasang, tetapi pautan laman web gagal. Betulkan konfigurasi laman web dalam tetapan perkhidmatan." -MCPInstallInterrupted: "Pemasangan terganggu kerana panel dimulakan semula. Semak status perkhidmatan sebelum meneruskan." -MCPInstallFailed: "Pemasangan MCP gagal. Semak Docker, imej dan log perkhidmatan panel." -MCPInstallRecoveryFailed: "Pemasangan gagal dan perkhidmatan sebelumnya tidak dapat dipulihkan sepenuhnya. Semak status perkhidmatan MCP sebelum meneruskan." +MCPInstallWebsiteFailed: "Perkhidmatan dipasang; pautan laman web gagal. Selesaikan dalam tetapan perkhidmatan." +MCPInstallInterrupted: "Mula semula panel mengganggu pemasangan. Sahkan status perkhidmatan." +MCPInstallFailed: "Pemasangan gagal. Semak Docker, imej dan log panel." +MCPInstallRecoveryFailed: "Pemasangan gagal dan pemulihan tidak lengkap. Sahkan status perkhidmatan." MCPInstallLocalImage: "Imej sudah tersedia secara setempat; langkau muat turun" MCPInstallVerifyImage: "Sahkan versi imej dan kontrak perkhidmatan terurus" MCPInstallVerifiedImage: "Pengesahan imej berjaya" +MCPInstallCleanup: "Bersihkan bekas terdahulu" +MCPInstallOldContainerRemoved: "Bekas terdahulu dipadam" +MCPInstallOldImageRemoved: "Imej terdahulu dipadam" +MCPInstallOldImageKept: "Imej terdahulu dikekalkan" +MCPInstallOldImageRetained: "Imej terdahulu masih dirujuk atau mempunyai tag lain; imej dikekalkan." +MCPInstallCleanupSkipped: "Bekas terdahulu tiada; langkau pembersihan imej." +MCPInstallCleanupFailed: "Perkhidmatan MCP telah dinaik taraf, tetapi pembersihan sumber lama belum selesai. Semak secara manual." diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 1dae6a82b..ef2826938 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "O servidor do painel verifica as permissões de ferramentas, MCPDocumentHTTPWarning: "Este endereço usa HTTP sem criptografia. Configure HTTPS antes de utilizá-lo em uma rede não confiável." # MCP management -ErrMCPConflict: "Os dados MCP foram alterados ou estão em conflito. Atualize a página e revise-os." -ErrMCPPolicyChanged: "O catálogo de ferramentas MCP ou a autorização foi alterado. Atualize, revise e salve a autorização." -ErrMCPCatalogUnavailable: "O catálogo de ferramentas MCP está indisponível. Verifique o estado do serviço." +ErrMCPConflict: "Conflito de dados do MCP. Atualize e confira." +ErrMCPSettingsChanged: "Configurações alteradas. Atualize e confira antes de salvar." +ErrMCPCredentialChanged: "Credencial alterada. Atualize e confira antes de enviar." +ErrMCPCredentialRevoked: "Credencial revogada. Crie outra." +ErrMCPCredentialNameExists: "Nome de credencial já existe. Escolha outro." +ErrMCPRequestReused: "Conflito na solicitação. Feche e reabra esta janela." +ErrMCPInstallationRunning: "Instalação em andamento. Consulte o log de instalação." +ErrMCPServiceOperationRunning: "Operação do serviço em andamento. Atualize ao terminar." +ErrMCPEndpointManagedByWebsite: "O site gerencia este endereço. Altere-o nas configurações do site." +ErrMCPWebsiteConfigurationExists: "Há um vínculo ou operação pendente. Desmarque “Vincular site após a instalação”." +ErrMCPWebsiteAlreadyBound: "Site já vinculado. Gerencie ou remova o vínculo atual." +ErrMCPWebsiteNotBound: "Nenhum site vinculado. Atualize antes de vincular." +ErrMCPWebsiteMissing: "O site original não existe. Remova o vínculo inválido e vincule novamente." +ErrMCPWebsiteOperationPending: "Operação do site não confirmada. Atualize e repita a operação original." +ErrMCPWebsiteDomainConflict: "A combinação de domínio e porta está em uso. Altere o domínio ou a porta." +ErrMCPWebsiteOwnershipChanged: "A propriedade do site mudou. Confira o site e o vínculo." +ErrMCPWebsiteDomainChanged: "O domínio mudou. Confira-o e atualize o estado do vínculo." +ErrMCPWebsiteDirectoryChanged: "O diretório ou as permissões mudaram. Restaure-os antes de tentar novamente." +ErrMCPWebsiteProxyModified: "Conflito de proxy. Restaure a configuração do proxy MCP." +ErrMCPWebsiteModified: "O protocolo ou as configurações mudaram. Confira e atualize." +ErrMCPWebsiteRequestChanged: "O registro da operação não corresponde. Atualize e confira antes de iniciar outra." +ErrMCPWebsiteProxyNotLoaded: "Proxy MCP não carregado. Confira as configurações do site e o OpenResty." +ErrMCPWebsiteStateUnconfirmed: "Estado do site não confirmado. Confira o site e o OpenResty e atualize." +ErrMCPPolicyChanged: "Catálogo ou permissões alterados. Atualize e confira antes de salvar." +ErrMCPCatalogUnavailable: "Catálogo indisponível. Confira o estado do serviço." ErrMCPDisabled: "O serviço MCP não está habilitado." -ErrMCPSecretKeyUnavailable: "A criptografia de credenciais MCP está indisponível. Tente novamente mais tarde." -ErrMCPSecretInvalid: "Não foi possível descriptografar o Token MCP armazenado. Gere um novo Token." -ErrMCPCredentialInactive: "A credencial MCP está desabilitada, expirada ou revogada." -ErrMCPAuditUnavailable: "A auditoria de operações MCP está indisponível. Verifique o resultado antes de continuar." -ErrMCPEndpointUnavailable: "Configure primeiro um endereço externo MCP válido." -ErrMCPWebsitePending: "O resultado da operação do site é incerto. Atualize o estado do vínculo antes de repetir a operação original." -ErrMCPWebsiteUnavailable: "O vínculo do site está indisponível. Verifique o estado do site e a configuração do OpenResty." +ErrMCPSecretKeyUnavailable: "Criptografia de credenciais indisponível. Tente mais tarde." +ErrMCPSecretInvalid: "Não foi possível descriptografar o Token. Gere-o novamente." +ErrMCPCredentialInactive: "A credencial está desabilitada, expirada ou revogada." +ErrMCPAuditUnavailable: "Auditoria indisponível. Confira primeiro o resultado da operação." +ErrMCPEndpointUnavailable: "Configure primeiro um endereço MCP válido." +ErrMCPWebsitePending: "Resultado do site não confirmado. Atualize e repita a operação original." +ErrMCPWebsiteUnavailable: "Vínculo indisponível. Confira o site e o OpenResty." ErrMCPWebsiteServiceRequired: "Instale primeiro o serviço 1Panel MCP." +ErrMCPWebsiteHTTPPortRequired: "Use a porta HTTP do OpenResty para vincular com HTTPS." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "Verificar o serviço MCP atual" @@ -556,10 +580,17 @@ MCPInstallSaving: "Salvar a configuração do serviço" MCPInstallCompleted: "Instalação do serviço MCP concluída" MCPInstallRollback: "A instalação falhou; restaurar o serviço MCP anterior" MCPInstallBindWebsite: "Criar o site vinculado" -MCPInstallWebsiteFailed: "O serviço MCP está instalado, mas a vinculação do site falhou. Resolva a configuração do site nas configurações do serviço." -MCPInstallInterrupted: "Uma reinicialização do painel interrompeu a instalação. Verifique o estado do serviço antes de continuar." -MCPInstallFailed: "A instalação do MCP falhou. Verifique o Docker, a imagem e os logs do serviço do painel." -MCPInstallRecoveryFailed: "A instalação falhou e o serviço anterior não pôde ser totalmente restaurado. Verifique o estado do serviço MCP antes de continuar." +MCPInstallWebsiteFailed: "Serviço instalado; vínculo do site falhou. Resolva nas configurações do serviço." +MCPInstallInterrupted: "O reinício do painel interrompeu a instalação. Confira o estado do serviço." +MCPInstallFailed: "Instalação falhou. Confira o Docker, a imagem e os logs do painel." +MCPInstallRecoveryFailed: "Instalação falhou e a recuperação está incompleta. Confira o estado do serviço." MCPInstallLocalImage: "A imagem já existe localmente; ignorar o download" MCPInstallVerifyImage: "Validar a versão da imagem e o contrato do serviço gerenciado" MCPInstallVerifiedImage: "A imagem passou na validação" +MCPInstallCleanup: "Limpar o contêiner anterior" +MCPInstallOldContainerRemoved: "Contêiner anterior removido" +MCPInstallOldImageRemoved: "Imagem anterior removida" +MCPInstallOldImageKept: "Imagem anterior mantida" +MCPInstallOldImageRetained: "A imagem anterior está referenciada ou tem outras tags; ela foi mantida." +MCPInstallCleanupSkipped: "O contêiner anterior não existe; ignorar a limpeza da imagem." +MCPInstallCleanupFailed: "O serviço MCP foi atualizado, mas a limpeza dos recursos antigos não foi concluída. Verifique-os manualmente." diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index 46510b890..35c902ee7 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "Сервер панели проверяет текущие MCPDocumentHTTPWarning: "Этот адрес использует незашифрованный HTTP. Перед использованием в недоверенной сети настройте HTTPS." # MCP management -ErrMCPConflict: "Данные MCP изменились или конфликтуют с существующими. Обновите страницу и проверьте их." -ErrMCPPolicyChanged: "Каталог инструментов MCP или разрешения изменились. Обновите страницу, проверьте и сохраните разрешения." -ErrMCPCatalogUnavailable: "Каталог инструментов MCP недоступен. Проверьте состояние службы." +ErrMCPConflict: "Конфликт данных MCP. Обновите и проверьте." +ErrMCPSettingsChanged: "Настройки службы изменились. Обновите и проверьте перед сохранением." +ErrMCPCredentialChanged: "Учётные данные изменились. Обновите и проверьте перед отправкой." +ErrMCPCredentialRevoked: "Учётные данные отозваны. Создайте новые." +ErrMCPCredentialNameExists: "Имя учётных данных уже существует. Выберите другое." +ErrMCPRequestReused: "Конфликт параметров запроса. Закройте и снова откройте окно." +ErrMCPInstallationRunning: "Идёт установка. Проверьте журнал установки." +ErrMCPServiceOperationRunning: "Выполняется операция службы. Обновите после её завершения." +ErrMCPEndpointManagedByWebsite: "Адрес управляется сайтом. Измените его в настройках сайта." +ErrMCPWebsiteConfigurationExists: "Есть привязка или незавершённая операция. Отключите «Привязать сайт после установки»." +ErrMCPWebsiteAlreadyBound: "Сайт уже привязан. Измените или удалите текущую привязку." +ErrMCPWebsiteNotBound: "Нет привязанного сайта. Обновите перед привязкой." +ErrMCPWebsiteMissing: "Исходный сайт отсутствует. Удалите недействительную привязку и привяжите снова." +ErrMCPWebsiteOperationPending: "Операция сайта не подтверждена. Обновите и повторите исходную операцию." +ErrMCPWebsiteDomainConflict: "Сочетание домена и порта занято. Измените домен или порт." +ErrMCPWebsiteOwnershipChanged: "Владелец сайта изменился. Проверьте сайт и настройки привязки." +ErrMCPWebsiteDomainChanged: "Домен сайта изменился. Проверьте его и обновите статус привязки." +ErrMCPWebsiteDirectoryChanged: "Каталог сайта или права изменились. Восстановите их перед повторной попыткой." +ErrMCPWebsiteProxyModified: "Конфликт конфигурации прокси. Восстановите настройки прокси MCP." +ErrMCPWebsiteModified: "Протокол или настройки сайта изменились. Проверьте и обновите." +ErrMCPWebsiteRequestChanged: "Запись операции не совпадает. Обновите и проверьте результат перед новой операцией." +ErrMCPWebsiteProxyNotLoaded: "Прокси MCP не загружен. Проверьте настройки сайта и OpenResty." +ErrMCPWebsiteStateUnconfirmed: "Статус сайта не подтверждён. Проверьте сайт и OpenResty, затем обновите." +ErrMCPPolicyChanged: "Каталог инструментов или права изменились. Обновите и проверьте перед сохранением." +ErrMCPCatalogUnavailable: "Каталог инструментов недоступен. Проверьте статус службы." ErrMCPDisabled: "Служба MCP не включена." -ErrMCPSecretKeyUnavailable: "Шифрование учётных данных MCP недоступно. Повторите попытку позже." -ErrMCPSecretInvalid: "Не удалось расшифровать сохранённый MCP Token. Создайте новый Token." -ErrMCPCredentialInactive: "Учётные данные MCP отключены, просрочены или отозваны." -ErrMCPAuditUnavailable: "Аудит операций MCP недоступен. Проверьте результат операции, прежде чем продолжить." -ErrMCPEndpointUnavailable: "Сначала настройте корректный внешний адрес MCP." -ErrMCPWebsitePending: "Результат операции с сайтом не подтверждён. Обновите состояние привязки перед повтором исходной операции." -ErrMCPWebsiteUnavailable: "Привязка сайта недоступна. Проверьте состояние сайта и настройки OpenResty." +ErrMCPSecretKeyUnavailable: "Шифрование учётных данных недоступно. Повторите позже." +ErrMCPSecretInvalid: "Не удалось расшифровать Token. Создайте его заново." +ErrMCPCredentialInactive: "Учётные данные отключены, просрочены или отозваны." +ErrMCPAuditUnavailable: "Аудит операций недоступен. Сначала проверьте результат операции." +ErrMCPEndpointUnavailable: "Сначала настройте корректный адрес MCP." +ErrMCPWebsitePending: "Результат операции сайта не подтверждён. Обновите и повторите исходную операцию." +ErrMCPWebsiteUnavailable: "Привязка сайта недоступна. Проверьте сайт и OpenResty." ErrMCPWebsiteServiceRequired: "Сначала установите службу 1Panel MCP." +ErrMCPWebsiteHTTPPortRequired: "Для привязки HTTPS используйте HTTP-порт OpenResty." + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "Проверить текущую службу MCP" @@ -556,10 +580,17 @@ MCPInstallSaving: "Сохранить конфигурацию службы" MCPInstallCompleted: "Установка службы MCP завершена" MCPInstallRollback: "Установка не удалась; восстановить предыдущую службу MCP" MCPInstallBindWebsite: "Создать привязанный сайт" -MCPInstallWebsiteFailed: "Служба MCP установлена, но привязать сайт не удалось. Исправьте конфигурацию сайта в настройках службы." -MCPInstallInterrupted: "Перезапуск панели прервал установку. Проверьте состояние службы перед продолжением." -MCPInstallFailed: "Не удалось установить MCP. Проверьте Docker, образ и журналы службы панели." -MCPInstallRecoveryFailed: "Установка не удалась, а предыдущую службу не удалось полностью восстановить. Проверьте состояние службы MCP перед продолжением." +MCPInstallWebsiteFailed: "Служба установлена, привязка сайта не удалась. Исправьте её в настройках службы." +MCPInstallInterrupted: "Перезапуск панели прервал установку. Проверьте статус службы." +MCPInstallFailed: "Установка не удалась. Проверьте Docker, образ и журналы панели." +MCPInstallRecoveryFailed: "Установка не удалась, служба восстановлена не полностью. Проверьте её статус." MCPInstallLocalImage: "Образ уже есть локально; пропустить загрузку" MCPInstallVerifyImage: "Проверить версию образа и контракт управляемой службы" MCPInstallVerifiedImage: "Образ успешно проверен" +MCPInstallCleanup: "Очистка предыдущего контейнера" +MCPInstallOldContainerRemoved: "Предыдущий контейнер удалён" +MCPInstallOldImageRemoved: "Предыдущий образ удалён" +MCPInstallOldImageKept: "Предыдущий образ сохранён" +MCPInstallOldImageRetained: "Предыдущий образ используется или имеет другие теги; он сохранён." +MCPInstallCleanupSkipped: "Предыдущий контейнер отсутствует; удаление образа пропущено." +MCPInstallCleanupFailed: "Служба MCP обновлена, но старые ресурсы очищены не полностью. Проверьте их вручную." diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index 25f46e08f..7dd2d5f03 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -533,18 +533,42 @@ MCPDocumentNotes: "Panel sunucusu her istekte araç, düğüm ve ilgili hesabın MCPDocumentHTTPWarning: "Bu adres şifrelenmemiş HTTP kullanıyor. Güvenilmeyen bir ağda kullanmadan önce HTTPS yapılandırın." # MCP management -ErrMCPConflict: "MCP verileri değişti veya mevcut verilerle çakışıyor. Sayfayı yenileyip kontrol edin." -ErrMCPPolicyChanged: "MCP araç kataloğu veya yetkilendirme değişti. Yenileyin, kontrol edin ve yetkilendirmeyi kaydedin." -ErrMCPCatalogUnavailable: "MCP araç kataloğu kullanılamıyor. Hizmet durumunu kontrol edin." +ErrMCPConflict: "MCP veri çakışması. Yenileyip kontrol edin." +ErrMCPSettingsChanged: "Hizmet ayarları değişti. Kaydetmeden önce yenileyip kontrol edin." +ErrMCPCredentialChanged: "Kimlik bilgisi değişti. Göndermeden önce yenileyip kontrol edin." +ErrMCPCredentialRevoked: "Kimlik bilgisi iptal edildi. Yenisini oluşturun." +ErrMCPCredentialNameExists: "Kimlik bilgisi adı mevcut. Başka bir ad seçin." +ErrMCPRequestReused: "İstek içeriği çakışıyor. Pencereyi kapatıp yeniden açın." +ErrMCPInstallationRunning: "Kurulum sürüyor. Kurulum günlüğünü kontrol edin." +ErrMCPServiceOperationRunning: "Hizmet işlemi sürüyor. Tamamlandığında yenileyin." +ErrMCPEndpointManagedByWebsite: "Bu adresi web sitesi yönetiyor. Site ayarlarından değiştirin." +ErrMCPWebsiteConfigurationExists: "Bir bağlantı veya bekleyen işlem var. “Kurulumdan sonra web sitesi bağla” seçimini kaldırın." +ErrMCPWebsiteAlreadyBound: "Web sitesi zaten bağlı. Mevcut bağlantıyı yönetin veya kaldırın." +ErrMCPWebsiteNotBound: "Bağlı web sitesi yok. Bağlamadan önce yenileyin." +ErrMCPWebsiteMissing: "Önceki web sitesi yok. Geçersiz bağlantıyı kaldırıp yeniden bağlayın." +ErrMCPWebsiteOperationPending: "Site işlemi doğrulanmadı. Yenileyip aynı işlemi yeniden deneyin." +ErrMCPWebsiteDomainConflict: "Alan adı ve port birleşimi kullanımda. Alan adını veya portu değiştirin." +ErrMCPWebsiteOwnershipChanged: "Site sahipliği değişti. Siteyi ve bağlantı ayarlarını kontrol edin." +ErrMCPWebsiteDomainChanged: "Alan adı değişti. Kontrol edip bağlantı durumunu yenileyin." +ErrMCPWebsiteDirectoryChanged: "Site dizini veya izinleri değişti. Yeniden denemeden önce geri yükleyin." +ErrMCPWebsiteProxyModified: "Proxy yapılandırması çakışıyor. MCP proxy ayarlarını geri yükleyin." +ErrMCPWebsiteModified: "Site protokolü veya ayarları değişti. Kontrol edip yenileyin." +ErrMCPWebsiteRequestChanged: "Site işlem kaydı uyuşmuyor. Yeni işlemden önce yenileyip sonucu kontrol edin." +ErrMCPWebsiteProxyNotLoaded: "MCP proxy yüklenmedi. Site ayarlarını ve OpenResty hizmetini kontrol edin." +ErrMCPWebsiteStateUnconfirmed: "Site durumu doğrulanmadı. Siteyi ve OpenResty hizmetini kontrol edip yenileyin." +ErrMCPPolicyChanged: "Araç kataloğu veya yetkiler değişti. Kaydetmeden önce yenileyip kontrol edin." +ErrMCPCatalogUnavailable: "Araç kataloğu kullanılamıyor. Hizmet durumunu kontrol edin." ErrMCPDisabled: "MCP hizmeti etkin değil." -ErrMCPSecretKeyUnavailable: "MCP kimlik bilgilerinin şifrelenmesi kullanılamıyor. Daha sonra tekrar deneyin." -ErrMCPSecretInvalid: "Kaydedilen MCP Token çözülemiyor. Token'ı yeniden oluşturun." -ErrMCPCredentialInactive: "MCP kimlik bilgileri devre dışı, süresi dolmuş veya iptal edilmiş." -ErrMCPAuditUnavailable: "MCP işlem denetimi kullanılamıyor. Devam etmeden önce işlem sonucunu doğrulayın." -ErrMCPEndpointUnavailable: "Önce geçerli bir harici MCP adresi yapılandırın." -ErrMCPWebsitePending: "Web sitesi işleminin sonucu doğrulanamadı. İlk işlemi yeniden denemeden önce bağlantı durumunu yenileyin." -ErrMCPWebsiteUnavailable: "Web sitesi bağlantısı kullanılamıyor. Site durumunu ve OpenResty yapılandırmasını kontrol edin." -ErrMCPWebsiteServiceRequired: "Önce 1Panel MCP hizmetini yükleyin." +ErrMCPSecretKeyUnavailable: "Kimlik bilgisi şifrelemesi kullanılamıyor. Daha sonra deneyin." +ErrMCPSecretInvalid: "Token çözülemiyor. Yeniden oluşturun." +ErrMCPCredentialInactive: "Kimlik bilgisi devre dışı, süresi dolmuş veya iptal edilmiş." +ErrMCPAuditUnavailable: "İşlem denetimi kullanılamıyor. Önce işlem sonucunu kontrol edin." +ErrMCPEndpointUnavailable: "Önce geçerli bir MCP adresi yapılandırın." +ErrMCPWebsitePending: "Site işleminin sonucu doğrulanmadı. Yenileyip aynı işlemi yeniden deneyin." +ErrMCPWebsiteUnavailable: "Site bağlantısı kullanılamıyor. Siteyi ve OpenResty hizmetini kontrol edin." +ErrMCPWebsiteServiceRequired: "Önce 1Panel MCP hizmetini kurun." + +ErrMCPWebsiteHTTPPortRequired: "HTTPS bağlantısı için OpenResty HTTP portunu kullanın." # MCP installation tasks PanelMCP: "1Panel MCP" @@ -556,10 +580,17 @@ MCPInstallSaving: "Hizmet yapılandırmasını kaydet" MCPInstallCompleted: "MCP hizmetinin kurulumu tamamlandı" MCPInstallRollback: "Kurulum başarısız oldu; önceki MCP hizmetini geri yükle" MCPInstallBindWebsite: "Bağlı web sitesini oluştur" -MCPInstallWebsiteFailed: "MCP hizmeti kuruldu ancak web sitesi bağlanamadı. Web sitesi yapılandırmasını hizmet ayarlarından düzenleyin." -MCPInstallInterrupted: "Panelin yeniden başlatılması kurulumu kesintiye uğrattı. Devam etmeden önce hizmet durumunu kontrol edin." -MCPInstallFailed: "MCP kurulumu başarısız oldu. Docker, imaj ve panel hizmeti günlüklerini kontrol edin." -MCPInstallRecoveryFailed: "Kurulum başarısız oldu ve önceki hizmet tam olarak geri yüklenemedi. Devam etmeden önce MCP hizmetinin durumunu kontrol edin." +MCPInstallWebsiteFailed: "Hizmet kuruldu; site bağlantısı başarısız. Hizmet ayarlarından düzeltin." +MCPInstallInterrupted: "Panel yeniden başlatıldığı için kurulum kesildi. Hizmet durumunu kontrol edin." +MCPInstallFailed: "Kurulum başarısız. Docker, imaj ve panel günlüklerini kontrol edin." +MCPInstallRecoveryFailed: "Kurulum başarısız ve hizmet tam geri yüklenemedi. Hizmet durumunu kontrol edin." MCPInstallLocalImage: "İmaj yerel olarak mevcut; indirmeyi atla" MCPInstallVerifyImage: "İmaj sürümünü ve yönetilen hizmet sözleşmesini doğrula" MCPInstallVerifiedImage: "İmaj doğrulaması başarılı" +MCPInstallCleanup: "Önceki konteyneri temizle" +MCPInstallOldContainerRemoved: "Önceki konteyner silindi" +MCPInstallOldImageRemoved: "Önceki imaj silindi" +MCPInstallOldImageKept: "Önceki imaj korundu" +MCPInstallOldImageRetained: "Önceki imaj kullanılıyor veya başka etiketleri var; korundu." +MCPInstallCleanupSkipped: "Önceki konteyner yok; imaj temizliği atlandı." +MCPInstallCleanupFailed: "MCP hizmeti yükseltildi ancak eski kaynakların temizliği tamamlanamadı. Elle kontrol edin." diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index d5ad312f4..a5e8f4107 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "工具、節點及所屬帳號目前的權限由面板伺服 MCPDocumentHTTPWarning: "目前位址使用未加密的 HTTP;在不可信任的網路使用前,請先設定 HTTPS。" # MCP management -ErrMCPConflict: "MCP 資料已變更或存在衝突,請重新整理後再次核對。" -ErrMCPPolicyChanged: "MCP 工具目錄或授權已變更,請重新整理後核對並儲存授權。" -ErrMCPCatalogUnavailable: "MCP 工具目錄暫時無法使用,請檢查服務狀態。" -ErrMCPDisabled: "MCP 服務尚未啟用。" -ErrMCPSecretKeyUnavailable: "MCP 存取憑證加密暫時無法使用,請稍後重試。" -ErrMCPSecretInvalid: "儲存的 MCP Token 無法解密,請重新產生 Token。" -ErrMCPCredentialInactive: "MCP 存取憑證已停用、過期或撤銷。" -ErrMCPAuditUnavailable: "MCP 操作稽核暫時無法使用,請核對操作結果後再繼續。" -ErrMCPEndpointUnavailable: "請先設定有效的 MCP 外部連線位址。" -ErrMCPWebsitePending: "網站操作結果待確認,請重新整理綁定狀態後重試原操作。" -ErrMCPWebsiteUnavailable: "網站綁定無法使用,請檢查網站狀態和 OpenResty 設定。" +ErrMCPConflict: "MCP 資料衝突,請重新整理後核對。" +ErrMCPSettingsChanged: "服務設定已更新,請重新整理後核對。" +ErrMCPCredentialChanged: "憑據已更新,請重新整理後核對。" +ErrMCPCredentialRevoked: "憑據已撤銷,請建立新憑據。" +ErrMCPCredentialNameExists: "憑據名稱已存在,請更換名稱。" +ErrMCPRequestReused: "請求內容衝突,請重新開啟操作視窗。" +ErrMCPInstallationRunning: "安裝進行中,請查看安裝日誌。" +ErrMCPServiceOperationRunning: "服務操作進行中,請完成後重新整理。" +ErrMCPEndpointManagedByWebsite: "接入位址由網站管理,請在網站設定中修改。" +ErrMCPWebsiteConfigurationExists: "已有綁定或待處理操作,請取消「安裝後綁定網站」。" +ErrMCPWebsiteAlreadyBound: "網站已綁定,請管理或解除現有綁定。" +ErrMCPWebsiteNotBound: "網站未綁定,請重新整理後綁定。" +ErrMCPWebsiteMissing: "原網站已不存在,請解除失效綁定後重新綁定。" +ErrMCPWebsiteOperationPending: "網站操作待確認,請重新整理後重試原操作。" +ErrMCPWebsiteDomainConflict: "網域與連接埠組合已占用,請更換網域或連接埠。" +ErrMCPWebsiteOwnershipChanged: "網站歸屬已變更,請核對網站與綁定設定。" +ErrMCPWebsiteDomainChanged: "網站網域已變更,請核對後重新整理綁定狀態。" +ErrMCPWebsiteDirectoryChanged: "網站目錄或權限已變更,請還原後重試。" +ErrMCPWebsiteProxyModified: "代理設定衝突,請還原 MCP 代理設定。" +ErrMCPWebsiteModified: "網站協定或設定已變更,請核對後重新整理。" +ErrMCPWebsiteRequestChanged: "網站操作記錄不一致,請重新整理核對後重新操作。" +ErrMCPWebsiteProxyNotLoaded: "MCP 代理未載入,請檢查網站設定和 OpenResty。" +ErrMCPWebsiteStateUnconfirmed: "網站狀態待確認,請檢查網站和 OpenResty 後重新整理。" +ErrMCPPolicyChanged: "工具目錄或授權已變更,請重新整理核對後儲存。" +ErrMCPCatalogUnavailable: "工具目錄無法使用,請檢查服務狀態。" +ErrMCPDisabled: "MCP 服務未啟用。" +ErrMCPSecretKeyUnavailable: "憑據加密無法使用,請稍後重試。" +ErrMCPSecretInvalid: "Token 無法解密,請重新產生。" +ErrMCPCredentialInactive: "憑據已停用、過期或撤銷。" +ErrMCPAuditUnavailable: "操作稽核無法使用,請先核對操作結果。" +ErrMCPEndpointUnavailable: "請先設定有效的 MCP 接入位址。" +ErrMCPWebsitePending: "網站操作結果待確認,請重新整理後重試原操作。" +ErrMCPWebsiteUnavailable: "網站綁定無法使用,請檢查網站和 OpenResty。" ErrMCPWebsiteServiceRequired: "請先安裝 1Panel MCP 服務。" +ErrMCPWebsiteHTTPPortRequired: "HTTPS 綁定請使用 OpenResty 的 HTTP 連接埠。" + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "檢查目前的 MCP 服務" @@ -556,10 +580,17 @@ MCPInstallSaving: "儲存服務設定" MCPInstallCompleted: "MCP 服務安裝完成" MCPInstallRollback: "安裝失敗,還原先前的 MCP 服務" MCPInstallBindWebsite: "建立綁定網站" -MCPInstallWebsiteFailed: "MCP 服務已安裝,但網站綁定失敗。請在服務設定中處理網站設定。" -MCPInstallInterrupted: "面板重新啟動導致安裝中斷。請檢查服務狀態後再操作。" -MCPInstallFailed: "MCP 安裝失敗。請檢查 Docker、映像及面板服務日誌。" -MCPInstallRecoveryFailed: "安裝失敗且原服務未能完整還原,請檢查 MCP 服務狀態後再操作。" +MCPInstallWebsiteFailed: "服務已安裝,網站綁定失敗,請前往服務設定處理。" +MCPInstallInterrupted: "面板重新啟動中斷安裝,請核對服務狀態。" +MCPInstallFailed: "安裝失敗,請檢查 Docker、映像檔和面板日誌。" +MCPInstallRecoveryFailed: "安裝失敗且原服務還原不完整,請核對服務狀態。" MCPInstallLocalImage: "本機映像已存在,略過下載" MCPInstallVerifyImage: "驗證映像版本及受管契約" MCPInstallVerifiedImage: "映像驗證通過" +MCPInstallCleanup: "正在清理舊容器" +MCPInstallOldContainerRemoved: "舊容器已刪除" +MCPInstallOldImageRemoved: "舊映像已刪除" +MCPInstallOldImageKept: "舊映像已保留" +MCPInstallOldImageRetained: "舊映像仍被參照或有其他標籤,已保留。" +MCPInstallCleanupSkipped: "舊容器已不存在,略過舊映像清理。" +MCPInstallCleanupFailed: "MCP 服務已升級,舊資源清理未完成,請手動檢查。" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 43c3b590e..bede9f3e2 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -533,19 +533,43 @@ MCPDocumentNotes: "工具、节点及所属账号当前权限由面板服务端 MCPDocumentHTTPWarning: "当前地址使用未加密 HTTP;在不可信网络使用前,请先配置 HTTPS。" # MCP management -ErrMCPConflict: "MCP 数据已变更或存在冲突,请刷新后重新核对。" -ErrMCPPolicyChanged: "MCP 工具目录或授权已变更,请刷新后重新核对并保存授权。" -ErrMCPCatalogUnavailable: "MCP 工具目录暂不可用,请检查服务状态。" +ErrMCPConflict: "MCP 数据冲突,请刷新后核对。" +ErrMCPSettingsChanged: "服务配置已更新,请刷新后核对。" +ErrMCPCredentialChanged: "凭据已更新,请刷新后核对。" +ErrMCPCredentialRevoked: "凭据已撤销,请创建新凭据。" +ErrMCPCredentialNameExists: "凭据名称已存在,请更换名称。" +ErrMCPRequestReused: "请求内容冲突,请重新打开操作窗口。" +ErrMCPInstallationRunning: "安装进行中,请查看安装日志。" +ErrMCPServiceOperationRunning: "服务操作进行中,请完成后刷新。" +ErrMCPEndpointManagedByWebsite: "接入地址由网站管理,请在网站设置中修改。" +ErrMCPWebsiteConfigurationExists: "已有绑定或待处理操作,请取消“安装后绑定网站”。" +ErrMCPWebsiteAlreadyBound: "网站已绑定,请管理或解除现有绑定。" +ErrMCPWebsiteNotBound: "网站未绑定,请刷新后绑定。" +ErrMCPWebsiteMissing: "原网站已不存在,请解除失效绑定后重新绑定。" +ErrMCPWebsiteOperationPending: "网站操作待确认,请刷新后重试原操作。" +ErrMCPWebsiteDomainConflict: "域名与端口组合已占用,请更换域名或端口。" +ErrMCPWebsiteOwnershipChanged: "网站归属已变化,请核对网站与绑定配置。" +ErrMCPWebsiteDomainChanged: "网站域名已变化,请核对后刷新绑定状态。" +ErrMCPWebsiteDirectoryChanged: "网站目录或权限已变化,请恢复后重试。" +ErrMCPWebsiteProxyModified: "代理配置冲突,请恢复 MCP 代理配置。" +ErrMCPWebsiteModified: "网站协议或配置已变化,请核对后刷新。" +ErrMCPWebsiteRequestChanged: "网站操作记录不一致,请刷新核对后重新操作。" +ErrMCPWebsiteProxyNotLoaded: "MCP 代理未加载,请检查网站配置和 OpenResty。" +ErrMCPWebsiteStateUnconfirmed: "网站状态待确认,请检查网站和 OpenResty 后刷新。" +ErrMCPPolicyChanged: "工具目录或授权已变化,请刷新核对后保存。" +ErrMCPCatalogUnavailable: "工具目录不可用,请检查服务状态。" ErrMCPDisabled: "MCP 服务未启用。" -ErrMCPSecretKeyUnavailable: "MCP 凭据加密暂不可用,请稍后重试。" -ErrMCPSecretInvalid: "存储的 MCP Token 无法解密,请重新生成 Token。" -ErrMCPCredentialInactive: "MCP 凭据已停用、过期或撤销。" -ErrMCPAuditUnavailable: "MCP 操作审计暂不可用,请核对操作结果后再继续。" -ErrMCPEndpointUnavailable: "请先配置有效的 MCP 外部接入地址。" -ErrMCPWebsitePending: "网站操作结果待确认,请刷新绑定状态后重试原操作。" -ErrMCPWebsiteUnavailable: "网站绑定不可用,请检查网站状态和 OpenResty 配置。" +ErrMCPSecretKeyUnavailable: "凭据加密不可用,请稍后重试。" +ErrMCPSecretInvalid: "Token 无法解密,请重新生成。" +ErrMCPCredentialInactive: "凭据已停用、过期或撤销。" +ErrMCPAuditUnavailable: "操作审计不可用,请先核对操作结果。" +ErrMCPEndpointUnavailable: "请先配置有效的 MCP 接入地址。" +ErrMCPWebsitePending: "网站操作结果待确认,请刷新后重试原操作。" +ErrMCPWebsiteUnavailable: "网站绑定不可用,请检查网站和 OpenResty。" ErrMCPWebsiteServiceRequired: "请先安装 1Panel MCP 服务。" +ErrMCPWebsiteHTTPPortRequired: "HTTPS 绑定请使用 OpenResty 的 HTTP 端口。" + # MCP installation tasks PanelMCP: "1Panel MCP" MCPInstallPrepare: "检查当前 MCP 服务" @@ -556,10 +580,17 @@ MCPInstallSaving: "保存服务配置" MCPInstallCompleted: "MCP 服务安装完成" MCPInstallRollback: "安装失败,恢复之前的 MCP 服务" MCPInstallBindWebsite: "创建绑定网站" -MCPInstallWebsiteFailed: "MCP 服务已安装,但网站绑定失败。请在服务设置中处理网站配置。" -MCPInstallInterrupted: "面板重启导致安装中断。请检查服务状态后再操作。" -MCPInstallFailed: "MCP 安装失败。请检查 Docker、镜像和面板服务日志。" -MCPInstallRecoveryFailed: "安装失败且原服务未能完整恢复,请检查 MCP 服务状态后再操作。" +MCPInstallWebsiteFailed: "服务已安装,网站绑定失败,请前往服务设置处理。" +MCPInstallInterrupted: "面板重启中断安装,请核对服务状态。" +MCPInstallFailed: "安装失败,请检查 Docker、镜像和面板日志。" +MCPInstallRecoveryFailed: "安装失败且原服务恢复不完整,请核对服务状态。" MCPInstallLocalImage: "本地镜像已存在,跳过下载" MCPInstallVerifyImage: "校验镜像版本和受管契约" MCPInstallVerifiedImage: "镜像校验通过" +MCPInstallCleanup: "正在清理旧容器" +MCPInstallOldContainerRemoved: "旧容器已删除" +MCPInstallOldImageRemoved: "旧镜像已删除" +MCPInstallOldImageKept: "旧镜像已保留" +MCPInstallOldImageRetained: "旧镜像仍被引用或存在其他标签,已保留。" +MCPInstallCleanupSkipped: "旧容器已不存在,跳过旧镜像清理。" +MCPInstallCleanupFailed: "MCP 服务已升级,旧资源清理未完成,请手动检查。" diff --git a/core/init/router/router.go b/core/init/router/router.go index 0b6a0ee0b..de003c01e 100644 --- a/core/init/router/router.go +++ b/core/init/router/router.go @@ -70,10 +70,11 @@ func setWebStatic(rootRouter *gin.RouterGroup) { func Routers() *gin.Engine { Router = gin.New() + Router.Use(middleware.MCPErrorMetadata()) Router.Use(middleware.MCPTokenBoundary()) Router.Use(middleware.MCPAPIIdentity()) Router.Use(i18n.UseI18n()) - Router.Use(middleware.WhiteAllow()) + Router.Use(middleware.WhiteAllow(middleware.MCPSourcePolicy())) Router.Use(middleware.BindDomain(middleware.MCPDomainPolicy)) swaggerRouter := Router.Group("1panel") @@ -92,7 +93,7 @@ func Routers() *gin.Engine { Router.Use(middleware.FrontendFallback()) Router.Use(middleware.WebSocketOriginGuard()) - Router.Use(middleware.OperationLog()) + Router.Use(middleware.OperationLog(middleware.MCPLogPolicy)) Router.Use(middleware.GlobalLoading()) Router.Use(xpack.AuthProvider.CoreAPIAuthMiddleware()) Router.Use(middleware.PasswordExpired()) diff --git a/core/middleware/ip_limit.go b/core/middleware/ip_limit.go index 3fa362b7c..782360987 100644 --- a/core/middleware/ip_limit.go +++ b/core/middleware/ip_limit.go @@ -1,27 +1,36 @@ package middleware import ( - "errors" "net" "strings" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/core/app/model" "github.com/1Panel-dev/1Panel/core/app/repo" - "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/common" - contract "github.com/1Panel-dev/1Panel/core/utils/mcp" - "github.com/1Panel-dev/1Panel/core/utils/mcpapi" "github.com/1Panel-dev/1Panel/core/utils/security" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) -func WhiteAllow() gin.HandlerFunc { +type SourcePolicy struct { + Bypass func(*gin.Context) bool + Resolve func(*gin.Context, string) (clientIP string, allowed bool) +} + +func WhiteAllow(policies ...SourcePolicy) gin.HandlerFunc { + policies = append([]SourcePolicy(nil), policies...) return func(c *gin.Context) { - if mcpapi.IsRequest(c.Request) && c.GetBool("MCP_API_AUTH") { - c.Next() - return + for _, policy := range policies { + if policy.Bypass == nil { + continue + } + allowed := policy.Bypass(c) + if c.IsAborted() { + return + } + if allowed { + c.Next() + return + } } tokenString := c.GetHeader("X-Panel-Local-Token") remoteIP := common.GetRealClientIP(c) @@ -37,40 +46,26 @@ func WhiteAllow() gin.HandlerFunc { helper.InternalServer(c, err) return } - clientIP, err := resolveMCPWebsiteClientIP(c, trustedProxies) - if err != nil { - rejectMCPWebsiteSource(c, err) - return - } - if c.Request.URL.Path == "/mcp" { - c.Set("MCP_CLIENT_IP", clientIP) - var policy model.MCPSetting - err := global.DB.Select("access_policy", "allowed_ips").First(&policy, 1).Error - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - c.AbortWithStatusJSON(503, gin.H{"code": "MCP_POLICY_UNAVAILABLE", "message": "MCP source policy is unavailable"}) + clientIP := "" + for _, policy := range policies { + if policy.Resolve == nil { + continue + } + resolved, allowed := policy.Resolve(c, trustedProxies) + if c.IsAborted() { return } - if errors.Is(err, gorm.ErrRecordNotFound) { - policy.AccessPolicy = "inherit" + if resolved != "" { + clientIP = resolved } - switch policy.AccessPolicy { - case "inherit": - case "all": + if allowed { c.Next() return - case "restricted": - entries, err := contract.DecodeAllowedIPs(policy.AllowedIPs) - if err == nil && len(entries) > 0 && contract.IPAllowed(entries, clientIP) { - c.Next() - return - } - c.AbortWithStatusJSON(403, gin.H{"code": "MCP_SOURCE_DENIED", "message": "Source IP is not allowed by MCP policy"}) - return - default: - c.AbortWithStatusJSON(403, gin.H{"code": "MCP_SOURCE_DENIED", "message": "MCP source policy is invalid"}) - return } } + if clientIP == "" { + clientIP = common.ResolveClientIP(c, trustedProxies) + } allowIPs, err := settingRepo.GetValueByKey("AllowIPs") if err != nil { diff --git a/core/middleware/mcp_error_metadata.go b/core/middleware/mcp_error_metadata.go new file mode 100644 index 000000000..b85bc418c --- /dev/null +++ b/core/middleware/mcp_error_metadata.go @@ -0,0 +1,15 @@ +package middleware + +import ( + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + mcpmeta "github.com/1Panel-dev/1Panel/core/utils/mcpapi/metadata" + "github.com/gin-gonic/gin" +) + +func MCPErrorMetadata() gin.HandlerFunc { + return helper.WithErrorMetadata(func(c *gin.Context, _ int, messageKey string, _ error) { + if messageKey == "ErrProxy" && c.GetBool("MCP_API_AUTH") && c.Request != nil && mcpmeta.TrustedRequest(c.Request) { + c.Header(mcpmeta.BusinessCodeHeader, "ErrProxy") + } + }) +} diff --git a/core/middleware/mcp_operation.go b/core/middleware/mcp_operation.go new file mode 100644 index 000000000..941c2772a --- /dev/null +++ b/core/middleware/mcp_operation.go @@ -0,0 +1,10 @@ +package middleware + +import ( + "github.com/1Panel-dev/1Panel/core/utils/mcpapi" + "github.com/gin-gonic/gin" +) + +func MCPLogPolicy(c *gin.Context) bool { + return mcpapi.IsRequest(c.Request) && c.GetBool("MCP_API_AUTH") +} diff --git a/core/middleware/mcp_source.go b/core/middleware/mcp_source.go new file mode 100644 index 000000000..f0fa76260 --- /dev/null +++ b/core/middleware/mcp_source.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "errors" + + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/global" + contract "github.com/1Panel-dev/1Panel/core/utils/mcp" + "github.com/1Panel-dev/1Panel/core/utils/mcpapi" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func MCPSourcePolicy() SourcePolicy { + return SourcePolicy{ + Bypass: func(c *gin.Context) bool { + return mcpapi.IsRequest(c.Request) && c.GetBool("MCP_API_AUTH") + }, + Resolve: resolveMCPSource, + } +} + +func resolveMCPSource(c *gin.Context, trustedProxies string) (string, bool) { + clientIP, err := resolveMCPWebsiteClientIP(c, trustedProxies) + if err != nil { + rejectMCPWebsiteSource(c, err) + return "", false + } + if c.Request.URL.Path != "/mcp" { + return clientIP, false + } + c.Set("MCP_CLIENT_IP", clientIP) + var policy model.MCPSetting + err = global.DB.Select("access_policy", "allowed_ips").First(&policy, 1).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + c.AbortWithStatusJSON(503, gin.H{"code": "MCP_POLICY_UNAVAILABLE", "message": "MCP source policy is unavailable"}) + return "", false + } + if errors.Is(err, gorm.ErrRecordNotFound) { + policy.AccessPolicy = "inherit" + } + switch policy.AccessPolicy { + case "inherit": + return clientIP, false + case "all": + return clientIP, true + case "restricted": + entries, err := contract.DecodeAllowedIPs(policy.AllowedIPs) + if err == nil && len(entries) > 0 && contract.IPAllowed(entries, clientIP) { + return clientIP, true + } + c.AbortWithStatusJSON(403, gin.H{"code": "MCP_SOURCE_DENIED", "message": "Source IP is not allowed by MCP policy"}) + default: + c.AbortWithStatusJSON(403, gin.H{"code": "MCP_SOURCE_DENIED", "message": "MCP source policy is invalid"}) + } + return "", false +} diff --git a/core/middleware/operation.go b/core/middleware/operation.go index 71a549d51..9bef5f5b0 100644 --- a/core/middleware/operation.go +++ b/core/middleware/operation.go @@ -23,7 +23,6 @@ import ( "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession" - "github.com/1Panel-dev/1Panel/core/utils/mcpapi" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "gorm.io/gorm" @@ -34,23 +33,18 @@ const ( headerOperationResolved = "X-Op-Resolved" ) -func OperationLog() gin.HandlerFunc { +type OperationLogPolicy func(*gin.Context) bool + +func OperationLog(policies ...OperationLogPolicy) gin.HandlerFunc { + policies = append([]OperationLogPolicy(nil), policies...) return func(c *gin.Context) { c.Request.Header.Del(headerNeedOperationResolve) - if mcpapi.IsRequest(c.Request) && c.GetBool("MCP_API_AUTH") { - c.Next() - return + for _, policy := range policies { + if policy != nil && policy(c) { + c.Next() + return + } } - switch c.Request.URL.Path { - case "/api/v2/core/auth/api/keys/reveal": - // Disclosure has its own audit and must not enter response-body capture. - c.Next() - return - case "/api/v2/core/mcp/credentials/create", "/api/v2/core/mcp/credentials/reveal", "/api/v2/core/mcp/credentials/rotate", "/api/v2/core/mcp/credentials/export": - c.Next() - return - } - if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet { c.Next() return diff --git a/core/utils/mcp/contract.go b/core/utils/mcp/contract.go index 144b8d5fe..0c2166ad4 100644 --- a/core/utils/mcp/contract.go +++ b/core/utils/mcp/contract.go @@ -34,12 +34,18 @@ type Error struct { InvocationID string `json:"invocationId,omitempty"` Status int `json:"-"` RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"` + ManagementReason string `json:"-"` } func (e *Error) Error() string { return e.Code } func Fail(code string, status int, message string) *Error { return &Error{Code: code, Status: status, Message: message} } +func Conflict(reason, message string) *Error { + err := Fail("MCP_CONFLICT", http.StatusConflict, message) + err.ManagementReason = reason + return err +} func Invalid() *Error { return Fail("MCP_INVALID_ARGUMENTS", 400, "Invalid tool arguments") } func Forbidden() *Error { return Fail("MCP_FORBIDDEN", 403, "This credential or its owner cannot perform this operation") diff --git a/core/utils/mcpruntime/docker.go b/core/utils/mcpruntime/docker.go index f09d101aa..28dc0a187 100644 --- a/core/utils/mcpruntime/docker.go +++ b/core/utils/mcpruntime/docker.go @@ -60,16 +60,32 @@ type DockerManager struct { client *http.Client installMu sync.Mutex installImageID string + installEnabled bool + installCleanup *cleanupReference } type dockerRequest struct { - Operation string `json:"operation"` - Image string `json:"image"` - Port int `json:"port"` - Enabled bool `json:"enabled"` - Restart bool `json:"restart"` - ImageID string `json:"imageID,omitempty"` - StreamProgress bool `json:"streamProgress,omitempty"` + Operation string `json:"operation"` + Image string `json:"image"` + Port int `json:"port"` + Enabled bool `json:"enabled"` + Restart bool `json:"restart"` + ImageID string `json:"imageID,omitempty"` + StreamProgress bool `json:"streamProgress,omitempty"` + Cleanup *cleanupReference `json:"cleanup,omitempty"` + DeleteOldImage bool `json:"deleteOldImage,omitempty"` +} + +type cleanupReference struct { + ContainerID string `json:"containerID"` + PreviousContainerID string `json:"previousContainerID"` + PreviousImageID string `json:"previousImageID"` +} + +type cleanupResult struct { + ContainerRemoved bool `json:"containerRemoved,omitempty"` + ImageRemoved bool `json:"imageRemoved,omitempty"` + ImageRetainedReason string `json:"imageRetainedReason,omitempty"` } type dockerResponse struct { @@ -82,10 +98,12 @@ type dockerResponse struct { Image string `json:"image"` ImageID string `json:"imageID"` } `json:"state"` - Catalog json.RawMessage `json:"catalog"` - Logs string `json:"logs"` - Error string `json:"error"` - ImageID string `json:"imageID"` + Catalog json.RawMessage `json:"catalog"` + Logs string `json:"logs"` + Error string `json:"error"` + ImageID string `json:"imageID"` + Cleanup *cleanupReference `json:"cleanup,omitempty"` + CleanupResult *cleanupResult `json:"cleanupResult,omitempty"` } func NewDocker(port int, image string) *DockerManager { @@ -96,12 +114,17 @@ func NewDocker(port int, image string) *DockerManager { } func (m *DockerManager) call(ctx context.Context, operation string, enabled, restart bool, imageID string) (dockerResponse, error) { + return m.send(ctx, dockerRequest{Operation: operation, Image: m.image, Port: m.port, Enabled: enabled, Restart: restart, ImageID: imageID}) +} + +func (m *DockerManager) send(ctx context.Context, request dockerRequest) (dockerResponse, error) { if err := ValidateDockerImage(m.image); err != nil { return dockerResponse{}, err } if m.port < 1024 || m.port > 65535 { return dockerResponse{}, errors.New("MCP listen port must be between 1024 and 65535") } + operation := request.Operation timeout := 65 * time.Second if operation == "catalog" || operation == "apply_catalog" || operation == "candidate_catalog" || operation == "logs" || operation == "prepare" { timeout = 5 * time.Second @@ -114,7 +137,6 @@ func (m *DockerManager) call(ctx context.Context, operation string, enabled, res } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - request := dockerRequest{Operation: operation, Image: m.image, Port: m.port, Enabled: enabled, Restart: restart, ImageID: imageID} data, err := json.Marshal(request) if err != nil { return dockerResponse{}, err @@ -210,6 +232,8 @@ func (m *DockerManager) Apply(ctx context.Context, enabled, restart bool) error func (m *DockerManager) InstallImage(ctx context.Context, enabled bool) error { m.installMu.Lock() m.installImageID = "" + m.installCleanup = nil + m.installEnabled = enabled m.installMu.Unlock() if err := m.pull(ctx); err != nil { return err @@ -223,13 +247,26 @@ func (m *DockerManager) InstallImage(ctx context.Context, enabled bool) error { m.installImageID = imageID m.installMu.Unlock() ReportProgress(ctx, Progress{Status: "starting"}) - _, err = m.call(ctx, "install", enabled, true, imageID) - return err + response, err := m.call(ctx, "install", enabled, true, imageID) + if err != nil { + return err + } + if response.Cleanup != nil { + if !validCleanupReference(response.Cleanup) { + return errors.New("MCP installation cleanup reference is invalid") + } + m.installMu.Lock() + value := *response.Cleanup + m.installCleanup = &value + m.installMu.Unlock() + } + return nil } func (m *DockerManager) RestoreAbsent(ctx context.Context) error { m.installMu.Lock() imageID := m.installImageID + m.installCleanup = nil m.installMu.Unlock() if imageID == "" { return nil @@ -245,6 +282,9 @@ func (m *DockerManager) Restore(ctx context.Context, imageID string, enabled boo if enabled && imageID == "" { return errors.New("MCP rollback requires the previous image identity") } + m.installMu.Lock() + m.installCleanup = nil + m.installMu.Unlock() _, err := m.call(ctx, "rollback", enabled, false, imageID) return err } diff --git a/core/utils/mcpruntime/finalize.go b/core/utils/mcpruntime/finalize.go new file mode 100644 index 000000000..41d137780 --- /dev/null +++ b/core/utils/mcpruntime/finalize.go @@ -0,0 +1,86 @@ +package mcpruntime + +import ( + "context" + "errors" + "regexp" +) + +var dockerContainerIDPattern = regexp.MustCompile(`^[a-f0-9]{64}$`) + +func validCleanupReference(reference *cleanupReference) bool { + return reference != nil && dockerContainerIDPattern.MatchString(reference.ContainerID) && + dockerContainerIDPattern.MatchString(reference.PreviousContainerID) && + reference.ContainerID != reference.PreviousContainerID && ValidDockerImageID(reference.PreviousImageID) +} + +// Finalize is called only after the new runtime settings have been committed. +// It uses the exact containers returned by this installation, never a tag or a scan. +func (m *DockerManager) Finalize(ctx context.Context, deleteOldImage bool) error { + m.installMu.Lock() + if m.installCleanup == nil { + m.installMu.Unlock() + return nil + } + reference, imageID, enabled := *m.installCleanup, m.installImageID, m.installEnabled + m.installMu.Unlock() + if !validCleanupReference(&reference) || !ValidDockerImageID(imageID) { + return errors.New("MCP installation cleanup reference is unavailable") + } + ReportProgress(ctx, Progress{Status: "cleanup"}) + response, err := m.send(ctx, dockerRequest{ + Operation: "finalize", Image: m.image, Port: m.port, Enabled: enabled, ImageID: imageID, + Cleanup: &reference, DeleteOldImage: deleteOldImage, + }) + if err != nil { + return err + } + if !validCleanupResult(response.CleanupResult, deleteOldImage, reference.PreviousImageID == imageID) { + return errors.New("MCP Docker agent returned an invalid cleanup result") + } + result := response.CleanupResult + if result.ContainerRemoved { + ReportProgress(ctx, Progress{Status: "cleanup_complete"}) + } + switch { + case result.ImageRemoved: + ReportProgress(ctx, Progress{Status: "old_image_removed"}) + case result.ImageRetainedReason == "not_requested": + ReportProgress(ctx, Progress{Status: "old_image_kept"}) + case result.ImageRetainedReason == "previous_missing": + ReportProgress(ctx, Progress{Status: "cleanup_skipped"}) + default: + ReportProgress(ctx, Progress{Status: "old_image_retained"}) + } + m.installMu.Lock() + if m.installCleanup != nil && *m.installCleanup == reference { + m.installCleanup = nil + } + m.installMu.Unlock() + return nil +} + +func validCleanupResult(result *cleanupResult, deleteOldImage, sameImage bool) bool { + if result == nil { + return false + } + if result.ImageRetainedReason == "previous_missing" { + return !result.ContainerRemoved && !result.ImageRemoved + } + if !result.ContainerRemoved { + return false + } + if result.ImageRemoved { + return deleteOldImage && !sameImage && result.ImageRetainedReason == "" + } + switch result.ImageRetainedReason { + case "not_requested": + return !deleteOldImage + case "same_image": + return deleteOldImage && sameImage + case "in_use", "multiple_tags": + return deleteOldImage && !sameImage + default: + return false + } +} diff --git a/frontend/src/api/interface/panel-mcp.ts b/frontend/src/api/interface/panel-mcp.ts index 77ccac05d..a983e7b55 100644 --- a/frontend/src/api/interface/panel-mcp.ts +++ b/frontend/src/api/interface/panel-mcp.ts @@ -109,6 +109,7 @@ export namespace PanelMCP { export interface WebsitePendingRequest { operation: 'bind' | 'update' | 'unbind'; requestId: string; + deleteWebsite?: boolean; domain?: string; enableSSL?: boolean; sslID?: number; @@ -123,6 +124,7 @@ export namespace PanelMCP { websiteStatus: string; requestId: string; revision: number; + websiteDeleted?: boolean; reason?: string; pendingRequest?: WebsitePendingRequest; } @@ -130,6 +132,9 @@ export namespace PanelMCP { requestId: string; revision: number; } + export interface WebsiteUnbind extends WebsiteMutation { + deleteWebsite?: boolean; + } export interface WebsiteBind extends WebsiteMutation { domain: string; enableSSL: boolean; @@ -141,6 +146,7 @@ export namespace PanelMCP { } export interface Installation { dockerImage: string; + deleteOldImage: boolean; listenPort: number; externalUrl: string; revision: number; diff --git a/frontend/src/api/modules/panel-mcp.ts b/frontend/src/api/modules/panel-mcp.ts index 87d99d7f7..492e44e49 100644 --- a/frontend/src/api/modules/panel-mcp.ts +++ b/frontend/src/api/modules/panel-mcp.ts @@ -15,7 +15,7 @@ export const bindPanelMCPWebsite = (params: PanelMCP.WebsiteBind) => http.postWithConfig(`${prefix}/website/bind`, params, { ...local, timeout: 120000 }); export const updatePanelMCPWebsite = (params: PanelMCP.WebsiteUpdate) => http.postWithConfig(`${prefix}/website/update`, params, { ...local, timeout: 120000 }); -export const unbindPanelMCPWebsite = (params: PanelMCP.WebsiteMutation) => +export const unbindPanelMCPWebsite = (params: PanelMCP.WebsiteUnbind) => http.postWithConfig(`${prefix}/website/unbind`, params, { ...local, timeout: 120000 }); export const savePanelMCPSettings = (params: PanelMCP.Settings) => http.postWithConfig(`${prefix}/settings`, params, { ...local, timeout: 60000 }); diff --git a/frontend/src/components/table/complex/ComplexTable.vue b/frontend/src/components/table/complex/ComplexTable.vue index bb25a1540..71e7af278 100644 --- a/frontend/src/components/table/complex/ComplexTable.vue +++ b/frontend/src/components/table/complex/ComplexTable.vue @@ -775,6 +775,11 @@ onBeforeUnmount(() => { flex: 1; @include flex-row(flex-end); + :deep(.el-pagination:not(.is-background)) { + --el-pagination-bg-color: transparent; + --el-pagination-button-disabled-bg-color: transparent; + } + :deep(.el-pagination__sizes .el-select) { width: 128px; min-width: 128px; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 28b398d7c..0e2503e56 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1822,29 +1822,26 @@ const message = { websiteUnavailable: 'Binding unavailable', websiteDomainInvalid: 'Enter a valid domain, optionally including a port.', websiteCertificateRequired: 'Select a valid certificate covering this domain.', - websiteCertificateEmpty: 'No matching certificate. Apply for or import one in certificate management.', - websiteUnbind: 'Unbind website', + websiteCertificateEmpty: 'No matching certificate. Apply for or import one.', + websiteUnbind: 'Unbind and delete website', websiteUnbindConfirm: - 'Remove the MCP reverse proxy and binding while retaining the website and certificate. Continue?', - websiteUncertain: - 'The website operation outcome is unconfirmed. Refresh its status; a pending operation can be retried with the original request.', + 'Unbind and delete the website and directory? The certificate stays. Rebinding creates a new website.', + websiteUncertain: 'Outcome unconfirmed. Refresh the status, then retry the original request.', websiteRetry: 'Retry pending operation', websiteSettings: 'Website settings', websiteCertificateUpdate: 'Change certificate', - websiteStatusLoadFailed: 'Unable to read the website binding status. Refresh and try again.', - websiteOperationFailed: 'The website binding operation failed. Check the current status and configuration.', - websiteConflict: 'The website binding configuration changed. Refresh the status and review it.', + websiteStatusLoadFailed: 'Cannot read the binding status. Refresh to retry.', + websiteOperationFailed: 'Website operation failed. Check its status and settings.', + websiteConflict: 'Binding settings changed. Refresh and review.', websiteBoundAddressHelp: 'The endpoint is generated from the bound website. Manage it in website settings.', hideToken: 'Hide Token', copyTokenFailed: 'Could not copy the Token. Please try again.', exportFailed: 'Could not export the connection guide. Please try again.', rotateTokenFailed: 'Could not regenerate the Token. Please try again.', - rotateTokenUncertain: - 'The regeneration result could not be confirmed. Refresh the credential and reveal its current Token before proceeding.', + rotateTokenUncertain: 'Token update unconfirmed. Refresh and reveal the current Token.', allowedIPs: 'Allowed IPs / CIDRs', allowedIPsPlaceholder: 'One IP address or CIDR per line; up to 64 entries', - allowedIPsInvalid: - 'Enter up to 64 valid IP addresses or CIDRs, each at most 64 characters. A restricted service list cannot be empty.', + allowedIPsInvalid: 'Use up to 64 valid IPs/CIDRs, max 64 characters each. A restricted list cannot be empty.', accessPolicy: 'MCP source policy', accessInherit: 'Inherit panel access policy', accessRestricted: 'Allow listed IPs / CIDRs only', @@ -1863,10 +1860,10 @@ const message = { alreadyCreated: 'Credential created. View the Token in Connection details.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Target node', + nodeNameUnavailable: 'Node name unavailable', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'Access is limited by node grants, tool grants, and account permissions. New nodes require separate authorization.', + nodeGrantHelp: 'Node, tool and account permissions apply. New nodes need separate grants.', nodeUnavailable: 'Node {0} is unavailable. Remove it before saving.', allCategories: 'All categories', toolCategory: 'Tool category', @@ -1884,7 +1881,7 @@ const message = { clearVisible: 'Revoke {0} matching tools', readOnlyVisible: 'Keep only read access for {0} matches', bulkHelp: - 'Bulk actions affect matching tools in all categories, including hidden items, regardless of "Selected only". New tools require separate authorization.', + 'Batch actions include hidden matches in all categories, ignoring "Selected only". New tools need separate grants.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1908,9 +1905,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Tool list unavailable. Check the service and refresh.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'The tool list has changed. Reopen the editor to continue.', + catalogChanged: 'Tool list updated. Reopen the editor.', grantsNeedReview: 'Some tool grants need to be reconfirmed.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1918,14 +1915,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'Replace in the configuration with the credential Token.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - 'Delete credential “{0}”? It will become invalid immediately. Historical invocation records will be retained.', + uncertain: 'Creation unconfirmed. Retry in this window, or check the list before creating again.', + revokeConfirm: 'Revoke “{0}”? This is irreversible; new calls will be rejected.', + deleteConfirm: 'Delete “{0}”? Access ends immediately; call history stays.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Data changed. Reload before submitting.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1938,7 +1933,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Service check failed. Check logs and version compatibility.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1948,14 +1943,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Delete old image', + deleteOldImageHelp: + 'After a successful upgrade, remove the unused old image. Cleanup failures do not affect the upgrade; see task logs.', installEmpty: - 'The 1Panel MCP service is not installed. Install and enable it to create access credentials and invoke MCP tools.', + 'The 1Panel MCP service is not installed. Install and enable it to create access credentials and call MCP tools.', + installWithCredentials: 'MCP is not installed. Credentials remain; calls are unavailable.', installTaskLog: 'Installation log', installNow: 'Install now', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'Reloading will replace unsaved configuration. Continue?', + reloadInstallConfirm: 'Reload and overwrite unsaved settings?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1963,8 +1962,8 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Leave blank to use the panel URL. The path must be /mcp.', - invalidUrl: 'Enter an HTTP/HTTPS URL with the /mcp path and no credentials, query parameters, or fragment.', + externalUrlHelp: 'Leave blank to use the panel URL. For a custom reverse proxy, enter its full access URL.', + invalidUrl: 'Enter a valid HTTP(S) URL without credentials, query parameters or a fragment.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 3046558dc..3b9d49cc4 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -1837,31 +1837,28 @@ const message = { websiteUnavailable: 'Vinculación no disponible', websiteDomainInvalid: 'Introduzca un dominio válido, con un puerto opcional.', websiteCertificateRequired: 'Seleccione un certificado válido que cubra este dominio.', - websiteCertificateEmpty: - 'No hay certificados coincidentes. Solicite o importe uno en la gestión de certificados.', - websiteUnbind: 'Desvincular sitio web', + websiteCertificateEmpty: 'No hay un certificado coincidente. Solicite o importe uno.', + websiteUnbind: 'Desvincular y eliminar sitio web', websiteUnbindConfirm: - 'Se eliminarán el proxy inverso MCP y la vinculación; se conservarán el sitio y el certificado. ¿Continuar?', - websiteUncertain: - 'El resultado de la operación no está confirmado. Actualice el estado; puede reintentar la operación pendiente con la solicitud original.', + '¿Desvincular y borrar el sitio y su directorio? Se conserva el certificado. Al volver a vincular se crea otro sitio.', + websiteUncertain: 'Resultado sin confirmar. Actualice el estado y reintente la solicitud original.', websiteRetry: 'Reintentar operación pendiente', websiteSettings: 'Configuración del sitio', websiteCertificateUpdate: 'Cambiar certificado', - websiteStatusLoadFailed: 'No se pudo leer el estado de vinculación. Actualice e inténtelo de nuevo.', - websiteOperationFailed: 'La operación de vinculación falló. Revise el estado y la configuración.', - websiteConflict: 'La configuración de vinculación cambió. Actualice el estado y revísela.', + websiteStatusLoadFailed: 'No se puede leer el estado del vínculo. Actualice.', + websiteOperationFailed: 'Falló la operación del sitio. Revise su estado y configuración.', + websiteConflict: 'Cambió la configuración del vínculo. Actualice y revise.', websiteBoundAddressHelp: 'La dirección se genera a partir del sitio vinculado. Adminístrela en la configuración del sitio.', hideToken: 'Ocultar Token', copyTokenFailed: 'No se pudo copiar el Token. Inténtelo de nuevo.', exportFailed: 'No se pudo exportar la guía de conexión. Inténtelo de nuevo.', rotateTokenFailed: 'No se pudo regenerar el Token. Inténtelo de nuevo.', - rotateTokenUncertain: - 'No se pudo confirmar la regeneración. Actualice la credencial y consulte su Token actual antes de continuar.', + rotateTokenUncertain: 'Cambio de Token sin confirmar. Actualice y consulte el Token actual.', allowedIPs: 'IP / CIDR permitidos', allowedIPsPlaceholder: 'Una dirección IP o CIDR por línea; hasta 64 entradas', allowedIPsInvalid: - 'Introduce hasta 64 direcciones IP o CIDR válidos, de hasta 64 caracteres cada uno. La lista del servicio no puede estar vacía si se restringen los orígenes.', + 'Hasta 64 IP/CIDR válidos, de hasta 64 caracteres. La lista restringida no puede estar vacía.', accessPolicy: 'Política de origen de MCP', accessInherit: 'Heredar la política de acceso del panel', accessRestricted: 'Permitir solo las IP / CIDR indicadas', @@ -1881,10 +1878,10 @@ const message = { alreadyCreated: 'Credencial creada. Consulte el Token en los detalles de conexión.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Nodo de destino', + nodeNameUnavailable: 'Nombre del nodo no disponible', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'El acceso depende de los nodos y herramientas autorizados y de los permisos de la cuenta. Los nodos nuevos requieren autorización adicional.', + nodeGrantHelp: 'Se aplican permisos de nodo, herramienta y cuenta. Los nodos nuevos requieren autorización.', nodeUnavailable: 'El nodo {0} no está disponible. Elimínelo antes de guardar.', allCategories: 'Todas las categorías', toolCategory: 'Categoría de herramientas', @@ -1902,7 +1899,7 @@ const message = { clearVisible: 'Revocar {0} herramientas coincidentes', readOnlyVisible: 'Conservar solo lectura para {0} coincidencias', bulkHelp: - 'Las acciones masivas afectan a las herramientas coincidentes de todas las categorías, incluidas las ocultas, independientemente de «Solo seleccionadas». Las herramientas nuevas requieren autorización adicional.', + 'Las acciones incluyen coincidencias ocultas de todas las categorías e ignoran «Solo seleccionadas». Las herramientas nuevas requieren autorización.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1926,9 +1923,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Lista de herramientas no disponible. Revise el servicio y actualice.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'La lista de herramientas se ha actualizado. Vuelva a abrir el editor para continuar.', + catalogChanged: 'Lista de herramientas actualizada. Reabra el editor.', grantsNeedReview: 'Es necesario volver a confirmar algunos permisos de herramientas.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1936,14 +1933,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'Sustituya en la configuración por el Token de la credencial.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - '¿Eliminar la credencial «{0}»? Dejará de ser válida inmediatamente. Se conservará el historial de llamadas.', + uncertain: 'Creación sin confirmar. Reintente en esta ventana o revise la lista antes de volver a crear.', + revokeConfirm: '¿Revocar «{0}»? Es irreversible y se rechazarán nuevas llamadas.', + deleteConfirm: '¿Eliminar «{0}»? Se invalidará de inmediato; se conserva el historial.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Los datos cambiaron. Recargue antes de enviar.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1956,7 +1951,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Falló la comprobación del servicio. Revise registros y compatibilidad de versiones.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1966,14 +1961,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Eliminar imagen anterior', + deleteOldImageHelp: + 'Tras actualizar correctamente, elimina la imagen anterior sin uso. Si la limpieza falla, la actualización se mantiene; consulte los registros de la tarea.', installEmpty: - 'El servicio 1Panel MCP no está instalado. Instálelo y actívelo para crear credenciales de acceso e invocar herramientas MCP.', + 'El servicio 1Panel MCP aún no está instalado. Instálelo y actívelo para crear credenciales de acceso y llamar a las herramientas MCP.', + installWithCredentials: 'MCP no está instalado. Se conservan las credenciales; no se permiten llamadas.', installTaskLog: 'Registro de instalación', installNow: 'Instalar ahora', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'Se reemplazará la configuración sin guardar. ¿Continuar?', + reloadInstallConfirm: '¿Recargar y sobrescribir los ajustes sin guardar?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1981,9 +1980,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Déjelo vacío para usar la URL del panel. La ruta debe ser /mcp.', - invalidUrl: - 'Introduzca una URL HTTP/HTTPS con la ruta /mcp, sin credenciales, parámetros de consulta ni fragmento.', + externalUrlHelp: + 'Déjelo vacío para usar la URL del panel. Para un proxy inverso propio, indique su URL de acceso completa.', + invalidUrl: 'Introduzca una URL HTTP(S) válida sin credenciales, parámetros de consulta ni fragmento.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index ed74d34b3..cf681f14b 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -1801,28 +1801,25 @@ const message = { websiteUnavailable: 'اتصال در دسترس نیست', websiteDomainInvalid: 'یک دامنه معتبر وارد کنید؛ می‌توانید پورت را نیز مشخص کنید.', websiteCertificateRequired: 'یک گواهی معتبر برای این دامنه انتخاب کنید.', - websiteCertificateEmpty: 'گواهی منطبق وجود ندارد. در مدیریت گواهی، گواهی درخواست یا وارد کنید.', - websiteUnbind: 'قطع اتصال وب‌سایت', - websiteUnbindConfirm: 'پراکسی معکوس MCP و اتصال حذف می‌شوند؛ وب‌سایت و گواهی حفظ می‌شوند. ادامه می‌دهید؟', - websiteUncertain: - 'نتیجه عملیات تأیید نشده است. وضعیت را تازه‌سازی کنید؛ عملیات در انتظار را می‌توان با درخواست اصلی دوباره اجرا کرد.', + websiteCertificateEmpty: 'گواهی منطبق وجود ندارد. گواهی درخواست یا وارد کنید.', + websiteUnbind: 'قطع اتصال و حذف وب‌سایت', + websiteUnbindConfirm: 'اتصال قطع و سایت و پوشه آن حذف شود؟ گواهی حفظ می‌شود. اتصال مجدد سایت تازه‌ای می‌سازد.', + websiteUncertain: 'نتیجه تأیید نشده است. وضعیت را تازه‌سازی و درخواست اصلی را دوباره اجرا کنید.', websiteRetry: 'تلاش مجدد عملیات در انتظار', websiteSettings: 'تنظیمات وب‌سایت', websiteCertificateUpdate: 'تغییر گواهی', - websiteStatusLoadFailed: 'خواندن وضعیت اتصال وب‌سایت ممکن نیست. تازه‌سازی کرده و دوباره تلاش کنید.', - websiteOperationFailed: 'عملیات اتصال وب‌سایت ناموفق بود. وضعیت و تنظیمات را بررسی کنید.', - websiteConflict: 'تنظیمات اتصال تغییر کرده است. وضعیت را تازه‌سازی و بررسی کنید.', + websiteStatusLoadFailed: 'وضعیت اتصال خوانده نشد. تازه‌سازی کنید.', + websiteOperationFailed: 'عملیات سایت ناموفق بود. وضعیت و تنظیمات را بررسی کنید.', + websiteConflict: 'تنظیمات اتصال تغییر کرد. تازه‌سازی و بررسی کنید.', websiteBoundAddressHelp: 'نشانی اتصال از وب‌سایت متصل ساخته می‌شود. آن را در تنظیمات وب‌سایت مدیریت کنید.', hideToken: 'پنهان کردن Token', copyTokenFailed: 'کپی Token انجام نشد. دوباره تلاش کنید.', exportFailed: 'صدور راهنمای اتصال انجام نشد. دوباره تلاش کنید.', rotateTokenFailed: 'تولید دوباره Token انجام نشد. دوباره تلاش کنید.', - rotateTokenUncertain: - 'نتیجه تولید دوباره تأیید نشد. پیش از ادامه، اعتبارنامه را تازه‌سازی و Token فعلی آن را مشاهده کنید.', + rotateTokenUncertain: 'تغییر Token تأیید نشده است. تازه‌سازی و Token فعلی را مشاهده کنید.', allowedIPs: 'IP / CIDR مجاز', allowedIPsPlaceholder: 'در هر خط یک آدرس IP یا CIDR؛ حداکثر 64 مورد', - allowedIPsInvalid: - 'حداکثر 64 آدرس IP یا CIDR معتبر با طول حداکثر 64 نویسه وارد کنید. هنگام محدود کردن مبدأ، فهرست سرویس نباید خالی باشد.', + allowedIPsInvalid: 'حداکثر 64 IP/CIDR معتبر، هرکدام تا 64 نویسه. فهرست محدود نباید خالی باشد.', accessPolicy: 'سیاست مبدأ MCP', accessInherit: 'ارث‌بری سیاست دسترسی پنل', accessRestricted: 'فقط IP / CIDR فهرست‌شده مجاز باشد', @@ -1841,9 +1838,10 @@ const message = { alreadyCreated: 'اعتبارنامه ایجاد شد. Token را در جزئیات اتصال مشاهده کنید.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'گره مقصد', + nodeNameUnavailable: 'نام گره در دسترس نیست', selectNodes: 'Select at least one node.', - nodeGrantHelp: 'دسترسی به مجوزهای گره، ابزار و حساب محدود است. گره‌های جدید به مجوز جداگانه نیاز دارند.', + nodeGrantHelp: 'مجوزهای گره، ابزار و حساب اعمال می‌شوند. گره جدید مجوز جداگانه می‌خواهد.', nodeUnavailable: 'گره {0} در دسترس نیست. پیش از ذخیره آن را حذف کنید.', allCategories: 'همه دسته‌ها', toolCategory: 'دسته ابزار', @@ -1861,7 +1859,7 @@ const message = { clearVisible: 'لغو اجازه {0} ابزار مطابق', readOnlyVisible: 'حفظ فقط دسترسی خواندن برای {0} مورد مطابق', bulkHelp: - 'عملیات گروهی بر ابزارهای مطابق در همه دسته‌ها، از جمله موارد پنهان، بدون توجه به «فقط انتخاب‌شده‌ها» اعمال می‌شود. ابزارهای جدید به مجوز جداگانه نیاز دارند.', + 'عملیات گروهی همه موارد منطبق پنهان در تمام دسته‌ها را مستقل از «فقط انتخاب‌شده‌ها» شامل می‌شود. ابزار جدید مجوز جداگانه می‌خواهد.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1885,9 +1883,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'فهرست ابزار در دسترس نیست. سرویس را بررسی و تازه‌سازی کنید.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'فهرست ابزارها به‌روز شده است. برای ادامه، پنجره ویرایش را دوباره باز کنید.', + catalogChanged: 'فهرست ابزار تغییر کرد. ویرایشگر را دوباره باز کنید.', grantsNeedReview: 'برخی مجوزهای ابزار باید دوباره تأیید شوند.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1895,13 +1893,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'در پیکربندی، را با Token اعتبارنامه جایگزین کنید.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: 'اطلاعات احراز هویت «{0}» حذف شود؟ بلافاصله نامعتبر خواهد شد. سوابق فراخوانی حفظ می\u200cشوند.', + uncertain: 'ایجاد تأیید نشده است. در همین پنجره تکرار کنید یا پیش از ایجاد مجدد، فهرست را بررسی کنید.', + revokeConfirm: '«{0}» لغو شود؟ برگشت‌پذیر نیست؛ فراخوانی‌های جدید رد می‌شوند.', + deleteConfirm: '«{0}» حذف شود؟ دسترسی فوراً پایان می‌یابد؛ تاریخچه حفظ می‌شود.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'داده تغییر کرد. پیش از ارسال دوباره بارگیری کنید.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1914,7 +1911,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'بررسی سرویس ناموفق بود. گزارش‌ها و سازگاری نسخه را بررسی کنید.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1924,14 +1921,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'حذف تصویر قبلی', + deleteOldImageHelp: + 'پس از ارتقای موفق، تصویر قبلیِ بدون استفاده حذف می‌شود. خطای پاک‌سازی بر ارتقا تأثیری ندارد؛ گزارش وظیفه را بررسی کنید.', installEmpty: - 'سرویس 1Panel MCP هنوز نصب نشده است. برای ایجاد اعتبارنامه‌های دسترسی و فراخوانی ابزارهای MCP، آن را نصب و فعال کنید.', + 'سرویس 1Panel MCP هنوز نصب نشده است. پس از نصب و فعال‌سازی، می‌توانید اعتبارنامه‌های دسترسی ایجاد کنید و ابزارهای MCP را فراخوانی کنید.', + installWithCredentials: 'MCP نصب نیست. اعتبارنامه‌ها حفظ شده‌اند؛ فراخوانی ممکن نیست.', installTaskLog: 'گزارش نصب', installNow: 'اکنون نصب کنید', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'بارگذاری دوباره، پیکربندی ذخیره‌نشده را جایگزین می‌کند. ادامه می‌دهید؟', + reloadInstallConfirm: 'بارگیری مجدد و بازنویسی تنظیمات ذخیره‌نشده انجام شود؟', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1939,8 +1940,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'برای استفاده از نشانی پنل، خالی بگذارید. مسیر باید /mcp باشد.', - invalidUrl: 'یک نشانی HTTP/HTTPS با مسیر /mcp و بدون اطلاعات احراز هویت، پارامترهای پرس‌وجو یا قطعه وارد کنید.', + externalUrlHelp: + 'برای استفاده از نشانی پنل، خالی بگذارید. برای پراکسی معکوس سفارشی، نشانی کامل دسترسی را وارد کنید.', + invalidUrl: 'نشانی معتبر HTTP(S) را بدون اطلاعات احراز هویت، پارامتر پرس‌وجو یا قطعه وارد کنید.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index c236cd502..e700e49d8 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1805,29 +1805,27 @@ const message = { websiteUnavailable: '関連付けを利用できません', websiteDomainInvalid: '有効なドメインを入力してください。ポートも指定できます。', websiteCertificateRequired: 'このドメインを対象とする有効な証明書を選択してください。', - websiteCertificateEmpty: '一致する証明書がありません。証明書管理で申請またはインポートしてください。', - websiteUnbind: '関連付けを解除', + websiteCertificateEmpty: '一致する証明書がありません。申請またはインポートしてください。', + websiteUnbind: '関連付けを解除してサイトを削除', websiteUnbindConfirm: - 'MCP リバースプロキシと関連付けを削除します。ウェブサイトと証明書は保持されます。続行しますか?', - websiteUncertain: - '操作結果を確認できません。状態を更新してください。保留中の操作は元のリクエストで再試行できます。', + '関連付けを解除し、サイトとディレクトリを削除しますか?証明書は保持され、再関連付け時に新規サイトを作成します。', + websiteUncertain: '結果は未確認です。状態を更新し、元のリクエストで再試行してください。', websiteRetry: '保留中の操作を再試行', websiteSettings: 'ウェブサイト設定', websiteCertificateUpdate: '証明書を変更', - websiteStatusLoadFailed: '関連付けの状態を取得できません。更新して再試行してください。', - websiteOperationFailed: '関連付け操作に失敗しました。状態と設定を確認してください。', - websiteConflict: '関連付けの設定が変更されました。状態を更新して確認してください。', + websiteStatusLoadFailed: '関連付け状態を取得できません。更新してください。', + websiteOperationFailed: 'サイト操作に失敗しました。状態と設定を確認してください。', + websiteConflict: '関連付け設定が変わりました。更新して確認してください。', websiteBoundAddressHelp: '接続先は関連付けたウェブサイトから生成されます。ウェブサイト設定で管理してください。', hideToken: 'Token を隠す', copyTokenFailed: 'Token をコピーできませんでした。もう一度お試しください。', exportFailed: '接続ガイドをエクスポートできませんでした。もう一度お試しください。', rotateTokenFailed: 'Token を再生成できませんでした。もう一度お試しください。', - rotateTokenUncertain: - '再生成の結果を確認できませんでした。認証情報を更新し、現在の Token を表示してから操作を続けてください。', + rotateTokenUncertain: 'Token 更新結果は未確認です。更新後、現在の Token を表示してください。', allowedIPs: '許可する IP / CIDR', allowedIPsPlaceholder: '1 行に IP アドレスまたは CIDR を 1 件入力(最大 64 件)', allowedIPsInvalid: - '有効な IP アドレスまたは CIDR を最大 64 件、各 64 文字以内で入力してください。接続元を制限する場合、サービスのリストは空にできません。', + '有効な IP/CIDR を最大 64 件、各 64 文字以内で入力してください。接続元制限時は空欄不可です。', accessPolicy: 'MCP 接続元ポリシー', accessInherit: 'パネルのアクセスポリシーを継承', accessRestricted: '指定した IP / CIDR のみ許可', @@ -1846,10 +1844,10 @@ const message = { alreadyCreated: '認証情報を作成しました。接続の詳細で Token を確認できます。', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: '対象ノード', + nodeNameUnavailable: 'ノード名を取得できません', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'アクセスはノードとツールの認可およびアカウント権限で制限されます。新しいノードには個別の認可が必要です。', + nodeGrantHelp: 'ノード・ツール・アカウントの権限が適用されます。新規ノードは個別認可が必要です。', nodeUnavailable: 'ノード {0} は利用できません。削除してから保存してください。', allCategories: 'すべてのカテゴリ', toolCategory: 'ツールカテゴリ', @@ -1867,7 +1865,7 @@ const message = { clearVisible: '一致する {0} 件の許可を解除', readOnlyVisible: '一致する {0} 件を読み取り専用にする', bulkHelp: - '一括操作は、非表示の項目を含む全カテゴリの一致するツールに適用され、「選択済みのみ」の設定には影響されません。新しいツールには個別の認可が必要です。', + '一括操作は「選択済みのみ」に関係なく、非表示を含む全カテゴリの一致項目が対象です。新規ツールは個別認可が必要です。', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1891,9 +1889,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'ツール一覧を取得できません。サービスを確認して更新してください。', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'ツール一覧が更新されました。編集画面を開き直して続行してください。', + catalogChanged: 'ツール一覧が更新されました。編集画面を開き直してください。', grantsNeedReview: '一部のツールの認可を再確認してください。', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1901,14 +1899,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: '設定の を認証情報の Token に置き換えてください。', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - '認証情報「{0}」を削除しますか?削除後は直ちに無効になります。過去の呼び出し記録は保持されます。', + uncertain: '作成結果は未確認です。同じ画面で再試行するか、再作成前に一覧を確認してください。', + revokeConfirm: '「{0}」を取り消しますか?元に戻せず、新しい呼び出しは拒否されます。', + deleteConfirm: '「{0}」を削除しますか?直ちに無効になり、呼び出し履歴は保持されます。', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'データが変わりました。再読み込み後に送信してください。', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1921,7 +1917,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'サービス確認に失敗しました。ログとバージョン互換性を確認してください。', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1931,14 +1927,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: '古いイメージを削除', + deleteOldImageHelp: + 'アップグレード成功後、使用されていない古いイメージを削除します。削除の失敗はアップグレードに影響しません。タスクログを確認してください。', installEmpty: '1Panel MCP サービスはまだインストールされていません。インストールして有効にすると、アクセス認証情報を作成して MCP ツールを呼び出せます。', + installWithCredentials: 'MCP は未インストールです。認証情報は保持されますが、呼び出せません。', installTaskLog: 'インストールログ', installNow: '今すぐインストール', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: '再読み込みすると未保存の設定が置き換えられます。続行しますか?', + reloadInstallConfirm: '未保存の設定を上書きして再読み込みしますか?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1946,9 +1946,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: '空欄の場合はパネルの URL を使用します。パスは /mcp にしてください。', - invalidUrl: - '認証情報、クエリパラメーター、フラグメントを含まない、パスが /mcp の HTTP/HTTPS URL を入力してください。', + externalUrlHelp: + '空欄の場合はパネルの URL を使用します。独自のリバースプロキシを使う場合は、完全なアクセス URL を入力してください。', + invalidUrl: '認証情報・クエリパラメーター・フラグメントを含まない有効な HTTP(S) URL を入力してください。', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 05207ef95..759b8c95d 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -1787,28 +1787,26 @@ const message = { websiteUnavailable: '연결 사용 불가', websiteDomainInvalid: '유효한 도메인을 입력하세요. 포트를 포함할 수 있습니다.', websiteCertificateRequired: '이 도메인에 유효한 인증서를 선택하세요.', - websiteCertificateEmpty: '일치하는 인증서가 없습니다. 인증서 관리에서 신청하거나 가져오세요.', - websiteUnbind: '웹사이트 연결 해제', - websiteUnbindConfirm: 'MCP 리버스 프록시와 연결을 제거하고 웹사이트와 인증서는 유지합니다. 계속하시겠습니까?', - websiteUncertain: - '작업 결과가 확인되지 않았습니다. 상태를 새로 고치세요. 대기 중인 작업은 원래 요청으로 다시 시도할 수 있습니다.', + websiteCertificateEmpty: '일치하는 인증서가 없습니다. 신청하거나 가져오세요.', + websiteUnbind: '연결 해제 및 웹사이트 삭제', + websiteUnbindConfirm: + '연결을 해제하고 사이트와 디렉터리를 삭제할까요? 인증서는 보존되며, 다시 연결하면 새 사이트를 만듭니다.', + websiteUncertain: '결과가 미확인 상태입니다. 상태를 새로고침한 뒤 원래 요청으로 재시도하세요.', websiteRetry: '대기 작업 다시 시도', websiteSettings: '웹사이트 설정', websiteCertificateUpdate: '인증서 변경', - websiteStatusLoadFailed: '웹사이트 연결 상태를 읽을 수 없습니다. 새로 고친 후 다시 시도하세요.', - websiteOperationFailed: '웹사이트 연결 작업에 실패했습니다. 현재 상태와 설정을 확인하세요.', - websiteConflict: '연결 설정이 변경되었습니다. 상태를 새로 고친 후 확인하세요.', + websiteStatusLoadFailed: '연결 상태를 읽을 수 없습니다. 새로고침하세요.', + websiteOperationFailed: '사이트 작업에 실패했습니다. 상태와 설정을 확인하세요.', + websiteConflict: '연결 설정이 변경되었습니다. 새로고침 후 확인하세요.', websiteBoundAddressHelp: '접속 주소는 연결된 웹사이트에서 생성됩니다. 웹사이트 설정에서 관리하세요.', hideToken: 'Token 숨기기', copyTokenFailed: 'Token을 복사하지 못했습니다. 다시 시도하세요.', exportFailed: '연결 안내서를 내보내지 못했습니다. 다시 시도하세요.', rotateTokenFailed: 'Token을 다시 생성하지 못했습니다. 다시 시도하세요.', - rotateTokenUncertain: - '재생성 결과를 확인할 수 없습니다. 자격 증명을 새로고침하고 현재 Token을 확인한 후 계속하세요.', + rotateTokenUncertain: 'Token 갱신 결과가 미확인입니다. 새로고침 후 현재 Token을 확인하세요.', allowedIPs: '허용된 IP / CIDR', allowedIPsPlaceholder: '한 줄에 IP 주소 또는 CIDR 하나씩 입력하세요. 최대 64개', - allowedIPsInvalid: - '유효한 IP 주소 또는 CIDR을 최대 64개, 각각 64자 이하로 입력하세요. 서비스 접속 원본을 제한할 때는 목록을 비울 수 없습니다.', + allowedIPsInvalid: '유효한 IP/CIDR을 최대 64개, 각 64자 이내로 입력하세요. 소스 제한 시 목록은 필수입니다.', accessPolicy: 'MCP 접속 원본 정책', accessInherit: '패널 접근 정책 상속', accessRestricted: '지정된 IP / CIDR만 허용', @@ -1827,9 +1825,10 @@ const message = { alreadyCreated: '자격 증명이 생성되었습니다. 연결 상세 정보에서 Token을 확인할 수 있습니다.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: '대상 노드', + nodeNameUnavailable: '노드 이름을 사용할 수 없습니다', selectNodes: 'Select at least one node.', - nodeGrantHelp: '접근 범위는 노드 및 도구 승인과 계정 권한으로 제한됩니다. 새 노드는 별도 승인이 필요합니다.', + nodeGrantHelp: '노드·도구·계정 권한이 적용됩니다. 새 노드는 별도 권한이 필요합니다.', nodeUnavailable: '노드 {0}을 사용할 수 없습니다. 저장하기 전에 제거하세요.', allCategories: '모든 카테고리', toolCategory: '도구 카테고리', @@ -1847,7 +1846,7 @@ const message = { clearVisible: '일치하는 도구 {0}개 권한 해제', readOnlyVisible: '일치하는 도구 {0}개를 읽기 전용으로 설정', bulkHelp: - '일괄 작업은 "선택한 항목만" 설정과 관계없이 숨겨진 항목을 포함하여 모든 범주의 일치하는 도구에 적용됩니다. 새 도구는 별도 승인이 필요합니다.', + '일괄 작업은 “선택 항목만”과 관계없이 모든 분류의 숨겨진 일치 항목도 포함합니다. 새 도구는 별도 권한이 필요합니다.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1871,9 +1870,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: '도구 목록을 사용할 수 없습니다. 서비스를 확인하고 새로고침하세요.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: '도구 목록이 업데이트되었습니다. 편집 창을 다시 열어 계속하세요.', + catalogChanged: '도구 목록이 갱신되었습니다. 편집 창을 다시 여세요.', grantsNeedReview: '일부 도구 권한을 다시 확인해야 합니다.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1881,13 +1880,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: '구성의 을 자격 증명 Token으로 바꾸세요.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: '자격 증명 “{0}”을 삭제하시겠습니까? 삭제 즉시 무효화되며 이전 호출 기록은 보존됩니다.', + uncertain: '생성 결과가 미확인입니다. 같은 창에서 재시도하거나, 다시 만들기 전에 목록을 확인하세요.', + revokeConfirm: '“{0}”을 철회할까요? 복구할 수 없으며 새 호출이 거부됩니다.', + deleteConfirm: '“{0}”을 삭제할까요? 즉시 무효화되며 호출 기록은 보존됩니다.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: '데이터가 변경되었습니다. 다시 불러온 뒤 제출하세요.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1900,7 +1898,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: '서비스 확인에 실패했습니다. 로그와 버전 호환성을 확인하세요.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1910,14 +1908,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: '이전 이미지 삭제', + deleteOldImageHelp: + '업그레이드 성공 후 사용하지 않는 이전 이미지를 삭제합니다. 정리 실패는 업그레이드에 영향을 주지 않으며 작업 로그에서 확인할 수 있습니다.', installEmpty: '1Panel MCP 서비스가 아직 설치되지 않았습니다. 설치하고 활성화하면 액세스 자격 증명을 생성하고 MCP 도구를 호출할 수 있습니다.', + installWithCredentials: 'MCP가 설치되지 않았습니다. 자격 증명은 보존되며 호출은 불가합니다.', installTaskLog: '설치 로그', installNow: '지금 설치', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: '다시 불러오면 저장하지 않은 구성을 덮어씁니다. 계속하시겠습니까?', + reloadInstallConfirm: '다시 불러와 저장하지 않은 설정을 덮어쓸까요?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1925,8 +1927,8 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: '비워 두면 패널 주소를 사용합니다. 경로는 /mcp여야 합니다.', - invalidUrl: '인증 정보, 쿼리 매개변수, 프래그먼트가 없고 경로가 /mcp인 HTTP/HTTPS 주소를 입력하세요.', + externalUrlHelp: '비워 두면 패널 주소를 사용합니다. 직접 구성한 리버스 프록시는 전체 접속 주소를 입력하세요.', + invalidUrl: '인증 정보, 쿼리 매개변수 또는 프래그먼트가 없는 유효한 HTTP(S) 주소를 입력하세요.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 69d92c67b..1a7d268b2 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -1777,27 +1777,26 @@ const message = { websiteUnavailable: 'ການຜູກບໍ່ພ້ອມໃຊ້', websiteDomainInvalid: 'ປ້ອນໂດເມນທີ່ຖືກຕ້ອງ ໂດຍສາມາດລະບຸພອດໄດ້.', websiteCertificateRequired: 'ເລືອກໃບຢັ້ງຢືນທີ່ຍັງໃຊ້ໄດ້ ແລະຄອບຄຸມໂດເມນນີ້.', - websiteCertificateEmpty: 'ບໍ່ມີໃບຢັ້ງຢືນທີ່ກົງກັນ. ກະລຸນາຂໍ ຫຼືນຳເຂົ້າໃນການຈັດການໃບຢັ້ງຢືນ.', - websiteUnbind: 'ຍົກເລີກການຜູກ', - websiteUnbindConfirm: 'ຈະລຶບພຣັອກຊີຍ້ອນກັບ MCP ແລະການຜູກ ແຕ່ເກັບເວັບໄຊແລະໃບຢັ້ງຢືນໄວ້. ສືບຕໍ່ບໍ?', - websiteUncertain: 'ຜົນການດຳເນີນງານຍັງບໍ່ຖືກຢືນຢັນ. ກະລຸນາໂຫຼດສະຖານະໃໝ່; ສາມາດລອງຄືນດ້ວຍຄຳຂໍເດີມ.', + websiteCertificateEmpty: 'ບໍ່ມີໃບຮັບຮອງທີ່ກົງກັນ. ຂໍ ຫຼືນຳເຂົ້າໃບຮັບຮອງ.', + websiteUnbind: 'ຍົກເລີກການຜູກ ແລະ ລຶບເວັບໄຊ', + websiteUnbindConfirm: 'ຍົກເລີກການຜູກ ແລະລຶບເວັບໄຊພ້ອມໂຟນເດີບໍ? ເກັບໃບຮັບຮອງໄວ້. ຜູກໃໝ່ຈະສ້າງເວັບໄຊໃໝ່.', + websiteUncertain: 'ຍັງບໍ່ຢືນຢັນຜົນ. ໂຫຼດສະຖານະຄືນ ແລ້ວລອງຄຳຂໍເດີມອີກຄັ້ງ.', websiteRetry: 'ລອງຄືນການດຳເນີນງານທີ່ລໍຖ້າ', websiteSettings: 'ການຕັ້ງຄ່າເວັບໄຊ', websiteCertificateUpdate: 'ປ່ຽນໃບຢັ້ງຢືນ', - websiteStatusLoadFailed: 'ບໍ່ສາມາດອ່ານສະຖານະການຜູກເວັບໄຊໄດ້. ໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.', - websiteOperationFailed: 'ການຜູກເວັບໄຊລົ້ມເຫຼວ. ກວດສອບສະຖານະແລະການຕັ້ງຄ່າ.', - websiteConflict: 'ການຕັ້ງຄ່າການຜູກປ່ຽນແລ້ວ. ໂຫຼດສະຖານະໃໝ່ແລະກວດສອບ.', + websiteStatusLoadFailed: 'ອ່ານສະຖານະການຜູກບໍ່ໄດ້. ໂຫຼດຄືນ.', + websiteOperationFailed: 'ການດຳເນີນງານເວັບໄຊລົ້ມເຫຼວ. ກວດສະຖານະ ແລະການຕັ້ງຄ່າ.', + websiteConflict: 'ການຕັ້ງຄ່າການຜູກປ່ຽນແລ້ວ. ໂຫຼດຄືນເພື່ອກວດສອບ.', websiteBoundAddressHelp: 'ທີ່ຢູ່ເຊື່ອມຕໍ່ສ້າງຈາກເວັບໄຊທີ່ຜູກ. ຈັດການໃນການຕັ້ງຄ່າເວັບໄຊ.', hideToken: 'ເຊື່ອງ Token', copyTokenFailed: 'ບໍ່ສາມາດຄັດລອກ Token ໄດ້. ກະລຸນາລອງໃໝ່.', exportFailed: 'ບໍ່ສາມາດສົ່ງອອກຄູ່ມືເຊື່ອມຕໍ່ໄດ້. ກະລຸນາລອງໃໝ່.', rotateTokenFailed: 'ບໍ່ສາມາດສ້າງ Token ໃໝ່ໄດ້. ກະລຸນາລອງໃໝ່.', - rotateTokenUncertain: - 'ບໍ່ສາມາດຢືນຢັນຜົນການສ້າງໃໝ່ໄດ້. ກະລຸນາໂຫຼດຂໍ້ມູນຢືນຢັນຄືນ ແລະ ເບິ່ງ Token ປັດຈຸບັນກ່ອນດຳເນີນການຕໍ່.', + rotateTokenUncertain: 'ຍັງບໍ່ຢືນຢັນການປ່ຽນ Token. ໂຫຼດຄືນ ແລ້ວເບິ່ງ Token ປັດຈຸບັນ.', allowedIPs: 'IP / CIDR ທີ່ອະນຸຍາດ', allowedIPsPlaceholder: 'ໜຶ່ງ IP ຫຼື CIDR ຕໍ່ແຖວ; ສູງສຸດ 64 ລາຍການ', allowedIPsInvalid: - 'ປ້ອນ IP ຫຼື CIDR ທີ່ຖືກຕ້ອງສູງສຸດ 64 ລາຍການ, ລາຍການລະບໍ່ເກີນ 64 ຕົວອັກສອນ. ເມື່ອຈຳກັດແຫຼ່ງເຂົ້າເຖິງ ລາຍການຂອງບໍລິການຕ້ອງບໍ່ຫວ່າງ.', + 'IP/CIDR ທີ່ຖືກຕ້ອງສູງສຸດ 64 ລາຍການ, ແຕ່ລະລາຍການບໍ່ເກີນ 64 ຕົວອັກສອນ. ລາຍການຈຳກັດແຫຼ່ງທີ່ມາຫ້າມວ່າງ.', accessPolicy: 'ນະໂຍບາຍແຫຼ່ງເຂົ້າເຖິງ MCP', accessInherit: 'ສືບທອດນະໂຍບາຍເຂົ້າເຖິງຂອງແຜງ', accessRestricted: 'ອະນຸຍາດສະເພາະ IP / CIDR ໃນລາຍການ', @@ -1816,9 +1815,10 @@ const message = { alreadyCreated: 'ສ້າງຂໍ້ມູນຢືນຢັນແລ້ວ. ເບິ່ງ Token ໄດ້ໃນລາຍລະອຽດການເຊື່ອມຕໍ່.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'ໂນດເປົ້າໝາຍ', + nodeNameUnavailable: 'ບໍ່ສາມາດສະແດງຊື່ໂນດໄດ້', selectNodes: 'Select at least one node.', - nodeGrantHelp: 'ການເຂົ້າເຖິງຖືກຈຳກັດໂດຍສິດໂນດ, ສິດເຄື່ອງມື ແລະ ສິດບັນຊີ. ໂນດໃໝ່ຕ້ອງໄດ້ຮັບອະນຸຍາດແຍກຕ່າງຫາກ.', + nodeGrantHelp: 'ສິດຂອງໂນດ, ເຄື່ອງມື ແລະບັນຊີຍັງມີຜົນ. ໂນດໃໝ່ຕ້ອງໃຫ້ສິດແຍກ.', nodeUnavailable: 'ໂນດ {0} ໃຊ້ງານບໍ່ໄດ້. ລຶບອອກກ່ອນບັນທຶກ.', allCategories: 'ທຸກໝວດໝູ່', toolCategory: 'ໝວດໝູ່ເຄື່ອງມື', @@ -1836,7 +1836,7 @@ const message = { clearVisible: 'ຖອນສິດ {0} ເຄື່ອງມືທີ່ກົງກັນ', readOnlyVisible: 'ໃຫ້ {0} ລາຍການທີ່ກົງກັນມີສິດອ່ານຢ່າງດຽວ', bulkHelp: - 'ການດຳເນີນການແບບກຸ່ມມີຜົນຕໍ່ເຄື່ອງມືທີ່ກົງກັນໃນທຸກໝວດໝູ່ ລວມທັງລາຍການທີ່ເຊື່ອງໄວ້ ໂດຍບໍ່ຂຶ້ນກັບ "ສະເພາະລາຍການທີ່ເລືອກ". ເຄື່ອງມືໃໝ່ຕ້ອງໄດ້ຮັບອະນຸຍາດແຍກຕ່າງຫາກ.', + 'ການດຳເນີນງານແບບກຸ່ມລວມລາຍການກົງກັນທີ່ເຊື່ອງໃນທຸກໝວດ ບໍ່ຂຶ້ນກັບ “ສະເພາະທີ່ເລືອກ”. ເຄື່ອງມືໃໝ່ຕ້ອງໃຫ້ສິດແຍກ.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1860,9 +1860,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'ລາຍການເຄື່ອງມືໃຊ້ບໍ່ໄດ້. ກວດບໍລິການ ແລ້ວໂຫຼດຄືນ.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'ລາຍການເຄື່ອງມືໄດ້ອັບເດດແລ້ວ. ກະລຸນາເປີດໜ້າຕ່າງແກ້ໄຂຄືນໃໝ່ເພື່ອດຳເນີນການຕໍ່.', + catalogChanged: 'ລາຍການເຄື່ອງມືປ່ຽນແລ້ວ. ເປີດໜ້າແກ້ໄຂໃໝ່.', grantsNeedReview: 'ຕ້ອງຢືນຢັນສິດເຄື່ອງມືບາງລາຍການອີກຄັ້ງ.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1870,13 +1870,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'ແທນທີ່ ໃນການຕັ້ງຄ່າດ້ວຍ Token ຂອງຂໍ້ມູນຢືນຢັນ.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: 'ລຶບຂໍ້ມູນຢັ້ງຢືນ “{0}” ຫຼືບໍ່? ຂໍ້ມູນຈະໃຊ້ບໍ່ໄດ້ທັນທີ ແລະປະຫວັດການເອີ້ນຈະຖືກເກັບໄວ້.', + uncertain: 'ຍັງບໍ່ຢືນຢັນການສ້າງ. ລອງໃນໜ້າເດີມ ຫຼືກວດລາຍການກ່ອນສ້າງຊ້ຳ.', + revokeConfirm: 'ຖອນ “{0}” ບໍ? ກູ້ຄືນບໍ່ໄດ້ ແລະຈະປະຕິເສດການເອີ້ນໃໝ່.', + deleteConfirm: 'ລຶບ “{0}” ບໍ? ຈະໝົດຜົນທັນທີ ແຕ່ເກັບປະຫວັດການເອີ້ນໄວ້.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'ຂໍ້ມູນປ່ຽນແລ້ວ. ໂຫຼດຄືນກ່ອນສົ່ງ.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1889,7 +1888,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'ກວດບໍລິການລົ້ມເຫຼວ. ກວດບັນທຶກ ແລະຄວາມເຂົ້າກັນຂອງລຸ້ນ.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1899,14 +1898,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'ລຶບອິມເມຈເກົ່າ', + deleteOldImageHelp: + 'ຫຼັງຈາກອັບເກຣດສຳເລັດ ຈະລຶບອິມເມຈເກົ່າທີ່ບໍ່ໄດ້ໃຊ້. ການລຶບລົ້ມເຫຼວບໍ່ກະທົບການອັບເກຣດ; ເບິ່ງບັນທຶກວຽກ.', installEmpty: - 'ບໍລິການ 1Panel MCP ຍັງບໍ່ໄດ້ຕິດຕັ້ງ. ຕິດຕັ້ງ ແລະ ເປີດໃຊ້ງານເພື່ອສ້າງຂໍ້ມູນຢັ້ງຢືນການເຂົ້າເຖິງ ແລະ ເອີ້ນໃຊ້ເຄື່ອງມື MCP.', + 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງບໍລິການ 1Panel MCP. ຫຼັງຈາກຕິດຕັ້ງ ແລະເປີດໃຊ້ ທ່ານຈະສາມາດສ້າງຂໍ້ມູນຢືນຢັນການເຂົ້າເຖິງ ແລະເອີ້ນໃຊ້ເຄື່ອງມື MCP ໄດ້.', + installWithCredentials: 'ຍັງບໍ່ຕິດຕັ້ງ MCP. ເກັບຂໍ້ມູນຢືນຢັນໄວ້ ແຕ່ຍັງເອີ້ນບໍ່ໄດ້.', installTaskLog: 'ບັນທຶກການຕິດຕັ້ງ', installNow: 'ຕິດຕັ້ງດຽວນີ້', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'ການໂຫຼດໃໝ່ຈະແທນທີ່ການຕັ້ງຄ່າທີ່ຍັງບໍ່ໄດ້ບັນທຶກ. ດຳເນີນຕໍ່ຫຼືບໍ່?', + reloadInstallConfirm: 'ໂຫຼດຄືນ ແລະຂຽນທັບຄ່າທີ່ຍັງບໍ່ບັນທຶກບໍ?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1914,8 +1917,8 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'ປ່ອຍວ່າງເພື່ອໃຊ້ທີ່ຢູ່ແຜງ. ເສັ້ນທາງຕ້ອງເປັນ /mcp.', - invalidUrl: 'ປ້ອນທີ່ຢູ່ HTTP/HTTPS ທີ່ມີເສັ້ນທາງ /mcp ໂດຍບໍ່ມີຂໍ້ມູນຢືນຢັນ, ພາຣາມິເຕີຄິວຣີ ຫຼື ສ່ວນຍ່ອຍ.', + externalUrlHelp: 'ປ່ອຍວ່າງເພື່ອໃຊ້ທີ່ຢູ່ແຜງ. ຖ້າໃຊ້ reverse proxy ທີ່ຕັ້ງເອງ, ໃຫ້ປ້ອນ URL ເຂົ້າເຖິງເຕັມ.', + invalidUrl: 'ປ້ອນ URL HTTP(S) ທີ່ຖືກຕ້ອງ ໂດຍບໍ່ມີຂໍ້ມູນຢືນຢັນ, ພາລາມິເຕີຄຳຖາມ ຫຼືສ່ວນ fragment.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 2b1eaaabf..6c89103e8 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1835,29 +1835,28 @@ const message = { websiteUnavailable: 'Pautan tidak tersedia', websiteDomainInvalid: 'Masukkan domain yang sah, dengan port pilihan.', websiteCertificateRequired: 'Pilih sijil sah yang meliputi domain ini.', - websiteCertificateEmpty: 'Tiada sijil sepadan. Mohon atau import sijil dalam pengurusan sijil.', - websiteUnbind: 'Nyahpaut laman web', - websiteUnbindConfirm: 'Proksi songsang MCP dan pautan akan dibuang; laman web dan sijil dikekalkan. Teruskan?', - websiteUncertain: - 'Hasil operasi belum disahkan. Muat semula status; operasi tertunda boleh dicuba semula menggunakan permintaan asal.', + websiteCertificateEmpty: 'Tiada sijil sepadan. Mohon atau import sijil.', + websiteUnbind: 'Nyahpaut dan padam laman web', + websiteUnbindConfirm: + 'Nyahpaut dan padam laman serta direktorinya? Sijil dikekalkan. Pautan semula mencipta laman baharu.', + websiteUncertain: 'Hasil belum disahkan. Muat semula status dan cuba semula permintaan asal.', websiteRetry: 'Cuba semula operasi tertunda', websiteSettings: 'Tetapan laman web', websiteCertificateUpdate: 'Tukar sijil', - websiteStatusLoadFailed: 'Status pautan laman web tidak dapat dibaca. Muat semula dan cuba lagi.', - websiteOperationFailed: 'Operasi pautan gagal. Semak status dan konfigurasi semasa.', - websiteConflict: 'Konfigurasi pautan telah berubah. Muat semula status dan semaknya.', + websiteStatusLoadFailed: 'Status pautan tidak dapat dibaca. Muat semula.', + websiteOperationFailed: 'Operasi laman gagal. Semak status dan tetapan.', + websiteConflict: 'Tetapan pautan berubah. Muat semula dan semak.', websiteBoundAddressHelp: 'Alamat sambungan dijana daripada laman web terpaut. Uruskannya dalam tetapan laman web.', hideToken: 'Sembunyikan Token', copyTokenFailed: 'Token tidak dapat disalin. Sila cuba lagi.', exportFailed: 'Panduan sambungan tidak dapat dieksport. Sila cuba lagi.', rotateTokenFailed: 'Token tidak dapat dijana semula. Sila cuba lagi.', - rotateTokenUncertain: - 'Hasil penjanaan semula tidak dapat disahkan. Muat semula kelayakan dan lihat Token semasanya sebelum meneruskan.', + rotateTokenUncertain: 'Kemas kini Token belum disahkan. Muat semula dan lihat Token semasa.', allowedIPs: 'IP / CIDR yang dibenarkan', allowedIPsPlaceholder: 'Satu alamat IP atau CIDR setiap baris; sehingga 64 entri', allowedIPsInvalid: - 'Masukkan sehingga 64 alamat IP atau CIDR yang sah, setiap satu tidak melebihi 64 aksara. Senarai perkhidmatan tidak boleh kosong apabila sumber dihadkan.', + 'Maksimum 64 IP/CIDR sah, setiap satu sehingga 64 aksara. Senarai terhad tidak boleh kosong.', accessPolicy: 'Dasar sumber MCP', accessInherit: 'Warisi dasar akses panel', accessRestricted: 'Benarkan IP / CIDR tersenarai sahaja', @@ -1876,9 +1875,10 @@ const message = { alreadyCreated: 'Kelayakan telah dicipta. Lihat Token dalam butiran sambungan.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Nod sasaran', + nodeNameUnavailable: 'Nama nod tidak tersedia', selectNodes: 'Select at least one node.', - nodeGrantHelp: 'Akses dihadkan oleh kebenaran nod, alat dan akaun. Nod baharu memerlukan kebenaran berasingan.', + nodeGrantHelp: 'Kebenaran nod, alat dan akaun terpakai. Nod baharu memerlukan kebenaran berasingan.', nodeUnavailable: 'Nod {0} tidak tersedia. Keluarkannya sebelum menyimpan.', allCategories: 'Semua kategori', toolCategory: 'Kategori alat', @@ -1896,7 +1896,7 @@ const message = { clearVisible: 'Batalkan kebenaran {0} alat yang sepadan', readOnlyVisible: 'Kekalkan akses baca sahaja untuk {0} padanan', bulkHelp: - 'Tindakan pukal mempengaruhi alat yang sepadan dalam semua kategori, termasuk item tersembunyi, tanpa mengira "Yang dipilih sahaja". Alat baharu memerlukan kebenaran berasingan.', + 'Tindakan pukal meliputi padanan tersembunyi semua kategori tanpa mengikut “Dipilih sahaja”. Alat baharu perlu kebenaran berasingan.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1920,9 +1920,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Senarai alat tidak tersedia. Semak perkhidmatan dan muat semula.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'Senarai alat telah dikemas kini. Buka semula editor untuk meneruskan.', + catalogChanged: 'Senarai alat dikemas kini. Buka semula penyunting.', grantsNeedReview: 'Sesetengah kebenaran alat perlu disahkan semula.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1930,14 +1930,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'Gantikan dalam konfigurasi dengan Token kelayakan.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - 'Padam kelayakan “{0}”? Kelayakan akan menjadi tidak sah serta-merta. Sejarah panggilan akan dikekalkan.', + uncertain: 'Penciptaan belum disahkan. Cuba semula di tetingkap ini atau semak senarai sebelum mencipta lagi.', + revokeConfirm: 'Batalkan “{0}”? Tidak boleh dipulihkan; panggilan baharu ditolak.', + deleteConfirm: 'Padam “{0}”? Akses tamat serta-merta; sejarah panggilan dikekalkan.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Data berubah. Muat semula sebelum menghantar.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1950,7 +1948,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Semakan perkhidmatan gagal. Semak log dan keserasian versi.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1960,14 +1958,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Padam imej lama', + deleteOldImageHelp: + 'Selepas naik taraf berjaya, padam imej lama yang tidak digunakan. Kegagalan pembersihan tidak menjejaskan naik taraf; lihat log tugas.', installEmpty: 'Perkhidmatan 1Panel MCP belum dipasang. Pasang dan aktifkannya untuk mencipta kelayakan akses dan memanggil alat MCP.', + installWithCredentials: 'MCP belum dipasang. Kelayakan dikekalkan; panggilan tidak tersedia.', installTaskLog: 'Log pemasangan', installNow: 'Pasang sekarang', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'Memuat semula akan menggantikan konfigurasi yang belum disimpan. Teruskan?', + reloadInstallConfirm: 'Muat semula dan timpa tetapan yang belum disimpan?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1975,8 +1977,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Biarkan kosong untuk menggunakan URL panel. Laluan mestilah /mcp.', - invalidUrl: 'Masukkan URL HTTP/HTTPS dengan laluan /mcp tanpa kelayakan, parameter pertanyaan atau fragmen.', + externalUrlHelp: + 'Biarkan kosong untuk menggunakan URL panel. Untuk proksi songsang sendiri, masukkan URL akses penuh.', + invalidUrl: 'Masukkan URL HTTP(S) yang sah tanpa kelayakan, parameter pertanyaan atau fragmen.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 80f423c81..ed4fe7d26 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1836,31 +1836,28 @@ const message = { websiteUnavailable: 'Vinculação indisponível', websiteDomainInvalid: 'Informe um domínio válido, com uma porta opcional.', websiteCertificateRequired: 'Selecione um certificado válido que cubra este domínio.', - websiteCertificateEmpty: - 'Nenhum certificado correspondente. Solicite ou importe um no gerenciamento de certificados.', - websiteUnbind: 'Desvincular site', + websiteCertificateEmpty: 'Nenhum certificado correspondente. Solicite ou importe um.', + websiteUnbind: 'Desvincular e excluir site', websiteUnbindConfirm: - 'O proxy reverso MCP e a vinculação serão removidos; o site e o certificado serão mantidos. Continuar?', - websiteUncertain: - 'O resultado da operação não foi confirmado. Atualize o status; a operação pendente pode ser repetida com a solicitação original.', + 'Desvincular e excluir o site e seu diretório? O certificado será mantido. Vincular novamente cria outro site.', + websiteUncertain: 'Resultado não confirmado. Atualize o status e repita a solicitação original.', websiteRetry: 'Repetir operação pendente', websiteSettings: 'Configurações do site', websiteCertificateUpdate: 'Alterar certificado', - websiteStatusLoadFailed: 'Não foi possível ler o status da vinculação. Atualize e tente novamente.', - websiteOperationFailed: 'A operação de vinculação falhou. Verifique o status e a configuração.', - websiteConflict: 'A configuração da vinculação mudou. Atualize o status e revise-a.', + websiteStatusLoadFailed: 'Não foi possível ler o status do vínculo. Atualize.', + websiteOperationFailed: 'Falha na operação do site. Confira o status e as configurações.', + websiteConflict: 'A configuração do vínculo mudou. Atualize e confira.', websiteBoundAddressHelp: 'O endereço é gerado a partir do site vinculado. Gerencie-o nas configurações do site.', hideToken: 'Ocultar Token', copyTokenFailed: 'Não foi possível copiar o Token. Tente novamente.', exportFailed: 'Não foi possível exportar o guia de conexão. Tente novamente.', rotateTokenFailed: 'Não foi possível gerar um novo Token. Tente novamente.', - rotateTokenUncertain: - 'Não foi possível confirmar a geração. Atualize a credencial e veja seu Token atual antes de continuar.', + rotateTokenUncertain: 'Alteração do Token não confirmada. Atualize e consulte o Token atual.', allowedIPs: 'IPs / CIDRs permitidos', allowedIPsPlaceholder: 'Um endereço IP ou CIDR por linha; até 64 entradas', allowedIPsInvalid: - 'Insira até 64 endereços IP ou CIDRs válidos, com até 64 caracteres cada. A lista do serviço não pode ficar vazia quando as origens são restritas.', + 'Até 64 IPs/CIDRs válidos, com até 64 caracteres cada. A lista restrita não pode ficar vazia.', accessPolicy: 'Política de origem do MCP', accessInherit: 'Herdar a política de acesso do painel', accessRestricted: 'Permitir apenas os IPs / CIDRs listados', @@ -1879,10 +1876,10 @@ const message = { alreadyCreated: 'Credencial criada. Veja o Token nos detalhes de conexão.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Nó de destino', + nodeNameUnavailable: 'Nome do nó indisponível', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'O acesso depende dos nós e ferramentas autorizados e das permissões da conta. Novos nós exigem autorização separada.', + nodeGrantHelp: 'Valem as permissões de nó, ferramenta e conta. Novos nós exigem autorização própria.', nodeUnavailable: 'O nó {0} está indisponível. Remova-o antes de salvar.', allCategories: 'Todas as categorias', toolCategory: 'Categoria de ferramentas', @@ -1900,7 +1897,7 @@ const message = { clearVisible: 'Revogar {0} ferramentas correspondentes', readOnlyVisible: 'Manter somente leitura para {0} correspondências', bulkHelp: - 'As ações em lote afetam as ferramentas correspondentes de todas as categorias, incluindo as ocultas, independentemente de “Somente selecionadas”. Novas ferramentas exigem autorização separada.', + 'Ações incluem correspondências ocultas de todas as categorias e ignoram “Somente selecionados”. Novas ferramentas exigem autorização própria.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1924,9 +1921,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Lista de ferramentas indisponível. Confira o serviço e atualize.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'A lista de ferramentas foi atualizada. Reabra o editor para continuar.', + catalogChanged: 'Lista de ferramentas atualizada. Reabra o editor.', grantsNeedReview: 'Algumas permissões de ferramentas precisam ser confirmadas novamente.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1934,14 +1931,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'Substitua na configuração pelo Token da credencial.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - 'Excluir a credencial “{0}”? Ela será invalidada imediatamente. O histórico de chamadas será mantido.', + uncertain: 'Criação não confirmada. Tente nesta janela ou confira a lista antes de criar novamente.', + revokeConfirm: 'Revogar “{0}”? É irreversível e novas chamadas serão recusadas.', + deleteConfirm: 'Excluir “{0}”? O acesso termina imediatamente; o histórico será mantido.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Os dados mudaram. Recarregue antes de enviar.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1954,7 +1949,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Falha na verificação do serviço. Confira os logs e a compatibilidade de versões.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1964,14 +1959,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Excluir imagem anterior', + deleteOldImageHelp: + 'Após a atualização bem-sucedida, remove a imagem anterior sem uso. Falhas na limpeza não afetam a atualização; consulte os logs da tarefa.', installEmpty: - 'O serviço 1Panel MCP ainda não está instalado. Instale-o e habilite-o para criar credenciais de acesso e chamar ferramentas MCP.', + 'O serviço 1Panel MCP ainda não está instalado. Instale-o e ative-o para criar credenciais de acesso e chamar ferramentas MCP.', + installWithCredentials: 'MCP não está instalado. Credenciais mantidas; chamadas indisponíveis.', installTaskLog: 'Registro de instalação', installNow: 'Instalar agora', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'A configuração não salva será substituída. Continuar?', + reloadInstallConfirm: 'Recarregar e sobrescrever configurações não salvas?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1979,9 +1978,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Deixe em branco para usar a URL do painel. O caminho deve ser /mcp.', - invalidUrl: - 'Insira uma URL HTTP/HTTPS com o caminho /mcp, sem credenciais, parâmetros de consulta ou fragmento.', + externalUrlHelp: + 'Deixe em branco para usar a URL do painel. Para um proxy reverso próprio, informe a URL de acesso completa.', + invalidUrl: 'Insira uma URL HTTP(S) válida sem credenciais, parâmetros de consulta ou fragmento.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 4e47ad3d6..06c8563f5 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1825,30 +1825,27 @@ const message = { websiteUnavailable: 'Привязка недоступна', websiteDomainInvalid: 'Введите допустимый домен, при необходимости с портом.', websiteCertificateRequired: 'Выберите действующий сертификат для этого домена.', - websiteCertificateEmpty: - 'Подходящих сертификатов нет. Выпустите или импортируйте сертификат в разделе управления сертификатами.', - websiteUnbind: 'Отвязать сайт', - websiteUnbindConfirm: 'Обратный прокси MCP и привязка будут удалены, сайт и сертификат сохранятся. Продолжить?', - websiteUncertain: - 'Результат операции не подтверждён. Обновите состояние; ожидающую операцию можно повторить с исходным запросом.', + websiteCertificateEmpty: 'Нет подходящего сертификата. Выпустите или импортируйте его.', + websiteUnbind: 'Отвязать и удалить сайт', + websiteUnbindConfirm: + 'Отключить привязку и удалить сайт с каталогом? Сертификат сохранится. Повторная привязка создаст новый сайт.', + websiteUncertain: 'Результат не подтверждён. Обновите статус и повторите исходный запрос.', websiteRetry: 'Повторить ожидающую операцию', websiteSettings: 'Настройки сайта', websiteCertificateUpdate: 'Изменить сертификат', - websiteStatusLoadFailed: 'Не удалось получить состояние привязки. Обновите и повторите попытку.', - websiteOperationFailed: 'Операция привязки завершилась ошибкой. Проверьте состояние и настройки.', - websiteConflict: 'Настройки привязки изменились. Обновите состояние и проверьте их.', + websiteStatusLoadFailed: 'Не удалось прочитать статус привязки. Обновите.', + websiteOperationFailed: 'Ошибка операции с сайтом. Проверьте статус и настройки.', + websiteConflict: 'Настройки привязки изменены. Обновите и проверьте.', websiteBoundAddressHelp: 'Адрес подключения формируется из привязанного сайта. Изменяйте его в настройках сайта.', hideToken: 'Скрыть Token', copyTokenFailed: 'Не удалось скопировать Token. Повторите попытку.', exportFailed: 'Не удалось экспортировать инструкцию по подключению. Повторите попытку.', rotateTokenFailed: 'Не удалось создать новый Token. Повторите попытку.', - rotateTokenUncertain: - 'Не удалось подтвердить результат создания. Обновите учётные данные и проверьте текущий Token, прежде чем продолжать.', + rotateTokenUncertain: 'Смена Token не подтверждена. Обновите данные и просмотрите текущий Token.', allowedIPs: 'Разрешённые IP / CIDR', allowedIPsPlaceholder: 'Один IP-адрес или CIDR в строке; не более 64 записей', - allowedIPsInvalid: - 'Введите не более 64 корректных IP-адресов или CIDR, до 64 символов каждый. При ограничении источников список сервиса не может быть пустым.', + allowedIPsInvalid: 'До 64 корректных IP/CIDR, каждый до 64 символов. Ограниченный список не может быть пустым.', accessPolicy: 'Политика источников MCP', accessInherit: 'Наследовать политику доступа панели', accessRestricted: 'Разрешить только указанные IP / CIDR', @@ -1868,10 +1865,10 @@ const message = { alreadyCreated: 'Учётные данные созданы. Token доступен в сведениях о подключении.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Целевой узел', + nodeNameUnavailable: 'Имя узла недоступно', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'Доступ ограничен разрешениями узлов, инструментов и аккаунта. Новые узлы требуют отдельного разрешения.', + nodeGrantHelp: 'Действуют права узла, инструмента и аккаунта. Новым узлам нужен отдельный доступ.', nodeUnavailable: 'Узел {0} недоступен. Удалите его перед сохранением.', allCategories: 'Все категории', toolCategory: 'Категория инструментов', @@ -1889,7 +1886,7 @@ const message = { clearVisible: 'Отозвать доступ к {0} совпадающим инструментам', readOnlyVisible: 'Оставить только чтение для {0} совпадений', bulkHelp: - 'Массовые действия применяются к подходящим инструментам во всех категориях, включая скрытые, независимо от параметра «Только выбранные». Новые инструменты требуют отдельного разрешения.', + 'Пакетные действия охватывают скрытые совпадения всех категорий, независимо от «Только выбранные». Новым инструментам нужен отдельный доступ.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1913,9 +1910,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Список инструментов недоступен. Проверьте службу и обновите.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'Список инструментов обновлён. Откройте редактор заново, чтобы продолжить.', + catalogChanged: 'Список инструментов обновлён. Откройте редактор заново.', grantsNeedReview: 'Некоторые разрешения инструментов нужно подтвердить повторно.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1923,14 +1920,12 @@ const message = { endpoint: 'Connection URL', clientConfig: 'Client configuration', connectionHelp: 'Замените в конфигурации на Token учётных данных.', - uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - 'Удалить учетные данные «{0}»? Они немедленно станут недействительными. История вызовов будет сохранена.', + uncertain: 'Создание не подтверждено. Повторите в этом окне или проверьте список перед новым созданием.', + revokeConfirm: 'Отозвать «{0}»? Это необратимо; новые вызовы будут отклонены.', + deleteConfirm: 'Удалить «{0}»? Доступ прекратится сразу; история вызовов сохранится.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Данные изменены. Перезагрузите перед отправкой.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1943,7 +1938,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Проверка службы не пройдена. Проверьте журналы и совместимость версий.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1953,14 +1948,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Удалить старый образ', + deleteOldImageHelp: + 'После успешного обновления удалить неиспользуемый старый образ. Ошибка очистки не влияет на обновление; см. журнал задачи.', installEmpty: - 'Служба 1Panel MCP ещё не установлена. Установите и включите её, чтобы создавать учётные данные доступа и вызывать инструменты MCP.', + 'Служба 1Panel MCP ещё не установлена. Установите и включите её, чтобы создавать учётные данные для доступа и вызывать инструменты MCP.', + installWithCredentials: 'MCP не установлен. Учётные данные сохранены; вызовы недоступны.', installTaskLog: 'Журнал установки', installNow: 'Установить сейчас', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'Повторная загрузка заменит несохранённую конфигурацию. Продолжить?', + reloadInstallConfirm: 'Перезагрузить и перезаписать несохранённые настройки?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1968,8 +1967,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Оставьте пустым, чтобы использовать адрес панели. Путь должен быть /mcp.', - invalidUrl: 'Укажите HTTP/HTTPS-адрес с путём /mcp без учётных данных, параметров запроса и фрагмента.', + externalUrlHelp: + 'Оставьте пустым, чтобы использовать адрес панели. Для своего обратного прокси укажите полный адрес доступа.', + invalidUrl: 'Введите корректный HTTP(S) URL без учётных данных, параметров запроса и фрагмента.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 2edf7cc90..dcd0e2e9e 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1831,30 +1831,26 @@ const message = { websiteUnavailable: 'Bağlantı kullanılamıyor', websiteDomainInvalid: 'Geçerli bir alan adı girin; isteğe bağlı olarak bağlantı noktası ekleyebilirsiniz.', websiteCertificateRequired: 'Bu alan adını kapsayan geçerli bir sertifika seçin.', - websiteCertificateEmpty: - 'Eşleşen sertifika yok. Sertifika yönetiminden bir sertifika isteyin veya içe aktarın.', - websiteUnbind: 'Web sitesi bağlantısını kaldır', + websiteCertificateEmpty: 'Uygun sertifika yok. Başvurun veya içe aktarın.', + websiteUnbind: 'Bağlantıyı kaldır ve siteyi sil', websiteUnbindConfirm: - 'MCP ters proxy ve bağlantı kaldırılacak; web sitesi ve sertifika korunacak. Devam edilsin mi?', - websiteUncertain: - 'İşlem sonucu doğrulanmadı. Durumu yenileyin; bekleyen işlem özgün istekle yeniden denenebilir.', + 'Bağlantı, site ve dizini silinsin mi? Sertifika korunur. Yeniden bağlama yeni site oluşturur.', + websiteUncertain: 'Sonuç doğrulanmadı. Durumu yenileyip özgün isteği yeniden deneyin.', websiteRetry: 'Bekleyen işlemi yeniden dene', websiteSettings: 'Web sitesi ayarları', websiteCertificateUpdate: 'Sertifikayı değiştir', - websiteStatusLoadFailed: 'Web sitesi bağlantı durumu okunamadı. Yenileyip tekrar deneyin.', - websiteOperationFailed: 'Web sitesi bağlama işlemi başarısız oldu. Durumu ve yapılandırmayı kontrol edin.', - websiteConflict: 'Bağlantı yapılandırması değişti. Durumu yenileyip inceleyin.', + websiteStatusLoadFailed: 'Bağlantı durumu okunamadı. Yenileyin.', + websiteOperationFailed: 'Site işlemi başarısız. Durum ve ayarları kontrol edin.', + websiteConflict: 'Bağlantı ayarları değişti. Yenileyip kontrol edin.', websiteBoundAddressHelp: 'Bağlantı adresi bağlı web sitesinden oluşturulur. Web sitesi ayarlarından yönetin.', hideToken: 'Token değerini gizle', copyTokenFailed: 'Token kopyalanamadı. Lütfen tekrar deneyin.', exportFailed: 'Bağlantı kılavuzu dışa aktarılamadı. Lütfen tekrar deneyin.', rotateTokenFailed: 'Token yeniden oluşturulamadı. Lütfen tekrar deneyin.', - rotateTokenUncertain: - 'Yeniden oluşturma sonucu doğrulanamadı. Devam etmeden önce kimlik bilgisini yenileyin ve geçerli Token değerini görüntüleyin.', + rotateTokenUncertain: 'Token değişikliği doğrulanmadı. Yenileyip mevcut Token değerini görüntüleyin.', allowedIPs: 'İzin verilen IP / CIDR adresleri', allowedIPsPlaceholder: 'Her satıra bir IP adresi veya CIDR; en fazla 64 kayıt', - allowedIPsInvalid: - 'Her biri en fazla 64 karakter olan, en fazla 64 geçerli IP adresi veya CIDR girin. Kaynaklar kısıtlandığında hizmet listesi boş bırakılamaz.', + allowedIPsInvalid: 'En çok 64 geçerli IP/CIDR, her biri en çok 64 karakter. Kısıtlı liste boş bırakılamaz.', accessPolicy: 'MCP kaynak politikası', accessInherit: 'Panel erişim politikasını devral', accessRestricted: 'Yalnızca listedeki IP / CIDR adreslerine izin ver', @@ -1873,10 +1869,10 @@ const message = { alreadyCreated: 'Kimlik bilgisi oluşturuldu. Token değerini bağlantı ayrıntılarında görüntüleyin.', nodes: 'Allowed nodes', primaryNode: 'Primary node', - targetNode: 'Target node ID', + targetNode: 'Hedef düğüm', + nodeNameUnavailable: 'Düğüm adı kullanılamıyor', selectNodes: 'Select at least one node.', - nodeGrantHelp: - 'Erişim, düğüm ve araç yetkileri ile hesap izinleriyle sınırlıdır. Yeni düğümler ayrıca yetkilendirilmelidir.', + nodeGrantHelp: 'Düğüm, araç ve hesap izinleri geçerlidir. Yeni düğümler ayrı yetki ister.', nodeUnavailable: 'Düğüm {0} kullanılamıyor. Kaydetmeden önce kaldırın.', allCategories: 'Tüm kategoriler', toolCategory: 'Araç kategorisi', @@ -1894,7 +1890,7 @@ const message = { clearVisible: 'Eşleşen {0} aracın iznini kaldır', readOnlyVisible: 'Eşleşen {0} araç için yalnızca okuma iznini koru', bulkHelp: - 'Toplu işlemler, "Yalnızca seçilenler" seçeneğinden bağımsız olarak gizli öğeler dahil tüm kategorilerdeki eşleşen araçları etkiler. Yeni araçlar ayrıca yetkilendirilmelidir.', + 'Toplu işlemler, “Yalnızca seçilenler” filtresinden bağımsız tüm kategorilerdeki gizli eşleşmeleri kapsar. Yeni araçlar ayrı yetki ister.', groupSelected: '{0} / {1} granted', endpointUnavailable: 'Connection URL unavailable. Refresh or contact an administrator.', internalEndpoint: 'Internal listening address', @@ -1918,9 +1914,9 @@ const message = { readOnly: 'Read only', write: 'Write', writeHelp: 'Write tools modify server resources. Grant access as required.', - catalogUnavailable: 'Tool catalog unavailable. Check service status and refresh.', + catalogUnavailable: 'Araç listesi kullanılamıyor. Hizmeti kontrol edip yenileyin.', catalogEmpty: 'No tools available for authorization.', - catalogChanged: 'Araç listesi güncellendi. Devam etmek için düzenleyiciyi yeniden açın.', + catalogChanged: 'Araç listesi güncellendi. Düzenleyiciyi yeniden açın.', grantsNeedReview: 'Bazı araç yetkilerinin yeniden onaylanması gerekir.', acceptToolPolicy: 'I have reviewed and confirmed the current tool authorization.', removeGrant: 'Remove grant', @@ -1929,13 +1925,12 @@ const message = { clientConfig: 'Client configuration', connectionHelp: 'Yapılandırmadaki değerini kimlik bilgisinin Token değeriyle değiştirin.', uncertain: - 'Creation has not been confirmed. Retry in this window or check the credential list before creating another.', - revokeConfirm: 'Revoke credential “{0}”? This cannot be undone. New calls will be rejected.', - deleteConfirm: - '“{0}” kimlik bilgisi silinsin mi? Hemen geçersiz olacaktır. Geçmiş çağrı kayıtları korunacaktır.', + 'Oluşturma doğrulanmadı. Bu pencerede yeniden deneyin veya tekrar oluşturmadan önce listeyi kontrol edin.', + revokeConfirm: '“{0}” iptal edilsin mi? Geri alınamaz; yeni çağrılar reddedilir.', + deleteConfirm: '“{0}” silinsin mi? Erişim hemen sonlanır; çağrı geçmişi korunur.', disableConfirm: 'Disable credential “{0}”? New calls will be rejected.', enableConfirm: 'Enable credential “{0}” with the current authorization?', - conflict: 'The data has changed. Reload the latest values before submitting.', + conflict: 'Veriler değişti. Göndermeden önce yeniden yükleyin.', invocations: 'Call records', requestId: 'Request ID', duration: 'Duration (ms)', @@ -1948,7 +1943,7 @@ const message = { serviceStatus: 'Service status', statusUnknown: 'Unknown', serviceUnsupported: 'This environment does not support 1Panel MCP deployment.', - serviceUnhealthy: 'Service health checks failed. Check logs and version compatibility.', + serviceUnhealthy: 'Hizmet kontrolü başarısız. Günlükleri ve sürüm uyumluluğunu kontrol edin.', statusUnsupported: 'Unsupported environment', statusNotInstalled: 'Not installed', statusStopped: 'Stopped', @@ -1958,14 +1953,18 @@ const message = { ready: 'Service ready', installTitle: 'Install 1Panel MCP', installEnable: 'Install and enable', + deleteOldImage: 'Eski imajı sil', + deleteOldImageHelp: + 'Başarılı yükseltmeden sonra kullanılmayan eski imajı siler. Temizleme hataları yükseltmeyi etkilemez; görev günlüklerini inceleyin.', installEmpty: - '1Panel MCP hizmeti henüz yüklü değil. Erişim kimlik bilgileri oluşturmak ve MCP araçlarını çağırmak için hizmeti yükleyip etkinleştirin.', + '1Panel MCP hizmeti henüz kurulu değil. Erişim kimlik bilgileri oluşturmak ve MCP araçlarını çağırmak için hizmeti kurup etkinleştirin.', + installWithCredentials: 'MCP kurulu değil. Kimlik bilgileri korunur; çağrılar kullanılamaz.', installTaskLog: 'Kurulum günlüğü', installNow: 'Şimdi yükle', upgradeTitle: 'Upgrade service', serviceConfiguration: 'Service configuration', imageUpgrade: 'Image upgrade', - reloadInstallConfirm: 'Yeniden yükleme, kaydedilmemiş yapılandırmayı değiştirir. Devam edilsin mi?', + reloadInstallConfirm: 'Yeniden yüklenip kaydedilmemiş ayarlar değiştirilsin mi?', serviceOperateConfirm: 'Confirm {0} for the 1Panel MCP service?', serviceAdminOnly: 'Only super administrators can manage the MCP service.', dockerImage: 'Docker image', @@ -1973,9 +1972,9 @@ const message = { boundWebsiteHelp: 'Manage the bound website from the website list. Delete it to disable the proxy.', containerName: 'Container name', imageId: 'Image ID', - externalUrlHelp: 'Panel adresini kullanmak için boş bırakın. Yol /mcp olmalıdır.', - invalidUrl: - 'Kimlik bilgileri, sorgu parametreleri veya parça içermeyen, /mcp yoluna sahip bir HTTP/HTTPS adresi girin.', + externalUrlHelp: + 'Panel adresini kullanmak için boş bırakın. Kendi ters proxy sunucunuz için tam erişim adresini girin.', + invalidUrl: 'Kimlik bilgisi, sorgu parametresi veya parça içermeyen geçerli bir HTTP(S) adresi girin.', listenPort: 'Local listening port', listenPortInvalid: 'Enter an integer port from 1024 to 65535.', logEmpty: 'No service logs', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 92c716070..918072f6e 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1726,25 +1726,25 @@ const message = { websiteUnavailable: '綁定不可用', websiteDomainInvalid: '請輸入有效網域,可包含連接埠。', websiteCertificateRequired: '請選擇涵蓋此網域且有效的憑證。', - websiteCertificateEmpty: '暫無相符憑證,請在憑證管理中申請或匯入。', - websiteUnbind: '解除綁定', - websiteUnbindConfirm: '將移除 MCP 反向代理及綁定關係,保留網站與憑證。是否繼續?', - websiteUncertain: '網站操作結果尚未確認,請重新整理狀態;待確認操作可使用原請求重試。', + websiteCertificateEmpty: '無符合憑證,請先申請或匯入。', + websiteUnbind: '解除綁定並刪除網站', + websiteUnbindConfirm: '解除綁定並刪除網站及目錄?憑證保留,重新綁定將建立新網站。', + websiteUncertain: '結果待確認,請重新整理狀態後以原請求重試。', websiteRetry: '重試待確認操作', websiteSettings: '網站設定', websiteCertificateUpdate: '修改憑證', - websiteStatusLoadFailed: '無法讀取網站綁定狀態,請重新整理後重試。', - websiteOperationFailed: '網站綁定操作失敗,請核對狀態及設定。', - websiteConflict: '網站綁定設定已變更,請重新整理狀態後核對。', + websiteStatusLoadFailed: '無法讀取綁定狀態,請重新整理。', + websiteOperationFailed: '網站操作失敗,請核對狀態和設定。', + websiteConflict: '綁定設定已變更,請重新整理核對。', websiteBoundAddressHelp: '接入位址由綁定網站產生,請透過網站設定管理。', hideToken: '隱藏 Token', copyTokenFailed: '複製 Token 失敗,請重試。', exportFailed: '匯出連線說明失敗,請重試。', rotateTokenFailed: '重新產生 Token 失敗,請重試。', - rotateTokenUncertain: '無法確認重新產生的結果,請重新整理憑證並檢視目前的 Token 後再操作。', + rotateTokenUncertain: 'Token 更新結果待確認,請重新整理後查看目前 Token。', allowedIPs: '允許的 IP / CIDR', allowedIPsPlaceholder: '每行一個 IP 位址或 CIDR,最多 64 筆', - allowedIPsInvalid: '請輸入最多 64 筆有效 IP 位址或 CIDR,每筆不超過 64 個字元;服務限定來源時,清單不得為空。', + allowedIPsInvalid: '最多 64 筆有效 IP/CIDR,每筆不超過 64 字元;限制來源時不得留空。', accessPolicy: 'MCP 來源策略', accessInherit: '繼承面板存取策略', accessRestricted: '僅允許指定 IP / CIDR', @@ -1763,9 +1763,10 @@ const message = { alreadyCreated: '憑證已建立,可在連線詳細資訊中查看 Token。', nodes: '節點授權', primaryNode: '主節點', - targetNode: '目標節點 ID', + targetNode: '目標節點', + nodeNameUnavailable: '節點名稱無法取得', selectNodes: '請至少選擇一個節點。', - nodeGrantHelp: '呼叫範圍受節點、工具及帳號權限限制;新增節點須個別授權。', + nodeGrantHelp: '受節點、工具和帳號權限限制;新增節點需另行授權。', nodeUnavailable: '節點 {0} 無法使用,請移除後儲存。', allCategories: '全部分類', toolCategory: '工具分類', @@ -1782,7 +1783,7 @@ const message = { selectVisible: '授權符合的 {0} 項', clearVisible: '取消符合的 {0} 項授權', readOnlyVisible: '符合的 {0} 項僅保留唯讀授權', - bulkHelp: '批次操作影響所有分類的符合工具,包含未顯示項目,不受「僅看已選」影響;新增工具須個別授權。', + bulkHelp: '批次操作含所有分類的隱藏符合項,不受「僅看已選」限制;新增工具需另行授權。', groupSelected: '已授權 {0} / {1} 項', endpointUnavailable: '連線位址無法使用,請重新整理或聯絡管理員。', internalEndpoint: '內部監聽位址', @@ -1805,9 +1806,9 @@ const message = { readOnly: '唯讀', write: '寫入', writeHelp: '寫入工具可修改伺服器資源,請依需要授權。', - catalogUnavailable: '工具清單無法使用,請檢查服務狀態後重新整理。', + catalogUnavailable: '工具清單無法使用,請檢查服務後重新整理。', catalogEmpty: '暫無可授權工具。', - catalogChanged: '工具清單已更新,請重新開啟編輯視窗後繼續。', + catalogChanged: '工具清單已更新,請重新開啟編輯視窗。', grantsNeedReview: '部分工具授權須重新確認。', acceptToolPolicy: '我已核對並確認目前工具授權。', removeGrant: '移除授權', @@ -1815,12 +1816,12 @@ const message = { endpoint: '連線位址', clientConfig: '用戶端設定', connectionHelp: '將設定中的 替換為憑證 Token。', - uncertain: '建立結果尚未確認。請在目前視窗重試,或先檢查憑證清單,避免重複建立。', - revokeConfirm: '確認撤銷憑證「{0}」?此操作無法復原,新的呼叫將被拒絕。', - deleteConfirm: '確認刪除憑據「{0}」?刪除後憑據立即失效,歷史呼叫記錄將保留。', + uncertain: '建立結果待確認,請在原視窗重試,或先核對清單,避免重複建立。', + revokeConfirm: '撤銷存取憑據「{0}」?無法復原,新的呼叫將被拒絕。', + deleteConfirm: '刪除存取憑據「{0}」?立即失效,歷史呼叫紀錄保留。', disableConfirm: '確認停用憑證「{0}」?停用後將拒絕新的呼叫。', enableConfirm: '確認啟用憑證「{0}」並套用目前授權?', - conflict: '資料已更新,請重新載入最新內容後提交。', + conflict: '資料已更新,請重新載入後提交。', invocations: '呼叫紀錄', requestId: '請求 ID', duration: '耗時(毫秒)', @@ -1833,7 +1834,7 @@ const message = { serviceStatus: '服務狀態', statusUnknown: '未知', serviceUnsupported: '目前環境不支援部署 1Panel MCP。', - serviceUnhealthy: '服務健康檢查失敗,請檢查日誌及版本相容性。', + serviceUnhealthy: '服務檢查失敗,請核對日誌和版本相容性。', statusUnsupported: '環境不支援', statusNotInstalled: '未安裝', statusStopped: '已停止', @@ -1843,13 +1844,16 @@ const message = { ready: '服務就緒', installTitle: '安裝 1Panel MCP', installEnable: '安裝並啟用', - installEmpty: '1Panel MCP 服務尚未安裝,安裝並啟用後即可建立存取憑證並呼叫 MCP 工具。', + deleteOldImage: '刪除舊映像', + deleteOldImageHelp: '升級成功後清理未被占用的舊映像;清理失敗不影響升級,可查看工作日誌。', + installEmpty: '1Panel MCP 服務尚未安裝,安裝並啟用後即可建立存取憑據並呼叫 MCP 工具。', + installWithCredentials: 'MCP 未安裝,存取憑據已保留,暫時無法呼叫。', installTaskLog: '安裝日誌', installNow: '立即安裝', upgradeTitle: '升級服務', serviceConfiguration: '服務設定', imageUpgrade: '映像升級', - reloadInstallConfirm: '重新載入將覆寫尚未儲存的設定,是否繼續?', + reloadInstallConfirm: '重新載入會覆蓋未儲存的設定,繼續?', serviceOperateConfirm: '確認{0} 1Panel MCP 服務?', serviceAdminOnly: '僅超級管理員可管理 MCP 服務。', dockerImage: 'Docker 映像', @@ -1857,8 +1861,8 @@ const message = { boundWebsiteHelp: '在網站清單管理綁定的網站;刪除網站可停用代理。', containerName: '容器名稱', imageId: '映像 ID', - externalUrlHelp: '留空使用面板位址,路徑須為 /mcp。', - invalidUrl: '請輸入路徑為 /mcp 的 HTTP/HTTPS 位址,不含認證資訊、查詢參數或片段。', + externalUrlHelp: '留空使用面板位址;自行設定反向代理時,請填寫完整存取位址。', + invalidUrl: '請輸入有效的 HTTP(S) 位址,不含認證資訊、查詢參數或片段。', listenPort: '本機監聽連接埠', listenPortInvalid: '請輸入 1024~65535 的整數連接埠。', logEmpty: '暫無服務日誌', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index e78d664d1..15ab27bda 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1740,25 +1740,25 @@ const message = { websiteUnavailable: '绑定不可用', websiteDomainInvalid: '请输入有效域名,可包含端口。', websiteCertificateRequired: '请选择覆盖该域名且有效的证书。', - websiteCertificateEmpty: '暂无匹配证书,请在证书管理中申请或导入。', - websiteUnbind: '解除绑定', - websiteUnbindConfirm: '将移除 MCP 反向代理及绑定关系,网站和证书予以保留。是否继续?', - websiteUncertain: '网站操作结果尚未确认,请刷新状态;待确认操作可使用原请求重试。', + websiteCertificateEmpty: '暂无匹配证书,请先申请或导入。', + websiteUnbind: '解绑并删除网站', + websiteUnbindConfirm: '解绑并删除网站及目录?证书保留,重新绑定将新建网站。', + websiteUncertain: '结果待确认,请刷新状态后使用原请求重试。', websiteRetry: '重试待确认操作', websiteSettings: '网站设置', websiteCertificateUpdate: '修改证书', - websiteStatusLoadFailed: '无法读取网站绑定状态,请刷新后重试。', - websiteOperationFailed: '网站绑定操作失败,请核对状态及配置。', - websiteConflict: '网站绑定配置已变化,请刷新状态后核对。', + websiteStatusLoadFailed: '绑定状态读取失败,请刷新。', + websiteOperationFailed: '网站操作失败,请核对状态和配置。', + websiteConflict: '绑定配置已变化,请刷新核对。', websiteBoundAddressHelp: '接入地址由绑定网站生成,请通过网站设置管理。', hideToken: '隐藏 Token', copyTokenFailed: '复制 Token 失败,请重试。', exportFailed: '导出接入说明失败,请重试。', rotateTokenFailed: '重新生成 Token 失败,请重试。', - rotateTokenUncertain: '未能确认重新生成结果,请刷新凭据并查看当前 Token 后再操作。', + rotateTokenUncertain: 'Token 更新结果待确认,请刷新后查看当前 Token。', allowedIPs: '允许的 IP / CIDR', allowedIPsPlaceholder: '每行一个 IP 地址或 CIDR,最多 64 条', - allowedIPsInvalid: '请输入最多 64 条有效 IP 地址或 CIDR,每条不超过 64 个字符;服务限定来源时列表不能为空。', + allowedIPsInvalid: '最多 64 条有效 IP/CIDR,每条不超过 64 字符;限定来源时不能为空。', accessPolicy: 'MCP 来源策略', accessInherit: '继承面板访问策略', accessRestricted: '仅允许指定 IP / CIDR', @@ -1777,9 +1777,10 @@ const message = { alreadyCreated: '凭据已创建,可在连接详情中查看 Token。', nodes: '节点授权', primaryNode: '主节点', - targetNode: '目标节点 ID', + targetNode: '目标节点', + nodeNameUnavailable: '节点名称不可用', selectNodes: '请至少选择一个节点。', - nodeGrantHelp: '调用范围受节点、工具及账号权限限制;新增节点需单独授权。', + nodeGrantHelp: '受节点、工具和账号权限限制;新增节点需单独授权。', nodeUnavailable: '节点 {0} 不可用,请移除后保存。', allCategories: '全部分类', toolCategory: '工具分类', @@ -1796,7 +1797,7 @@ const message = { selectVisible: '授权匹配的 {0} 项', clearVisible: '取消匹配的 {0} 项授权', readOnlyVisible: '匹配的 {0} 项仅保留只读授权', - bulkHelp: '批量操作影响所有分类的匹配工具,包含未显示项,不受“仅看已选”影响;新增工具需单独授权。', + bulkHelp: '批量操作含所有分类的隐藏匹配项,不受“仅看已选”限制;新增工具需单独授权。', groupSelected: '已授权 {0} / {1} 项', endpointUnavailable: '接入地址不可用,请刷新或联系管理员。', internalEndpoint: '内部监听地址', @@ -1814,9 +1815,9 @@ const message = { readOnly: '只读', write: '写入', writeHelp: '写入工具可修改服务器资源,请按需授权。', - catalogUnavailable: '工具列表不可用,请检查服务状态后刷新。', + catalogUnavailable: '工具列表不可用,请检查服务后刷新。', catalogEmpty: '暂无可授权工具。', - catalogChanged: '工具列表已更新,请重新打开编辑窗口后继续。', + catalogChanged: '工具列表已更新,请重开编辑窗口。', grantsNeedReview: '部分工具授权需重新确认。', acceptToolPolicy: '我已核对并确认当前工具授权。', removeGrant: '移除授权', @@ -1824,12 +1825,12 @@ const message = { endpoint: '接入地址', clientConfig: '客户端配置', connectionHelp: '将配置中的 替换为凭据 Token。', - uncertain: '创建结果尚未确认。请在当前窗口重试,或先检查凭据列表,避免重复创建。', - revokeConfirm: '确认撤销凭据“{0}”?该操作不可恢复,新的调用将被拒绝。', - deleteConfirm: '确认删除凭据“{0}”?删除后凭据立即失效,历史调用记录将保留。', + uncertain: '创建结果待确认,请在原窗口重试,或先核对列表,避免重复创建。', + revokeConfirm: '撤销凭据“{0}”?不可恢复,新的调用将被拒绝。', + deleteConfirm: '删除凭据“{0}”?立即失效,历史调用记录保留。', disableConfirm: '确认停用凭据“{0}”?停用后将拒绝新的调用。', enableConfirm: '确认启用凭据“{0}”并应用当前授权?', - conflict: '数据已更新,请重新加载最新内容后提交。', + conflict: '数据已更新,请重新加载后提交。', invocations: '调用记录', requestId: '请求 ID', duration: '耗时(毫秒)', @@ -1842,7 +1843,7 @@ const message = { serviceStatus: '服务状态', statusUnknown: '未知', serviceUnsupported: '当前环境不支持部署 1Panel MCP。', - serviceUnhealthy: '服务健康检查失败,请检查日志及版本兼容性。', + serviceUnhealthy: '服务检查失败,请核对日志和版本兼容性。', statusUnsupported: '环境不支持', statusNotInstalled: '未安装', statusStopped: '已停止', @@ -1852,13 +1853,16 @@ const message = { ready: '服务就绪', installTitle: '安装 1Panel MCP', installEnable: '安装并启用', + deleteOldImage: '删除旧镜像', + deleteOldImageHelp: '升级成功后清理未被占用的旧镜像;清理失败不影响升级,可查看任务日志。', installEmpty: '1Panel MCP 服务尚未安装,安装并启用后即可创建访问凭据并调用 MCP 工具。', + installWithCredentials: 'MCP 未安装,凭据已保留,暂不可调用。', installTaskLog: '安装日志', installNow: '立即安装', upgradeTitle: '升级服务', serviceConfiguration: '服务配置', imageUpgrade: '镜像升级', - reloadInstallConfirm: '重新加载将覆盖未保存的配置,是否继续?', + reloadInstallConfirm: '重新加载会覆盖未保存的配置,继续?', serviceOperateConfirm: '确认{0} 1Panel MCP 服务?', serviceAdminOnly: '仅超级管理员可管理 MCP 服务。', dockerImage: 'Docker 镜像', @@ -1866,8 +1870,8 @@ const message = { boundWebsiteHelp: '在网站列表管理绑定的网站;删除网站可取消代理。', containerName: '容器名称', imageId: '镜像 ID', - externalUrlHelp: '留空使用面板地址,路径须为 /mcp。', - invalidUrl: '请输入路径为 /mcp 的 HTTP/HTTPS 地址,不含认证信息、查询参数或片段。', + externalUrlHelp: '留空使用面板地址;自建反代请填写完整访问地址。', + invalidUrl: '请输入有效的 HTTP(S) 地址,不含认证信息、查询参数或片段。', listenPort: '本地监听端口', listenPortInvalid: '请输入 1024~65535 的整数端口。', logEmpty: '暂无服务日志', diff --git a/frontend/src/utils/panel-mcp.ts b/frontend/src/utils/panel-mcp.ts index 38761fb0f..e68ccde5c 100644 --- a/frontend/src/utils/panel-mcp.ts +++ b/frontend/src/utils/panel-mcp.ts @@ -154,19 +154,20 @@ export const changeVisibleMCPGrants = ( return [...result]; }; -export const isMCPRevisionConflict = (error: unknown) => { - const candidate = error as { - code?: number; +export const getMCPConflict = (error: unknown) => { + type ConflictResponse = { errorCode?: string; - response?: { status?: number; data?: { code?: number; errorCode?: string } }; + reason?: string; + message?: string; }; - return ( - candidate?.code === 409 || - candidate?.errorCode === 'MCP_CONFLICT' || - candidate?.response?.data?.code === 409 || - candidate?.response?.data?.errorCode === 'MCP_CONFLICT' || - candidate?.response?.status === 409 - ); + const candidate = error as ConflictResponse & { response?: { data?: ConflictResponse } }; + const response = candidate?.response?.data ?? candidate; + return response?.errorCode === 'MCP_CONFLICT' ? response : undefined; +}; + +export const isMCPRevisionConflict = (error: unknown) => { + const reason = getMCPConflict(error)?.reason; + return reason === 'settings_revision_changed' || reason === 'credential_revision_changed'; }; export const isMCPPolicyChanged = (error: unknown) => { diff --git a/frontend/src/views/ai/mcp/panel/credential-editor.vue b/frontend/src/views/ai/mcp/panel/credential-editor.vue index 5e2bd1532..1963f010f 100644 --- a/frontend/src/views/ai/mcp/panel/credential-editor.vue +++ b/frontend/src/views/ai/mcp/panel/credential-editor.vue @@ -17,7 +17,7 @@ /> - {{ created.name }} + {{ created.name }} @@ -30,7 +30,7 @@
{{ clientConfig }}
@@ -90,7 +90,7 @@
{{ $t('panelMCP.nodeUnavailable', [id]) }} @@ -113,7 +113,9 @@
-
+
{{ $t('panelMCP.selectedTools', [form.tools.length, nodeTools.length]) }} · {{ $t('panelMCP.filteredTools', [filteredTools.length]) }} @@ -121,14 +123,9 @@
- + + {{ $t('panelMCP.selectedOnly') }} +
-

+

{{ $t('panelMCP.bulkHelp') }}

-
+
@@ -190,7 +189,7 @@