feat: add MCP error metadata handling and improve source policy resolution (#14270)

* feat: add MCP error metadata handling and improve source policy resolution

* fix: adjust height-diff for improved table layout
This commit is contained in:
蘭
2026-11-02 22:17:12 +08:00
committed by GitHub
parent 8e1b761f4c
commit e29679768b
68 changed files with 2613 additions and 997 deletions
+26 -4
View File
@@ -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)
}
}
}
+11 -10
View File
@@ -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"`
}
+14 -13
View File
@@ -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"`
}
+292 -21
View File
@@ -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 {
+21
View File
@@ -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
}
+11
View File
@@ -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)
+74
View File
@@ -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
}
+1
View File
@@ -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")
+13
View File
@@ -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)
})
}
+45 -15
View File
@@ -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":
+105
View File
@@ -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
}
+6 -6
View File
@@ -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) {
+25 -5
View File
@@ -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)
}
}
}
+6 -4
View File
@@ -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) {
+59 -3
View File
@@ -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)
}
+8 -7
View File
@@ -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 {
+10 -7
View File
@@ -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"`
}
+9 -6
View File
@@ -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
+2 -2
View File
@@ -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)
})
+3 -11
View File
@@ -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()
+20 -5
View File
@@ -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 != "" {
+63 -18
View File
@@ -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
}
+96 -44
View File
@@ -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
+46 -15
View File
@@ -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."
+45 -14
View File
@@ -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."
+45 -14
View File
@@ -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 ارتقا یافت، اما پاک‌سازی منابع قبلی کامل نشد. آن‌ها را دستی بررسی کنید."
+46 -15
View File
@@ -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 サービスは更新されましたが、旧リソースの削除は完了していません。手動で確認してください。"
+45 -14
View File
@@ -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 서비스는 업그레이드되었지만 이전 리소스 정리가 완료되지 않았습니다. 직접 확인하세요."
+47 -16
View File
@@ -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 ແລ້ວ ແຕ່ການລ້າງຊັບພະຍາກອນເກົ່າຍັງບໍ່ສຳເລັດ. ກະລຸນາກວດສອບເອງ."
+47 -16
View File
@@ -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."
+45 -14
View File
@@ -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."
+45 -14
View File
@@ -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 обновлена, но старые ресурсы очищены не полностью. Проверьте их вручную."
+46 -15
View File
@@ -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."
+46 -15
View File
@@ -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 服務已升級,舊資源清理未完成,請手動檢查。"
+45 -14
View File
@@ -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 服务已升级,旧资源清理未完成,请手动检查。"
+3 -2
View File
@@ -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())
+32 -37
View File
@@ -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 {
+15
View File
@@ -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")
}
})
}
+10
View File
@@ -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")
}
+57
View File
@@ -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
}
+9 -15
View File
@@ -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
+6
View File
@@ -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")
+54 -14
View File
@@ -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
}
+86
View File
@@ -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
}
}
+6
View File
@@ -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;
+1 -1
View File
@@ -15,7 +15,7 @@ export const bindPanelMCPWebsite = (params: PanelMCP.WebsiteBind) =>
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/bind`, params, { ...local, timeout: 120000 });
export const updatePanelMCPWebsite = (params: PanelMCP.WebsiteUpdate) =>
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/update`, params, { ...local, timeout: 120000 });
export const unbindPanelMCPWebsite = (params: PanelMCP.WebsiteMutation) =>
export const unbindPanelMCPWebsite = (params: PanelMCP.WebsiteUnbind) =>
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/unbind`, params, { ...local, timeout: 120000 });
export const savePanelMCPSettings = (params: PanelMCP.Settings) =>
http.postWithConfig(`${prefix}/settings`, params, { ...local, timeout: 60000 });
@@ -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;
+28 -29
View File
@@ -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 <MCP_TOKEN> 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',
+29 -30
View File
@@ -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 <MCP_TOKEN> 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',
+29 -27
View File
@@ -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: 'در پیکربندی، <MCP_TOKEN> را با 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',
+28 -28
View File
@@ -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: '設定の <MCP_TOKEN> を認証情報の 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',
+28 -26
View File
@@ -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: '구성의 <MCP_TOKEN>을 자격 증명 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',
+28 -25
View File
@@ -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: 'ແທນທີ່ <MCP_TOKEN> ໃນການຕັ້ງຄ່າດ້ວຍ 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',
+29 -26
View File
@@ -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 <MCP_TOKEN> 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',
+29 -30
View File
@@ -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 <MCP_TOKEN> 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',
+30 -30
View File
@@ -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: 'Замените <MCP_TOKEN> в конфигурации на 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',
+29 -30
View File
@@ -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 <MCP_TOKEN> 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',
+27 -23
View File
@@ -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: '將設定中的 <MCP_TOKEN> 替換為憑證 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: '暫無服務日誌',
+26 -22
View File
@@ -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: '将配置中的 <MCP_TOKEN> 替换为凭据 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: '暂无服务日志',
+12 -11
View File
@@ -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) => {
@@ -17,7 +17,7 @@
/>
<el-form label-position="top" class="min-w-0">
<el-form-item :label="$t('commons.table.name')">
<span class="break-words">{{ created.name }}</span>
<span class="break-words text-[var(--el-text-color-regular)]">{{ created.name }}</span>
</el-form-item>
<el-form-item v-if="secret" label="Token">
<el-input :model-value="secret" readonly autocomplete="off" class="[&_input]:!font-mono">
@@ -30,7 +30,7 @@
<CopyButton :content="clientConfig" :is-icon="false" />
</div>
<pre
class="m-0 max-h-80 overflow-auto whitespace-pre-wrap break-all rounded border border-solid border-[var(--el-border-color-light)] bg-[var(--el-fill-color-light)] p-3 font-mono text-xs leading-5"
class="mcp-client-config m-0 max-h-80 overflow-auto whitespace-pre-wrap break-all rounded border border-solid border-[var(--el-border-color-light)] bg-[var(--el-fill-color-light)] p-3 font-mono text-xs leading-5"
>{{ clientConfig }}</pre>
</div>
</el-form-item>
@@ -90,7 +90,7 @@
<div
v-for="id in unavailableNodes"
:key="id"
class="mt-2 flex w-full min-w-0 flex-wrap items-center gap-2 rounded border border-solid border-[var(--el-color-warning-light-5)] bg-[var(--el-color-warning-light-9)] p-3"
class="mcp-unavailable-grant mt-2 flex w-full min-w-0 flex-wrap items-center gap-2 rounded border border-solid border-[var(--el-color-warning-light-5)] bg-[var(--el-color-warning-light-9)] p-3"
>
<span class="min-w-0 flex-1 break-all">{{ $t('panelMCP.nodeUnavailable', [id]) }}</span>
<el-button type="danger" link @click="form.nodeIds = form.nodeIds.filter((node) => node !== id)">
@@ -113,7 +113,9 @@
<el-option :label="$t('panelMCP.write')" value="write" />
</el-select>
</div>
<div class="mb-2 w-full min-w-0 text-xs leading-5 text-[var(--el-text-color-secondary)]">
<div
class="mcp-tool-summary mb-2 w-full min-w-0 text-xs leading-5 text-[var(--el-text-color-secondary)]"
>
<span>{{ $t('panelMCP.selectedTools', [form.tools.length, nodeTools.length]) }}</span>
<span>· {{ $t('panelMCP.filteredTools', [filteredTools.length]) }}</span>
<span v-if="hiddenSelectedCount" class="block">
@@ -121,14 +123,9 @@
</span>
</div>
<div class="mcp-grant-toolbar mb-2 flex w-full min-w-0 flex-wrap items-center justify-between gap-2">
<label class="inline-flex min-h-8 cursor-pointer items-center gap-2 text-sm leading-5">
<input
v-model="selectedOnly"
type="checkbox"
class="mcp-selected-only h-3.5 w-3.5 shrink-0 accent-[var(--el-color-primary)]"
/>
<span>{{ $t('panelMCP.selectedOnly') }}</span>
</label>
<el-checkbox v-model="selectedOnly" class="mcp-selected-only !mr-0">
{{ $t('panelMCP.selectedOnly') }}
</el-checkbox>
<el-dropdown
trigger="click"
placement="bottom-end"
@@ -166,13 +163,15 @@
</template>
</el-dropdown>
</div>
<p class="mb-3 mt-0 w-full text-xs leading-5 text-[var(--el-text-color-secondary)]">
<p class="mcp-tool-help mb-3 mt-0 w-full text-xs leading-5 text-[var(--el-text-color-secondary)]">
{{ $t('panelMCP.bulkHelp') }}
</p>
<div
class="mcp-tool-picker grid w-full min-w-0 overflow-hidden rounded border border-solid border-[var(--el-border-color-light)] md:grid-cols-[10rem_minmax(0,1fr)]"
>
<div class="border-0 border-b border-solid border-[var(--el-border-color-light)] p-3 md:hidden">
<div
class="mcp-tool-category-filter border-0 border-b border-solid border-[var(--el-border-color-light)] p-3 md:hidden"
>
<label for="mcp-tool-category" class="mb-1 block text-xs text-[var(--el-text-color-secondary)]">
{{ $t('panelMCP.toolCategory') }}
</label>
@@ -190,7 +189,7 @@
</div>
<nav
:aria-label="$t('panelMCP.toolCategory')"
class="hidden min-w-0 flex-col gap-1 border-0 border-r border-solid border-[var(--el-border-color-light)] bg-[var(--el-fill-color-lighter)] p-2 md:flex"
class="mcp-tool-sidebar hidden min-w-0 flex-col gap-1 border-0 border-r border-solid border-[var(--el-border-color-light)] bg-[var(--el-fill-color-lighter)] p-2 md:flex"
>
<button
type="button"
@@ -244,7 +243,7 @@
<el-checkbox-group v-model="form.tools" class="w-full min-w-0">
<section v-for="group in toolGroups" :key="group.id" class="min-w-0">
<div
class="flex flex-wrap items-center justify-between gap-2 bg-[var(--el-fill-color-lighter)] px-3 py-2 text-xs leading-5 text-[var(--el-text-color-secondary)]"
class="mcp-tool-group-header flex flex-wrap items-center justify-between gap-2 bg-[var(--el-fill-color-lighter)] px-3 py-2 text-xs leading-5 text-[var(--el-text-color-secondary)]"
>
<span class="min-w-0 break-words font-medium">
{{ categoryLabel(group.id, group.name) }}
@@ -287,14 +286,14 @@
</div>
<p
v-if="tool.description && !expandedToolIds.includes(tool.toolId)"
class="mb-0 mt-1 truncate pl-6 text-xs leading-5 text-[var(--el-text-color-secondary)]"
class="mcp-tool-description mb-0 mt-1 truncate pl-6 text-xs leading-5 text-[var(--el-text-color-secondary)]"
>
{{ tool.description }}
</p>
<div
v-if="expandedToolIds.includes(tool.toolId)"
:id="`mcp-tool-detail-${tool.toolId}`"
class="mt-2 space-y-1 pl-6 text-xs leading-5 text-[var(--el-text-color-secondary)]"
class="mcp-tool-description mt-2 space-y-1 pl-6 text-xs leading-5 text-[var(--el-text-color-secondary)]"
>
<div v-if="tool.name !== tool.toolId" class="break-all font-mono">
{{ tool.toolId }}
@@ -319,7 +318,7 @@
<div
v-for="id in unavailableTools"
:key="id"
class="mt-2 flex w-full min-w-0 flex-wrap items-center gap-2 rounded border border-solid border-[var(--el-color-warning-light-5)] bg-[var(--el-color-warning-light-9)] p-3"
class="mcp-unavailable-grant mt-2 flex w-full min-w-0 flex-wrap items-center gap-2 rounded border border-solid border-[var(--el-color-warning-light-5)] bg-[var(--el-color-warning-light-9)] p-3"
>
<span class="min-w-0 flex-1 break-all font-mono">{{ id }}</span>
<el-button type="danger" link @click="removeUnavailableTool(id)">
@@ -355,7 +354,10 @@
</el-form-item>
</el-form>
<template #footer>
<p v-if="!created" class="mb-3 mt-0 text-xs leading-5 text-[var(--el-text-color-secondary)]">
<p
v-if="!created"
class="mcp-selection-summary mb-3 mt-0 text-xs leading-5 text-[var(--el-text-color-secondary)]"
>
{{
$t('panelMCP.selectionSummary', [
selectedToolSummary.read,
@@ -672,7 +674,7 @@ const submit = async () => {
emit('changed');
} catch (error) {
if (ticket !== version) return;
if (isMCPRevisionConflict(error)) {
if (isMCPRevisionConflict(error) || isMCPPolicyChanged(error)) {
conflict.value = true;
policyConflict.value = isMCPPolicyChanged(error);
emit('changed');
@@ -710,9 +712,73 @@ defineExpose({ open });
</script>
<style scoped lang="scss">
.input-help {
color: var(--el-text-color-regular);
}
.mcp-tool-picker :deep(.el-checkbox__label) {
min-width: 0;
flex: 1;
line-height: 1.5;
}
html.dark {
.mcp-grant-toolbar,
.mcp-tool-summary,
.mcp-tool-help,
.mcp-tool-description,
.mcp-selection-summary {
color: var(--el-text-color-regular);
}
#mcp-tool-category {
color-scheme: dark;
}
.mcp-tool-picker,
.mcp-client-config {
color: var(--el-text-color-regular);
border-color: var(--el-border-color);
}
.mcp-tool-picker {
background-color: var(--el-fill-color-blank);
}
.mcp-tool-sidebar,
.mcp-tool-category-filter {
border-color: var(--el-border-color);
background-color: var(--el-bg-color);
}
#mcp-tool-category {
border-color: var(--el-border-color);
}
.mcp-category-button {
&[aria-pressed='true'] {
color: var(--el-text-color-primary);
background-color: var(--el-fill-color-dark);
box-shadow: inset 3px 0 0 var(--el-color-primary);
}
&[aria-pressed='false']:hover {
background-color: var(--el-fill-color-dark);
}
}
.mcp-tool-group-header {
color: var(--el-text-color-regular);
background-color: var(--el-bg-color);
}
.mcp-tool-row {
border-color: var(--el-border-color);
}
.mcp-unavailable-grant {
color: var(--el-text-color-regular);
border-color: var(--el-color-warning);
}
}
</style>
+226 -159
View File
@@ -29,162 +29,167 @@
</template>
<template v-if="!showInstallEmpty" #rightToolBar><TableRefresh @search="refresh" /></template>
<template v-if="!showInstallEmpty" #main>
<el-card
v-if="serviceChecked && !serviceReady"
:class="credentialTableMasked ? 'mask-prompt' : '!mb-4'"
role="status"
>
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-2">
<template v-if="dockerStopped">
<span>{{ $t('container.serviceUnavailable') }}</span>
<div class="relative min-w-0">
<el-card
v-if="serviceChecked && !serviceReady"
:class="credentialTableMasked ? 'mask-prompt mcp-service-prompt' : '!mb-4'"
role="status"
>
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-2">
<template v-if="dockerStopped">
<span>{{ $t('container.serviceUnavailable') }}</span>
<el-button
type="primary"
link
:disabled="!canManageService || service?.busy"
@click="goDocker('ContainerSetting')"
>
【 {{ $t('container.setting') }} 】
</el-button>
<span>{{ $t('container.startIn') }}</span>
</template>
<span v-else>{{ servicePrompt }}</span>
<el-button
v-if="canInstallService || (dockerMissing && canManageService)"
type="primary"
link
:disabled="!canManageService || service?.busy"
@click="goDocker('ContainerSetting')"
icon="Download"
:disabled="service?.busy"
@click="openInstallation"
>
【 {{ $t('container.setting') }} 】
{{ $t('panelMCP.installNow') }}
</el-button>
<span>{{ $t('container.startIn') }}</span>
</template>
<span v-else>{{ servicePrompt }}</span>
<el-button
v-if="canInstallService || (dockerMissing && canManageService)"
type="primary"
link
icon="Download"
:disabled="service?.busy"
@click="openInstallation"
<el-button v-else-if="serviceStatus" type="primary" link @click="service?.openStatus()">
{{ $t('commons.table.status') }}
</el-button>
<el-button type="primary" link :disabled="service?.busy" @click="service?.refreshService()">
{{ $t('commons.button.refresh') }}
</el-button>
</div>
<p v-if="serviceStatus && !serviceStatus.canManageService" class="input-help">
{{ $t('panelMCP.serviceAdminOnly') }}
</p>
</el-card>
<div :class="{ mask: credentialTableMasked }" :inert="credentialTableMasked">
<el-alert
v-if="serviceReady && !catalogAvailable && !catalogLoading"
:title="$t('panelMCP.catalogUnavailable')"
type="warning"
:closable="false"
class="!mb-4"
>
{{ $t('panelMCP.installNow') }}
</el-button>
<el-button v-else-if="serviceStatus" type="primary" link @click="service?.openStatus()">
{{ $t('commons.table.status') }}
</el-button>
<el-button type="primary" link :disabled="service?.busy" @click="service?.refreshService()">
{{ $t('commons.button.refresh') }}
</el-button>
</div>
<p v-if="serviceStatus && !serviceStatus.canManageService" class="input-help">
{{ $t('panelMCP.serviceAdminOnly') }}
</p>
</el-card>
<div :class="{ mask: credentialTableMasked }" :inert="credentialTableMasked">
<el-alert
v-if="serviceReady && !catalogAvailable && !catalogLoading"
:title="$t('panelMCP.catalogUnavailable')"
type="warning"
:closable="false"
class="!mb-4"
>
<el-button link type="primary" @click="loadCatalog">
{{ $t('commons.button.refresh') }}
</el-button>
</el-alert>
<el-alert
v-else-if="serviceReady && catalogAvailable && !tools.length"
:title="$t('panelMCP.catalogEmpty')"
type="info"
:closable="false"
class="!mb-4"
/>
<div class="w-full min-w-0 overflow-x-auto">
<ComplexTable
:data="displayItems"
:pagination-config="pagination"
:page-sizes="[10, 20, 50, 100]"
:scrollbar-always-on="isCompactTable"
:height-diff="410"
@search="search"
>
<el-table-column
:label="$t('commons.table.name')"
prop="name"
:width="isCompactTable ? 160 : 220"
<el-button link type="primary" @click="loadCatalog">
{{ $t('commons.button.refresh') }}
</el-button>
</el-alert>
<el-alert
v-else-if="serviceReady && catalogAvailable && !tools.length"
:title="$t('panelMCP.catalogEmpty')"
type="info"
:closable="false"
class="!mb-4"
/>
<div class="w-full min-w-0 overflow-x-auto">
<ComplexTable
:data="displayItems"
:pagination-config="pagination"
:page-sizes="[10, 20, 50, 100]"
:scrollbar-always-on="isCompactTable"
:height-diff="310"
@search="search"
>
<template #default="{ row }">
<el-button
type="primary"
link
class="min-w-0 max-w-full !whitespace-normal !text-left break-all"
@click="showConnection(row)"
>
{{ row.name }}
</el-button>
<div class="break-all text-xs text-[var(--el-text-color-secondary)]">
{{ row.keyHint }}
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" width="120">
<template #default="{ row }">
<el-tag
size="small"
round
effect="light"
:type="
row.status === 'Enable'
? 'success'
: row.status === 'Disable'
? 'warning'
: 'info'
"
>
{{ $t('apiKeyManagement.status' + row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.tools')" :min-width="isCompactTable ? 280 : 360">
<template #default="{ row }">
<div class="flex min-w-0 max-w-full max-h-28 flex-wrap gap-1 overflow-y-auto">
<el-tag
v-for="tool in row.tools"
:key="tool"
size="small"
class="!h-auto min-h-5 max-w-full !whitespace-normal break-all"
:type="isReadOnly(tool) ? 'info' : 'warning'"
<el-table-column
:label="$t('commons.table.name')"
prop="name"
:width="isCompactTable ? 160 : 220"
>
<template #default="{ row }">
<el-button
type="primary"
link
class="min-w-0 max-w-full !whitespace-normal !text-left break-all"
@click="showConnection(row)"
>
{{ toolLabel(tool) }}
</el-tag>
<span v-if="!row.tools.length">{{ $t('panelMCP.noTools') }}</span>
</div>
<div v-if="needsReview(row)" class="mt-1 text-xs text-[var(--el-color-warning)]">
{{ $t('panelMCP.grantsNeedReview') }}
</div>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.nodes')" width="160">
<template #default="{ row }">
<div class="flex min-w-0 max-w-full max-h-28 flex-wrap gap-1 overflow-y-auto">
{{ row.name }}
</el-button>
<div class="break-all text-xs text-[var(--el-text-color-secondary)]">
{{ row.keyHint }}
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" width="120">
<template #default="{ row }">
<el-tag
v-for="id in row.nodeIds"
:key="id"
size="small"
class="!h-auto min-h-5 max-w-full !whitespace-normal break-all"
round
effect="light"
:type="
row.status === 'Enable'
? 'success'
: row.status === 'Disable'
? 'warning'
: 'info'
"
>
{{ nodeLabel(id) }}
{{ $t('apiKeyManagement.status' + row.status) }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('apiKeyManagement.expiresAt')" width="180">
<template #default="{ row }">
{{
row.expiresAt
? dateFormat(null, null, row.expiresAt)
: $t('apiKeyManagement.never')
}}
</template>
</el-table-column>
<fu-table-operations
:buttons="buttons"
:ellipsis="isCompactTable ? 0 : 2"
:width="isCompactTable ? 100 : undefined"
:min-width="isCompactTable ? 'auto' : 230"
:label="$t('commons.table.operate')"
:fixed="isCompactTable ? false : 'right'"
/>
</ComplexTable>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.tools')" :min-width="isCompactTable ? 280 : 360">
<template #default="{ row }">
<div class="flex min-w-0 max-w-full max-h-28 flex-wrap gap-1 overflow-y-auto">
<el-tag
v-for="tool in row.tools"
:key="tool"
size="small"
class="!h-auto min-h-5 max-w-full !whitespace-normal break-all"
:type="isReadOnly(tool) ? 'info' : 'warning'"
>
{{ toolLabel(tool) }}
</el-tag>
<span v-if="!row.tools.length">{{ $t('panelMCP.noTools') }}</span>
</div>
<div
v-if="needsReview(row)"
class="mt-1 text-xs text-[var(--el-color-warning)]"
>
{{ $t('panelMCP.grantsNeedReview') }}
</div>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.nodes')" width="160">
<template #default="{ row }">
<div class="flex min-w-0 max-w-full max-h-28 flex-wrap gap-1 overflow-y-auto">
<el-tag
v-for="id in row.nodeIds"
:key="id"
size="small"
class="!h-auto min-h-5 max-w-full !whitespace-normal break-all"
>
{{ nodeLabel(id) }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('apiKeyManagement.expiresAt')" width="180">
<template #default="{ row }">
{{
row.expiresAt
? dateFormat(null, null, row.expiresAt)
: $t('apiKeyManagement.never')
}}
</template>
</el-table-column>
<fu-table-operations
:buttons="buttons"
:ellipsis="isCompactTable ? 0 : 2"
:width="isCompactTable ? 100 : undefined"
:min-width="isCompactTable ? 'auto' : 230"
:label="$t('commons.table.operate')"
:fixed="isCompactTable ? false : 'right'"
/>
</ComplexTable>
</div>
</div>
</div>
</template>
@@ -204,6 +209,7 @@
</NoSuchService>
</LayoutContent>
<CredentialEditor
v-if="!credentialsUnavailable"
ref="editor"
:tools="tools"
:tool-node-ids="toolNodeIds"
@@ -356,13 +362,16 @@
</span>
</el-form-item>
</el-form>
<div v-if="clientConfig" class="mt-5 flex flex-wrap items-center justify-between gap-2">
<div
v-if="clientConfig"
class="mcp-client-config-title mt-5 flex flex-wrap items-center justify-between gap-2"
>
<span>{{ $t('panelMCP.clientConfig') }}</span>
<CopyButton :content="clientConfig" :is-icon="false" />
</div>
<pre
v-if="clientConfig"
class="mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-all rounded border border-solid border-[var(--el-border-color)] bg-[var(--el-fill-color-light)] p-3 font-mono text-xs leading-5"
class="mcp-client-config mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-all rounded border border-solid border-[var(--el-border-color)] bg-[var(--el-fill-color-light)] p-3 font-mono text-xs leading-5"
>{{ clientConfig }}</pre>
<template #footer>
<div class="flex flex-wrap items-center justify-end gap-2 [&>.el-button]:!ml-0">
@@ -461,6 +470,7 @@ const serviceReady = computed(
serviceStatus.value.status === 'running' &&
serviceStatus.value.ready,
);
const credentialsUnavailable = computed(() => serviceChecked.value && !serviceStatus.value?.installed);
const canInstallService = computed(
() =>
serviceStatus.value?.status === 'not_installed' &&
@@ -475,7 +485,7 @@ const servicePrompt = computed(() => {
if (dockerMissing.value) return i18n.global.t('cronjob.library.noSuchApp', ['Docker']);
if (dockerStopped.value) return i18n.global.t('container.serviceUnavailable');
if (current.status === 'not_installed' && !current.installed) {
return i18n.global.t('panelMCP.installEmpty');
return i18n.global.t(hasStoredCredentials.value ? 'panelMCP.installWithCredentials' : 'panelMCP.installEmpty');
}
if (current.status === 'unsupported') return i18n.global.t('panelMCP.serviceUnsupported');
if (current.installed && (current.status === 'stopped' || !current.enabled)) {
@@ -507,10 +517,14 @@ const canManageCredential = computed(() => hasManagePermissionAccess('mcp_access
const deletingCredentialID = ref('');
const canReveal = computed(
() =>
canManageCredential.value && detailStatus.value === 'Enable' && detail.value?.id !== deletingCredentialID.value,
!credentialsUnavailable.value &&
canManageCredential.value &&
detailStatus.value === 'Enable' &&
detail.value?.id !== deletingCredentialID.value,
);
const canRotate = computed(
() =>
!credentialsUnavailable.value &&
canManageCredential.value &&
!!detail.value &&
detail.value.id !== deletingCredentialID.value &&
@@ -530,6 +544,7 @@ const editor = ref<InstanceType<typeof CredentialEditor>>();
const invocations = ref<InstanceType<typeof Invocations>>();
const now = ref(Date.now());
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0, cacheSizeKey: 'panel-mcp-credentials-page' });
const hasStoredCredentials = computed(() => credentialsLoaded.value && pagination.total > 0);
const showInstallEmpty = computed(
() =>
serviceChecked.value &&
@@ -542,7 +557,7 @@ const showInstallEmpty = computed(
);
const pageLoading = computed(() => !serviceChecked.value || (loading.value && !showInstallEmpty.value));
const credentialTableMasked = computed(
() => !serviceReady.value && (!credentialsLoaded.value || pagination.total === 0),
() => credentialsUnavailable.value || (!serviceReady.value && !hasStoredCredentials.value),
);
const clientConfig = computed(() =>
!connectionVisible.value || (detail.value && !connectionToken.value)
@@ -656,6 +671,7 @@ const refresh = () => {
if (serviceReady.value) void loadCatalog();
};
const showConnection = (entry?: PanelMCP.Credential) => {
if (credentialsUnavailable.value) return;
clearConnectionSecret();
detail.value = entry;
selectedClient.value = 'generic';
@@ -663,6 +679,7 @@ const showConnection = (entry?: PanelMCP.Credential) => {
};
const currentConnection = (ticket: number, reference: PanelMCP.Reference) =>
ticket === connectionVersion &&
!credentialsUnavailable.value &&
connectionVisible.value &&
globalStore.isLogin &&
canManageCredential.value &&
@@ -806,8 +823,9 @@ const exportConnection = async () => {
}
};
const operate = async (entry: PanelMCP.Credential, revoke = false) => {
if (mutating.value) return;
if (credentialsUnavailable.value || mutating.value) return;
const version = pageVersion;
const accessVersion = credentialAccessVersion;
const currentStatus = getMCPCredentialStatus(entry);
if (!revoke && currentStatus !== 'Enable' && currentStatus !== 'Disable') {
await search();
@@ -823,7 +841,13 @@ const operate = async (entry: PanelMCP.Credential, revoke = false) => {
} catch {
return;
}
if (!isCurrentPage(version) || mutating.value) return;
if (
!isCurrentPage(version) ||
accessVersion !== credentialAccessVersion ||
credentialsUnavailable.value ||
mutating.value
)
return;
mutating.value = true;
try {
const reference = { id: entry.id, revision: entry.revision };
@@ -841,7 +865,8 @@ const operate = async (entry: PanelMCP.Credential, revoke = false) => {
}
};
const deleteCredential = async (entry: PanelMCP.Credential) => {
if (!active || !globalStore.isLogin || !canManageCredential.value || mutating.value) return;
if (credentialsUnavailable.value || !active || !globalStore.isLogin || !canManageCredential.value || mutating.value)
return;
const version = pageVersion;
const accessVersion = credentialAccessVersion;
const reference = { id: entry.id, revision: entry.revision };
@@ -860,6 +885,7 @@ const deleteCredential = async (entry: PanelMCP.Credential) => {
if (
!isCurrentPage(version) ||
accessVersion !== credentialAccessVersion ||
credentialsUnavailable.value ||
!globalStore.isLogin ||
!canManageCredential.value
)
@@ -892,40 +918,48 @@ const deleteCredential = async (entry: PanelMCP.Credential) => {
}
};
const buttons = computed<FuTableOperationButton<PanelMCP.Credential>[]>(() => [
{ label: i18n.global.t('panelMCP.connection'), click: showConnection },
{
label: i18n.global.t('panelMCP.connection'),
disabled: () => credentialsUnavailable.value,
click: showConnection,
},
{
label: i18n.global.t('commons.button.edit'),
permission: 'mcp_access_manage',
show: (row) => row.status !== 'Revoked',
disabled: () => mutating.value || !catalogAvailable.value,
click: (row) => editor.value?.open(row),
disabled: () => credentialsUnavailable.value || mutating.value || !catalogAvailable.value,
click: (row) => {
if (!credentialsUnavailable.value) editor.value?.open(row);
},
},
{ label: i18n.global.t('panelMCP.invocations'), click: (row) => invocations.value?.open(row) },
{
label: i18n.global.t('commons.button.disable'),
permission: 'mcp_access_manage',
show: (row) => row.status === 'Enable',
disabled: () => mutating.value,
disabled: () => credentialsUnavailable.value || mutating.value,
click: (row) => operate(row),
},
{
label: i18n.global.t('commons.button.enable'),
permission: 'mcp_access_manage',
show: (row) => row.status === 'Disable',
disabled: (row) => mutating.value || !catalogAvailable.value || needsReview(row),
disabled: (row) =>
credentialsUnavailable.value || mutating.value || !catalogAvailable.value || needsReview(row),
click: (row) => operate(row),
},
{
label: i18n.global.t('apiKeyManagement.revoke'),
permission: 'mcp_access_manage',
show: (row) => row.status !== 'Revoked',
disabled: () => mutating.value,
disabled: () => credentialsUnavailable.value || mutating.value,
click: (row) => operate(row, true),
},
{
label: i18n.global.t('commons.button.delete'),
permission: 'mcp_access_manage',
disabled: () => mutating.value || !globalStore.isLogin || !canManageCredential.value,
disabled: () =>
credentialsUnavailable.value || mutating.value || !globalStore.isLogin || !canManageCredential.value,
click: deleteCredential,
},
]);
@@ -984,6 +1018,18 @@ watch(
{ flush: 'sync' },
);
watch(canManageService, () => navigationVersion++, { flush: 'sync' });
watch(
credentialsUnavailable,
(value) => {
if (value) {
credentialAccessVersion++;
clearConnectionSecret();
connectionVisible.value = false;
detail.value = undefined;
}
},
{ flush: 'sync' },
);
watch(
detailStatus,
(value) => {
@@ -1003,6 +1049,16 @@ onBeforeUnmount(deactivate);
</script>
<style scoped lang="scss">
.input-help {
color: var(--el-text-color-regular);
}
.mcp-service-prompt {
top: 50%;
left: 50%;
width: max-content;
}
.connection-details {
:deep(.el-descriptions__table) {
table-layout: fixed;
@@ -1015,4 +1071,15 @@ onBeforeUnmount(deactivate);
overflow-wrap: anywhere;
}
}
html.dark {
.mcp-client-config-title,
.mcp-client-config {
color: var(--el-text-color-regular);
}
.mcp-client-config {
border-color: var(--el-border-color);
}
}
</style>
+43 -20
View File
@@ -17,13 +17,13 @@
<template #prefix>{{ $t('commons.table.status') }}</template>
<el-option v-for="value in results" :key="value" :value="value" :label="resultLabel(value)" />
</el-select>
<el-button class="ml-auto" icon="Refresh" :loading="loading" @click="search">
<el-button class="ml-auto" icon="Refresh" :loading="loading" @click="refresh">
{{ $t('commons.button.refresh') }}
</el-button>
</div>
<div class="w-full min-w-0 overflow-x-auto" v-loading="loading">
<ComplexTable
:data="items"
:data="displayItems"
:pagination-config="pagination"
:page-sizes="[10, 20, 50, 100]"
:height="500"
@@ -38,16 +38,21 @@
<el-table-column
:label="$t('panelMCP.credentials')"
prop="credentialName"
min-width="140"
min-width="120"
show-overflow-tooltip
/>
<el-table-column :label="$t('panelMCP.tools')" prop="toolId" min-width="170" show-overflow-tooltip>
<el-table-column :label="$t('panelMCP.tools')" prop="toolId" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span class="font-mono text-xs">{{ row.toolId }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.targetNode')" prop="nodeId" min-width="120" />
<el-table-column :label="$t('commons.table.status')" min-width="95">
<el-table-column
:label="$t('panelMCP.targetNode')"
prop="nodeName"
min-width="160"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.table.status')" min-width="90">
<template #default="{ row }">
<el-tag
size="small"
@@ -71,17 +76,7 @@
<span class="font-mono text-xs">{{ row.code }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.duration')" prop="durationMs" min-width="120" align="right" />
<el-table-column
:label="$t('panelMCP.requestId')"
prop="requestId"
min-width="220"
show-overflow-tooltip
>
<template #default="{ row }">
<span class="font-mono text-xs">{{ row.requestId }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('panelMCP.duration')" prop="durationMs" min-width="110" align="right" />
</ComplexTable>
</div>
<template #footer>
@@ -93,20 +88,41 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, onDeactivated, reactive, ref } from 'vue';
import { computed, onBeforeUnmount, onDeactivated, reactive, ref } from 'vue';
import type { PanelMCP } from '@/api/interface/panel-mcp';
import { searchPanelMCPInvocations } from '@/api/modules/panel-mcp';
import { getPanelMCPNodes, searchPanelMCPInvocations } from '@/api/modules/panel-mcp';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { dateFormat } from '@/utils/date';
import { isUsableMCPNodes } from '@/utils/panel-mcp';
import i18n from '@/lang';
const { globalStore } = useGlobalStore();
const visible = ref(false);
const loading = ref(false);
const credential = ref<PanelMCP.Credential>();
const items = ref<PanelMCP.Invocation[]>([]);
const nodes = ref<PanelMCP.Node[]>([]);
const nodeLabel = (id: string) =>
id === 'local'
? globalStore.getMasterAlias()
: nodes.value.find((node) => node.id === id)?.name || i18n.global.t('panelMCP.nodeNameUnavailable');
const displayItems = computed(() => items.value.map((item) => ({ ...item, nodeName: nodeLabel(item.nodeId) })));
const result = ref('');
const results = ['running', 'success', 'denied', 'failed'];
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0, cacheSizeKey: 'panel-mcp-invocations-page' });
let version = 0;
let nodesVersion = 0;
const loadNodes = async () => {
if (!visible.value) return;
const ticket = ++nodesVersion;
try {
const response = await getPanelMCPNodes();
if (ticket !== nodesVersion || !visible.value) return;
nodes.value = isUsableMCPNodes(response.data) ? response.data : [];
} catch {
if (ticket === nodesVersion && visible.value) nodes.value = [];
}
};
const resultLabel = (value: string) =>
i18n.global.t(
`panelMCP.${{ running: 'resultRunning', success: 'resultSuccess', denied: 'resultDenied', failed: 'resultFailed' }[value] || 'resultFailed'}`,
@@ -138,20 +154,27 @@ const resetSearch = () => {
pagination.currentPage = 1;
void search();
};
const refresh = () => {
void loadNodes();
void search();
};
const open = (item?: PanelMCP.Credential) => {
version++;
credential.value = item;
result.value = '';
items.value = [];
nodes.value = [];
pagination.currentPage = 1;
visible.value = true;
void search();
refresh();
};
const close = () => {
version++;
nodesVersion++;
visible.value = false;
loading.value = false;
items.value = [];
nodes.value = [];
};
onDeactivated(close);
onBeforeUnmount(close);
+76 -12
View File
@@ -184,7 +184,7 @@
}}
</span>
</el-form-item>
<el-collapse v-model="advancedSettings" class="mt-4">
<el-collapse v-model="advancedSettings" class="mcp-advanced-settings mt-4">
<el-collapse-item :title="$t('app.advanced')" name="source">
<el-form-item :label="$t('panelMCP.accessPolicy')" prop="accessPolicy">
<el-select v-model="settings.accessPolicy" class="w-full">
@@ -249,6 +249,12 @@
:maxlength="512"
/>
</el-form-item>
<el-form-item v-if="installIsUpgrade" prop="deleteOldImage">
<el-checkbox v-model="installation.deleteOldImage">
{{ $t('panelMCP.deleteOldImage') }}
</el-checkbox>
<span class="input-help mt-1 leading-5">{{ $t('panelMCP.deleteOldImageHelp') }}</span>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
@@ -298,7 +304,7 @@
/>
<span class="input-help mt-2 leading-5">{{ $t('panelMCP.externalUrlHelp') }}</span>
</el-form-item>
<el-collapse v-if="!installIsUpgrade" v-model="advancedInstallation" class="mt-4">
<el-collapse v-if="!installIsUpgrade" v-model="advancedInstallation" class="mcp-advanced-settings mt-4">
<el-collapse-item :title="$t('app.advanced')" name="website">
<el-form-item :label="$t('panelMCP.accessPolicy')" prop="accessPolicy">
<el-select v-model="installation.accessPolicy" class="w-full">
@@ -492,6 +498,7 @@ const settingsRef = ref<FormInstance>();
const installRef = ref<FormInstance>();
const installation = reactive<PanelMCP.Installation>({
dockerImage: DEFAULT_MCP_DOCKER_IMAGE,
deleteOldImage: false,
listenPort: 18081,
externalUrl: '',
revision: 0,
@@ -519,11 +526,14 @@ const installationDirty = computed(() => {
if (!snapshot) return false;
return (
installation.dockerImage !== snapshot.dockerImage ||
(installIsUpgrade.value && installation.deleteOldImage) ||
(!installIsUpgrade.value && hasMCPSettingsChanges({ ...snapshot, ...installation }, snapshot))
);
});
const allowedIPsText = ref('');
const installAllowedIPsText = ref('');
let settingsDraftVersion = 0;
watch([settings, allowedIPsText], () => settingsDraftVersion++, { deep: true, flush: 'sync' });
watch(
allowedIPsText,
(value) => {
@@ -602,11 +612,17 @@ const settingsRules = {
if (!value) return done();
try {
const url = new URL(value);
const authority = /^https?:\/\/([^/]+)/i.exec(value)?.[1];
if (
authority &&
!/[%@]/.test(authority) &&
['http:', 'https:'].includes(url.protocol) &&
!/^[\s\u0085]|[\s\u0085]$/u.test(value) &&
new TextEncoder().encode(value).length <= 2048 &&
!/[\r\n]/.test(value) &&
url.pathname === '/mcp' &&
Array.from(value).every((char) => char.charCodeAt(0) > 0x20 && char.charCodeAt(0) !== 0x7f) &&
!/[\\?#]/.test(value) &&
!/%(?![0-9a-f]{2})/i.test(value) &&
!/%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value) &&
!url.username &&
!url.password &&
!url.search &&
@@ -648,6 +664,7 @@ const onInstallationValidate = async (prop: string | string[], valid: boolean) =
let active = false;
let pageVersion = 0;
let requestVersion = 0;
let settingsReadVersion = 0;
let logVersion = 0;
let timer: ReturnType<typeof setInterval> | undefined;
let logTimer: ReturnType<typeof setInterval> | undefined;
@@ -709,8 +726,10 @@ const closeWhenIdle = (done: () => void) => {
};
const cancelSettings = () => {
if (busy.value) return;
if (savedSettings.value) Object.assign(settings, savedSettings.value);
settingsReadVersion++;
if (savedSettings.value) Object.assign(settings, copyMCPSettings(savedSettings.value));
allowedIPsText.value = settings.allowedIPs.join('\n');
conflict.value = false;
settingsVisible.value = false;
};
const closeSettingsWhenIdle = (done: () => void) => {
@@ -740,14 +759,23 @@ const changeSettingsTab = async (name: string | number) => {
return false;
}
};
const reloadSettings = async (version = pageVersion) => {
const reloadSettings = async (version = pageVersion, savedDraftVersion?: number) => {
if (!isCurrentPage(version) || !canEditService.value) return false;
const ticket = ++settingsReadVersion;
const draftVersion = savedDraftVersion ?? settingsDraftVersion;
const preserveDraft = savedDraftVersion === undefined && settingsDirty.value;
const response = await getPanelMCPSettings();
if (!isCurrentPage(version)) return;
if (!isCurrentPage(version) || !canEditService.value || ticket !== settingsReadVersion) return false;
const loaded = copyMCPSettings(response.data);
savedSettings.value = { ...loaded };
if (preserveDraft || settingsDraftVersion !== draftVersion) {
if (savedSettings.value && loaded.revision !== savedSettings.value.revision) conflict.value = true;
return false;
}
savedSettings.value = copyMCPSettings(loaded);
Object.assign(settings, loaded);
allowedIPsText.value = settings.allowedIPs.join('\n');
conflict.value = false;
return true;
};
const copyInstallation = (source: PanelMCP.Settings, upgrading: boolean) => {
installSnapshot.value = copyMCPSettings(source);
@@ -755,6 +783,7 @@ const copyInstallation = (source: PanelMCP.Settings, upgrading: boolean) => {
const { dockerImage, listenPort, externalUrl, revision, accessPolicy } = source;
Object.assign(installation, {
dockerImage,
deleteOldImage: false,
listenPort,
externalUrl,
revision,
@@ -843,6 +872,7 @@ const websiteChanged = async (value: PanelMCP.WebsiteBinding) => {
};
const saveSettings = async () => {
if (busy.value || conflict.value || !canEditService.value || !savedSettings.value) return;
settingsReadVersion++;
const version = pageVersion;
busy.value = true;
try {
@@ -858,6 +888,7 @@ const saveSettings = async () => {
const { runtimeType, dockerImage, enabled, revision } = savedSettings.value;
const { listenPort, externalUrl, accessPolicy } = settings;
const allowedIPs = accessPolicy === 'restricted' ? [...settings.allowedIPs] : [];
const savedDraftVersion = settingsDraftVersion;
await savePanelMCPSettings({
runtimeType,
dockerImage,
@@ -869,8 +900,8 @@ const saveSettings = async () => {
allowedIPs,
});
if (!isCurrentPage(version)) return;
await reloadSettings(version);
if (!isCurrentPage(version)) return;
const synchronized = await reloadSettings(version, savedDraftVersion);
if (!isCurrentPage(version) || !synchronized) return;
emit('changed');
settingsVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@@ -916,7 +947,7 @@ const install = async () => {
if (!(await websiteRef.value?.validate())) return;
}
if (!isCurrentPage(version) || !canEditService.value) return;
const { dockerImage } = installation;
const { dockerImage, deleteOldImage } = installation;
const source = installIsUpgrade.value ? installSnapshot.value : installation;
const { listenPort, externalUrl, revision, accessPolicy } = source;
const allowedIPs = accessPolicy === 'restricted' ? [...source.allowedIPs] : [];
@@ -937,6 +968,7 @@ const install = async () => {
: undefined;
const response = await installPanelMCPService({
dockerImage,
deleteOldImage: installIsUpgrade.value && deleteOldImage,
listenPort,
externalUrl,
revision,
@@ -1030,6 +1062,7 @@ const activate = () => {
};
const clearManagementState = () => {
pageVersion++;
settingsReadVersion++;
clearLogState();
installTaskLog.value?.handleClose();
settingsVisible.value = installVisible.value = logsVisible.value = false;
@@ -1045,7 +1078,15 @@ const clearManagementState = () => {
allowedIPsText.value = '';
installAllowedIPsText.value = '';
const { dockerImage, listenPort, externalUrl, revision, accessPolicy } = settings;
Object.assign(installation, { dockerImage, listenPort, externalUrl, revision, accessPolicy, allowedIPs: [] });
Object.assign(installation, {
dockerImage,
deleteOldImage: false,
listenPort,
externalUrl,
revision,
accessPolicy,
allowedIPs: [],
});
};
watch(
logsVisible,
@@ -1096,6 +1137,10 @@ defineExpose({ openInstall, openStatus, openInstallLogs, refreshService, operate
</script>
<style scoped lang="scss">
.input-help {
color: var(--el-text-color-regular);
}
.mcp-details {
:deep(.el-descriptions__table) {
table-layout: fixed;
@@ -1107,4 +1152,23 @@ defineExpose({ openInstall, openStatus, openInstallLogs, refreshService, operate
overflow-wrap: anywhere;
}
}
html.dark .mcp-advanced-settings {
--el-collapse-content-bg-color: transparent;
--el-collapse-border-color: var(--el-border-color);
:deep(.el-collapse-item__content) {
padding: 16px 12px 20px;
color: var(--el-text-color-regular);
}
:deep(.mcp-website-binding) {
margin-top: 4px;
padding-top: 20px;
}
:deep(.mcp-website-details .el-descriptions__label) {
width: 104px;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<section class="min-w-0 border-t border-[var(--el-border-color-lighter)] pt-4">
<section class="mcp-website-binding min-w-0 border-t border-[var(--el-border-color-lighter)] pt-4">
<div class="mb-4 flex min-w-0 flex-wrap items-center justify-between gap-2">
<span class="font-medium">{{ $t('panelMCP.websiteBinding') }}</span>
<el-tag
@@ -147,7 +147,7 @@
{{ $t('commons.button.cancel') }}
</el-button>
</template>
<el-button :loading="reading" :disabled="disabled || !canManage || busy" @click="load">
<el-button :loading="reading" :disabled="disabled || !canManage || busy" @click="refresh">
{{ $t('commons.button.refresh') }}
</el-button>
</div>
@@ -167,6 +167,7 @@ import {
} from '@/api/modules/panel-mcp';
import { listLocalNodeSSL } from '@/api/modules/website';
import {
getMCPConflict,
getMCPWebsiteHostname,
isMCPMutationUncertain,
isMCPRevisionConflict,
@@ -238,9 +239,9 @@ let certificateVersion = 0;
let certificatePromise: Promise<void> | undefined;
const current = (version: number) => active && props.canManage && globalStore.isLogin && version === generation;
const acceptBinding = (value: PanelMCP.WebsiteBinding) => {
const acceptBinding = (value: PanelMCP.WebsiteBinding, syncDraft = true) => {
binding.value = value;
if (value.pendingRequest) {
if (syncDraft && value.pendingRequest) {
const request = value.pendingRequest;
if (request.domain !== undefined) props.draft.domain = request.domain;
if (request.enableSSL !== undefined) props.draft.enableSSL = request.enableSSL;
@@ -264,37 +265,44 @@ const validBinding = (value: unknown): value is PanelMCP.WebsiteBinding => {
Number.isSafeInteger(item.sslID) &&
item.sslID >= 0 &&
typeof item.requestId === 'string' &&
(item.websiteDeleted === undefined || typeof item.websiteDeleted === 'boolean') &&
(item.state !== 'pending' || !!item.pendingRequest) &&
(!item.pendingRequest ||
(['bind', 'update', 'unbind'].includes(item.pendingRequest.operation) &&
typeof item.pendingRequest.requestId === 'string' &&
!!item.pendingRequest.requestId))
!!item.pendingRequest.requestId &&
(item.pendingRequest.deleteWebsite === undefined ||
typeof item.pendingRequest.deleteWebsite === 'boolean')))
);
};
const read = async (version: number) => {
const read = async (version: number, syncDraft = true) => {
const ticket = ++readVersion;
const response = await getPanelMCPWebsite();
if (!current(version) || ticket !== readVersion) return;
if (!validBinding(response.data)) throw new Error('Invalid MCP website status');
acceptBinding(response.data);
acceptBinding(response.data, syncDraft);
return response.data;
};
const load = async () => {
const load = async (refreshCertificates = false) => {
if (!active || !props.canManage || busy.value || props.disabled) return;
const version = generation;
reading.value = true;
try {
await read(version);
if (current(version)) errorKey.value = '';
} catch {
if (current(version)) errorKey.value = 'panelMCP.websiteStatusLoadFailed';
const requests: Promise<unknown>[] = [read(version, !refreshCertificates)];
if (refreshCertificates) requests.push(loadCertificates(true));
const [status] = await Promise.allSettled(requests);
if (current(version)) {
errorKey.value = status.status === 'fulfilled' ? '' : 'panelMCP.websiteStatusLoadFailed';
}
} finally {
if (current(version)) reading.value = false;
}
};
const loadCertificates = async () => {
if (!active || !props.canManage || certificatesLoaded.value) return;
if (certificatePromise) return certificatePromise;
const refresh = () => load(true);
const loadCertificates = async (force = false) => {
if (!active || !props.canManage) return;
if (certificatePromise && !force) return certificatePromise;
if (certificatesLoaded.value && !force) return;
const version = generation;
const ticket = ++certificateVersion;
certificatesBusy.value = true;
@@ -309,8 +317,11 @@ const loadCertificates = async () => {
expireDate: item.expireDate,
}));
certificatesLoaded.value = true;
certificateError.value = '';
} catch {
if (current(version)) certificateError.value = i18n.global.t('panelMCP.websiteCertificateRequired');
if (current(version) && ticket === certificateVersion) {
certificateError.value = i18n.global.t('panelMCP.websiteCertificateRequired');
}
} finally {
if (ticket === certificateVersion) certificatesBusy.value = false;
}
@@ -343,6 +354,7 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
emit('busy', true);
errorKey.value = '';
localPending.value = { ...operation };
const deletesWebsite = operation.operation === 'unbind' && operation.deleteWebsite === true;
try {
const latest = binding.value || (await read(version));
if (!current(version) || !latest) return false;
@@ -361,9 +373,13 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
enableSSL: operation.enableSSL === true,
sslID: operation.enableSSL ? operation.sslID || 0 : 0,
})
: await unbindPanelMCPWebsite(request);
: await unbindPanelMCPWebsite({
...request,
...(deletesWebsite ? { deleteWebsite: true } : {}),
});
if (!current(version)) return false;
if (!validBinding(response.data)) throw { code: 503 };
if (deletesWebsite && response.data.websiteDeleted !== true) throw { code: 503 };
acceptBinding(response.data);
if (response.data.state !== (operation.operation === 'unbind' ? 'unbound' : 'bound')) throw { code: 503 };
localPending.value = undefined;
@@ -381,6 +397,11 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
: uncertain.value
? 'panelMCP.websiteUncertain'
: 'panelMCP.websiteOperationFailed';
const conflict = getMCPConflict(error);
if (conflict?.reason === 'website_domain_conflict' && typeof conflict.message === 'string') {
domainError.value = conflict.message;
errorKey.value = '';
}
if (!uncertain.value) localPending.value = undefined;
try {
const verified = await read(version);
@@ -389,6 +410,7 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
verified &&
verified.requestId === operation.requestId &&
!verified.pendingRequest &&
(!deletesWebsite || verified.websiteDeleted === true) &&
verified.state === (operation.operation === 'unbind' ? 'unbound' : 'bound')
) {
localPending.value = undefined;
@@ -448,12 +470,13 @@ const requestUnbind = async (cancelPending: boolean) => {
await ElMessageBox.confirm(
i18n.global.t('panelMCP.websiteUnbindConfirm'),
i18n.global.t('panelMCP.websiteUnbind'),
{ type: 'warning', confirmButtonType: 'danger' },
);
} catch {
return;
}
if (!current(version) || busy.value || props.disabled) return;
await perform({ operation: 'unbind', requestId: uuidv4() });
await perform({ operation: 'unbind', requestId: uuidv4(), deleteWebsite: true });
};
const unbind = () => requestUnbind(false);
const cancelPending = () => requestUnbind(true);
@@ -533,6 +556,14 @@ defineExpose({ validate, load, busy, binding });
</script>
<style scoped>
.input-help {
color: var(--el-text-color-regular);
}
html.dark .mcp-website-binding {
border-color: var(--el-border-color-lighter);
}
.mcp-website-details :deep(.el-descriptions__table) {
table-layout: fixed;
}
@@ -227,6 +227,10 @@ defineExpose({
</script>
<style lang="scss" scoped>
.input-help {
color: var(--el-text-color-regular);
}
.pageRoute {
font-size: 12px;
margin-left: 5px;
@@ -1,6 +1,8 @@
<template>
<DrawerPro v-model="open" :header="$t('menu.config')" @close="handleClose" size="normal">
<div class="w-full min-w-0 overflow-hidden rounded border border-solid border-[var(--el-border-color)]">
<div
class="mcp-server-config w-full min-w-0 overflow-hidden rounded border border-solid border-[var(--el-border-color)]"
>
<codemirror
:autofocus="true"
:placeholder="$t('commons.msg.noneData')"
@@ -9,7 +11,6 @@
style="height: 300px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="prettyJson"
@@ -27,12 +28,19 @@
<script lang="ts" setup>
import { AI } from '@/api/interface/ai';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { usePreferredDark } from '@vueuse/core';
const extensions = [javascript(), oneDark];
const { themeConfig } = useGlobalStore();
const prefersDark = usePreferredDark();
const extensions = computed(() => {
const dark = themeConfig.value.theme === 'dark' || (themeConfig.value.theme === 'auto' && prefersDark.value);
return [javascript(), ...(dark ? [oneDark] : [])];
});
const open = ref(false);
const jsonObj = ref({
@@ -57,3 +65,9 @@ defineExpose({
acceptParams,
});
</script>
<style scoped>
html.dark .mcp-server-config {
border-color: var(--el-border-color);
}
</style>
@@ -344,3 +344,9 @@ defineExpose({
acceptParams,
});
</script>
<style scoped>
.input-help {
color: var(--el-text-color-regular);
}
</style>