mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-03 00:04:53 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 服务已升级,旧资源清理未完成,请手动检查。"
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '暫無服務日誌',
|
||||
|
||||
@@ -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: '暂无服务日志',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user