feat: merge mcp server from dev (#8499)

This commit is contained in:
ChengPlay
2025-04-28 07:21:47 +00:00
committed by GitHub
parent 8338582051
commit e5bd5d17ba
51 changed files with 2378 additions and 107 deletions
+2 -1
View File
@@ -17,7 +17,8 @@ var (
appInstallService = service.NewIAppInstalledService()
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
aiToolService = service.NewIAIToolService()
aiToolService = service.NewIAIToolService()
mcpServerService = service.NewIMcpServerService()
containerService = service.NewIContainerService()
composeTemplateService = service.NewIComposeTemplateService()
+166
View File
@@ -0,0 +1,166 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/gin-gonic/gin"
)
// @Tags McpServer
// @Summary List mcp servers
// @Accept json
// @Param request body request.McpServerSearch true "request"
// @Success 200 {object} response.McpServersRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/search [post]
func (b *BaseApi) PageMcpServers(c *gin.Context) {
var req request.McpServerSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list := mcpServerService.Page(req)
helper.SuccessWithData(c, list)
}
// @Tags McpServer
// @Summary Create mcp server
// @Accept json
// @Param request body request.McpServerCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server [post]
func (b *BaseApi) CreateMcpServer(c *gin.Context) {
var req request.McpServerCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Update mcp server
// @Accept json
// @Param request body request.McpServerUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/update [post]
func (b *BaseApi) UpdateMcpServer(c *gin.Context) {
var req request.McpServerUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Update(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Delete mcp server
// @Accept json
// @Param request body request.McpServerDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/del [post]
func (b *BaseApi) DeleteMcpServer(c *gin.Context) {
var req request.McpServerDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Delete(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Operate mcp server
// @Accept json
// @Param request body request.McpServerOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/op [post]
func (b *BaseApi) OperateMcpServer(c *gin.Context) {
var req request.McpServerOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Operate(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomain true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/bind [post]
func (b *BaseApi) BindMcpDomain(c *gin.Context) {
var req request.McpBindDomain
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.BindDomain(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Update bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomainUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/update [post]
func (b *BaseApi) UpdateMcpBindDomain(c *gin.Context) {
var req request.McpBindDomainUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.UpdateBindDomain(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags McpServer
// @Summary Get bin Domain for mcp server
// @Accept json
// @Success 200 {object} response.McpBindDomainRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/get [get]
func (b *BaseApi) GetMcpBindDomain(c *gin.Context) {
res, err := mcpServerService.GetBindDomain()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
+47
View File
@@ -0,0 +1,47 @@
package request
import "github.com/1Panel-dev/1Panel/agent/app/dto"
type McpServerSearch struct {
dto.PageInfo
Name string `json:"name"`
Sync bool `json:"sync"`
}
type McpServerCreate struct {
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"`
}
type McpServerUpdate struct {
ID uint `json:"id" validate:"required"`
McpServerCreate
}
type McpServerDelete struct {
ID uint `json:"id" validate:"required"`
}
type McpServerOperate struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
}
type McpBindDomain struct {
Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
type McpBindDomainUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
+5
View File
@@ -284,3 +284,8 @@ type ChangeDatabase struct {
DatabaseID uint `json:"databaseID" validate:"required"`
DatabaseType string `json:"databaseType" validate:"required"`
}
type WebsiteProxyDel struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
}
+26
View File
@@ -0,0 +1,26 @@
package response
import (
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type McpServersRes struct {
Items []McpServerDTO `json:"items"`
Total int64 `json:"total"`
}
type McpServerDTO struct {
model.McpServer
Environments []request.Environment `json:"environments"`
Volumes []request.Volume `json:"volumes"`
}
type McpBindDomainRes struct {
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
AcmeAccountID uint `json:"acmeAccountID"`
AllowIPs []string `json:"allowIPs"`
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
}
+18
View File
@@ -0,0 +1,18 @@
package model
type McpServer struct {
BaseModel
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"`
Dir string `json:"dir"`
HostIP string `json:"hostIP"`
}
+7
View File
@@ -15,6 +15,7 @@ type IGroupRepo interface {
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
WithByDefault(isDefault bool) DBOption
WithByWebsiteDefault() DBOption
}
func NewIGroupRepo() IGroupRepo {
@@ -62,3 +63,9 @@ func (g *GroupRepo) Delete(opts ...DBOption) error {
}
return db.Delete(&model.Group{}).Error
}
func (g *GroupRepo) WithByWebsiteDefault() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("is_default = ? AND type = ?", 1, "website")
}
}
+56
View File
@@ -0,0 +1,56 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
type McpServerRepo struct {
}
type IMcpServerRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error)
GetFirst(opts ...DBOption) (*model.McpServer, error)
Create(mcpServer *model.McpServer) error
Save(mcpServer *model.McpServer) error
DeleteBy(opts ...DBOption) error
List(opts ...DBOption) ([]model.McpServer, error)
}
func NewIMcpServerRepo() IMcpServerRepo {
return &McpServerRepo{}
}
func (m McpServerRepo) Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) {
var servers []model.McpServer
db := getDb(opts...).Model(&model.McpServer{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
return count, servers, err
}
func (m McpServerRepo) GetFirst(opts ...DBOption) (*model.McpServer, error) {
var mcpServer model.McpServer
if err := getDb(opts...).First(&mcpServer).Error; err != nil {
return nil, err
}
return &mcpServer, nil
}
func (m McpServerRepo) List(opts ...DBOption) ([]model.McpServer, error) {
var mcpServers []model.McpServer
if err := getDb(opts...).Find(&mcpServers).Error; err != nil {
return nil, err
}
return mcpServers, nil
}
func (m McpServerRepo) Create(mcpServer *model.McpServer) error {
return getDb().Create(mcpServer).Error
}
func (m McpServerRepo) Save(mcpServer *model.McpServer) error {
return getDb().Save(mcpServer).Error
}
func (m McpServerRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.McpServer{}).Error
}
+3
View File
@@ -198,6 +198,9 @@ func (a AppService) GetApp(ctx *gin.Context, key string) (*response.AppDTO, erro
latestVersion = detail.Version
continue
}
if key == "openresty" && !common.CompareAppVersion(detail.Version, "1.27") {
continue
}
versionsRaw = append(versionsRaw, detail.Version)
}
appDTO.Versions = common.GetSortedVersions(versionsRaw)
+2 -1
View File
@@ -12,7 +12,8 @@ var (
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
aiRepo = repo.NewIAiRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
+641
View File
@@ -0,0 +1,641 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/cmd/server/mcp"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"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/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
"path"
"strconv"
"strings"
)
type McpServerService struct{}
type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes
Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error
Delete(id uint) error
Operate(req request.McpServerOperate) error
GetBindDomain() (response.McpBindDomainRes, error)
BindDomain(req request.McpBindDomain) error
UpdateBindDomain(req request.McpBindDomainUpdate) error
}
func NewIMcpServerService() IMcpServerService {
return &McpServerService{}
}
func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes {
var (
res response.McpServersRes
items []response.McpServerDTO
)
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data {
_ = syncMcpServerContainerStatus(&item)
serverDTO := response.McpServerDTO{
McpServer: item,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
project, _ := docker.GetComposeProject(item.Name, path.Join(global.Dir.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
})
}
}
}
items = append(items, serverDTO)
}
res.Total = total
res.Items = items
return res
}
func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if mcpServer.Port != req.Port {
if err := checkPortExist(req.Port); err != nil {
return err
}
}
if mcpServer.ContainerName != req.ContainerName {
if err := checkContainerName(req.ContainerName); err != nil {
return err
}
}
mcpServer.Name = req.Name
mcpServer.ContainerName = req.ContainerName
mcpServer.Port = req.Port
mcpServer.Command = req.Command
mcpServer.BaseURL = req.BaseURL
mcpServer.SsePath = req.SsePath
mcpServer.HostIP = req.HostIP
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
mcpDir := path.Join(global.Dir.McpDir, mcpServer.Name)
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
mcpServer.Status = constant.StatusStarting
if err := mcpServerRepo.Save(mcpServer); err != nil {
return err
}
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Create(create request.McpServerCreate) error {
servers, _ := mcpServerRepo.List()
for _, server := range servers {
if server.Port == create.Port {
return buserr.New("ErrPortInUsed")
}
if server.ContainerName == create.ContainerName {
return buserr.New("ErrContainerName")
}
if server.Name == create.Name {
return buserr.New("ErrNameIsExist")
}
if server.SsePath == create.SsePath {
return buserr.New("ErrSsePath")
}
}
if err := checkPortExist(create.Port); err != nil {
return err
}
if err := checkContainerName(create.ContainerName); err != nil {
return err
}
mcpDir := path.Join(global.Dir.McpDir, create.Name)
mcpServer := &model.McpServer{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.StatusStarting,
BaseURL: create.BaseURL,
SsePath: create.SsePath,
Dir: mcpDir,
HostIP: create.HostIP,
}
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
filesOP := files.NewFileOp()
if !filesOP.Stat(mcpDir) {
_ = filesOP.CreateDir(mcpDir, 0644)
}
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
if err := mcpServerRepo.Create(mcpServer); err != nil {
return err
}
addProxy(mcpServer)
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Delete(id uint) error {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(id))
if err != nil {
return err
}
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.McpDir, mcpServer.Name))
websiteID := GetWebsiteID()
if websiteID > 0 {
websiteService := NewIWebsiteService()
delProxyReq := request.WebsiteProxyDel{
ID: websiteID,
Name: mcpServer.Name,
}
_ = websiteService.DeleteProxy(delProxyReq)
}
return mcpServerRepo.DeleteBy(repo.WithByID(id))
}
func (m McpServerService) Operate(req request.McpServerOperate) error {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
var out string
switch req.Operate {
case "start":
out, err = compose.Up(composePath)
mcpServer.Status = constant.StatusRunning
case "stop":
out, err = compose.Down(composePath)
mcpServer.Status = constant.StatusStopped
case "restart":
out, err = compose.Restart(composePath)
mcpServer.Status = constant.StatusRunning
}
if err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = out
}
return mcpServerRepo.Save(mcpServer)
}
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
var res response.McpBindDomainRes
websiteID := GetWebsiteID()
if websiteID == 0 {
return res, nil
}
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
if err != nil {
return res, nil
}
res.WebsiteID = website.ID
res.Domain = website.PrimaryDomain
if website.WebsiteSSLID > 0 {
res.SSLID = website.WebsiteSSLID
ssl, _ := websiteSSLRepo.GetFirst(repo.WithByID(website.WebsiteSSLID))
res.AcmeAccountID = ssl.AcmeAccountID
}
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
res.AllowIPs = GetAllowIps(website)
return res, nil
}
func (m McpServerService) BindDomain(req request.McpBindDomain) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
createWebsiteReq := request.WebsiteCreate{
Domains: []request.WebsiteDomain{
{
Domain: req.Domain,
Port: 80,
},
},
Alias: strings.ToLower(req.Domain),
Type: constant.Static,
WebsiteGroupID: group.ID,
}
if req.SSLID > 0 {
createWebsiteReq.WebsiteSSLID = req.SSLID
createWebsiteReq.EnableSSL = true
}
websiteService := NewIWebsiteService()
if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
return err
}
website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
if err != nil {
return err
}
_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
if len(ipList) > 0 {
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
}
if err = addMCPProxy(website.ID); err != nil {
return err
}
return nil
}
func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
websiteService := NewIWebsiteService()
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
if req.SSLID > 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
Type: "existed",
WebsiteSSLID: req.SSLID,
HttpConfig: "HTTPSOnly",
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
if website.WebsiteSSLID > 0 && req.SSLID == 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: false,
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
go updateMcpConfig(website.ID)
return nil
}
func updateMcpConfig(websiteID uint) {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return
}
website, _ := websiteRepo.GetFirst(repo.WithByID(websiteID))
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
go func() {
for _, server := range servers {
if server.BaseURL != baseUrl {
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
_ = updateMcpServer(&server)
}
}
}()
}
func addProxy(server *model.McpServer) {
websiteID := GetWebsiteID()
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return
}
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
return
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err))
return
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
global.LOG.Errorf("update nginx config failed, err: %v", err)
return
}
}
func addMCPProxy(websiteID uint) error {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return nil
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
if err != nil {
return err
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return err
}
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
}
for _, server := range servers {
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
err = errors.New("error")
return err
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
go updateMcpServer(&server)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return err
}
return nil
}
func updateMcpServer(mcpServer *model.McpServer) error {
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
_ = mcpServerRepo.Save(mcpServer)
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = err.Error()
}
return mcpServerRepo.Save(mcpServer)
}
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env := make(gotenv.Env)
env["CONTAINER_NAME"] = mcpServer.ContainerName
env["COMMAND"] = mcpServer.Command
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
env["BASE_URL"] = mcpServer.BaseURL
env["SSE_PATH"] = mcpServer.SsePath
env["HOST_IP"] = mcpServer.HostIP
envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr
return env
}
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
var composeContent []byte
if mcpServer.ID == 0 {
composeContent = mcp.DefaultMcpCompose
} else {
composeContent = []byte(mcpServer.DockerCompose)
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
return err
}
services, serviceValid := composeMap["services"].(map[string]interface{})
if !serviceValid {
return buserr.New("ErrFileParse")
}
serviceName := ""
serviceValue := make(map[string]interface{})
if mcpServer.ID > 0 {
serviceName = mcpServer.Name
serviceValue = services[serviceName].(map[string]interface{})
} else {
for name, service := range services {
serviceName = name
serviceValue = service.(map[string]interface{})
break
}
delete(services, serviceName)
}
delete(serviceValue, "environment")
if len(environments) > 0 {
envMap := make(map[string]string)
for _, env := range environments {
envMap[env.Key] = env.Value
}
serviceValue["environment"] = envMap
}
delete(serviceValue, "volumes")
if len(volumes) > 0 {
volumeList := make([]string, 0)
for _, volume := range volumes {
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
serviceValue["volumes"] = volumeList
}
services[mcpServer.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
mcpServer.DockerCompose = string(composeByte)
return nil
}
func startMcp(mcpServer *model.McpServer) {
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.StatusNormal {
_, _ = compose.Down(composePath)
}
if out, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = out
} else {
mcpServer.Status = constant.StatusRunning
mcpServer.Message = ""
}
_ = syncMcpServerContainerStatus(mcpServer)
}
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
containerNames := []string{mcpServer.ContainerName}
cli, err := docker.NewClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
if len(containers) == 0 {
mcpServer.Status = constant.StatusStopped
return mcpServerRepo.Save(mcpServer)
}
container := containers[0]
switch container.State {
case "exited":
mcpServer.Status = constant.StatusError
case "running":
mcpServer.Status = constant.StatusRunning
case "paused":
mcpServer.Status = constant.StatusStopped
case "restarting":
mcpServer.Status = constant.StatusRestarting
default:
if mcpServer.Status != constant.StatusBuilding {
mcpServer.Status = constant.StatusStopped
}
}
return mcpServerRepo.Save(mcpServer)
}
func GetWebsiteID() uint {
websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
if websiteID.Value == "" {
return 0
}
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint)
}
+24
View File
@@ -93,6 +93,7 @@ type IWebsiteService interface {
UpdateProxyCache(req request.NginxProxyCacheUpdate) (err error)
GetProxyCache(id uint) (res response.NginxProxyCache, err error)
ClearProxyCache(req request.NginxCommonReq) error
DeleteProxy(req request.WebsiteProxyDel) (err error)
GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error)
UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error)
@@ -1856,6 +1857,29 @@ func (w WebsiteService) ClearProxyCache(req request.NginxCommonReq) error {
return nil
}
func (w WebsiteService) DeleteProxy(req request.WebsiteProxyDel) (err error) {
fileOp := files.NewFileOp()
website, err := websiteRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
_ = fileOp.CreateDir(includeDir, 0755)
}
fileName := fmt.Sprintf("%s.conf", req.Name)
includePath := path.Join(includeDir, fileName)
backName := fmt.Sprintf("%s.bak", req.Name)
backPath := path.Join(includeDir, backName)
_ = fileOp.DeleteFile(includePath)
_ = fileOp.DeleteFile(backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}
func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) {
var (
website model.Website
+19
View File
@@ -0,0 +1,19 @@
services:
mcp-server:
image: supercorp/supergateway:latest
container_name: ${CONTAINER_NAME}
restart: unless-stopped
ports:
- "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}"
command: [
"--stdio", "${COMMAND}",
"--port", "${PANEL_APP_PORT_HTTP}",
"--baseUrl", "${BASE_URL}",
"--ssePath", "${SSE_PATH}",
"--messagePath", "${SSE_PATH}/messages"
]
networks:
- 1panel-network
networks:
1panel-network:
external: true
+8
View File
@@ -0,0 +1,8 @@
package mcp
import (
_ "embed"
)
//go:embed compose.yml
var DefaultMcpCompose []byte
@@ -46,3 +46,6 @@ var Upstream []byte
//go:embed php_extensions.json
var PHPExtensionsJson []byte
//go:embed sse.conf
var SSE []byte
+7
View File
@@ -0,0 +1,7 @@
location ^~ /github {
proxy_pass http://127.0.0.1:8001/github;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection '';
chunked_transfer_encoding off;
}
+1
View File
@@ -41,6 +41,7 @@ type SystemDir struct {
RuntimeDir string
RecycleBinDir string
SSLLogDir string
McpDir string
}
type LogConfig struct {
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: 'Creating local server backup accounts is not supported ye
#app
ErrPortInUsed: '{{ .detail }} port is already occupied!'
ErrAppLimit: 'The number of applications installed has exceeded the limit'
ErrAppRequired: 'Please install the {{ .detail }} application first'
ErrNotInstall: 'Application not installed'
ErrPortInOtherApp: '{{ .port }} port is already occupied by application {{ .apps }}!'
ErrDbUserNotValid: 'Existing database, username and password do not match!'
ErrDockerComposeNotValid: 'The docker-compose file format is incorrect'
ErrUpdateBuWebsite: 'The application was updated successfully, but the website configuration file modification failed. Please check the configuration! '
Err1PanelNetworkFailed: 'Default container network creation failed! {{ .detail }}'
ErrFileParse: 'Application docker-compose file parsing failed!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} file parsing failed {{ .err }}'
ErrAppDirNull: 'The version folder does not exist'
LocalAppErr: 'Application {{ .name }} sync failed! {{ .err }}'
ErrContainerName: 'Container name already exists'
ErrAppSystemRestart: '1Panel restart caused the task to terminate'
ErrCreateHttpClient: 'Failed to create request {{ .err }}'
ErrHttpReqTimeOut: 'Request timed out {{ .err }}'
ErrHttpReqFailed: 'Request failed {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: 'Image pull timeout'
ErrContainerNotFound: '{{ .name }} container does not exist'
ErrContainerMsg: '{{ .name }} container is abnormal. Please check the log on the container page for details'
ErrAppBackup: '{{ .name }} application backup failed {{ .err }}'
ErrImagePull: 'Image pull failed {{ .err }}'
ErrVersionTooLow: 'The current 1Panel version is too low to update the App Store. Please upgrade the version before operating.'
ErrAppNameExist: 'The application name already exists'
AppStoreIsSyncing: 'The App Store is syncing, please try again later'
@@ -80,8 +76,6 @@ ErrAppUpgrade: 'Application {{ .name }} upgrade failed {{ .err }}'
AppRecover: 'Rollback application {{ .name }}'
PullImageStart: 'Start pulling image {{ .name }}'
PullImageSuccess: 'Image pull successful'
UpgradeAppStart: 'Start upgrading application {{ .name }}'
UpgradeAppSuccess: 'Application {{ .name }} upgraded successfully'
AppStoreIsLastVersion: 'The App Store is already the latest version'
AppStoreSyncSuccess: 'App Store synchronization successful'
SyncAppDetail: 'Synchronize application configuration'
@@ -91,8 +85,6 @@ MoveSiteToDir: 'Migrate site directory to {{ .name }}'
ErrMoveSiteDir: 'Failed to migrate site directory'
MoveSiteDirSuccess: 'Successful migration of website directory'
DeleteRuntimePHP: 'Delete PHP runtime'
CustomAppStoreNotConfig: 'Please set the offline package address in the app store'
CustomAppStoreNotFound: 'Failed to obtain the app store package, please check whether it exists'
CustomAppStoreFileValid: 'App store packages need to be in .tar.gz format'
PullImageTimeout: 'Pull image timeout, please try to increase the image acceleration or change to another image acceleration'
ErrAppIsDown: '{{ .name }} application status is abnormal, please check'
@@ -115,9 +107,7 @@ ErrInvalidChar: 'Illegal characters are not allowed'
ErrPathNotDelete: 'The selected directory cannot be deleted'
#website
ErrDomainIsExist: 'Domain name already exists'
ErrAliasIsExist: 'Alias already exists'
ErrAppDelete: 'Other websites use this app and cannot delete it'
ErrBackupMatch: 'The backup file does not match some of the current website data {{ .detail }}'
ErrBackupExist: 'The corresponding part of the source data in the backup file does not exist {{ .detail }}'
ErrPHPResource: 'The local operating environment does not support switching! '
@@ -179,7 +169,6 @@ ErrPortRules: 'Port number does not match, please re-enter!'
ErrPgImagePull: 'Image pull timed out, please configure image acceleration or manually pull the {{ .name }} image and try again'
#runtime
ErrDirNotFound: 'The build folder does not exist! Please check the file integrity!'
ErrFileNotExist: '{{ .detail }} file does not exist! Please check the integrity of the source file!'
ErrImageBuildErr: 'Image build failed'
ErrImageExist: 'Image already exists!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: 'Rule does not exist'
ErrParseIP: 'Wrong IP format'
ErrDefaultIP: 'default is a reserved name, please change it to another name'
ErrGroupInUse: 'IP group is used by blacklist/whitelist and cannot be deleted'
ErrIPGroupAclUse: "IP group is used by custom rules of website {{ .name }}, cannot be deleted"
ErrGroupExist: 'IP group name already exists'
ErrIPRange: 'Wrong IP range'
ErrIPExist: 'IP already exists'
@@ -262,6 +252,7 @@ ErrDBNotExist: 'Database does not exist'
allow: 'allow'
deny: 'deny'
OpenrestyNotFound: 'Openresty is not installed'
remoteIpIsNull: "IP list is empty"
#task
TaskStart: '{{ .name }} task starts [START]'
@@ -387,7 +378,7 @@ ContainerStartCheck: 'Check if the container has been started'
# task - image
ImageBuild: 'Image Build'
ImageBuildStdoutCheck: 'Parse image output content'
ImaegBuildRes: 'Image build output: {{ .name }}'
ImageBuildRes: 'Image build output: {{ .name }}'
ImagePull: 'Pull image'
ImageRepoAuthFromDB: 'Get repository authentication information from the database'
ImaegPullRes: 'Image pull output: {{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: 'The format of the warning message is incorrect, please check and try
ErrAlertPush: 'Error in pushing alert information, please check and try again!'
ErrAlertSave: 'Error saving the alarm information, please check and try again!'
ErrAlertSync: 'Alarm information synchronization error, please check and try again!'
ErrAlertRemote: 'Alarm message remote error, please check and try again!'
ErrAlertRemote: 'Alarm message remote error, please check and try again!'
#task - runtime
ErrInstallExtension: "An installation task is already in progress, please wait for the task to finish"
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: 'ローカル サーバーのバックアップ アカウ
#app
ErrPortInUsed: '{{ .detail }} ポートはすでに使用されています!'
ErrAppLimit: 'インストールされているアプリケーションの数が制限を超えました'
ErrAppRequired: 'まず {{ .detail }} アプリケーションをインストールしてください'
ErrNotInstall: 'アプリケーションがインストールされていません'
ErrPortInOtherApp: '{{ .port }} ポートは既にアプリケーション {{ .apps }} によって使用されています!'
ErrDbUserNotValid: '既存のデータベース、ユーザー名、およびパスワードが一致しません!'
ErrDockerComposeNotValid: 'docker-compose ファイルの形式が正しくありません'
ErrUpdateBuWebsite: 'アプリケーションは正常に更新されましたが、Web サイト構成ファイルの変更に失敗しました。設定を確認してください! '
Err1PanelNetworkFailed: 'デフォルトのコンテナ ネットワークの作成に失敗しました。 {{ .detail }}'
ErrFileParse: 'アプリケーションの docker-compose ファイルの解析に失敗しました!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} ファイルの解析に失敗しました {{ .err
ErrAppDirNull: 'バージョン フォルダーが存在しません'
LocalAppErr: 'アプリケーション {{ .name }} の同期に失敗しました! {{ .err }}'
ErrContainerName: 'コンテナ名が既に存在します'
ErrAppSystemRestart: '1Panel の再起動によりタスクが終了しました'
ErrCreateHttpClient: 'リクエスト {{ .err }} の作成に失敗しました'
ErrHttpReqTimeOut: 'リクエストがタイムアウトしました {{ .err }}'
ErrHttpReqFailed: 'リクエストが失敗しました {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: 'イメージ プル タイムアウト'
ErrContainerNotFound: '{{ .name }} コンテナが存在しません'
ErrContainerMsg: '{{ .name }} コンテナが異常です。詳細についてはコンテナページのログを確認してください。'
ErrAppBackup: '{{ .name }} アプリケーションのバックアップに失敗しました。エラー {{ .err }}'
ErrImagePull: 'イメージのプルに失敗しました {{ .err }}'
ErrVersionTooLow: '現在の 1Panel のバージョンが低すぎるため、App Store を更新できません。操作する前にバージョンをアップグレードしてください。'
ErrAppNameExist: 'アプリケーション名がすでに存在します'
AppStoreIsSyncing: 'App Store が同期中です。しばらくしてからもう一度お試しください'
@@ -80,8 +76,6 @@ ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに
AppRecover: 'アプリケーション {{ .name }} をロールバックします'
PullImageStart: "イメージ {{ .name }} のプルを開始します"
PullImageSuccess: 'イメージのプルが成功しました'
UpgradeAppStart: 'アプリケーション {{ .name }} のアップグレードを開始します'
UpgradeAppSuccess: 'アプリケーション {{ .name }} が正常にアップグレードされました'
AppStoreIsLastVersion: 'App Store はすでに最新バージョンです'
AppStoreSyncSuccess: 'App Store の同期が成功しました'
SyncAppDetail: 'アプリケーション構成を同期する'
@@ -91,8 +85,6 @@ MoveSiteToDir: 'サイト ディレクトリを {{ .name }} に移行します'
ErrMoveSiteDir: 'サイト ディレクトリの移行に失敗しました'
MoveSiteDirSuccess: 'Web サイト ディレクトリの移行に成功しました'
DeleteRuntimePHP: 'PHP ランタイムを削除する'
CustomAppStoreNotConfig: 'アプリストアでオフライン パッケージ アドレスを設定してください'
CustomAppStoreNotFound: 'アプリストア パッケージを取得できませんでした。存在するかどうかを確認してください'
CustomAppStoreFileValid: 'App Store パッケージは .tar.gz 形式である必要があります'
PullImageTimeout: 'プル イメージのタイムアウトです。イメージのアクセラレーションを増やすか、別のイメージのアクセラレーションに変更してください'
ErrAppIsDown: '{{ .name }} アプリケーションの状態が異常です。確認してください'
@@ -115,9 +107,7 @@ ErrInvalidChar: '不正な文字は許可されません'
ErrPathNotDelete: '選択されたディレクトリは削除できません'
#website
ErrDomainIsExist: 'ドメイン名は既に存在します'
ErrAliasIsExist: 'エイリアスがすでに存在します'
ErrAppDelete: '他のウェブサイトがこのアプリを使用しているため、削除できません'
ErrBackupMatch: 'バックアップ ファイルは、現在の Web サイト データ {{ .detail }} の一部と一致しません'
ErrBackupExist: 'バックアップ ファイル内のソース データの対応する部分が存在しません {{ .detail }}'
ErrPHPResource: 'ローカルオペレーティング環境は切り替えをサポートしていません! '
@@ -179,7 +169,6 @@ ErrPortRules: 'ポート番号が一致しません。再入力してくださ
ErrPgImagePull: 'イメージのプルがタイムアウトしました。イメージのアクセラレーションを設定するか、{{ .name }} イメージを手動でプルして再試行してください'
#runtime
ErrDirNotFound: 'ビルド フォルダーが存在しません。ファイルの整合性を確認してください。'
ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。'
ErrImageBuildErr: 'イメージのビルドに失敗しました'
ErrImageExist: '画像がすでに存在します!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: 'ルールが存在しません'
ErrParseIP: 'IP 形式が間違っています'
ErrDefaultIP: 'デフォルトは予約名です。別の名前に変更してください'
ErrGroupInUse: 'IP グループはブラックリスト/ホワイトリストで使用されており、削除できません'
ErrIPGroupAclUse: "IPグループはウェブサイト {{ .name }} のカスタムルールで使用されているため、削除できません"
ErrGroupExist: 'IP グループ名がすでに存在します'
ErrIPRange: 'IP 範囲が間違っています'
ErrIPExist: 'IP がすでに存在します'
@@ -262,6 +252,7 @@ ErrDBNotExist: 'データベースが存在しません'
allow: '許可'
deny: '拒否'
OpenrestyNotFound: 'Openresty がインストールされていません'
remoteIpIsNull: "IPリストが空です"
#task
TaskStart: '{{ .name }} タスクが開始されました [START]'
@@ -387,7 +378,7 @@ ContainerStartCheck: 'コンテナが起動されているかどうかを確認
#task - image
ImageBuild: 'イメージビルド'
ImageBuildStdoutCheck: '画像出力コンテンツを解析する'
ImaegBuildRes: 'イメージビルド出力: {{ .name }}'
ImageBuildRes: 'イメージビルド出力: {{ .name }}'
ImagePull: '画像をプル'
ImageRepoAuthFromDB: 'データベースからリポジトリ認証情報を取得する'
ImaegPullRes: "画像プル出力: {{ .name }}"
@@ -415,4 +406,7 @@ ErrAlert: '警告メッセージの形式が正しくありません。確認し
ErrAlertPush: 'アラート情報のプッシュ中にエラーが発生しました。確認してもう一度お試しください。'
ErrAlertSave: 'アラーム情報の保存中にエラーが発生しました。確認してもう一度お試しください。'
ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。'
ErrAlertRemote: 'アラーム メッセージのリモート エラーです。確認してもう一度お試しください。'
ErrAlertRemote: 'アラーム メッセージのリモート エラーです。確認してもう一度お試しください。'
#task - runtime
ErrInstallExtension: "インストールタスクが進行中です、タスクが終了するのを待ってください"
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: '로컬 서버 백업 계정 생성은 아직 지원되지
#앱
ErrPortInUsed: '{{ .detail }} 포트가 이미 사용 중입니다!'
ErrAppLimit: '설치된 애플리케이션 수가 한도를 초과했습니다'
ErrAppRequired: '먼저 {{ .detail }} 애플리케이션을 설치하세요'
ErrNotInstall: '응용 프로그램이 설치되지 않았습니다'
ErrPortInOtherApp: '{{ .port }} 포트는 이미 {{ .apps }} 애플리케이션에 의해 사용되고 있습니다!'
ErrDbUserNotValid: '기존 데이터베이스, 사용자 이름 및 비밀번호가 일치하지 않습니다!'
ErrDockerComposeNotValid: 'docker-compose 파일 형식이 올바르지 않습니다'
ErrUpdateBuWebsite: '응용 프로그램이 성공적으로 업데이트되었지만, 웹사이트 구성 파일 수정에 실패했습니다. 구성을 확인하세요! '
Err1PanelNetworkFailed: '기본 컨테이너 네트워크 생성에 실패했습니다! {{ .세부 사항 }}'
ErrFileParse: '응용 프로그램 docker-compose 파일 구문 분석에 실패했습니다!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} 파일 구문 분석에 실패했습니다 {{ .err
ErrAppDirNull: '버전 폴더가 존재하지 않습니다'
LocalAppErr: '애플리케이션 {{ .name }} 동기화에 실패했습니다! {{ .err }}'
ErrContainerName: '컨테이너 이름이 이미 존재합니다'
ErrAppSystemRestart: '1패널 재시작으로 인해 작업이 종료되었습니다'
ErrCreateHttpClient: '요청 {{ .err }}을(를) 생성하지 못했습니다.'
ErrHttpReqTimeOut: '요청 시간이 초과되었습니다 {{ .err }}'
ErrHttpReqFailed: '요청이 실패했습니다 {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: '이미지 풀링 시간 초과'
ErrContainerNotFound: '{{ .name }} 컨테이너가 존재하지 않습니다'
ErrContainerMsg: '{{ .name }} 컨테이너가 비정상입니다. 자세한 내용은 컨테이너 페이지의 로그를 확인하세요.'
ErrAppBackup: '{{ .name }} 애플리케이션 백업에 실패했습니다 {{ .err }}'
ErrImagePull: '이미지 가져오기에 실패했습니다 {{ .err }}'
ErrVersionTooLow: '현재 1Panel 버전이 너무 낮아 App Store를 업데이트할 수 없습니다. 작동하시기 전에 버전을 업그레이드하세요.'
ErrAppNameExist: '응용 프로그램 이름이 이미 존재합니다'
AppStoreIsSyncing: 'App Store가 동기화 중입니다. 나중에 다시 시도하세요.'
@@ -80,8 +76,6 @@ ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습
AppRecover: '롤백 애플리케이션 {{ .name }}'
PullImageStart: '이미지 {{ .name }} 가져오기 시작'
PullImageSuccess: '이미지 가져오기 성공'
UpgradeAppStart: '애플리케이션 {{ .name }} 업그레이드 시작'
UpgradeAppSuccess: '애플리케이션 {{ .name }}이 성공적으로 업그레이드되었습니다.'
AppStoreIsLastVersion: '앱스토어가 이미 최신 버전입니다'
AppStoreSyncSuccess: '앱스토어 동기화 성공'
SyncAppDetail: '애플리케이션 구성 동기화'
@@ -91,8 +85,6 @@ MoveSiteToDir: '사이트 디렉토리를 {{ .name }}로 마이그레이션'
ErrMoveSiteDir: '사이트 디렉토리를 마이그레이션하지 못했습니다'
MoveSiteDirSuccess: '웹사이트 디렉토리 마이그레이션 성공'
DeleteRuntimePHP: 'PHP 런타임 삭제'
CustomAppStoreNotConfig: '앱 스토어에서 오프라인 패키지 주소를 설정해주세요'
CustomAppStoreNotFound: '앱 스토어 패키지를 가져오지 못했습니다. 해당 패키지가 있는지 확인하세요'
CustomAppStoreFileValid: '앱 스토어 패키지는 .tar.gz 형식이어야 합니다.'
PullImageTimeout: '이미지 가져오기 시간 초과, 이미지 가속을 높이거나 다른 이미지 가속으로 변경해 보세요.'
ErrAppIsDown: '{{ .name }} 애플리케이션 상태가 비정상적입니다. 확인해 주세요'
@@ -115,9 +107,7 @@ ErrInvalidChar: '불법 문자는 허용되지 않습니다'
ErrPathNotDelete: '선택한 디렉토리를 삭제할 수 없습니다'
#웹사이트
ErrDomainIsExist: '도메인 이름이 이미 존재합니다'
ErrAliasIsExist: '별칭이 이미 존재합니다'
ErrAppDelete: '다른 웹사이트에서 이 앱을 사용하고 있어 삭제할 수 없습니다'
ErrBackupMatch: '백업 파일이 현재 웹사이트 데이터 중 일부 {{ .detail }}와 일치하지 않습니다.'
ErrBackupExist: '백업 파일에 있는 소스 데이터의 해당 부분이 존재하지 않습니다 {{ .detail }}'
ErrPHPResource: '로컬 운영 환경이 전환을 지원하지 않습니다! '
@@ -179,7 +169,6 @@ ErrPortRules: '포트 번호가 일치하지 않습니다. 다시 입력하세
ErrPgImagePull: '이미지 풀링 시간이 초과되었습니다. 이미지 가속을 구성하거나 {{ .name }} 이미지를 수동으로 풀링한 다음 다시 시도하세요.'
#실행 시간
ErrDirNotFound: '빌드 폴더가 존재하지 않습니다! 파일 무결성을 확인하세요!'
ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다! 소스 파일의 무결성을 확인하세요!'
ErrImageBuildErr: '이미지 빌드 실패'
ErrImageExist: '이미지가 이미 존재합니다!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: '규칙이 존재하지 않습니다'
ErrParseIP: '잘못된 IP 형식'
ErrDefaultIP: '기본값은 예약된 이름입니다. 다른 이름으로 변경해 주세요'
ErrGroupInUse: 'IP 그룹이 블랙리스트/화이트리스트에 사용 중이므로 삭제할 수 없습니다.'
ErrIPGroupAclUse: "IP 그룹은 웹사이트 {{ .name }} 의 사용자 정의 규칙에 사용되므로 삭제할 수 없습니다"
ErrGroupExist: 'IP 그룹 이름이 이미 존재합니다'
ErrIPRange: '잘못된 IP 범위'
ErrIPExist: 'IP가 이미 존재합니다'
@@ -262,6 +252,7 @@ ErrDBNotExist: '데이터베이스가 존재하지 않습니다'
allow: '허용하다'
deny: '거부하다'
OpenrestyNotFound: 'Openresty가 설치되지 않았습니다'
remoteIpIsNull: "IP 목록이 비어 있습니다"
#일
TaskStart: '[START] {{ .name }} 작업이 시작됩니다.'
@@ -387,7 +378,7 @@ ContainerStartCheck: '컨테이너가 시작되었는지 확인'
# 작업 - 이미지
ImageBuild: '이미지 빌드'
ImageBuildStdoutCheck: '이미지 출력 콘텐츠 구문 분석'
ImaegBuildRes: '이미지 빌드 출력: {{ .name }}'
ImageBuildRes: '이미지 빌드 출력: {{ .name }}'
ImagePull: '이미지 가져오기'
ImageRepoAuthFromDB: '데이터베이스에서 저장소 인증 정보 가져오기'
ImaegPullRes: '이미지 풀 출력: {{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: '경고 메시지의 형식이 올바르지 않습니다. 확인하고
ErrAlertPush: '알림 정보를 푸시하는 중 오류가 발생했습니다. 확인하고 다시 시도하세요!'
ErrAlertSave: '알람 정보를 저장하는 중 오류가 발생했습니다. 확인하고 다시 시도하세요!'
ErrAlertSync: '알람 정보 동기화 오류입니다. 확인하고 다시 시도하세요!'
ErrAlertRemote: '알람 메시지 원격 오류, 확인하고 다시 시도하세요!'
ErrAlertRemote: '알람 메시지 원격 오류, 확인하고 다시 시도하세요!'
#task - runtime
ErrInstallExtension: "이미 설치 작업이 진행 중입니다. 작업이 완료될 때까지 기다려 주세요."
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: 'Membuat akaun sandaran pelayan tempatan belum disokong la
#app
ErrPortInUsed: 'Port {{ .detail }} sudah diduduki!'
ErrAppLimit: 'Bilangan aplikasi yang dipasang telah melebihi had'
ErrAppRequired: 'Sila pasang aplikasi {{ .detail }} dahulu'
ErrNotInstall: 'Aplikasi tidak dipasang'
ErrPortInOtherApp: 'Port {{ .port }} sudah diduduki oleh aplikasi {{ .apps }}!'
ErrDbUserNotValid: 'Pangkalan data sedia ada, nama pengguna dan kata laluan tidak sepadan!'
ErrDockerComposeNotValid: 'Format fail karang docker tidak betul'
ErrUpdateBuWebsite: 'Aplikasi telah berjaya dikemas kini, tetapi pengubahsuaian fail konfigurasi tapak web gagal. Sila semak konfigurasi! '
Err1PanelNetworkFailed: 'Pembuatan rangkaian kontena lalai gagal! {{ .detail }}'
ErrFileParse: 'Penghuraian fail karang docker aplikasi gagal!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} penghuraian fail gagal {{ .err }}'
ErrAppDirNull: 'Folder versi tidak wujud'
LocalAppErr: 'Penyegerakan {{ .name }} aplikasi gagal! {{ .err }}'
ErrContainerName: 'Nama kontena sudah wujud'
ErrAppSystemRestart: '1Panel restart menyebabkan tugas ditamatkan'
ErrCreateHttpClient: 'Gagal membuat permintaan {{ .err }}'
ErrHttpReqTimeOut: 'Permintaan tamat masa {{ .err }}'
ErrHttpReqFailed: 'Permintaan gagal {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: 'Tamat masa tarik imej'
ErrContainerNotFound: 'Bekas {{ .name }} tidak wujud'
ErrContainerMsg: Bekas '{{ .name }} adalah tidak normal. Sila semak log pada halaman kontena untuk butiran'
ErrAppBackup: '{{ .name }} sandaran aplikasi gagal {{ .err }}'
ErrImagePull: 'Tarik imej gagal {{ .err }}'
ErrVersionTooLow: 'Versi 1Panel semasa terlalu rendah untuk mengemas kini App Store. Sila tingkatkan versi sebelum beroperasi.'
ErrAppNameExist: 'Nama aplikasi sudah wujud'
AppStoreIsSyncing: 'App Store sedang menyegerak, sila cuba sebentar lagi'
@@ -80,8 +76,6 @@ ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}'
AppRecover: 'Aplikasi tarik balik {{ .name }}'
PullImageStart: 'Mula tarik imej {{ .name }}'
PullImageSuccess: 'Tarik imej berjaya'
UpgradeAppStart: 'Mulakan naik taraf aplikasi {{ .name }}'
UpgradeAppSuccess: 'Aplikasi {{ .name }} berjaya dinaik taraf'
AppStoreIsLastVersion: 'App Store sudah pun versi terkini'
AppStoreSyncSuccess: 'Penyegerakan App Store berjaya'
SyncAppDetail: 'Segerakkan konfigurasi aplikasi'
@@ -91,8 +85,6 @@ MoveSiteToDir: 'Pindahkan direktori tapak ke {{ .name }}'
ErrMoveSiteDir: 'Gagal memindahkan direktori tapak'
MoveSiteDirSuccess: 'Penghijrahan direktori tapak web yang berjaya'
DeleteRuntimePHP: 'Padam masa jalan PHP'
CustomAppStoreNotConfig: 'Sila tetapkan alamat pakej luar talian dalam gedung aplikasi'
CustomAppStoreNotFound: 'Gagal mendapatkan pakej kedai aplikasi, sila semak sama ada ia wujud'
CustomAppStoreFileValid: 'Pakej gedung apl perlu dalam format .tar.gz'
PullImageTimeout: 'Tarik tamat masa imej, sila cuba tingkatkan pecutan imej atau tukar kepada pecutan imej lain'
ErrAppIsDown: 'Status permohonan {{ .name }} tidak normal, sila semak'
@@ -115,9 +107,7 @@ ErrInvalidChar: 'Aksara haram tidak dibenarkan'
ErrPathNotDelete: 'Direktori yang dipilih tidak boleh dipadamkan'
#laman web
ErrDomainIsExist: 'Nama domain sudah wujud'
ErrAliasIsExist: 'Alias sudah wujud'
ErrAppDelete: 'Laman web lain menggunakan aplikasi ini dan tidak boleh memadamkannya'
ErrBackupMatch: 'Fail sandaran tidak sepadan dengan beberapa data tapak web semasa {{ .detail }}'
ErrBackupExist: 'Bahagian sepadan data sumber dalam fail sandaran tidak wujud {{ .detail }}'
ErrPHPResource: 'Persekitaran operasi tempatan tidak menyokong penukaran! '
@@ -179,7 +169,6 @@ ErrPortRules: 'Nombor port tidak sepadan, sila masukkan semula!'
ErrPgImagePull: 'Tarikh imej tamat masa, sila konfigurasikan pecutan imej atau tarik imej {{ .name }} secara manual dan cuba lagi'
#masa berjalan
ErrDirNotFound: 'Folder binaan tidak wujud! Sila semak integriti fail!'
ErrFileNotExist: 'Fail {{ .detail }} tidak wujud! Sila semak integriti fail sumber!'
ErrImageBuildErr: 'Pembinaan imej gagal'
ErrImageExist: 'Imej sudah wujud!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: 'Peraturan tidak wujud'
ErrParseIP: 'Format IP salah'
ErrDefaultIP: 'default ialah nama simpanan, sila tukar kepada nama lain'
ErrGroupInUse: 'Kumpulan IP digunakan oleh senarai hitam/senarai putih dan tidak boleh dipadamkan'
ErrIPGroupAclUse: "Kumpulan IP digunakan oleh peraturan tersuai tapak web {{ .name }}, tidak boleh dipadamkan"
ErrGroupExist: 'Nama kumpulan IP sudah wujud'
ErrIPRange: 'Julat IP salah'
ErrIPExist: 'IP sudah wujud'
@@ -262,6 +252,7 @@ ErrDBNotExist: 'Pangkalan data tidak wujud'
allow: 'membenarkan'
deny: 'menafikan'
OpenrestyNotFound: 'Openresty tidak dipasang'
remoteIpIsNull: "Senarai IP kosong"
#tugas
TaskStart: '{{ .name }} Task mula [MULA]'
@@ -387,7 +378,7 @@ ContainerStartCheck: 'Periksa sama ada bekas telah dimulakan'
# tugas - imej
ImageBuild: 'Membina Imej'
ImageBuildStdoutCheck: 'Menghuraikan kandungan output imej'
ImaegBuildRes: 'Output binaan imej: {{ .name }}'
ImageBuildRes: 'Output binaan imej: {{ .name }}'
ImagePull: 'Tarik imej'
ImageRepoAuthFromDB: 'Dapatkan maklumat pengesahan repositori daripada pangkalan data'
ImaegPullRes: 'Output tarik imej: {{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: 'Format mesej amaran tidak betul, sila semak dan cuba lagi!'
ErrAlertPush: 'Ralat dalam menolak maklumat amaran, sila semak dan cuba lagi!'
ErrAlertSave: 'Ralat menyimpan maklumat penggera, sila semak dan cuba lagi!'
ErrAlertSync: 'Ralat penyegerakan maklumat penggera, sila semak dan cuba lagi!'
ErrAlertRemote: 'Ralat jauh mesej penggera, sila semak dan cuba lagi!'
ErrAlertRemote: 'Ralat jauh mesej penggera, sila semak dan cuba lagi!'
#task - runtime
ErrInstallExtension: "Tugas pemasangan sudah sedang berjalan, silakan tunggu tugas selesai"
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: 'A criação de contas de backup do servidor local ainda n
#aplicativo
ErrPortInUsed: 'A porta {{ .detail }} já está ocupada!'
ErrAppLimit: 'O número de aplicativos instalados excedeu o limite'
ErrAppRequired: 'Instale primeiro o aplicativo {{ .detail }}'
ErrNotInstall: 'Aplicativo não instalado'
ErrPortInOtherApp: 'A porta {{ .port }} já está ocupada pelo aplicativo {{ .apps }}!'
ErrDbUserNotValid: 'Banco de dados existente, nome de usuário e senha não correspondem!'
ErrDockerComposeNotValid: 'O formato do arquivo docker-compose está incorreto'
ErrUpdateBuWebsite: 'O aplicativo foi atualizado com sucesso, mas a modificação do arquivo de configuração do site falhou. Por favor, verifique a configuração! '
Err1PanelNetworkFailed: 'Falha na criação da rede de contêiner padrão! {{ .detalhe }}'
ErrFileParse: 'Falha na análise do arquivo docker-compose do aplicativo!'
@@ -59,7 +57,6 @@ ErrFileParseApp: 'Falha na análise do arquivo {{ .name }} {{ .err }}'
ErrAppDirNull: 'A pasta da versão não existe'
LocalAppErr: 'Falha na sincronização do aplicativo {{ .name }}! {{ .err }}'
ErrContainerName: 'O nome do contêiner já existe'
ErrAppSystemRestart: '1A reinicialização do painel causou o encerramento da tarefa'
ErrCreateHttpClient: 'Falha ao criar solicitação {{ .err }}'
ErrHttpReqTimeOut: 'Tempo limite da solicitação esgotado {{ .err }}'
ErrHttpReqFailed: 'Falha na solicitação {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: 'Tempo limite de extração de imagem'
ErrContainerNotFound: '{{ .name }} container não existe'
ErrContainerMsg: 'O contêiner {{ .name }} está anormal. Por favor, verifique o log na página do contêiner para obter detalhes.'
ErrAppBackup: '{{ .name }} falha no backup do aplicativo {{ .err }}'
ErrImagePull: 'Falha na extração de imagem {{ .err }}'
ErrVersionTooLow: 'A versão atual do 1Panel é muito antiga para atualizar a App Store. Por favor, atualize a versão antes de operar.'
ErrAppNameExist: 'O nome do aplicativo já existe'
AppStoreIsSyncing: 'A App Store está sincronizando, tente novamente mais tarde'
@@ -80,8 +76,6 @@ ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}'
AppRecover: 'Reverter aplicativo {{ .name }}'
PullImageStart: 'Comece a extrair a imagem {{ .name }}'
PullImageSuccess: 'Imagem retirada com sucesso'
UpgradeAppStart: 'Iniciar atualização do aplicativo {{ .name }}'
UpgradeAppSuccess: 'Aplicativo {{ .name }} atualizado com sucesso'
AppStoreIsLastVersion: 'A App Store já é a versão mais recente'
AppStoreSyncSuccess: 'Sincronização da App Store bem-sucedida'
SyncAppDetail: 'Sincronizar configuração do aplicativo'
@@ -91,8 +85,6 @@ MoveSiteToDir: 'Migrar diretório do site para {{ .name }}'
ErrMoveSiteDir: 'Falha ao migrar o diretório do site'
MoveSiteDirSuccess: 'Migração bem-sucedida do diretório do site'
DeleteRuntimePHP: 'Excluir tempo de execução do PHP'
CustomAppStoreNotConfig: 'Defina o endereço do pacote offline na app store'
CustomAppStoreNotFound: 'Falha ao obter o pacote da app store, verifique se ele existe'
CustomAppStoreFileValid: 'Os pacotes da App Store precisam estar no formato .tar.gz'
PullImageTimeout: 'Tempo limite para puxar imagem, tente aumentar a aceleração da imagem ou altere para outra aceleração de imagem'
ErrAppIsDown: 'O status do aplicativo {{ .name }} é anormal, verifique'
@@ -115,9 +107,7 @@ ErrInvalidChar: 'Caracteres ilegais não são permitidos'
ErrPathNotDelete: 'O diretório selecionado não pode ser excluído'
#site
ErrDomainIsExist: 'Nome de domínio já existe'
ErrAliasIsExist: 'Alias já existe'
ErrAppDelete: 'Outros sites usam este aplicativo e não podem excluí-lo'
ErrBackupMatch: 'O arquivo de backup não corresponde a alguns dados atuais do site {{ .detail }}'
ErrBackupExist: 'A parte correspondente dos dados de origem no arquivo de backup não existe {{ .detail }}'
ErrPHPResource: 'O ambiente operacional local não suporta comutação! '
@@ -179,7 +169,6 @@ ErrPortRules: 'O número da porta não corresponde, digite novamente!'
ErrPgImagePull: 'Tempo limite para extração de imagem. Configure a aceleração de imagem ou extraia manualmente a imagem {{ .name }} e tente novamente'
#tempo de execução
ErrDirNotFound: 'A pasta de compilação não existe! Verifique a integridade do arquivo!'
ErrFileNotExist: 'O arquivo {{ .detail }} não existe! Verifique a integridade do arquivo de origem!'
ErrImageBuildErr: 'Falha na criação da imagem'
ErrImageExist: 'A imagem já existe!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: 'A regra não existe'
ErrParseIP: 'Formato de IP errado'
ErrDefaultIP: 'padrão é um nome reservado, altere-o para outro nome'
ErrGroupInUse: 'O grupo de IP é usado pela lista negra/lista branca e não pode ser excluído'
ErrIPGroupAclUse: "O grupo de IP está sendo usado por regras personalizadas do site {{ .name }}, não pode ser excluído"
ErrGroupExist: 'O nome do grupo IP já existe'
ErrIPRange: 'Intervalo de IP errado'
ErrIPExist: 'IP já existe'
@@ -262,6 +252,7 @@ ErrDBNotExist: 'O banco de dados não existe'
allow: 'permitir'
deny: 'negar'
OpenrestyNotFound: 'Openresty não está instalado'
remoteIpIsNull: "A lista de IP está vazia"
#tarefa
TaskStart: '{{ .name }} A tarefa inicia [START]'
@@ -387,7 +378,7 @@ ContainerStartCheck: 'Verifique se o contêiner foi iniciado'
# tarefa - imagem
ImageBuild: 'Construção de imagem'
ImageBuildStdoutCheck: 'Analisar conteúdo de saída da imagem'
ImaegBuildRes: 'Saída da criação da imagem: {{ .name }}'
ImageBuildRes: 'Saída da criação da imagem: {{ .name }}'
ImagePull: 'Puxar imagem'
ImageRepoAuthFromDB: 'Obter informações de autenticação do repositório do banco de dados'
ImaegPullRes: 'Saída de extração de imagem: {{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: 'O formato da mensagem de aviso está incorreto, verifique e tente nov
ErrAlertPush: 'Erro ao enviar informações de alerta, verifique e tente novamente!'
ErrAlertSave: 'Erro ao salvar as informações do alarme. Verifique e tente novamente!'
ErrAlertSync: 'Erro de sincronização de informações de alarme, verifique e tente novamente!'
ErrAlertRemote: 'Erro remoto na mensagem de alarme, verifique e tente novamente!'
ErrAlertRemote: 'Erro remoto na mensagem de alarme, verifique e tente novamente!'
#task - runtime
ErrInstallExtension: "Já existe uma tarefa de instalação em andamento, aguarde a conclusão da tarefa"
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: 'Создание учетных записей резер
#приложение
ErrPortInUsed: '{{ .detail }} порт уже занят!'
ErrAppLimit: 'Количество установленных приложений превысило лимит'
ErrAppRequired: 'Сначала установите приложение {{ .detail }}'
ErrNotInstall: 'Приложение не установлено'
ErrPortInOtherApp: 'Порт {{ .port }} уже занят приложением {{ .apps }}!'
ErrDbUserNotValid: 'Существующая база данных, имя пользователя и пароль не совпадают!'
ErrDockerComposeNotValid: 'Неверный формат файла docker-compose'
ErrUpdateBuWebsite: 'Приложение успешно обновлено, но изменение файла конфигурации веб-сайта не удалось. Пожалуйста, проверьте конфигурацию! '
Err1PanelNetworkFailed: 'Создание сети контейнеров по умолчанию не удалось! {{ .detail }}'
ErrFileParse: 'Ошибка анализа файла docker-compose приложения!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} анализ файла не удался {{ .err
ErrAppDirNull: 'Папка версии не существует'
LocalAppErr: 'Синхронизация приложения {{ .name }} не удалась! {{ .err }}'
ErrContainerName: 'Имя контейнера уже существует'
ErrAppSystemRestart: '1Перезапуск панели привел к завершению задачи'
ErrCreateHttpClient: 'Не удалось создать запрос {{ .err }}'
ErrHttpReqTimeOut: 'Истекло время ожидания запроса {{ .err }}'
ErrHttpReqFailed: 'Запрос не выполнен {{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: 'Истекло время ожидания извлече
ErrContainerNotFound: 'Контейнер {{ .name }} не существует'
ErrContainerMsg: 'Контейнер {{ .name }} ненормален. Подробности смотрите в журнале на странице контейнера.'
ErrAppBackup: '{{ .name }} резервное копирование приложения не удалось {{ .err }}'
ErrImagePull: 'Не удалось извлечь изображение {{ .err }}'
ErrVersionTooLow: 'Текущая версия 1Panel слишком низкая для обновления App Store. Пожалуйста, обновите версию перед началом работы.'
ErrAppNameExist: 'Имя приложения уже существует'
AppStoreIsSyncing: 'App Store синхронизируется, повторите попытку позже'
@@ -80,8 +76,6 @@ ErrAppUpgrade: 'Обновление приложения {{ .name }} не уд
AppRecover: 'Откатить приложение {{ .name }}'
PullImageStart: 'Начать извлечение изображения {{ .name }}'
PullImageSuccess: 'Изображение извлечено успешно'
UpgradeAppStart: 'Начать обновление приложения {{ .name }}'
UpgradeAppSuccess: 'Приложение {{ .name }} успешно обновлено'
AppStoreIsLastVersion: 'В App Store уже установлена последняя версия'
AppStoreSyncSuccess: 'Синхронизация с App Store прошла успешно'
SyncAppDetail: 'Синхронизировать конфигурацию приложения'
@@ -91,8 +85,6 @@ MoveSiteToDir: 'Перенести каталог сайта в {{ .name }}'
ErrMoveSiteDir: 'Не удалось перенести каталог сайта'
MoveSiteDirSuccess: 'Успешная миграция каталога веб-сайта'
DeleteRuntimePHP: 'Удалить среду выполнения PHP'
CustomAppStoreNotConfig: 'Укажите адрес автономного пакета в магазине приложений'
CustomAppStoreNotFound: 'Не удалось получить пакет магазина приложений. Проверьте, существует ли он'
CustomAppStoreFileValid: 'Пакеты магазина приложений должны быть в формате .tar.gz'
PullImageTimeout: 'Истекло время ожидания извлечения изображения. Попробуйте увеличить ускорение изображения или выбрать другое ускорение изображения'
ErrAppIsDown: '{{ .name }} статус приложения ненормальный, проверьте'
@@ -115,9 +107,7 @@ ErrInvalidChar: 'Недопустимые символы не допускают
ErrPathNotDelete: 'Выбранный каталог не может быть удален'
#веб-сайт
ErrDomainIsExist: 'Доменное имя уже существует'
ErrAliasIsExist: 'Псевдоним уже существует'
ErrAppDelete: 'Другие веб-сайты используют это приложение и не могут его удалить'
ErrBackupMatch: 'Файл резервной копии не соответствует некоторым текущим данным веб-сайта {{ .detail }}'
ErrBackupExist: 'Соответствующая часть исходных данных в файле резервной копии не существует {{ .detail }}'
ErrPHPResource: 'Локальная операционная среда не поддерживает переключение!'
@@ -179,7 +169,6 @@ ErrPortRules: 'Номер порта не совпадает, введите з
ErrPgImagePull: 'Время извлечения изображения истекло. Настройте ускорение изображения или вручную извлеките изображение {{ .name }} и повторите попытку'
#время выполнения
ErrDirNotFound: 'Папка сборки не существует! Проверьте целостность файла!'
ErrFileNotExist: 'Файл {{ .detail }} не существует! Проверьте целостность исходного файла!'
ErrImageBuildErr: 'Сборка образа не удалась'
ErrImageExist: 'Изображение уже существует!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: 'Правило не существует'
ErrParseIP: 'Неправильный формат IP'
ErrDefaultIP: 'по умолчанию это зарезервированное имя, пожалуйста, измените его на другое имя'
ErrGroupInUse: 'IP-группа используется черным/белым списком и не может быть удалена'
ErrIPGroupAclUse: "Группа IP используется пользовательскими правилами сайта {{ .name }}, невозможно удалить"
ErrGroupExist: 'Имя группы IP уже существует'
ErrIPRange: 'Неверный диапазон IP-адресов'
ErrIPExist: 'IP-адрес уже существует'
@@ -262,6 +252,7 @@ ErrDBNotExist: 'База данных не существует'
allow: 'разрешить'
deny: 'отрицать'
OpenrestyNotFound: 'Openresty не установлен'
remoteIpIsNull: "Список IP пуст"
#задача
TaskStart: '{{ .name }} Задача начинается [START]'
@@ -387,7 +378,7 @@ ContainerStartCheck: 'Проверить, запущен ли контейнер
# задача - изображение
ImageBuild: 'Создание изображения'
ImageBuildStdoutCheck: 'Анализ содержимого выходного изображения'
ImaegBuildRes: 'Выходные данные сборки образа: {{ .name }}'
ImageBuildRes: 'Выходные данные сборки образа: {{ .name }}'
ImagePull: 'Вытащить изображение'
ImageRepoAuthFromDB: 'Получить информацию об аутентификации репозитория из базы данных'
ImaegPullRes: 'Выход извлечения изображения: {{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: 'Формат предупреждающего сообщения не
ErrAlertPush: 'Ошибка при отправке оповещения. Проверьте и повторите попытку!'
ErrAlertSave: 'Ошибка сохранения информации о тревоге, проверьте и повторите попытку!'
ErrAlertSync: 'Ошибка синхронизации информации о тревоге, проверьте и повторите попытку!'
ErrAlertRemote: 'Ошибка удаленного сообщения об ошибке, проверьте и повторите попытку!'
ErrAlertRemote: 'Ошибка удаленного сообщения об ошибке, проверьте и повторите попытку!'
#task - runtime
ErrInstallExtension: "Уже выполняется задача установки, подождите, пока задача завершится"
+7 -13
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: '暫時不支援建立本機伺服器備份帳號'
#app
ErrPortInUsed: '{{ .detail }} 連接埠已被佔用!'
ErrAppLimit: '應用程式超出安裝數量限制'
ErrAppRequired: '請先安裝{{ .detail }} 應用'
ErrNotInstall: '應用程式未安裝'
ErrPortInOtherApp: '{{ .port }} 連接埠已被應用程式{{ .apps }} 佔用!'
ErrDbUserNotValid: '存量資料庫,使用者名稱密碼不符!'
ErrDockerComposeNotValid: 'docker-compose 檔案格式錯誤'
ErrUpdateBuWebsite: '應用程式更新成功,但網站設定檔修改失敗,請檢查設定! '
Err1PanelNetworkFailed: '預設容器網路建立失敗! {{ .detail }}'
ErrFileParse: '應用docker-compose 檔案解析失敗!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} 檔案解析失敗{{ .err }}'
ErrAppDirNull: '版本資料夾不存在'
LocalAppErr: '應用程式{{ .name }} 同步失敗!{{ .err }}'
ErrContainerName: '容器名稱已存在'
ErrAppSystemRestart: '1Panel 重新啟動導致任務終止'
ErrCreateHttpClient: '建立請求失敗{{ .err }}'
ErrHttpReqTimeOut: '請求逾時{{ .err }}'
ErrHttpReqFailed: '請求失敗{{ .err }}'
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: '鏡像拉取逾時'
ErrContainerNotFound: '{{ .name }} 容器不存在'
ErrContainerMsg: '{{ .name }} 容器異常,請在容器頁面上查看日誌'
ErrAppBackup: '{{ .name }} 應用備份失敗 {{ .err }}'
ErrImagePull: '鏡像拉取失敗{{ .err }}'
ErrVersionTooLow: '目前1Panel 版本過低,無法更新應用程式商店,請升級版本之後操作'
ErrAppNameExist: '應用程式名稱已存在'
AppStoreIsSyncing: '應用程式商店正在同步中,請稍後再試'
@@ -80,8 +76,6 @@ ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}'
AppRecover: '應用程式{{ .name }} 回滾'
PullImageStart: '開始拉取鏡像{{ .name }}'
PullImageSuccess: '鏡像拉取成功'
UpgradeAppStart: '開始升級應用程式{{ .name }}'
UpgradeAppSuccess: '應用程式{{ .name }} 升級成功'
AppStoreIsLastVersion: '應用程式商店已經是最新版本'
AppStoreSyncSuccess: '應用程式商店同步成功'
SyncAppDetail: '同步應用程式設定'
@@ -91,8 +85,6 @@ MoveSiteToDir: '遷移網站目錄到{{ .name }}'
ErrMoveSiteDir: '遷移網站目錄失敗'
MoveSiteDirSuccess: '遷移網站目錄成功'
DeleteRuntimePHP: '刪除PHP 運行環境'
CustomAppStoreNotConfig: '請在應用程式商店設定離線套件位址'
CustomAppStoreNotFound: '應用程式商店套件取得失敗,請檢查是否存在'
CustomAppStoreFileValid: '應用程式商店包需要.tar.gz 格式'
PullImageTimeout: '拉取鏡像逾時,請嘗試增加鏡像加速或更換其他鏡像加速'
ErrAppIsDown: '{{ .name }} 應用程式狀態異常,請檢查'
@@ -115,9 +107,7 @@ ErrInvalidChar: '禁止使用非法字元'
ErrPathNotDelete: '所選目錄不可刪除'
#website
ErrDomainIsExist: '網域已存在'
ErrAliasIsExist: '代號已存在'
ErrAppDelete: '其他網站使用此應用程式,無法刪除'
ErrBackupMatch: '該備份檔案與目前網站部分資料不符{{ .detail }}'
ErrBackupExist: '該備份檔案對應部分來源資料不存在{{ .detail }}'
ErrPHPResource: '本地運行環境不支援切換! '
@@ -179,7 +169,6 @@ ErrPortRules: '連接埠數目不匹配,請重新輸入!'
ErrPgImagePull: '鏡像拉取逾時,請配置鏡像加速或手動拉取{{ .name }} 鏡像後重試'
#runtime
ErrDirNotFound: 'build 資料夾不存在!請檢查檔案完整性!'
ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!'
ErrImageBuildErr: '鏡像build 失敗'
ErrImageExist: '鏡像已存在!'
@@ -219,6 +208,7 @@ ErrRuleNotExist: '規則不存在'
ErrParseIP: 'IP 格式錯誤'
ErrDefaultIP: 'default 為保留名稱,請更換其他名稱'
ErrGroupInUse: 'IP 群組被駭/白名單使用,無法刪除'
ErrIPGroupAclUse: "IP 群組被網站 {{ .name }} 自定義規則使用,無法刪除"
ErrGroupExist: 'IP 群組名稱已存在'
ErrIPRange: 'IP 範圍錯誤'
ErrIPExist: 'IP 已存在'
@@ -262,6 +252,7 @@ ErrDBNotExist: '資料庫不存在'
allow: '允許'
deny: '禁止'
OpenrestyNotFound: 'Openresty 未安裝'
remoteIpIsNull: "IP 列表為空"
#task
TaskStart: '{{ .name }} 任務開始[START]'
@@ -387,7 +378,7 @@ ContainerStartCheck: '檢查容器是否已啟動'
# task - image
ImageBuild: '鏡像建置'
ImageBuildStdoutCheck: '解析鏡像輸出內容'
ImaegBuildRes: '鏡像建置輸出:{{ .name }}'
ImageBuildRes: '鏡像建置輸出:{{ .name }}'
ImagePull: '拉取鏡像'
ImageRepoAuthFromDB: '從資料庫取得倉庫認證資訊'
ImaegPullRes: '鏡像拉取輸出:{{ .name }}'
@@ -415,4 +406,7 @@ ErrAlert: '警告訊息格式錯誤,請檢查後重試!'
ErrAlertPush: '警告訊息推送錯誤,請檢查後重試!'
ErrAlertSave: '警告訊息儲存錯誤,請檢查後重試!'
ErrAlertSync: '警告訊息同步錯誤,請檢查後重試!'
ErrAlertRemote: '警告訊息遠端錯誤,請檢查後重試!'
ErrAlertRemote: '警告訊息遠端錯誤,請檢查後重試!'
#task - runtime
ErrInstallExtension: "已有安裝任務正在進行,請等待任務結束"
+1 -12
View File
@@ -42,11 +42,9 @@ ErrBackupLocalCreate: "暂不支持创建本地服务器备份账号"
#app
ErrPortInUsed: "{{ .detail }} 端口已被占用!"
ErrAppLimit: "应用超出安装数量限制"
ErrAppRequired: "请先安装 {{ .detail }} 应用"
ErrNotInstall: "应用未安装"
ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!"
ErrDbUserNotValid: "存量数据库,用户名密码不匹配!"
ErrDockerComposeNotValid: "docker-compose 文件格式错误"
ErrUpdateBuWebsite: '应用更新成功,但是网站配置文件修改失败,请检查配置!'
Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}'
ErrFileParse: '应用 docker-compose 文件解析失败!'
@@ -59,7 +57,6 @@ ErrFileParseApp: '{{ .name }} 文件解析失败 {{ .err }}'
ErrAppDirNull: '版本文件夹不存在'
LocalAppErr: "应用 {{ .name }} 同步失败!{{ .err }}"
ErrContainerName: "容器名称已存在"
ErrAppSystemRestart: "1Panel 重启导致任务终止"
ErrCreateHttpClient: "创建请求失败 {{ .err }}"
ErrHttpReqTimeOut: "请求超时 {{ .err }}"
ErrHttpReqFailed: "请求失败 {{ .err }}"
@@ -69,7 +66,6 @@ ErrImagePullTimeOut: '镜像拉取超时'
ErrContainerNotFound: '{{ .name }} 容器不存在'
ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日志'
ErrAppBackup: '{{ .name }} 应用备份失败 {{ .err }}'
ErrImagePull: '镜像拉取失败 {{ .err }}'
ErrVersionTooLow: '当前 1Panel 版本过低,无法更新应用商店,请升级版本之后操作'
ErrAppNameExist: '应用名称已存在'
AppStoreIsSyncing: '应用商店正在同步中,请稍后再试'
@@ -80,8 +76,6 @@ ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}"
AppRecover: "应用 {{ .name }} 回滚 "
PullImageStart: "开始拉取镜像 {{ .name }}"
PullImageSuccess: "镜像拉取成功"
UpgradeAppStart: "开始升级应用 {{ .name }}"
UpgradeAppSuccess: "应用 {{ .name }} 升级成功"
AppStoreSyncSuccess: "应用商店同步成功"
SyncAppDetail: "同步应用配置"
AppVersionNotMatch: "{{ .name }} 应用需要更高的 1Panel 版本,跳过同步"
@@ -90,8 +84,6 @@ MoveSiteToDir: "迁移网站目录到 {{ .name }}"
ErrMoveSiteDir: "迁移网站目录失败"
MoveSiteDirSuccess: "迁移网站目录成功"
DeleteRuntimePHP: "删除 PHP 运行环境"
CustomAppStoreNotConfig: "请在应用商店设置离线包地址"
CustomAppStoreNotFound: "应用商店包获取失败,请检查是否存在"
CustomAppStoreFileValid: "应用商店包需要 .tar.gz 格式"
PullImageTimeout: "拉取镜像超时,请尝试增加镜像加速或者更换其他镜像加速"
ErrAppIsDown: "{{ .name }} 应用状态异常,请检查"
@@ -114,9 +106,7 @@ ErrInvalidChar: "禁止使用非法字符"
ErrPathNotDelete: "所选目录不可删除"
#website
ErrDomainIsExist: "域名已存在"
ErrAliasIsExist: "代号已存在"
ErrAppDelete: '其他网站使用此应用,无法删除'
ErrBackupMatch: '该备份文件与当前网站部分数据不匹配 {{ .detail }}'
ErrBackupExist: '该备份文件对应部分源数据不存在 {{ .detail }}'
ErrPHPResource: '本地运行环境不支持切换!'
@@ -179,7 +169,6 @@ ErrPortRules: "端口数目不匹配,请重新输入!"
ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 {{ .name }} 镜像后重试"
#runtime
ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!"
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
ErrImageBuildErr: "镜像 build 失败"
ErrImageExist: "镜像已存在!"
@@ -389,7 +378,7 @@ ContainerStartCheck: "检查容器是否已启动"
# task - image
ImageBuild: "镜像构建"
ImageBuildStdoutCheck: "解析镜像输出内容"
ImaegBuildRes: "镜像构建输出:{{ .name }}"
ImageBuildRes: "镜像构建输出:{{ .name }}"
ImagePull: "拉取镜像"
ImageRepoAuthFromDB: "从数据库获取仓库认证信息"
ImaegPullRes: "镜像拉取输出:{{ .name }}"
+1
View File
@@ -30,4 +30,5 @@ func Init() {
global.Dir.RuntimeDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/runtime"))
global.Dir.RecycleBinDir, _ = fileOp.CreateDirWithPath(true, "/.1panel_clash")
global.Dir.SSLLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/ssl"))
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
}
+1
View File
@@ -33,6 +33,7 @@ func InitAgentDB() {
migrations.UpdateWebsite,
migrations.UpdateWebsiteAcmeAccount,
migrations.UpdateAppInstall,
migrations.AddMcpServer,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+14
View File
@@ -60,6 +60,10 @@ var AddTable = &gormigrate.Migration{
&model.WebsiteSSL{},
&model.Group{},
&model.AppIgnoreUpgrade{},
&model.McpServer{},
&model.MonitorBase{},
&model.MonitorIO{},
&model.MonitorNetwork{},
)
},
}
@@ -362,3 +366,13 @@ var UpdateAppInstall = &gormigrate.Migration{
return nil
},
}
var AddMcpServer = &gormigrate.Migration{
ID: "20250428-add-mcpServer",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
return err
}
return nil
},
}
@@ -24,5 +24,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/domain/bind", baseApi.BindDomain)
aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain)
aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer)
aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer)
aiToolsRouter.POST("/mcp/server/del", baseApi.DeleteMcpServer)
aiToolsRouter.POST("/mcp/server/op", baseApi.OperateMcpServer)
aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain)
aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain)
aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain)
}
}
+1
View File
@@ -58,6 +58,7 @@ var WebUrlMap = map[string]struct{}{
"/ai": {},
"/ai/model": {},
"/ai/gpu": {},
"/ai/mcp": {},
"/containers": {},
"/containers/container/operate": {},
+1
View File
@@ -20,6 +20,7 @@ func LoadMenus() string {
Children: []dto.ShowMenu{
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "OllamaModel", Path: "/ai/model"},
{ID: "42", Disabled: false, Title: "aiTools.gpu.gpu", IsShow: true, Label: "GPU", Path: "/ai/gpu"},
{ID: "43", Disabled: false, Title: "aiTools.mcp.server", IsShow: true, Label: "MCPServer", Path: "/ai/mcp"},
}},
{ID: "5", Disabled: false, Title: "menu.database", IsShow: true, Label: "Database-Menu", Path: "/databases"},
{ID: "6", Disabled: false, Title: "menu.container", IsShow: true, Label: "Container-Menu", Path: "/containers"},
+1 -1
View File
@@ -296,7 +296,7 @@ var AddMFAInterval = &gormigrate.Migration{
}
var UpdateXpackHideMemu = &gormigrate.Migration{
ID: "20250414-update-xpack-hide-menu",
ID: "20250429-update-xpack-hide-menu",
Migrate: func(tx *gorm.DB) error {
if err := tx.Model(&model.Setting{}).Where("key = ?", "HideMenu").Updates(map[string]interface{}{"key": "HideMenu", "value": helper.LoadMenus()}).Error; err != nil {
return err
+71
View File
@@ -109,4 +109,75 @@ export namespace AI {
connUrl: string;
acmeAccountID: number;
}
export interface Environment {
key: string;
value: string;
}
export interface Volume {
source: string;
target: string;
}
export interface McpServer {
id: number;
name: string;
status: string;
baseUrl: string;
ssePath: string;
command: string;
port: number;
message: string;
createdAt?: string;
containerName: string;
environments: Environment[];
volumes: Volume[];
dir?: string;
hostIP: string;
protocol: string;
url: string;
}
export interface McpServerSearch extends ReqPage {
name: string;
}
export interface McpServerDelete {
id: number;
}
export interface McpServerOperate {
id: number;
operate: string;
}
export interface McpBindDomain {
domain: string;
sslID: number;
ipList: string;
}
export interface McpDomainRes {
domain: string;
sslID: number;
acmeAccountID: number;
allowIPs: string[];
websiteID?: number;
connUrl: string;
}
export interface McpBindDomainUpdate {
websiteID: number;
sslID: number;
ipList: string;
}
export interface ImportMcpServer {
name: string;
command: string;
ssePath: string;
containerName: string;
environments: Environment[];
}
}
+32
View File
@@ -39,3 +39,35 @@ export const getBindDomain = (req: AI.BindDomainReq) => {
export const updateBindDomain = (req: AI.BindDomain) => {
return http.post(`/ai/domain/update`, req);
};
export const pageMcpServer = (req: AI.McpServerSearch) => {
return http.post<ResPage<AI.McpServer>>(`/ai/mcp/search`, req);
};
export const createMcpServer = (req: AI.McpServer) => {
return http.post(`/ai/mcp/server`, req);
};
export const updateMcpServer = (req: AI.McpServer) => {
return http.post(`/ai/mcp/server/update`, req);
};
export const deleteMcpServer = (req: AI.McpServerDelete) => {
return http.post(`/ai/mcp/server/del`, req);
};
export const operateMcpServer = (req: AI.McpServerOperate) => {
return http.post(`/ai/mcp/server/op`, req);
};
export const bindMcpDomain = (req: AI.McpBindDomain) => {
return http.post(`/ai/mcp/domain/bind`, req);
};
export const getMcpDomain = () => {
return http.get<AI.McpDomainRes>(`/ai/mcp/domain/get`);
};
export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => {
return http.post(`/ai/mcp/domain/update`, req);
};
+23
View File
@@ -664,6 +664,29 @@ const message = {
migModeHelper: 'Used to create MIG instances for physical isolation of the GPU at the user level.',
migModeNA: 'Not Supported',
},
mcp: {
server: 'MCP Server',
create: 'Add MCP Server',
edit: 'Edit MCP Server',
commandHelper: 'For example: npx -y {0}',
baseUrl: 'External Access Path',
baseUrlHelper: 'For example: http://192.168.1.2:8000',
ssePath: 'SSE Path',
ssePathHelper: 'For example: /sse, note not to duplicate with other servers',
environment: 'Environment Variables',
envKey: 'Variable Name',
envValue: 'Variable Value',
externalUrl: 'External Connection Address',
operatorHelper: 'Will perform {1} operation on {0}, continue?',
domain: 'Default Access Address',
domainHelper: 'For example: 192.168.1.1 or example.com',
bindDomain: 'Bind Website',
commandPlaceHolder: 'Currently only supports npx and binary startup commands',
importMcpJson: 'Import MCP Server Configuration',
importMcpJsonError: 'mcpServers structure is incorrect',
bindDomainHelper:
'After binding the website, it will modify the access address of all installed MCP Servers and close external access to the ports',
},
},
container: {
create: 'Create',
+23
View File
@@ -653,6 +653,29 @@ const message = {
migModeHelper: 'ユーザーレベルでGPUの物理的分離を行うためのMIGインスタンスを作成するために使用されます',
migModeNA: 'サポートされていません',
},
mcp: {
server: 'MCP サーバー',
create: 'サーバーを追加',
edit: 'サーバーを編集',
commandHelper: ': npx -y {0}',
baseUrl: '外部アクセスパス',
baseUrlHelper: ': http://192.168.1.2:8000',
ssePath: 'SSE パス',
ssePathHelper: ': /sse, 他のサーバーと重複しないように注意してください',
environment: '環境変数',
envKey: '変数名',
envValue: '変数値',
externalUrl: '外部接続アドレス',
operatorHelper: '{0} {1} 操作を実行します続行しますか',
domain: 'デフォルトアクセスアドレス',
domainHelper: ': 192.168.1.1 または example.com',
bindDomain: 'ウェブサイトをバインド',
commandPlaceHolder: '現在npx およびバイナリスタートアップコマンドのみをサポートしています',
importMcpJson: 'MCP サーバー設定をインポート',
importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper:
'ウェブサイトをバインドした後インストールされたすべての MCP サーバーのアクセスアドレスを変更しポートへの外部アクセスを閉じます',
},
},
container: {
create: 'コンテナを作成します',
+23
View File
@@ -649,6 +649,29 @@ const message = {
migModeHelper: '사용자 수준에서 GPU 물리적으로 분리하는 MIG 인스턴스를 생성하는 사용됩니다.',
migModeNA: '지원되지 않음',
},
mcp: {
server: 'MCP サーバー',
create: 'サーバーを追加',
edit: 'サーバーを編集',
commandHelper: ': npx -y {0}',
baseUrl: '外部アクセスパス',
baseUrlHelper: ': http://192.168.1.2:8000',
ssePath: 'SSE パス',
ssePathHelper: ': /sse, 他のサーバーと重複しないように注意してください',
environment: '環境変数',
envKey: '変数名',
envValue: '変数値',
externalUrl: '外部接続アドレス',
operatorHelper: '{0} {1} 操作を実行します続行しますか',
domain: 'デフォルトアクセスアドレス',
domainHelper: ': 192.168.1.1 または example.com',
bindDomain: 'ウェブサイトをバインド',
commandPlaceHolder: '현재 npx 바이너리 시작 명령만 지원합니다',
importMcpJson: 'MCP サーバー設定をインポート',
importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper:
'웹사이트를 바인딩한 , 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
},
},
container: {
create: '컨테이너 만들기',
+23
View File
@@ -666,6 +666,29 @@ const message = {
migModeHelper: 'Digunakan untuk membuat contoh MIG bagi pengasingan fizikal GPU pada tahap pengguna.',
migModeNA: 'Tidak Disokong',
},
mcp: {
server: 'Pelayan MCP',
create: 'Tambah Pelayan',
edit: 'Edit Pelayan',
commandHelper: 'Contoh: npx -y {0}',
baseUrl: 'Laluan Akses Luar',
baseUrlHelper: 'Contoh: http://192.168.1.2:8000',
ssePath: 'Laluan SSE',
ssePathHelper: 'Contoh: /sse, berhati-hati jangan bertindan dengan pelayan lain',
environment: 'Pemboleh Ubah Persekitaran',
envKey: 'Nama Pemboleh Ubah',
envValue: 'Nilai Pemboleh Ubah',
externalUrl: 'Alamat Sambungan Luar',
operatorHelper: 'Akan melakukan operasi {1} pada {0}, teruskan?',
domain: 'Alamat Akses Lalai',
domainHelper: 'Contoh: 192.168.1.1 atau example.com',
bindDomain: 'Sematkan Laman Web',
commandPlaceHolder: 'Kini hanya menyokong perintah pelancaran npx dan binari',
importMcpJson: 'Import Konfigurasi Pelayan MCP',
importMcpJsonError: 'Struktur mcpServers tidak betul',
bindDomainHelper:
'Setelah mengikat laman web, ia akan mengubah alamat akses semua Pelayan MCP yang dipasang dan menutup akses luaran ke pelabuhan',
},
},
container: {
create: 'Cipta kontena',
+23
View File
@@ -662,6 +662,29 @@ const message = {
migModeHelper: 'Usado para criar instâncias MIG para isolamento físico da GPU no nível do usuário.',
migModeNA: 'Não Suportado',
},
mcp: {
server: 'Servidor MCP',
create: 'Adicionar Servidor',
edit: 'Editar Servidor',
commandHelper: 'Por exemplo: npx -y {0}',
baseUrl: 'Caminho de Acesso Externo',
baseUrlHelper: 'Por exemplo: http://192.168.1.2:8000',
ssePath: 'Caminho SSE',
ssePathHelper: 'Por exemplo: /sse, tome cuidado para não duplicar com outros servidores',
environment: 'Variáveis de Ambiente',
envKey: 'Nome da Variável',
envValue: 'Valor da Variável',
externalUrl: 'Endereço de Conexão Externo',
operatorHelper: 'Será realizada a operação {1} no {0}, continuar?',
domain: 'Endereço de Acesso Padrão',
domainHelper: 'Por exemplo: 192.168.1.1 ou example.com',
bindDomain: 'Vincular Site',
commandPlaceHolder: 'Atualmente, apenas comandos de inicialização npx e binários são suportados',
importMcpJson: 'Importar Configuração do Servidor MCP',
importMcpJsonError: 'A estrutura mcpServers está incorreta',
bindDomainHelper:
'Após vincular o site, ele modificará o endereço de acesso de todos os servidores MCP instalados e fechará o acesso externo às portas',
},
},
container: {
create: 'Criar contêiner',
+23
View File
@@ -662,6 +662,29 @@ const message = {
'Используется для создания MIG-инстансов для физической изоляции GPU на уровне пользователя.',
migModeNA: 'Не поддерживается',
},
mcp: {
server: 'Сервер MCP',
create: 'Добавить сервер',
edit: 'Редактировать сервер',
commandHelper: 'Например: npx -y {0}',
baseUrl: 'Внешний путь доступа',
baseUrlHelper: 'Например: http://192.168.1.2:8000',
ssePath: 'Путь SSE',
ssePathHelper: 'Например: /sse, будьте осторожны, чтобы не дублировать с другими серверами',
environment: 'Переменные среды',
envKey: 'Имя переменной',
envValue: 'Значение переменной',
externalUrl: 'Внешний адрес подключения',
operatorHelper: 'Будет выполнена операция {1} на {0}, продолжить?',
domain: 'Адрес доступа по умолчанию',
domainHelper: 'Например: 192.168.1.1 или example.com',
bindDomain: 'Привязать сайт',
commandPlaceHolder: 'В настоящее время поддерживаются только команды запуска npx и двоичных файлов',
importMcpJson: 'Импортировать конфигурацию сервера MCP',
importMcpJsonError: 'Структура mcpServers некорректна',
bindDomainHelper:
'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам',
},
},
container: {
create: 'Создать контейнер',
+22
View File
@@ -642,6 +642,28 @@ const message = {
migModeHelper: '用於建立 MIG 實例在用戶層實現 GPU 的物理隔離',
migModeNA: '不支援',
},
mcp: {
server: 'MCP Server',
create: '创建 MCP Server',
edit: '編輯 MCP Server',
commandHelper: '例如npx -y {0}',
baseUrl: '外部訪問路徑',
baseUrlHelper: '例如http://192.168.1.2:8000',
ssePath: 'SSE 路徑',
ssePathHelper: '例如/sse,注意不要與其他 Server 重複',
environment: '環境變數',
envKey: '變數名',
envValue: '變數值',
externalUrl: '外部連接地址',
operatorHelper: '將對 {0} 進行 {1} 操作是否繼續',
domain: '默認訪問地址',
domainHelper: '例如192.168.1.1 或者 example.com',
bindDomain: '綁定網站',
commandPlaceHolder: '當前僅支持 npx 二進制啟動的命令',
importMcpJson: '導入 MCP Server配置',
importMcpJsonError: 'mcpServers 結構不正確',
bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址並關閉端口的外部訪問',
},
},
container: {
create: '創建容器',
+22
View File
@@ -641,6 +641,28 @@ const message = {
migModeNA: '不支持',
shr: '共享显存',
},
mcp: {
server: 'MCP Server',
create: '创建 MCP Server',
edit: '编辑 MCP Server',
commandHelper: '例如npx -y {0}',
baseUrl: '外部访问路径',
baseUrlHelper: '例如http://192.168.1.1:8000',
ssePath: 'SSE 路径',
ssePathHelper: '例如/sse,注意不要与其他 Server 重复',
environment: '环境变量',
envKey: '变量名',
envValue: '变量值',
externalUrl: '外部连接地址',
operatorHelper: '将对 {0} 进行 {1} 操作是否继续',
domain: '默认访问地址',
domainHelper: '例如192.168.1.1 或者 example.com',
bindDomain: '绑定网站',
commandPlaceHolder: '当前仅支持 npx 二进制启动的命令',
importMcpJson: '导入 MCP Server 配置',
importMcpJsonError: 'mcpServers 结构不正确',
bindDomainHelper: '绑定网站之后会修改所有已安装 MCP Server 的访问地址并关闭端口的外部访问',
},
},
container: {
create: '创建容器',
+9
View File
@@ -20,6 +20,15 @@ const databaseRouter = {
requiresAuth: true,
},
},
{
path: '/ai/mcp',
name: 'MCPServer',
component: () => import('@/views/ai/mcp/server/index.vue'),
meta: {
title: 'MCP',
requiresAuth: true,
},
},
{
path: '/ai/gpu',
name: 'GPU',
+17
View File
@@ -0,0 +1,17 @@
<template>
<div>
<RouterButton :buttons="buttons" />
<LayoutContent>
<router-view></router-view>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
const buttons = [
{
label: 'Servers',
path: '/ai/mcp/servers',
},
];
</script>
@@ -0,0 +1,228 @@
<template>
<DrawerPro v-model="open" :header="$t('aiTools.mcp.bindDomain')" @close="handleClose" size="normal">
<div v-loading="loading">
<el-form ref="formRef" label-position="top" @submit.prevent :model="req" :rules="rules">
<el-alert class="common-prompt" :closable="false" type="warning">
<template #default>
<ul>
<li>{{ $t('aiTools.proxy.proxyHelper1') }}</li>
<li>{{ $t('aiTools.proxy.proxyHelper2') }}</li>
<li>{{ $t('aiTools.proxy.proxyHelper3') }}</li>
</ul>
</template>
</el-alert>
<el-form-item :label="$t('website.domain')" prop="domain">
<el-input v-model.trim="req.domain" :disabled="operate === 'update'" />
<span class="input-help">
{{ $t('aiTools.proxy.proxyHelper4') }}
</span>
<span class="input-help">
{{ $t('aiTools.proxy.proxyHelper6') }}
<el-link class="pageRoute" icon="Position" @click="toWebsite(req.websiteID)" type="primary">
{{ $t('firewall.quickJump') }}
</el-link>
</span>
<el-text type="danger">{{ $t('aiTools.mcp.bindDomainHelper') }}</el-text>
</el-form-item>
<el-form-item :label="$t('xpack.waf.whiteList') + ' IP'" prop="ipList">
<el-input
:rows="3"
type="textarea"
clearable
v-model="req.ipList"
:placeholder="$t('xpack.waf.ipGroupHelper')"
/>
<span class="input-help">
{{ $t('aiTools.proxy.whiteListHelper') }}
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="req.enableSSL" @change="changeSSL">
{{ $t('website.enable') + ' ' + 'HTTPS' }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('website.acmeAccountManage')" prop="acmeAccountID" v-if="req.enableSSL">
<el-select v-model="req.acmeAccountID" :placeholder="$t('website.selectAcme')" @change="listSSL">
<el-option :key="0" :label="$t('website.imported')" :value="0"></el-option>
<el-option
v-for="(acme, index) in acmeAccounts"
:key="index"
:label="acme.email"
:value="acme.id"
>
<span>
{{ acme.email }}
<el-tag class="ml-5">{{ getAccountName(acme.type) }}</el-tag>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.ssl')" prop="sslID" v-if="req.enableSSL">
<el-select
v-model="req.sslID"
:placeholder="$t('website.selectSSL')"
@change="changeSSl(req.sslID)"
>
<el-option
v-for="(ssl, index) in ssls"
:key="index"
:label="ssl.primaryDomain"
:value="ssl.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="handleClose">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.add') }}
</el-button>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { Website } from '@/api/interface/website';
import { listSSL, searchAcmeAccount } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
import { getAccountName } from '@/utils/util';
import { bindMcpDomain, getMcpDomain, updateMcpDomain } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const open = ref(false);
const operate = ref('create');
const loading = ref(false);
const ssls = ref([]);
const websiteSSL = ref<Website.SSL>();
const acmeAccounts = ref();
const formRef = ref();
const req = ref({
domain: '',
sslID: undefined,
ipList: '',
acmeAccountID: 0,
enableSSL: false,
allowIPs: [],
websiteID: 0,
});
const rules = reactive<FormRules>({
domain: [Rules.domainWithPort],
sslID: [Rules.requiredSelectBusiness],
});
const emit = defineEmits(['close']);
const handleClose = () => {
emit('close');
open.value = false;
};
const acceptParams = () => {
search();
open.value = true;
};
const changeSSl = (sslid: number) => {
const res = ssls.value.filter((element: Website.SSL) => {
return element.id == sslid;
});
websiteSSL.value = res[0];
};
const changeSSL = () => {
if (!req.value.enableSSL) {
req.value.sslID = undefined;
} else {
listAcmeAccount();
}
};
const listSSLs = () => {
const sslReq = {
acmeAccountID: String(req.value.acmeAccountID),
};
listSSL(sslReq).then((res) => {
ssls.value = res.data || [];
if (ssls.value.length > 0) {
let exist = false;
for (const ssl of ssls.value) {
if (ssl.id === req.value.sslID) {
exist = true;
break;
}
}
if (!exist) {
req.value.sslID = ssls.value[0].id;
}
changeSSl(req.value.sslID);
} else {
req.value.sslID = undefined;
}
});
};
const listAcmeAccount = () => {
searchAcmeAccount({ page: 1, pageSize: 100 }).then((res) => {
acmeAccounts.value = res.data.items || [];
listSSLs();
});
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (operate.value === 'update') {
await updateMcpDomain(req.value);
} else {
await bindMcpDomain(req.value);
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
handleClose();
});
};
const search = async () => {
try {
const res = await getMcpDomain();
if (res.data.websiteID > 0) {
operate.value = 'update';
req.value.domain = res.data.domain;
req.value.websiteID = res.data.websiteID;
if (res.data.allowIPs && res.data.allowIPs.length > 0) {
req.value.ipList = res.data.allowIPs.join('\n');
}
if (res.data.sslID > 0) {
req.value.enableSSL = true;
req.value.sslID = res.data.sslID;
req.value.acmeAccountID = res.data.acmeAccountID;
listAcmeAccount();
}
}
} catch (e) {}
};
const toWebsite = (websiteID: number) => {
if (websiteID != undefined && websiteID > 0) {
window.location.href = `/websites/${websiteID}/config/basic`;
} else {
window.location.href = '/websites';
}
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.pageRoute {
font-size: 12px;
margin-left: 5px;
}
</style>
@@ -0,0 +1,54 @@
<template>
<DrawerPro v-model="open" :header="$t('menu.config')" @close="handleClose" size="normal">
<codemirror
:autofocus="true"
:placeholder="$t('commons.msg.noneData')"
:indent-with-tab="true"
:tabSize="4"
style="height: 300px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="prettyJson"
:disabled="true"
/>
<CopyButton :content="prettyJson" class="mt-2" />
<template #footer>
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { AI } from '@/api/interface/ai';
import { ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
const extensions = [javascript(), oneDark];
const open = ref(false);
const jsonObj = ref({
mcpServers: {},
});
const prettyJson = ref('');
const handleClose = () => {
open.value = false;
};
const acceptParams = (mcpServer: AI.McpServer) => {
jsonObj.value.mcpServers = {};
jsonObj.value.mcpServers[mcpServer.name] = {
url: mcpServer.baseUrl + mcpServer.ssePath,
};
prettyJson.value = JSON.stringify(jsonObj.value, null, 2);
open.value = true;
};
defineExpose({
acceptParams,
});
</script>
@@ -0,0 +1,80 @@
<template>
<el-dialog v-model="submitVisible" :destroy-on-close="true" :close-on-click-modal="false" width="40%">
<template #header>
{{ $t('aiTools.mcp.importMcpJson') }}
</template>
<div>
<el-input
v-model="mcpServerJson"
type="textarea"
:rows="15"
placeholder='{
"mcpServers": {
"postgres": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://localhost/mydb"
]
}
}
}'
></el-input>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onConfirm">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import i18n from '@/lang';
import { ref } from 'vue';
const submitVisible = ref(false);
const mcpServerJson = ref();
const mcpServerConfig = ref();
const acceptParams = (): void => {
mcpServerJson.value = '';
submitVisible.value = true;
};
const emit = defineEmits(['confirm', 'cancel']);
const onConfirm = async () => {
try {
const data = JSON.parse(mcpServerJson.value);
if (!data.mcpServers || typeof data.mcpServers !== 'object') {
throw new Error(i18n.global.t('mcp.importMcpJsonError'));
}
mcpServerConfig.value = Object.entries(data.mcpServers).map(([name, config]: any) => ({
name,
command: [config.command, ...config.args].join(' '),
environments: config.env ? Object.entries(config.env).map(([key, value]) => ({ key, value })) : [],
ssePath: '/' + name,
containerName: name,
}));
} catch (error) {
return;
}
emit('confirm', mcpServerConfig.value);
submitVisible.value = false;
};
const onCancel = async () => {
emit('cancel');
submitVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>
+256
View File
@@ -0,0 +1,256 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'Servers'" v-loading="loading">
<template #toolbar>
<div class="flex flex-wrap gap-3">
<el-button type="primary" @click="openCreate">
{{ $t('aiTools.mcp.create') }}
</el-button>
<el-button type="primary" plain @click="openDomain">
{{ $t('aiTools.mcp.bindDomain') }}
</el-button>
</div>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column
:label="$t('commons.table.name')"
fix
prop="name"
width="200px"
show-overflow-tooltip
>
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="openDetail(row)">
{{ row.name }}
</el-text>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.mcp.externalUrl')" prop="baseUrl" min-width="200px">
<template #default="{ row }">
{{ row.baseUrl + row.ssePath }}
<CopyButton :content="row.baseUrl + row.ssePath" type="icon" />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status" width="120px">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
popper-class="max-h-[300px] overflow-auto"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.button.log')" prop="path" width="120px">
<template #default="{ row }">
<el-button
@click="openLog(row)"
link
type="primary"
:disabled="
row.status !== 'Running' && row.status !== 'Rrror' && row.status !== 'Restarting'
"
>
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
width="180"
fix
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 2"
:min-width="mobile ? 'auto' : 200"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<McpServerOperate ref="createRef" @close="searchWithTimeOut" />
<OpDialog ref="opRef" @search="search" />
<ComposeLogs ref="composeLogRef" />
<BindDomain ref="bindDomainRef" @close="searchWithTimeOut" />
<Config ref="configRef" />
</div>
</template>
<script lang="ts" setup>
import { AI } from '@/api/interface/ai';
import { deleteMcpServer, operateMcpServer, pageMcpServer } from '@/api/modules/ai';
import RouterMenu from '@/views/ai/mcp/index.vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/util';
import McpServerOperate from './operate/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue';
import { GlobalStore } from '@/store';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import BindDomain from './bind/index.vue';
import Config from './config/index.vue';
const globalStore = GlobalStore();
const loading = ref(false);
const createRef = ref();
const opRef = ref();
const composeLogRef = ref();
const bindDomainRef = ref();
const configRef = ref();
const items = ref<AI.McpServer[]>([]);
const paginationConfig = reactive({
cacheSizeKey: 'mcp-server-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
const mobile = computed(() => {
return globalStore.isMobile();
});
const buttons = [
{
label: i18n.global.t('menu.config'),
click: (row: AI.McpServer) => {
openConfig(row);
},
},
{
label: i18n.global.t('commons.button.edit'),
click: (row: AI.McpServer) => {
openDetail(row);
},
},
{
label: i18n.global.t('commons.button.start'),
click: (row: AI.McpServer) => {
opServer(row, 'start');
},
disabled: (row: AI.McpServer) => {
return row.status === 'Running';
},
},
{
label: i18n.global.t('commons.button.stop'),
click: (row: AI.McpServer) => {
opServer(row, 'stop');
},
disabled: (row: AI.McpServer) => {
return row.status === 'Stopped';
},
},
{
label: i18n.global.t('commons.button.restart'),
click: (row: AI.McpServer) => {
opServer(row, 'restart');
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.McpServer) => {
deleteServer(row);
},
},
];
const searchWithTimeOut = () => {
search();
setTimeout(() => {
search();
}, 1000);
};
const search = () => {
loading.value = true;
pageMcpServer({
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
name: '',
}).then((res) => {
items.value = res.data.items;
paginationConfig.total = res.data.total;
loading.value = false;
});
};
const openDetail = (row: AI.McpServer) => {
createRef.value.acceptParams(row);
};
const openCreate = () => {
let maxPort = 7999;
if (items.value && items.value.length > 0) {
maxPort = Math.max(...items.value.map((item) => item.port));
}
createRef.value.acceptParams({ port: maxPort + 1 });
};
const openLog = (row: AI.McpServer) => {
composeLogRef.value.acceptParams({ compose: row.dir + '/docker-compose.yml', resource: row.name });
};
const deleteServer = async (row: AI.McpServer) => {
try {
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: [row.name],
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('aiTools.mcp.server'),
i18n.global.t('commons.button.delete'),
]),
api: deleteMcpServer,
params: { id: row.id },
});
} catch (error) {}
};
const opServer = async (row: AI.McpServer, operate: string) => {
ElMessageBox.confirm(
i18n.global.t('aiTools.mcp.operatorHelper', [
i18n.global.t('aiTools.mcp.server'),
i18n.global.t('commons.button.' + operate),
]),
i18n.global.t('commons.button.' + operate),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
try {
await operateMcpServer({ id: row.id, operate: operate });
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
} catch (error) {}
});
};
const openDomain = () => {
bindDomainRef.value.acceptParams();
};
const openConfig = (row: AI.McpServer) => {
configRef.value.acceptParams(row);
};
onMounted(() => {
search();
});
</script>
@@ -0,0 +1,275 @@
<template>
<DrawerPro
v-model="open"
:header="$t('aiTools.mcp.' + mode)"
:resource="mcpServer.name"
@close="handleClose"
size="large"
>
<el-form
ref="mcpServerForm"
label-position="top"
:model="mcpServer"
label-width="125px"
:rules="rules"
v-loading="loading"
>
<el-form-item>
<el-button @click="importRef.acceptParams()" type="primary" plain>
{{ $t('aiTools.mcp.importMcpJson') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="mcpServer.name" :disabled="mode == 'edit'" />
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="command">
<el-input
v-model="mcpServer.command"
type="textarea"
:rows="3"
:placeholder="$t('aiTools.mcp.commandPlaceHolder')"
></el-input>
<span class="input-help">
{{ $t('aiTools.mcp.commandHelper', ['@modelcontextprotocol/server-github']) }}
</span>
</el-form-item>
<div>
<el-text>{{ $t('aiTools.mcp.environment') }}</el-text>
<div class="mt-1">
<el-row :gutter="20" v-for="(env, index) in mcpServer.environments" :key="index">
<el-col :span="8">
<el-form-item :prop="`environments.${index}.key`" :rules="rules.key">
<el-input v-model="env.key" :placeholder="$t('mcp.envKey')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :prop="`environments.${index}.value`" :rules="rules.value">
<el-input v-model="env.value" :placeholder="$t('mcp.envValue')" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item>
<el-button type="primary" @click="removeEnv(index)" link class="mt-1">
{{ $t('commons.button.delete') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="4">
<el-button class="mb-2" @click="addEnv">{{ $t('commons.button.add') }}</el-button>
</el-col>
</el-row>
</div>
</div>
<Volumes :volumes="mcpServer.volumes" class="mb-2" />
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input v-model.number="mcpServer.port" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="$t('app.allowPort')" prop="hostIP">
<el-switch v-model="mcpServer.hostIP" :active-value="'0.0.0.0'" :inactive-value="'127.0.0.1'" />
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('aiTools.mcp.baseUrl')" prop="url">
<el-input v-model.trim="mcpServer.url">
<template #prepend>
<el-select v-model="mcpServer.protocol" class="pre-select">
<el-option label="http" value="http://" />
<el-option label="https" value="https://" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('aiTools.mcp.baseUrlHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="mcpServer.containerName"></el-input>
</el-form-item>
<el-form-item :label="$t('aiTools.mcp.ssePath')" prop="ssePath">
<el-input v-model.trim="mcpServer.ssePath"></el-input>
<span class="input-help">
{{ $t('aiTools.mcp.ssePathHelper') }}
</span>
</el-form-item>
</el-form>
<template #footer>
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(mcpServerForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
<Import ref="importRef" @confirm="getImport" />
</template>
<script lang="ts" setup>
import { AI } from '@/api/interface/ai';
import { createMcpServer, getMcpDomain, updateMcpServer } from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref, watch } from 'vue';
import Volumes from '../volume/index.vue';
import Import from '../import/index.vue';
const open = ref(false);
const mode = ref('create');
const loading = ref(false);
const mcpServerForm = ref();
const importRef = ref();
const newMcpServer = () => {
return {
id: 0,
name: '',
port: 8000,
status: '',
message: '',
baseUrl: '',
ssePath: '',
command: '',
containerName: '',
environments: [],
volumes: [],
hostIP: '127.0.0.1',
protocol: 'http://',
url: '',
};
};
const em = defineEmits(['close']);
const mcpServer = ref(newMcpServer());
const rules = ref({
name: [Rules.requiredInput, Rules.appName],
command: [Rules.requiredInput],
port: [Rules.requiredInput, Rules.port],
containerName: [Rules.requiredInput],
url: [Rules.requiredInput],
ssePath: [Rules.requiredInput],
key: [Rules.requiredInput],
value: [Rules.requiredInput],
});
const hasWebsite = ref(false);
const acceptParams = async (params: AI.McpServer) => {
hasWebsite.value = false;
mode.value = params.id ? 'edit' : 'create';
let mcpDomainRes;
try {
mcpDomainRes = await getMcpDomain();
if (mcpDomainRes.data.connUrl != '') {
hasWebsite.value = true;
}
} catch (error) {}
if (mode.value == 'edit') {
mcpServer.value = params;
if (!mcpServer.value.environments) {
mcpServer.value.environments = [];
}
if (!mcpServer.value.volumes) {
mcpServer.value.volumes = [];
}
const parts = mcpServer.value.baseUrl.split(/(https?:\/\/)/).filter(Boolean);
mcpServer.value.protocol = parts[0];
mcpServer.value.url = parts[1];
} else {
mcpServer.value = newMcpServer();
if (params.port) {
mcpServer.value.port = params.port;
}
if (mcpDomainRes.data && mcpDomainRes.data.connUrl != '') {
const parts = mcpDomainRes.data.connUrl.split(/(https?:\/\/)/).filter(Boolean);
mcpServer.value.protocol = parts[0];
mcpServer.value.url = parts[1];
mcpServer.value.baseUrl = mcpDomainRes.data.connUrl;
}
}
open.value = true;
};
watch(
() => mcpServer.value.name,
(newVal) => {
if (newVal && mode.value == 'create') {
mcpServer.value.containerName = newVal;
mcpServer.value.ssePath = '/' + newVal;
}
},
{ deep: true },
);
const addEnv = () => {
mcpServer.value.environments.push({
key: '',
value: '',
});
};
const removeEnv = (index: number) => {
mcpServer.value.environments.splice(index, 1);
};
const handleClose = () => {
open.value = false;
em('close', false);
};
const getImport = async (data: AI.ImportMcpServer[]) => {
if (!data) {
return;
}
const importServer = data[0];
mcpServer.value.name = importServer.name;
mcpServer.value.containerName = importServer.containerName;
mcpServer.value.ssePath = importServer.ssePath;
mcpServer.value.command = importServer.command;
mcpServer.value.environments = importServer.environments || [];
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid) => {
if (!valid) {
return;
}
let request = true;
if (mcpServer.value.hostIP != '0.0.0.0' && !hasWebsite.value) {
await ElMessageBox.confirm(i18n.global.t('app.installWarn'), i18n.global.t('app.checkTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).catch(() => {
request = false;
});
}
if (!request) {
return;
}
try {
loading.value = true;
mcpServer.value.baseUrl = mcpServer.value.protocol + mcpServer.value.url;
if (mode.value == 'create') {
await createMcpServer(mcpServer.value);
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
} else {
await updateMcpServer(mcpServer.value);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
}
handleClose();
} finally {
loading.value = false;
}
});
};
defineExpose({
acceptParams,
});
</script>
@@ -0,0 +1,60 @@
<template>
<div class="mt-2">
<el-text>{{ $t('container.mount') }}</el-text>
<div class="mt-2">
<el-row :gutter="20" v-for="(volume, index) in volumes" :key="index">
<el-col :span="8">
<el-form-item :prop="`volumes.${index}.source`" :rules="rules.value">
<el-input v-model="volume.source" :placeholder="$t('container.hostOption')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :prop="`volumes.${index}.target`" :rules="rules.value">
<el-input v-model="volume.target" :placeholder="$t('container.containerDir')" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item>
<el-button type="primary" @click="removeEnv(index)" link class="mt-1">
{{ $t('commons.button.delete') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="4">
<el-button @click="addEnv">{{ $t('commons.button.add') }}</el-button>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, reactive } from 'vue';
import { FormRules } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { AI } from '@/api/interface/ai';
const props = defineProps({
volumes: {
type: Array<AI.Volume>,
required: true,
},
});
const rules = reactive<FormRules>({
value: [Rules.requiredInput],
});
const addEnv = () => {
props.volumes.push({
source: '',
target: '',
});
};
const removeEnv = (index: number) => {
props.volumes.splice(index, 1);
};
</script>
@@ -1,5 +1,5 @@
<template>
<DrawerPro v-model="open" :header="$t('website.create')" size="large" @close="handleClose">
<DrawerPro v-model="open" :header="$t('website.create')" size="60%" @close="handleClose">
<template #buttons>
<span class="drawer-header-button">
<template v-for="item in WebsiteTypes" :key="item.value">