Compare commits

...
Author SHA1 Message Date
王贺 0be45b6a55 Modify the install script 2025-06-10 17:18:51 +08:00
CityFun c922615183 feat: fix issue with build frontend failed (#8994) 2025-06-10 16:57:45 +08:00
CityFun 8b2990fca4 feat: change 1panel v1 docs url (#8993) 2025-06-10 16:22:04 +08:00
longjuan 37b5cd743a feat: add name attrs to login inputs for password managers to recognize (#8953) 2025-06-10 16:21:59 +08:00
玲珑芯ExquisiteCore 3b1324db08 feat: Move current status to first position in MySQL settings and set… (#8882)
* feat: Move current status to first position in MySQL and Redis settings and set as default tab
2025-06-10 16:21:53 +08:00
Alan f56bbb7dc1 ini filetype support (#8628) 2025-06-10 16:21:43 +08:00
CityFun cff649ae95 fix: Fix issue with update website config (#8610) 2025-06-10 16:21:38 +08:00
Wankko Ree cb87ca4361 fix: the docker image which no tag but used be pruned (#8529) 2025-06-10 16:21:32 +08:00
igophper 165d2b1290 fix(cronjob): prevent unnecessary log updates and clean log formatting (#8481)
Refs #8425
2025-06-10 16:21:25 +08:00
igophper 189555e16c fix(editor): reset edit state after not saving content (#8474)
Refs #8461
2025-06-10 16:21:12 +08:00
ChengPlay 2d8250926f Fix issue with errors when reverse proxy source text includes comments. (#8444)
Refs https://github.com/1Panel-dev/1Panel/issues/8442
2025-06-10 16:20:52 +08:00
ChengPlay 47e343712e feat: change commandPlaceHolder translate (#8391) 2025-06-10 16:20:33 +08:00
ssongliu 11ffb4f2be fix: Fix the mysql root account permission switchover exception (#8378) 2025-06-10 16:20:21 +08:00
zhengkunwang e7670f96de feat: update mcp proxy_pass (#8374) 2025-04-10 16:45:24 +08:00
zhengkunwang 342c04b30a fix: fix issue with mcp bind website (#8356) 2025-04-09 03:55:56 +00:00
zhengkunwang 5ab8377ac1 feat: update allowPort check (#8355) 2025-04-09 02:47:54 +00:00
ssongliu 74eb2c8dd6 fix: Fix the problem that jumping to a directory that does not exist … (#8353) 2025-04-08 14:33:54 +00:00
zhengkunwang f136d97941 change mcp helper (#8352) 2025-04-08 10:59:53 +00:00
ssongliu a5629e3f42 fix: Fix the problem of failed MySQL permission modification (#8351) 2025-04-08 10:49:53 +00:00
zhengkunwang 515d6ca500 fix: fix bug with mcp-server status error (#8349) 2025-04-08 09:21:53 +00:00
zhengkunwang c0ccc0318d feat: add warn text for mcp server (#8346) 2025-04-08 05:55:52 +00:00
zhengkunwang 8196c11010 feat: import mcp server support env (#8344) 2025-04-08 03:11:52 +00:00
ssongliu 170bd22062 fix: Modify database verification rules (#8342) 2025-04-08 02:39:52 +00:00
zhengkunwang 097a33c68f style: update style for mcp server (#8338) 2025-04-07 11:09:51 +00:00
zhengkunwang 3be3bf9926 feat: remove env from mcp config (#8337) 2025-04-07 10:03:50 +00:00
zhengkunwang 038f2b4f54 feat: change mcp list style (#8336) 2025-04-07 09:35:50 +00:00
zhengkunwang f84e284c20 feat: change DNSTypes order (#8335) 2025-04-07 09:01:50 +00:00
ssongliu 48e75b5916 fix: Set bridge as the default network for container creation. (#8332) 2025-04-07 07:47:49 +00:00
zhengkunwang a12ceaee6e feat: add config for mcp server (#8331) 2025-04-07 07:41:51 +00:00
zhengkunwang 9050680115 feat: update mcp form label (#8327) 2025-04-07 02:49:49 +00:00
zhengkunwang 0022a9b508 fix: fix issue with install app no label (#8326) 2025-04-07 02:47:49 +00:00
zhengkunwang 3437d62f31 feat: Website SSL DNS Type support WestCN (#8305) 2025-04-02 10:21:54 +00:00
ssongliu dc880835d0 fix: Modify the container terminal connection mode (#8301) 2025-04-02 10:19:54 +00:00
ssongliu 1663bf8677 fix: Regenerates the interface document (#8298) 2025-04-02 09:07:54 +00:00
ssongliu aed236cbc9 fix: Ignore the scheduled task backup content failed to be uploaded (#8297)
Refs #7869
2025-04-02 09:05:54 +00:00
ssongliu 90be74504e feat: Support the retention of remote backup files when deleting sche… (#8296)
Refs #8236
2025-04-02 09:03:55 +00:00
zhengkunwang bba8aab18c feat: Add MCP management (#8299) 2025-04-02 09:01:54 +00:00
ssongliu 47d135ecca fix: Optimize database password verification and password change (#8294) 2025-04-02 16:03:44 +08:00
ssongliu d96fe49993 fix: Resolve cloudns reference error (#8293) 2025-04-02 03:41:53 +00:00
ssongliu ac396b61ce fix: Modify the panel password verification rule (#8291)
Refs #4290
2025-04-02 03:39:54 +00:00
ssongliu 6798d0c7ee fix: Fixed an update failure when the container network is bridge (#8290)
Refs #8209
2025-04-02 03:37:53 +00:00
亖 1490ed7541 dev:修复注释路由前缀 (#8292) 2025-04-02 10:10:00 +08:00
白铭骢 (Mingcong Bai) 48b3032de6 chore(deps): update modernc.org/sqlite to v1.29.7 (#8260)
With modernc.org/sqlite@v1.29.1, 1Panel fails to build on LoongArch with
the following error:

../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:13:35: undefined: sqlite3_index_constraint
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:14:35: undefined: sqlite3_index_orderby
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:15:35: undefined: sqlite3_index_constraint_usage
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:19:5: undefined: Xsqlite3_threadsafe
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:11: undefined: Xsqlite3_config
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:32: undefined: SQLITE_CONFIG_MUTEX
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:122: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:30:8: undefined: Xsqlite3_errstr
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:40:17: undefined: Sqlite3_mutex_methods
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: too many errors

Upstream introduced LoongArch (loong64) target support in
modernc.org/sqlite@v1.29.7. Take a conservative approach and bump
modernc.org/sqlite to v1.29.7, this will allow for 1Panel to build on
LoongArch (both frontend and backend).

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2025-03-27 13:45:10 +08:00
sunsky ed12bec460 feat: add DNS provider ClouDNS (#8207)
* feat: add DNS provider ClouDNS

* feat: ClouDNS account parameter validation
2025-03-25 10:12:14 +08:00
dependabot[bot] 3c4a1ea614 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#8229)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 17:38:30 +08:00
99 changed files with 4576 additions and 1989 deletions
+2
View File
@@ -68,4 +68,6 @@ var (
favoriteService = service.NewIFavoriteService()
websiteCAService = service.NewIWebsiteCAService()
mcpServerService = service.NewIMcpServerService()
)
+12 -3
View File
@@ -410,11 +410,20 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if _, err := os.Stat(req.Path); err != nil {
helper.SuccessWithData(c, false)
fileOp := files.NewFileOp()
if fileOp.Stat(req.Path) {
helper.SuccessWithData(c, true)
return
}
helper.SuccessWithData(c, true)
if req.WithInit {
if err := fileOp.CreateDir(req.Path, 0644); err != nil {
helper.SuccessWithData(c, false)
return
}
helper.SuccessWithData(c, true)
return
}
helper.SuccessWithData(c, false)
}
// @Tags File
+8 -8
View File
@@ -14,7 +14,7 @@ import (
// @Success 200 {object} response.HostToolRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool [post]
// @Router /hosts/tool [post]
func (b *BaseApi) GetToolStatus(c *gin.Context) {
var req request.HostToolReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -36,7 +36,7 @@ func (b *BaseApi) GetToolStatus(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/create [post]
// @Router /hosts/tool/create [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"}
func (b *BaseApi) InitToolConfig(c *gin.Context) {
var req request.HostToolCreate
@@ -58,7 +58,7 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/operate [post]
// @Router /hosts/tool/operate [post]
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
func (b *BaseApi) OperateTool(c *gin.Context) {
var req request.HostToolReq
@@ -80,7 +80,7 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
// @Success 200 {object} response.HostToolConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/config [post]
// @Router /hosts/tool/config [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"}
func (b *BaseApi) OperateToolConfig(c *gin.Context) {
var req request.HostToolConfig
@@ -103,7 +103,7 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
// @Success 200 {string} logContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/log [post]
// @Router /hosts/tool/log [post]
func (b *BaseApi) GetToolLog(c *gin.Context) {
var req request.HostToolLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -125,7 +125,7 @@ func (b *BaseApi) GetToolLog(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process [post]
// @Router /hosts/tool/supervisor/process [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"}
func (b *BaseApi) OperateProcess(c *gin.Context) {
var req request.SupervisorProcessConfig
@@ -147,7 +147,7 @@ func (b *BaseApi) OperateProcess(c *gin.Context) {
// @Success 200 {object} response.SupervisorProcessConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process [get]
// @Router /hosts/tool/supervisor/process [get]
func (b *BaseApi) GetProcess(c *gin.Context) {
configs, err := hostToolService.GetSupervisorProcessConfig()
if err != nil {
@@ -164,7 +164,7 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
// @Success 200 {string} content
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process/file [post]
// @Router /hosts/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq
+167
View File
@@ -0,0 +1,167 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/constant"
"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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(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.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
+8 -8
View File
@@ -12,7 +12,7 @@ import (
// @Success 200 {object} dto.SSHInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/search [post]
// @Router /hosts/ssh/search [post]
func (b *BaseApi) GetSSHInfo(c *gin.Context) {
info, err := sshService.GetSSHInfo()
if err != nil {
@@ -29,7 +29,7 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/operate [post]
// @Router /hosts/ssh/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"}
func (b *BaseApi) OperateSSH(c *gin.Context) {
var req dto.Operate
@@ -51,7 +51,7 @@ func (b *BaseApi) OperateSSH(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/update [post]
// @Router /hosts/ssh/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"}
func (b *BaseApi) UpdateSSH(c *gin.Context) {
var req dto.SSHUpdate
@@ -73,7 +73,7 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/conffile/update [post]
// @Router /hosts/conffile/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
var req dto.SSHConf
@@ -95,7 +95,7 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/generate [post]
// @Router /hosts/ssh/generate [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
func (b *BaseApi) GenerateSSH(c *gin.Context) {
var req dto.GenerateSSH
@@ -117,7 +117,7 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
// @Success 200 {string} data
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/secret [post]
// @Router /hosts/ssh/secret [post]
func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
var req dto.GenerateLoad
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -139,7 +139,7 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
// @Success 200 {object} dto.SSHLog
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/log [post]
// @Router /hosts/ssh/log [post]
func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
var req dto.SearchSSHLog
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -159,7 +159,7 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
// @Success 200 {string} data
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/conf [get]
// @Router /hosts/ssh/conf [get]
func (b *BaseApi) LoadSSHConf(c *gin.Context) {
data, err := sshService.LoadSSHConf()
if err != nil {
+24 -23
View File
@@ -96,7 +96,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
}
source := c.Query("source")
var containerID string
var initCmd string
var initCmd []string
switch source {
case "redis":
containerID, initCmd, err = loadRedisInitCmd(c)
@@ -113,11 +113,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
return
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand("clear && " + initCmd)
slave, err := terminal.NewCommand(initCmd)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
@@ -137,62 +137,63 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
}
func loadRedisInitCmd(c *gin.Context) (string, string, error) {
func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
from := c.Query("from")
commands := "redis-cli"
commands := []string{"exec", "-it"}
database, err := databaseService.Get(name)
if err != nil {
return "", "", fmt.Errorf("no such database in db, err: %v", err)
return "", nil, fmt.Errorf("no such database in db, err: %v", err)
}
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
name = redisInfo.ContainerName
commands = append(commands, []string{name, "redis-cli"}...)
if len(database.Password) != 0 {
commands = "redis-cli -a " + database.Password + " --no-auth-warning"
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
} else {
commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
if len(database.Password) != 0 {
commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
}
name = "1Panel-redis-cli-tools"
commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
if len(database.Password) != 0 {
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
}
return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
return name, commands, nil
}
func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
if cmd.CheckIllegal(name) {
return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
}
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
containerName := ollamaInfo.ContainerName
return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
}
func loadContainerInitCmd(c *gin.Context) (string, string, error) {
func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
if cmd.CheckIllegal(user, containerID, command) {
return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
}
if len(command) == 0 || len(containerID) == 0 {
return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
}
command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
commands := []string{"exec", "-it", containerID, command}
if len(user) != 0 {
command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
commands = []string{"exec", "-it", "-u", user, containerID, command}
}
return containerID, command, nil
return containerID, commands, nil
}
func wshandleError(ws *websocket.Conn, err error) bool {
+7 -5
View File
@@ -71,14 +71,16 @@ type CronjobDownload struct {
}
type CronjobClean struct {
IsDelete bool `json:"isDelete"`
CleanData bool `json:"cleanData"`
CronjobID uint `json:"cronjobID" validate:"required"`
IsDelete bool `json:"isDelete"`
CleanData bool `json:"cleanData"`
CleanRemoteData bool `json:"cleanRemoteData"`
CronjobID uint `json:"cronjobID" validate:"required"`
}
type CronjobBatchDelete struct {
CleanData bool `json:"cleanData"`
IDs []uint `json:"ids" validate:"required"`
CleanData bool `json:"cleanData"`
CleanRemoteData bool `json:"cleanRemoteData"`
IDs []uint `json:"ids" validate:"required"`
}
type CronjobInfo struct {
+10
View File
@@ -0,0 +1,10 @@
package dto
type DockerComposeService struct {
Image string `yaml:"image"`
ContainerName string `yaml:"container_name"`
Restart string `yaml:"restart"`
Ports []string `yaml:"ports"`
Environment []string `yaml:"environment"`
Command []string `yaml:"command"`
}
+2 -1
View File
@@ -76,7 +76,8 @@ type FileRename struct {
}
type FilePathCheck struct {
Path string `json:"path" validate:"required"`
Path string `json:"path" validate:"required"`
WithInit bool `json:"withInit"`
}
type FilePathsCheck struct {
+57
View File
@@ -0,0 +1,57 @@
package request
import "github.com/1Panel-dev/1Panel/backend/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 Environment struct {
Key string `json:"key"`
Value string `json:"value"`
}
type Volume struct {
Source string `json:"source"`
Target string `json:"target"`
}
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"`
}
+26
View File
@@ -0,0 +1,26 @@
package response
import (
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/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
@@ -16,6 +16,7 @@ type IGroupRepo interface {
Delete(opts ...DBOption) error
CancelDefault(groupType string) error
WithByHostDefault() DBOption
WithByWebsiteDefault() DBOption
}
func NewIGroupRepo() IGroupRepo {
@@ -56,6 +57,12 @@ func (u *GroupRepo) WithByHostDefault() DBOption {
}
}
func (u *GroupRepo) WithByWebsiteDefault() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("is_default = ? AND type = ?", 1, "website")
}
}
func (u *GroupRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
+56
View File
@@ -0,0 +1,56 @@
package repo
import "github.com/1Panel-dev/1Panel/backend/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
@@ -839,6 +839,9 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
}, commonRepo.WithByID(appInstall.ID))
if appKey == "mysql" || appKey == "postgresql" {
return nil
}
}
if param == "user-password" {
oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword)
+7 -5
View File
@@ -450,12 +450,14 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
networkSettings := oldContainer.NetworkSettings
bridgeNetworkSettings := networkSettings.Networks[data.Network]
if bridgeNetworkSettings.IPAMConfig != nil {
ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address
data.Ipv4 = ipv4Address
ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address
data.Ipv6 = ipv6Address
if data.Network != "bridge" {
data.Ipv4 = bridgeNetworkSettings.IPAMConfig.IPv4Address
data.Ipv6 = bridgeNetworkSettings.IPAMConfig.IPv6Address
}
} else {
data.Ipv4 = bridgeNetworkSettings.IPAddress
if data.Network != "bridge" {
data.Ipv4 = bridgeNetworkSettings.IPAddress
}
}
data.Cmd = oldContainer.Config.Cmd
+11 -2
View File
@@ -114,8 +114,17 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
if err != nil {
return err
}
if !req.CleanRemoteData {
for key := range accountMap {
if key != constant.Local {
delete(accountMap, key)
}
}
}
cronjob.RetainCopies = 0
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
if len(accountMap) != 0 {
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
}
} else {
u.removeExpiredLog(cronjob)
}
@@ -249,7 +258,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
global.Cron.Remove(cron.EntryID(idItem))
}
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil {
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, CleanRemoteData: req.CleanRemoteData, IsDelete: true}); err != nil {
return err
}
if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil {
+8 -1
View File
@@ -322,7 +322,8 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
if len(account) != 0 {
global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc))
if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
return "", err
global.LOG.Errorf("upload file to %s failed, err: %v", account, err)
continue
}
global.LOG.Debugf("upload successful!")
}
@@ -349,12 +350,18 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
if cronjob.Type == "snapshot" {
for _, account := range accounts {
if len(account) != 0 {
if _, ok := accountMap[account]; !ok {
continue
}
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
}
}
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz")))
} else {
for _, account := range accounts {
if _, ok := accountMap[account]; !ok {
continue
}
if len(account) != 0 {
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
}
+2
View File
@@ -46,4 +46,6 @@ var (
phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
favoriteRepo = repo.NewIFavoriteRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
)
+643
View File
@@ -0,0 +1,643 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/nginx"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/cmd/server/mcp"
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
"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(constant.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(commonRepo.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(constant.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.RuntimeStarting
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(constant.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(constant.McpDir, create.Name)
mcpServer := &model.McpServer{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.RuntimeStarting,
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(commonRepo.WithByID(id))
if err != nil {
return err
}
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(constant.McpDir, mcpServer.Name))
websiteID := GetWebsiteID()
if websiteID > 0 {
websiteService := NewIWebsiteService()
delProxyReq := request.WebsiteProxyDel{
ID: websiteID,
Name: mcpServer.Name,
}
_ = websiteService.DeleteProxy(delProxyReq)
}
return mcpServerRepo.DeleteBy(commonRepo.WithByID(id))
}
func (m McpServerService) Operate(req request.McpServerOperate) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.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.RuntimeRunning
case "stop":
out, err = compose.Down(composePath)
mcpServer.Status = constant.RuntimeStopped
case "restart":
out, err = compose.Restart(composePath)
mcpServer.Status = constant.RuntimeRunning
}
if err != nil {
mcpServer.Status = constant.RuntimeError
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(commonRepo.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(commonRepo.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(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
createWebsiteReq := request.WebsiteCreate{
PrimaryDomain: req.Domain,
Alias: strings.ToLower(req.Domain),
Type: constant.Static,
WebsiteGroupID: group.ID,
}
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 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 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(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
websiteService := NewIWebsiteService()
website, err := websiteRepo.GetFirst(commonRepo.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(commonRepo.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(commonRepo.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(constant.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(commonRepo.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(constant.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(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
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(constant.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(constant.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.RuntimeNormal {
_, _ = compose.Down(composePath)
}
if out, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = out
} else {
mcpServer.Status = constant.RuntimeRunning
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.RuntimeStopped
return mcpServerRepo.Save(mcpServer)
}
container := containers[0]
switch container.State {
case "exited":
mcpServer.Status = constant.RuntimeError
case "running":
mcpServer.Status = constant.RuntimeRunning
case "paused":
mcpServer.Status = constant.RuntimeStopped
case "restarting":
mcpServer.Status = constant.RuntimeReStarting
default:
if mcpServer.Status != constant.RuntimeBuildIng {
mcpServer.Status = constant.RuntimeStopped
}
}
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)
}
+22 -5
View File
@@ -427,7 +427,9 @@ func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error {
}
}
website.PrimaryDomain = req.PrimaryDomain
website.WebsiteGroupID = req.WebsiteGroupID
if req.WebsiteGroupID > 0 {
website.WebsiteGroupID = req.WebsiteGroupID
}
website.Remark = req.Remark
website.IPV6 = req.IPV6
@@ -496,6 +498,10 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil {
return err
}
websiteID := GetWebsiteID()
if req.ID == websiteID {
_ = settingRepo.Update("MCP_WEBSITE_ID", "0")
}
tx.Commit()
uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias))
@@ -1764,11 +1770,22 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
}
directives := config.GetDirectives()
location, ok := directives[0].(*components.Location)
if !ok {
err = errors.New("error")
return
var (
location *components.Location
ok bool
)
for _, directive := range directives {
if directive.GetName() == "location" {
location, ok = directive.(*components.Location)
if ok {
break
}
}
}
if location == nil {
return nil, buserr.New("ErrConfigParse")
}
proxyConfig.ProxyPass = location.ProxyPass
proxyConfig.Cache = location.Cache
if location.CacheTime > 0 {
+1
View File
@@ -35,6 +35,7 @@ var WebUrlMap = map[string]struct{}{
"/ai": {},
"/ai/model": {},
"/ai/gpu": {},
"/ai/mcp": {},
"/containers": {},
"/containers/container": {},
+1
View File
@@ -17,4 +17,5 @@ var (
RuntimeDir = path.Join(DataDir, "runtime")
RecycleBinDir = "/.1panel_clash"
SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
McpDir = path.Join(DataDir, "mcp")
)
+1
View File
@@ -13,6 +13,7 @@ const (
RuntimeStopped = "stopped"
RuntimeUnhealthy = "unhealthy"
RuntimeCreating = "creating"
RuntimeReStarting = "restarting"
RuntimePHP = "php"
RuntimeNode = "node"
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "mode: "
#ai-tool
ErrOpenrestyInstall: 'Please install Openresty first'
ErrSSL: "Certificate content is empty, please check the certificate!"
ErrSsePath: "SSE path is duplicated"
#mobile app
ErrVerifyToken: 'Token verification error, please reset and scan again.'
+1
View File
@@ -285,6 +285,7 @@ SystemMode: "モード: "
#ai-tool
ErrOpenrestyInstall: 'まず Openresty をインストールしてください'
ErrSSL: "証明書の内容が空です。証明書を確認してください!"
ErrSsePath: "SSE パスが重複しています"
#mobile app
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "모드: "
#ai-tool
ErrOpenrestyInstall: '먼저 Openresty를 설치하세요'
ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!"
ErrSsePath: "SSE 경로가 중복되었습니다"
#mobile app
+1 -1
View File
@@ -287,7 +287,7 @@ SystemMode: "Mod: "
#ai-tool
ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu'
ErrSSL: "Kandungan sijil kosong, sila periksa sijil!"
ErrSsePath: "Laluan SSE bertindan"
#mobile app
ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.'
+1
View File
@@ -285,6 +285,7 @@ SystemMode: "modo: "
#ai-tool
ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro'
ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!"
ErrSsePath: "Caminho SSE duplicado"
#mobile app
ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "режим: "
#ai-tool
ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала"
ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!"
ErrSsePath: "Путь SSE дублируется"
#mobile app
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "模式: "
#ai-tool
ErrOpenrestyInstall: "請先安裝 Openresty"
ErrSSL: "證書內容為空,請檢查證書!"
ErrSsePath: "SSE 路徑重複"
#mobile app
ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "模式: "
#ai-tool
ErrOpenrestyInstall: '请先安装 Openresty'
ErrSSL: "证书内容为空,请检查证书!"
ErrSsePath: "SSE 路径重复"
#mobile app
ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
+4 -1
View File
@@ -23,8 +23,11 @@ func Init() {
constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
constant.McpDir = path.Join(constant.DataDir, "mcp")
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir,
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir}
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir,
constant.SSLLogDir, constant.McpDir}
fileOp := files.NewFileOp()
for _, dir := range dirs {
+2
View File
@@ -106,6 +106,8 @@ func Init() {
migrations.AddAppMenu,
migrations.AddAppPanelName,
migrations.AddLicenseVerify,
migrations.AddMcpServer,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
@@ -459,3 +459,13 @@ var AddLicenseVerify = &gormigrate.Migration{
return nil
},
}
var AddMcpServer = &gormigrate.Migration{
ID: "20250401-add-mcp-server",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
return err
}
return nil
},
}
+1
View File
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
&ProcessRouter{},
&WebsiteCARouter{},
&AIToolsRouter{},
&McpServerRouter{},
}
}
+9
View File
@@ -26,5 +26,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)
}
}
+21
View File
@@ -0,0 +1,21 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
type McpServerRouter struct {
}
func (m *McpServerRouter) InitRouter(Router *gin.RouterGroup) {
mcpRouter := Router.Group("mcp")
mcpRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
mcpRouter.POST("/search", baseApi.PageMcpServers)
mcpRouter.POST("/server", baseApi.CreateMcpServer)
}
}
+3 -1
View File
@@ -46,7 +46,9 @@ func NewLocation(directive IDirective) *Location {
location.Host = params[1]
}
case "proxy_cache":
location.Cache = true
if params[0] != "off" {
location.Cache = true
}
case "if":
if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" {
dirs := dir.GetBlock().GetDirectives()
+37 -12
View File
@@ -3,6 +3,7 @@ package ssl
import (
"crypto"
"encoding/json"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"os"
"strings"
"time"
@@ -20,6 +21,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/cloudns"
"github.com/go-acme/lego/v4/providers/dns/dnspod"
"github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
@@ -75,6 +77,7 @@ const (
Volcengine DnsType = "Volcengine"
CloudFlare DnsType = "CloudFlare"
CloudDns DnsType = "CloudDns"
ClouDNS DnsType = "ClouDNS"
FreeMyIP DnsType = "FreeMyIP"
NameSilo DnsType = "NameSilo"
NameCheap DnsType = "NameCheap"
@@ -83,21 +86,26 @@ const (
TencentCloud DnsType = "TencentCloud"
HuaweiCloud DnsType = "HuaweiCloud"
RainYun DnsType = "RainYun"
WestCN DnsType = "WestCN"
)
type DNSParam struct {
ID string `json:"id"`
Token string `json:"token"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Email string `json:"email"`
APIkey string `json:"apiKey"`
APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"`
Region string `json:"region"`
ClientID string `json:"clientID"`
Password string `json:"password"`
ID string `json:"id"`
Token string `json:"token"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Email string `json:"email"`
APIkey string `json:"apiKey"`
APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"`
Region string `json:"region"`
ClientID string `json:"clientID"`
Password string `json:"password"`
AuthID string `json:"authID"`
SubAuthID string `json:"subAuthID"`
AuthPassword string `json:"authPassword"`
Username string `json:"username"`
}
var (
@@ -158,6 +166,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
clouddnsConfig.PollingInterval = pollingInterval
clouddnsConfig.TTL = ttl
p, err = clouddns.NewDNSProviderConfig(clouddnsConfig)
case ClouDNS:
cloudnsConfig := cloudns.NewDefaultConfig()
cloudnsConfig.AuthID = param.AuthID
cloudnsConfig.SubAuthID = param.SubAuthID
cloudnsConfig.AuthPassword = param.AuthPassword
cloudnsConfig.PropagationTimeout = propagationTimeout
cloudnsConfig.PollingInterval = pollingInterval
cloudnsConfig.TTL = ttl
p, err = cloudns.NewDNSProviderConfig(cloudnsConfig)
case FreeMyIP:
freeMyIpConfig := freemyip.NewDefaultConfig()
freeMyIpConfig.Token = param.Token
@@ -219,6 +236,14 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
rainyunConfig.PollingInterval = pollingInterval
rainyunConfig.TTL = ttl
p, err = rainyun.NewDNSProviderConfig(rainyunConfig)
case WestCN:
westcnConfig := westcn.NewDefaultConfig()
westcnConfig.Username = param.Username
westcnConfig.Password = param.Password
westcnConfig.PropagationTimeout = propagationTimeout
westcnConfig.PollingInterval = pollingInterval
westcnConfig.TTL = ttl
p, err = westcn.NewDNSProviderConfig(westcnConfig)
}
if err != nil {
return err
+2 -7
View File
@@ -25,8 +25,8 @@ type LocalCommand struct {
pty *os.File
}
func NewCommand(initCmd string) (*LocalCommand, error) {
cmd := exec.Command("bash")
func NewCommand(initCmd []string) (*LocalCommand, error) {
cmd := exec.Command("docker", initCmd...)
if term := os.Getenv("TERM"); term != "" {
cmd.Env = append(os.Environ(), "TERM="+term)
} else {
@@ -38,11 +38,6 @@ func NewCommand(initCmd string) (*LocalCommand, error) {
return nil, errors.Wrapf(err, "failed to start command")
}
if len(initCmd) != 0 {
time.Sleep(100 * time.Millisecond)
_, _ = pty.Write([]byte(initCmd + "\n"))
}
lcmd := &LocalCommand{
closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout,
+3 -3
View File
@@ -6,15 +6,15 @@ command -v wget >/dev/null || {
}
if [ ! -f "1pctl" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1pctl
wget https://github.com/1Panel-dev/installer/raw/dev/1pctl
fi
if [ ! -f "1panel.service" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1panel.service
wget https://github.com/1Panel-dev/installer/raw/dev/1panel.service
fi
if [ ! -f "install.sh" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/install.sh
wget https://github.com/1Panel-dev/installer/raw/dev/install.sh
fi
if [ ! -f "GeoIP.mmdb" ]; then
+629 -626
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+378 -384
View File
@@ -3342,6 +3342,8 @@ definitions:
type: string
lastCleanTime:
type: string
licenseVerify:
type: string
localTime:
type: string
menuTabs:
@@ -11237,381 +11239,6 @@ paths:
formatEN: update group [name][type]
formatZH: 更新组 [name][type]
paramKeys: []
/host/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/host/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/host/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/host/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/host/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/host/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/host/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/host/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/host/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/host/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/host/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/host/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/host/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/host/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/host/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts:
post:
consumes:
@@ -11878,6 +11505,32 @@ paths:
formatEN: update quick command [name]
formatZH: 更新快捷命令 [name]
paramKeys: []
/hosts/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/hosts/del:
post:
consumes:
@@ -12197,6 +11850,157 @@ paths:
summary: Page host
tags:
- Host
/hosts/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/hosts/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/hosts/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/hosts/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/hosts/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/hosts/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/hosts/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/hosts/test/byid/{id}:
post:
consumes:
@@ -12240,6 +12044,204 @@ paths:
summary: Test host conn by info
tags:
- Host
/hosts/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/hosts/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/hosts/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/hosts/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/hosts/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/hosts/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/hosts/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts/tree:
post:
consumes:
@@ -16766,15 +16768,7 @@ schemes:
- https
securityDefinitions:
ApiKeyAuth:
description: |-
Custom Token Format, Format: md5('1panel' + API-Key + UnixTimestamp).
```
eg:
curl -X GET "http://localhost:4004/api/v1/dashboard/current" \
-H "1Panel-Token: <1panel_token>" \
-H "1Panel-Timestamp: <current_unix_timestamp>"
```
- `1Panel-Token` is the key for the panel API Key.
description: '- `1Panel-Token` is the key for the panel API Key.'
in: header
name: 1Panel-Token
type: apiKey
+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
+3
View File
@@ -37,3 +37,6 @@ var DomainNotFoundHTML []byte
//go:embed stop.html
var StopHTML []byte
//go:embed sse.conf
var SSE []byte
+1
View File
@@ -7,6 +7,7 @@ location ^~ /test {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
+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;
}
+71
View File
@@ -109,4 +109,75 @@ export namespace AI {
websiteID?: number;
connUrl: string;
}
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[];
}
}
+1
View File
@@ -81,6 +81,7 @@ export namespace Cronjob {
export interface CronjobDelete {
ids: Array<number>;
cleanData: boolean;
cleanRemoteData: boolean;
}
export interface UpdateStatus {
id: number;
+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);
};
+6 -2
View File
@@ -27,8 +27,12 @@ export const searchRecords = (params: Cronjob.SearchRecord) => {
return http.post<ResPage<Cronjob.Record>>(`cronjobs/search/records`, params);
};
export const cleanRecords = (id: number, cleanData: boolean) => {
return http.post(`cronjobs/records/clean`, { cronjobID: id, cleanData: cleanData });
export const cleanRecords = (id: number, cleanData: boolean, cleanRemoteData: boolean) => {
return http.post(`cronjobs/records/clean`, {
cronjobID: id,
cleanData: cleanData,
cleanRemoteData: cleanRemoteData,
});
};
export const getRecordDetail = (params: string) => {
+2 -2
View File
@@ -49,8 +49,8 @@ export const SaveFileContent = (params: File.FileEdit) => {
return http.post<File.File>('files/save', params);
};
export const CheckFile = (path: string) => {
return http.post<boolean>('files/check', { path: path });
export const CheckFile = (path: string, withInit: boolean) => {
return http.post<boolean>('files/check', { path: path, withInit: withInit });
};
export const BatchCheckFiles = (paths: string[]) => {
+1 -1
View File
@@ -308,7 +308,7 @@ const onSubmit = async () => {
MsgError(i18n.global.t('commons.msg.fileNameErr'));
return;
}
const res = await CheckFile(baseDir.value + file.raw.name);
const res = await CheckFile(baseDir.value + file.raw.name, false);
if (res.data) {
MsgError(i18n.global.t('commons.msg.fileExist'));
return;
+2 -1
View File
@@ -121,7 +121,8 @@ const checkIllegal = (rule: any, value: any, callback: any) => {
value.indexOf('`') !== -1 ||
value.indexOf('(') !== -1 ||
value.indexOf(')') !== -1 ||
value.indexOf("'") !== -1
value.indexOf('>') !== -1 ||
value.indexOf('<') !== -1
) {
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
} else {
+26 -14
View File
@@ -95,6 +95,10 @@ export const Languages = [
label: 'css',
value: ['css'],
},
{
label: 'ini',
value: ['ini'],
},
];
export const Rewrites = [
@@ -169,21 +173,13 @@ export const DNSTypes = [
value: 'HuaweiCloud',
},
{
label: i18n.global.t('website.volcengine'),
value: 'Volcengine',
},
{
label: 'DNSPod (' + i18n.global.t('ssl.deprecated') + ')',
value: 'DnsPod',
label: 'GoDaddy',
value: 'Godaddy',
},
{
label: 'Cloudflare',
value: 'CloudFlare',
},
{
label: 'CloudDNS',
value: 'CloudDns',
},
{
label: 'NameSilo',
value: 'NameSilo',
@@ -193,12 +189,16 @@ export const DNSTypes = [
value: 'NameCheap',
},
{
label: 'Name.com',
value: 'NameCom',
label: 'CloudDNS',
value: 'CloudDns',
},
{
label: 'GoDaddy',
value: 'Godaddy',
label: 'ClouDNS',
value: 'ClouDNS',
},
{
label: 'Name.com',
value: 'NameCom',
},
{
label: 'FreeMyIP',
@@ -208,6 +208,18 @@ export const DNSTypes = [
label: i18n.global.t('website.rainyun'),
value: 'RainYun',
},
{
label: i18n.global.t('website.westCN'),
value: 'WestCN',
},
{
label: i18n.global.t('website.volcengine'),
value: 'Volcengine',
},
{
label: 'DNSPod (' + i18n.global.t('ssl.deprecated') + ')',
value: 'DnsPod',
},
];
export const Fields = [
+32 -2
View File
@@ -191,6 +191,7 @@ const message = {
rePassword: 'Confirm password is inconsistent with the password.',
requiredInput: 'This field is required.',
requiredSelect: 'Select an item in the list',
illegalChar: 'Injection of characters & ; $ \' ` ( ) " > < | is currently not supported',
illegalInput: `This field mustn't contains illegal characters.`,
commonName:
'This field must start with non-special characters and must consist of English, Chinese, numbers, ".", "-", and "_" characters with a length of 1-128.',
@@ -427,6 +428,8 @@ const message = {
permission: 'Permissions',
permissionForIP: 'IP',
permissionAll: 'All of them(%)',
localhostHelper:
'Configuring database permissions as "localhost" for container deployment will prevent external access to the container. Please choose carefully!',
databaseConnInfo: 'Connection info',
rootPassword: 'Root password',
serviceName: 'Service Name',
@@ -438,6 +441,7 @@ const message = {
remoteConnHelper:
'Remote connection to MySQL as user root may have security risks. Therefore, perform this operation with caution.',
changePassword: 'Password',
changeConnHelper: 'This operation will modify the current database {0}. Do you want to continue?',
changePasswordHelper:
'The database has been associated with an application. Changing the password will change the database password of the application at the same time. The change takes effect after the application restarts.',
@@ -1009,6 +1013,7 @@ const message = {
errHandle: 'Cronjob execution failure',
noRecord: 'Trigger the Cron Job, and you will see the records here.',
cleanData: 'Clean data',
cleanRemoteData: 'Delete remote data',
cleanDataHelper: 'Delete the backup file generated during this task.',
noLogs: 'No task output yet...',
errPath: 'Backup path [{0}] error, cannot download!',
@@ -1380,6 +1385,7 @@ const message = {
existFileHelper: 'The uploaded file contains a file with the same name, do you want to overwrite it?',
existFileSize: 'File size (new -> old)',
existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution!',
noSuchFile: 'The file or directory was not found. Please check and try again.',
},
ssh: {
setting: 'Setting',
@@ -2256,6 +2262,7 @@ const message = {
"When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.",
huaweicloud: 'Huawei Cloud',
rainyun: 'Rain Yun',
westCN: 'West CN',
},
php: {
short_open_tag: 'Short tag support',
@@ -2537,8 +2544,8 @@ const message = {
env: 'Environments',
noenv: 'None',
net: 'Network connections',
laddr: 'Source address/port',
raddr: 'Destination address/port',
laddr: 'Local address/port',
raddr: 'Remote address/port',
stopProcess: 'End',
viewDetails: 'Details',
stopProcessWarn: 'Are you sure you want to end this process (PID:{0})?',
@@ -2597,6 +2604,29 @@ const message = {
proxyHelper6: 'To disable proxy configuration, you can delete it from the website list.',
whiteListHelper: 'Restrict access to only IPs in the whitelist',
},
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',
},
};
export default {
+31
View File
@@ -190,6 +190,7 @@ const message = {
rePassword: 'パスワードがパスワードと矛盾することを確認してください。',
requiredInput: 'この項目は必須です。',
requiredSelect: 'リスト内のアイテムを選択します',
illegalChar: '現在、文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません',
illegalInput: `このフィールドには違法なキャラクターが含まれてはなりません。`,
commonName:
'このフィールドは、特別なキャラクターではなく、英語、中国語、数字で構成されている必要があります。「。」、「」、および「_」文字が1〜128の文字で構成されている必要があります。',
@@ -427,6 +428,8 @@ const message = {
permission: '権限',
permissionForIP: 'ip',
permissionAll: 'それらすべて(%)',
localhostHelper:
'コンテナ展開でデータベース権限を"localhost"に設定すると、コンテナ外部からのアクセスができなくなります。慎重に選択してください!',
databaseConnInfo: '接続情報',
rootPassword: 'ルートパスワード',
serviceName: 'サービス名',
@@ -438,6 +441,7 @@ const message = {
remoteConnHelper:
'ユーザールートとしてのMySQLへのリモート接続には、セキュリティリスクがある場合があります。したがって、この操作を慎重に実行します。',
changePassword: 'パスワード',
changeConnHelper: 'この操作は現在のデータベース {0} を変更します。続行しますか?',
changePasswordHelper:
'データベースはアプリケーションに関連付けられています。パスワードを変更すると、アプリケーションのデータベースパスワードが同時に変更されます。アプリケーションが再起動した後、変更は有効になります。',
@@ -994,6 +998,7 @@ const message = {
errHandle: 'cronjob実行障害',
noRecord: 'Cronジョブをトリガーすると、ここにレコードが表示されます。',
cleanData: 'クリーンデータ',
cleanRemoteData: 'リモートデータを削除',
cleanDataHelper: 'このタスク中に生成されたバックアップファイルを削除します。',
noLogs: 'タスク出力はまだありません...',
errPath: 'バックアップパス[{0}]エラー、ダウンロードできません!',
@@ -1358,6 +1363,7 @@ const message = {
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?',
existFileSize: 'ファイルサイズ(新しい -> 古い)',
existFileDirHelper: '選択したファイル/フォルダーには同じ名前のものが既に存在します。慎重に操作してください!',
noSuchFile: 'ファイルまたはディレクトリが見つかりませんでした。確認して再試行してください。',
},
ssh: {
setting: '設定',
@@ -2228,6 +2234,8 @@ const message = {
sniHelper:
'逆プロキシバックエンドがHTTPSの場合、Origin SNIを設定する必要がある場合があります。詳細については、CDNサービスプロバイダーのドキュメントを参照してください。',
huaweicloud: 'huaweiCloud',
rainyun: 'Rain Yun',
westCN: 'West CN',
},
php: {
short_open_tag: '短いタグサポート',
@@ -2566,6 +2574,29 @@ const message = {
proxyHelper6: 'プロキシ設定を無効にするには、ウェブサイトリストから削除できます。',
whiteListHelper: 'ホワイトリスト内のIPのみアクセスを許可する',
},
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 サーバーのアクセスアドレスを変更し、ポートへの外部アクセスを閉じます',
},
};
export default {
...fit2cloudJaLocale,
+30
View File
@@ -190,6 +190,7 @@ const message = {
rePassword: '확인 비밀번호가 비밀번호와 일치하지 않습니다.',
requiredInput: '이 필드는 필수 항목입니다.',
requiredSelect: '목록에서 항목을 선택하세요',
illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다',
illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.',
commonName:
'이 필드는 특수 문자로 시작할 수 없으며, 영어, 한자, 숫자, ".", "-", "_" 문자로 구성되어야 하며 길이는 1-128자여야 합니다.',
@@ -429,6 +430,8 @@ const message = {
permission: '권한',
permissionForIP: 'IP',
permissionAll: '모두(%)',
localhostHelper:
'컨테이너 배포 시 데이터베이스 권한을 "localhost"로 설정하면 컨테이너 외부에서 접근할 수 없게 됩니다. 신중하게 선택하세요!',
databaseConnInfo: '연결 정보',
rootPassword: '루트 비밀번호',
serviceName: '서비스 이름',
@@ -440,6 +443,7 @@ const message = {
remoteConnHelper:
'MySQL 의 root 사용자로 원격 접속은 보안 위험을 초래할 수 있습니다. 따라서 이 작업은 신중히 수행해야 합니다.',
changePassword: '비밀번호',
changeConnHelper: '이 작업은 현재 데이터베이스 {0}을(를) 수정합니다. 계속하시겠습니까?',
changePasswordHelper:
'데이터베이스가 애플리케이션과 연결되어 있습니다. 비밀번호를 변경하면 애플리케이션의 데이터베이스 비밀번호도 변경됩니다. 변경 사항은 애플리케이션이 재시작된 후에 적용됩니다.',
confChange: '설정',
@@ -988,6 +992,7 @@ const message = {
errHandle: '크론 작업 실행 실패',
noRecord: '크론 작업을 트리거하고 나면 여기에 레코드가 표시됩니다.',
cleanData: '데이터 정리',
cleanRemoteData: '원격 데이터 삭제',
cleanDataHelper: '이 작업에서 생성된 백업 파일을 삭제합니다.',
noLogs: '작업 출력이 아직 없습니다...',
errPath: '백업 경로 [{0}] 오류, 다운로드할 수 없습니다!',
@@ -1344,6 +1349,7 @@ const message = {
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
existFileSize: '파일 크기 (새로운 -> 오래된)',
existFileDirHelper: '선택한 파일/폴더에 동일한 이름이 이미 존재합니다. 신중하게 작업하세요!',
noSuchFile: '파일 또는 디렉터리를 찾을 수 없습니다. 확인 후 다시 시도하세요.',
},
ssh: {
setting: '설정',
@@ -2190,6 +2196,7 @@ const message = {
'역방향 프록시 백엔드가 HTTPS 인 경우 원본 SNI 를 설정해야 할 수 있습니다. 자세한 내용은 CDN 서비스 제공자의 문서를 참조하세요.',
huaweicloud: '화웨이 클라우드',
rainyun: '레인윤',
westCN: 'West CN',
},
php: {
short_open_tag: '짧은 태그 지원',
@@ -2526,6 +2533,29 @@ const message = {
proxyHelper6: '프록시 구성을 비활성화하려면 웹사이트 목록에서 삭제할 수 있습니다.',
whiteListHelper: '화이트리스트에 있는 IP만 접근 허용',
},
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 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
},
};
export default {
+30
View File
@@ -193,6 +193,7 @@ const message = {
rePassword: 'Pengesahan kata laluan tidak sepadan dengan kata laluan.',
requiredInput: 'Ruangan ini wajib diisi.',
requiredSelect: 'Pilih satu item dalam senarai',
illegalChar: 'Suntikan aksara & ; $ \' ` ( ) " > < | tidak disokong buat masa ini',
illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.',
commonName:
'Ruangan ini mesti bermula dengan aksara bukan khas dan mesti terdiri daripada aksara rumi, Cina, nombor, ".", "-", dan "_" dengan panjang 1-128 aksara.',
@@ -435,6 +436,8 @@ const message = {
permission: 'Kebenaran',
permissionForIP: 'IP',
permissionAll: 'Kesemuanya(%)',
localhostHelper:
'Mengkonfigurasi kebenaran pangkalan data sebagai "localhost" untuk penyebaran kontena akan menghalang akses luar ke kontena. Sila pilih dengan teliti!',
databaseConnInfo: 'Maklumat sambungan',
rootPassword: 'Kata laluan root',
serviceName: 'Nama Perkhidmatan',
@@ -446,6 +449,7 @@ const message = {
remoteConnHelper:
'Sambungan jauh ke MySQL sebagai pengguna root mungkin mempunyai risiko keselamatan. Oleh itu, lakukan operasi ini dengan berhati-hati.',
changePassword: 'Kata laluan',
changeConnHelper: 'Operasi ini akan mengubah pangkalan data semasa {0}. Adakah anda ingin meneruskan?',
changePasswordHelper:
'Pangkalan data telah dikaitkan dengan aplikasi. Menukar kata laluan akan menukar kata laluan pangkalan data aplikasi pada masa yang sama. Perubahan ini akan berkuat kuasa selepas aplikasi dimulakan semula.',
@@ -1023,6 +1027,7 @@ const message = {
errHandle: 'Kegagalan pelaksanaan tugas cron',
noRecord: 'Picu Tugas Cron, dan anda akan melihat rekod di sini.',
cleanData: 'Bersihkan data',
cleanRemoteData: 'Padam data jarak jauh',
cleanDataHelper: 'Padam fail sandaran yang dijana semasa tugas ini.',
noLogs: 'Tiada keluaran tugas lagi...',
errPath: 'Laluan sandaran [{0}] salah, tidak boleh dimuat turun!',
@@ -1401,6 +1406,7 @@ const message = {
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',
existFileSize: 'Saiz fail (baru -> lama)',
existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!',
noSuchFile: 'Fail atau direktori tidak ditemui. Sila periksa dan cuba lagi.',
},
ssh: {
setting: 'tetapan',
@@ -2279,6 +2285,7 @@ const message = {
'Apabila backend proksi terbalik adalah HTTPS, anda mungkin perlu menetapkan sumber SNI. Sila rujuk dokumentasi penyedia perkhidmatan CDN untuk butiran.',
huaweicloud: 'Huawei Cloud',
rainyun: 'Rain Yun',
westCN: 'West CN',
},
php: {
short_open_tag: 'Sokongan tag pendek',
@@ -2625,6 +2632,29 @@ const message = {
proxyHelper6: 'Untuk melumpuhkan konfigurasi proksi, anda boleh memadamnya dari senarai laman web.',
whiteListHelper: 'Hadkan akses kepada hanya IP dalam senarai putih',
},
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',
},
};
export default {
+31
View File
@@ -192,6 +192,7 @@ const message = {
rePassword: 'A confirmação da senha não corresponde à senha.',
requiredInput: 'Este campo é obrigatório.',
requiredSelect: 'Selecione um item na lista',
illegalChar: 'Atualmente não há suporte para injeção dos caracteres & ; $ \' ` ( ) " > < |',
illegalInput: 'Este campo não deve conter caracteres ilegais.',
commonName:
'Este campo deve começar com caracteres não especiais e consistir em letras, números, ".", "-", e "_" com comprimento de 1-128.',
@@ -433,6 +434,8 @@ const message = {
permission: 'Permissões',
permissionForIP: 'IP',
permissionAll: 'Todos (% de)',
localhostHelper:
'Configurar permissões de banco de dados como "localhost" para implantação em contêiner impedirá o acesso externo ao contêiner. Por favor, escolha com cuidado!',
databaseConnInfo: 'Informações de conexão',
rootPassword: 'Senha root',
serviceName: 'Nome do serviço',
@@ -444,6 +447,7 @@ const message = {
remoteConnHelper:
'Conectar-se ao MySQL como usuário root pode representar riscos de segurança. Realize esta operação com cautela.',
changePassword: 'Senha',
changeConnHelper: 'Esta operação modificará o banco de dados atual {0}. Deseja continuar?',
changePasswordHelper:
'O banco de dados está associado a um aplicativo. Alterar a senha alterará a senha do banco de dados do aplicativo ao mesmo tempo. A mudança surtirá efeito após a reinicialização do aplicativo.',
@@ -1014,6 +1018,7 @@ const message = {
errHandle: 'Falha na execução do Cronjob',
noRecord: 'Acione a tarefa Cron e você verá os registros aqui.',
cleanData: 'Limpar dados',
cleanRemoteData: 'Excluir dados remotos',
cleanDataHelper: 'Excluir o arquivo de backup gerado durante esta tarefa.',
noLogs: 'Ainda não há saída de tarefa...',
errPath: 'Caminho de backup [{0}] com erro, não é possível fazer o download!',
@@ -1388,6 +1393,7 @@ const message = {
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',
existFileSize: 'Tamanho do arquivo (novo -> antigo)',
existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!',
noSuchFile: 'O arquivo ou diretório não foi encontrado. Por favor, verifique e tente novamente.',
},
ssh: {
setting: 'configuração',
@@ -2275,6 +2281,8 @@ const message = {
sniHelper:
'Quando o proxy reverso de backend for HTTPS, você pode precisar configurar o SNI de origem. Consulte a documentação do provedor de serviços CDN para mais detalhes.',
huaweicloud: 'Huawei Cloud',
rainyun: 'Rain Yun',
westCN: 'West CN',
},
php: {
short_open_tag: 'Suporte para short tags',
@@ -2622,6 +2630,29 @@ const message = {
proxyHelper6: 'Para desativar a configuração de proxy, você pode excluí-la da lista de sites.',
whiteListHelper: 'Restringir o acesso apenas aos IPs na lista branca',
},
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',
},
};
export default {
+30
View File
@@ -191,6 +191,7 @@ const message = {
rePassword: 'Подтверждение пароля не совпадает с паролем.',
requiredInput: 'Это поле обязательно для заполнения.',
requiredSelect: 'Выберите элемент из списка',
illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |',
illegalInput: 'Это поле не должно содержать недопустимых символов.',
commonName:
'Это поле должно начинаться с неспециальных символов и должно состоять из английских букв, китайских иероглифов, цифр, ".", "-" и "_" длиной 1-128.',
@@ -429,6 +430,8 @@ const message = {
permission: 'Разрешения',
permissionForIP: 'IP',
permissionAll: 'Все (%)',
localhostHelper:
'Настройка разрешений базы данных как "localhost" для развертывания контейнера предотвратит внешний доступ к контейнеру. Пожалуйста, выбирайте внимательно!',
databaseConnInfo: 'Информация о подключении',
rootPassword: 'Пароль root',
serviceName: 'Имя сервиса',
@@ -440,6 +443,7 @@ const message = {
remoteConnHelper:
'Удаленное подключение к MySQL как пользователь root может иметь риски безопасности. Поэтому выполняйте эту операцию с осторожностью.',
changePassword: 'Пароль',
changeConnHelper: 'Эта операция изменит текущую базу данных {0}. Продолжить?',
changePasswordHelper:
'База данных была связана с приложением. Изменение пароля изменит пароль базы данных приложения одновременно. Изменение вступит в силу после перезапуска приложения.',
@@ -1017,6 +1021,7 @@ const message = {
errHandle: 'Сбой выполнения задачи Cron',
noRecord: 'Запустите задачу Cron, и вы увидите записи здесь.',
cleanData: 'Очистить данные',
cleanRemoteData: 'Удалить удалённые данные',
cleanDataHelper: 'Удалить файл резервной копии, созданный во время этой задачи.',
noLogs: 'Пока нет вывода задачи...',
errPath: 'Ошибка пути резервной копии [{0}], невозможно скачать!',
@@ -1390,6 +1395,7 @@ const message = {
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
existFileSize: 'Размер файла (новый -> старый)',
existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!',
noSuchFile: 'Файл или каталог не найдены. Пожалуйста, проверьте и повторите попытку.',
},
ssh: {
setting: 'настройка',
@@ -2276,6 +2282,7 @@ const message = {
'Когда бэкенд обратного прокси использует HTTPS, может потребоваться установить origin SNI. Подробности см. в документации провайдера CDN.',
huaweicloud: 'Huawei Cloud',
rainyun: 'Rain Yun',
westCN: 'West CN',
},
php: {
short_open_tag: 'Поддержка коротких тегов',
@@ -2619,6 +2626,29 @@ const message = {
proxyHelper6: 'Чтобы отключить настройку прокси, вы можете удалить её из списка сайтов.',
whiteListHelper: 'Ограничить доступ только для IP-адресов из белого списка',
},
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 и закроет внешний доступ к портам',
},
};
export default {
+30 -2
View File
@@ -192,6 +192,7 @@ const message = {
rePassword: '密碼不一致,請檢查後重新輸入',
requiredInput: '請填寫必填項',
requiredSelect: '請選擇必選項',
illegalChar: '暫不支援注入字元 & ; $ \' ` ( ) " > < |',
illegalInput: '輸入框中存在不合法字元',
commonName: '支援非特殊字元開頭,英文、中文、數字、.-和_,長度1-128',
userName: '支援英文、中文、數字和_,長度3-30',
@@ -418,6 +419,7 @@ const message = {
permission: '權限',
permissionForIP: '指定 IP',
permissionAll: '所有人(%)',
localhostHelper: '將容器部署的資料庫權限配置為"localhost"會導致容器外部無法存取,請謹慎選擇!',
databaseConnInfo: '連接資訊',
rootPassword: 'root 密碼',
serviceName: '服務名稱',
@@ -428,6 +430,7 @@ const message = {
remoteHelper: '多個 ip 以逗號分隔,例:172.16.10.111,172.16.10.112',
remoteConnHelper: 'root 帳號遠端連接 MySQL 有安全風險,開啟需謹慎!',
changePassword: '改密',
changeConnHelper: '此操作將修改當前資料庫 {0},是否繼續?',
changePasswordHelper: '目前資料庫已經關聯應用,修改密碼將同步修改應用中資料庫密碼,修改後重啟生效。',
portHelper: '該埠為容器對外暴露埠,修改需要單獨儲存並且重啟容器!',
@@ -962,6 +965,7 @@ const message = {
errHandle: '任務執行失敗',
noRecord: '目前計劃任務暫未產生記錄',
cleanData: '刪除備份檔案',
cleanRemoteData: '刪除遠端備份檔案',
cleanDataHelper: '刪除該任務執行過程中產生的備份檔案',
noLogs: '暫無任務輸出...',
errPath: '備份路徑 [{0}] 錯誤,無法下載!',
@@ -1315,6 +1319,7 @@ const message = {
existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?',
existFileSize: '文件大小(新->舊)',
existFileDirHelper: '選擇的檔案/資料夾存在同名,請謹慎操作!',
noSuchFile: '找不到該檔案或目錄,請檢查後重試。',
},
ssh: {
setting: '設定',
@@ -2112,6 +2117,7 @@ const message = {
sniHelper: '反代後端為 https 的時候可能需要設定回源 SNI,具體需要看 CDN 服務商文件',
huaweicloud: '華為雲',
rainyun: '雨雲',
westCN: '西部數據',
},
php: {
short_open_tag: '短標籤支援',
@@ -2375,8 +2381,8 @@ const message = {
env: '環境變數',
noenv: '無',
net: '網路連線',
laddr: '源地址/埠',
raddr: '目標地址/埠',
laddr: '本地地址/埠',
raddr: '远程地址/埠',
stopProcess: '結束',
viewDetails: '查看詳情',
stopProcessWarn: '是否確定結束此行程 (PID:{0})?',
@@ -2433,6 +2439,28 @@ const message = {
proxyHelper6: '如需關閉代理配置,可以在網站列表中刪除',
whiteListHelper: '限制僅白名單中的 IP 可瀏覽',
},
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 的訪問地址,並關閉端口的外部訪問',
},
};
export default {
...fit2cloudTwLocale,
+30 -2
View File
@@ -191,6 +191,7 @@ const message = {
rePassword: '密码不一致,请检查后重新输入',
requiredInput: '请填写必填项',
requiredSelect: '请选择必选项',
illegalChar: '暂不支持注入字符 & ; $ \' ` ( ) " > < |',
illegalInput: '输入框中存在不合法字符',
commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128',
userName: '支持英文、中文、数字和_,长度3-30',
@@ -419,6 +420,7 @@ const message = {
permission: '权限',
permissionForIP: '指定 IP',
permissionAll: '所有人(%)',
localhostHelper: '将容器部署的数据库权限配置为 localhost 会导致容器外部无法访问,请谨慎选择!',
databaseConnInfo: '连接信息',
rootPassword: 'root 密码',
serviceName: '服务名称',
@@ -429,6 +431,7 @@ const message = {
remoteHelper: '多个 ip 以逗号分隔,例:172.16.10.111,172.16.10.112',
remoteConnHelper: 'root 帐号远程连接 MySQL 有安全风险,开启需谨慎!',
changePassword: '改密',
changeConnHelper: '此操作将修改当前数据库 {0},是否继续?',
changePasswordHelper: '当前数据库已经关联应用,修改密码将同步修改应用中数据库密码,修改后重启生效。',
portHelper: '该端口为容器对外暴露端口,修改需要单独保存并且重启容器!',
@@ -963,6 +966,7 @@ const message = {
errHandle: '任务执行失败',
noRecord: '当前计划任务暂未产生记录',
cleanData: '删除备份文件',
cleanRemoteData: '删除远程备份文件',
cleanDataHelper: '删除该任务执行过程中产生的备份文件',
noLogs: '暂无任务输出...',
errPath: '备份路径 [{0}] 错误,无法下载!',
@@ -1317,6 +1321,7 @@ const message = {
existFileHelper: '上传的文件存在同名文件,是否覆盖?',
existFileSize: '文件大小 (新 -> 旧)',
existFileDirHelper: '选择的文件/文件夹存在同名,请谨慎操作!',
noSuchFile: '未能找到该文件或目录,请检查后重试',
},
ssh: {
setting: '配置',
@@ -2113,6 +2118,7 @@ const message = {
sniHelper: '反代后端为 https 的时候可能需要设置回源 SNI,具体需要看 CDN 服务商文档',
huaweicloud: '华为云',
rainyun: '雨云',
westCN: '西部数码',
},
php: {
short_open_tag: '短标签支持',
@@ -2377,8 +2383,8 @@ const message = {
env: '环境变量',
noenv: '无',
net: '网络连接',
laddr: '源地址/端口',
raddr: '目标地址/端口',
laddr: '本地地址/端口',
raddr: '远程地址/端口',
stopProcess: '结束',
viewDetails: '查看详情',
stopProcessWarn: '是否确定结束此进程 (PID:{0})?',
@@ -2435,6 +2441,28 @@ const message = {
proxyHelper6: '如需关闭代理配置,可以在网站列表中删除',
whiteListHelper: '限制仅白名单中的 IP 可访问',
},
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 的访问地址,并关闭端口的外部访问',
},
};
export default {
...fit2cloudZhLocale,
+9
View File
@@ -19,6 +19,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',
+4 -1
View File
@@ -49,7 +49,7 @@ const GlobalStore = defineStore({
state.themeConfig.theme === 'dark' ||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro' : 'https://1panel.cn/docs'),
docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro' : 'https://1panel.cn/docs/v1'),
},
actions: {
setOpenMenuTabs(openMenuTabs: boolean) {
@@ -71,6 +71,9 @@ const GlobalStore = defineStore({
this.csrfToken = token;
},
updateLanguage(language: any) {
if (language === 'pt-BR') {
language = 'pt-br';
}
this.language = language;
localStorage.setItem('lang', language);
},
+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,254 @@
<template>
<el-drawer
v-model="open"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader :header="$t('mcp.bindDomain')" :back="handleClose" />
</template>
<div v-loading="loading">
<el-form ref="formRef" label-position="top" @submit.prevent :model="req" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-alert class="common-prompt" :closable="false" type="warning">
<template #default>
<ul>
<li>{{ $t('aitool.proxyHelper1') }}</li>
<li>{{ $t('aitool.proxyHelper2') }}</li>
<li>{{ $t('aitool.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('aitool.proxyHelper4') }}
</span>
<span class="input-help">
{{ $t('aitool.proxyHelper6') }}
<el-link
class="pageRoute"
icon="Position"
@click="toWebsite(req.websiteID)"
type="primary"
>
{{ $t('firewall.quickJump') }}
</el-link>
</span>
<el-text type="danger">{{ $t('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('aitool.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-col>
</el-row>
</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>
</el-drawer>
</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 listSSL = () => {
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 || [];
listSSL();
});
};
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,63 @@
<template>
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader :header="$t('menu.config')" :back="handleClose" />
</template>
<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>
</el-drawer>
</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('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>
+253
View File
@@ -0,0 +1,253 @@
<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('mcp.create') }}
</el-button>
<el-button type="primary" plain @click="openDomain">
{{ $t('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('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 !== 'error' && 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/compose-log/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('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('mcp.operatorHelper', [i18n.global.t('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,284 @@
<template>
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader
:header="$t('mcp.' + mode)"
:hideResource="mode == 'create'"
:resource="mcpServer.name"
:back="handleClose"
/>
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form ref="mcpServerForm" label-position="top" :model="mcpServer" label-width="125px" :rules="rules">
<el-form-item>
<el-button @click="importRef.acceptParams()" type="primary" plain>
{{ $t('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('mcp.commandPlaceHolder')"
></el-input>
<span class="input-help">
{{ $t('mcp.commandHelper', ['@modelcontextprotocol/server-github']) }}
</span>
</el-form-item>
<div>
<el-text>{{ $t('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('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('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('mcp.ssePath')" prop="ssePath">
<el-input v-model.trim="mcpServer.ssePath"></el-input>
<span class="input-help">
{{ $t('mcp.ssePathHelper') }}
</span>
</el-form-item>
</el-form>
</el-col>
</el-row>
<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>
</el-drawer>
<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>
@@ -546,6 +546,7 @@ const onOpenDialog = async (
rowData: Partial<Container.ContainerHelper> = {
cmd: [],
cmdStr: '',
network: 'bridge',
publishAllPorts: false,
exposedPorts: [],
cpuShares: 1024,
@@ -70,9 +70,10 @@ const acceptParams = async (): Promise<void> => {
unUsedList.value = [];
for (const item of list) {
if (
!item.tags ||
item.tags.length === 0 ||
(item.tags.length === 1 && item.tags[0].indexOf('<none>') !== -1 && !item.isUsed)
(!item.tags ||
item.tags.length === 0 ||
(item.tags.length === 1 && item.tags[0].indexOf('<none>') !== -1)) &&
!item.isUsed
) {
unTagList.value.push(item);
}
+8 -1
View File
@@ -163,6 +163,11 @@
<el-form class="mt-4 mb-1" v-if="showClean" ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
<el-checkbox
v-if="cleanData"
v-model="cleanRemoteData"
:label="$t('cronjob.cleanRemoteData')"
/>
<span class="input-help">
{{ $t('cronjob.cleanDataHelper') }}
</span>
@@ -197,6 +202,7 @@ const operateIDs = ref();
const opRef = ref();
const showClean = ref();
const cleanData = ref();
const cleanRemoteData = ref(true);
const data = ref();
const paginationConfig = reactive({
@@ -282,6 +288,7 @@ const onDelete = async (row: Cronjob.CronjobInfo | null) => {
let ids = [];
showClean.value = false;
cleanData.value = false;
cleanRemoteData.value = true;
if (row) {
ids = [row.id];
names = [row.name];
@@ -312,7 +319,7 @@ const onDelete = async (row: Cronjob.CronjobInfo | null) => {
const onSubmitDelete = async () => {
loading.value = true;
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value })
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value, cleanRemoteData: cleanRemoteData.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
+38 -17
View File
@@ -223,6 +223,7 @@
<el-form ref="deleteForm" label-position="left" v-loading="delLoading">
<el-form-item>
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
<el-checkbox v-if="cleanData" v-model="cleanRemoteData" :label="$t('cronjob.cleanRemoteData')" />
<span class="input-help">
{{ $t('cronjob.cleanDataHelper') }}
</span>
@@ -282,6 +283,7 @@ const currentRecordDetail = ref<string>('');
const deleteVisible = ref();
const delLoading = ref();
const cleanData = ref();
const cleanRemoteData = ref(true);
const acceptParams = async (params: DialogProps): Promise<void> => {
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey));
@@ -393,6 +395,7 @@ const search = async () => {
if (!currentRecord.value) {
currentRecord.value = records.value[0];
} else {
let oldRecordID = currentRecord.value.id;
let beDelete = true;
for (const item of records.value) {
if (item.id === currentRecord.value.id) {
@@ -404,6 +407,9 @@ const search = async () => {
if (beDelete) {
currentRecord.value = records.value[0];
}
if (oldRecordID === currentRecord.value.id && currentRecord.value.status !== 'Waiting') {
return;
}
}
if (currentRecord.value?.records) {
loadRecord(currentRecord.value);
@@ -417,7 +423,7 @@ const forDetail = async (row: Cronjob.Record) => {
const loadRecord = async (row: Cronjob.Record) => {
if (row.records) {
const res = await getRecordLog(row.id);
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/\r/g, '');
if (currentRecordDetail.value === log) {
return;
}
@@ -433,26 +439,30 @@ const loadRecord = async (row: Cronjob.Record) => {
};
const onClean = async () => {
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.msg.deleteTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
}).then(async () => {
await cleanRecords(dialogData.value.rowData.id, cleanData.value)
.then(() => {
delLoading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
delLoading.value = false;
});
});
if (!isBackup()) {
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.msg.deleteTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
}).then(async () => {
await cleanRecords(dialogData.value.rowData.id, cleanData.value, cleanRemoteData.value)
.then(() => {
delLoading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
delLoading.value = false;
});
});
} else {
deleteVisible.value = true;
}
};
const cleanRecord = async () => {
delLoading.value = true;
await cleanRecords(dialogData.value.rowData.id, cleanData.value)
await cleanRecords(dialogData.value.rowData.id, cleanData.value, cleanRemoteData.value)
.then(() => {
delLoading.value = false;
deleteVisible.value = false;
@@ -464,6 +474,17 @@ const cleanRecord = async () => {
});
};
function isBackup() {
return (
dialogData.value.rowData!.type === 'app' ||
dialogData.value.rowData!.type === 'website' ||
dialogData.value.rowData!.type === 'database' ||
dialogData.value.rowData!.type === 'directory' ||
dialogData.value.rowData!.type === 'snapshot' ||
dialogData.value.rowData!.type === 'log'
);
}
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
@@ -18,6 +18,7 @@
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model="form.password"></el-input>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="permission">
<el-select v-model="form.permission">
@@ -25,10 +26,13 @@
<el-option
v-if="form.from !== 'local'"
value="localhost"
:label="$t('terminal.localhost')"
:label="$t('terminal.localhost') + '(localhost)'"
/>
<el-option value="ip" :label="$t('database.permissionForIP')" />
</el-select>
<span v-if="form.from !== 'local'" class="input-help">
{{ $t('database.localhostHelper') }}
</span>
</el-form-item>
<el-form-item v-if="form.permission === 'ip'" prop="permissionIPs">
<el-input clearable :rows="3" type="textarea" v-model="form.permissionIPs" />
@@ -60,7 +64,6 @@ import { bindUser } from '@/api/modules/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { checkIp } from '@/utils/util';
const loading = ref();
const bindVisible = ref(false);
@@ -79,21 +82,11 @@ const confirmDialogRef = ref();
const rules = reactive({
username: [Rules.requiredInput, Rules.name],
password: [Rules.paramComplexity],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
permission: [Rules.requiredSelect],
permissionIPs: [{ validator: checkIPs, trigger: 'blur', required: true }],
permissionIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
function checkIPs(rule: any, value: any, callback: any) {
let ips = form.permissionIPs.split(',');
for (const item of ips) {
if (checkIp(item)) {
return callback(new Error(i18n.global.t('commons.rule.ip')));
}
}
callback();
}
interface DialogProps {
from: string;
database: string;
@@ -9,7 +9,7 @@
<template #header>
<DrawerHeader :header="$t('database.databaseConnInfo')" :back="handleClose" />
</template>
<el-form @submit.prevent v-loading="loading" ref="formRef" :model="form" label-position="top">
<el-form @submit.prevent v-loading="loading" ref="formRef" :rules="rules" :model="form" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('database.containerConn')" v-if="form.from === 'local'">
@@ -75,11 +75,7 @@
/>
<span class="input-help">{{ $t('database.remoteConnHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('database.rootPassword')"
:rules="Rules.paramComplexity"
prop="password"
>
<el-form-item :label="$t('database.rootPassword')" prop="password">
<el-input
style="width: calc(100% - 205px)"
type="password"
@@ -93,6 +89,7 @@
{{ $t('commons.button.random') }}
</el-button>
</el-button-group>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
</div>
@@ -110,9 +107,6 @@
</el-row>
</el-form>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit" @cancel="loadPassword"></ConfirmDialog>
<ConfirmDialog ref="confirmAccessDialogRef" @confirm="onSubmitAccess" @cancel="loadAccess"></ConfirmDialog>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="dialogVisible = false">
@@ -132,7 +126,6 @@ import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { getDatabase, loadRemoteAccess, updateMysqlAccess, updateMysqlPassword } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { GetAppConnInfo } from '@/api/modules/app';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
@@ -148,6 +141,7 @@ const form = reactive({
password: '',
serviceName: '',
containerName: '',
oldPrivilege: false,
privilege: false,
port: 0,
@@ -158,8 +152,9 @@ const form = reactive({
remoteIP: '',
});
const confirmDialogRef = ref();
const confirmAccessDialogRef = ref();
const rules = reactive({
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@@ -200,6 +195,7 @@ const loadAccess = async () => {
if (form.from === 'local') {
const res = await loadRemoteAccess(form.type, form.database);
form.privilege = res.data;
form.oldPrivilege = res.data;
}
};
@@ -227,68 +223,72 @@ const loadPassword = async () => {
form.remoteIP = res.data.address;
};
const onSubmit = async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.password,
};
loading.value = true;
await updateMysqlPassword(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
ElMessageBox.confirm(
i18n.global.t('database.changeConnHelper', [i18n.global.t('commons.login.password')]),
i18n.global.t('commons.msg.infoTitle'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
).then(async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.password,
};
loading.value = true;
await updateMysqlPassword(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
});
};
const onSubmitAccess = async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.privilege ? '%' : 'localhost',
};
loading.value = true;
await updateMysqlAccess(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
const onSaveAccess = async () => {
ElMessageBox.confirm(
i18n.global.t('database.changeConnHelper', [i18n.global.t('database.remoteAccess')]),
i18n.global.t('commons.msg.infoTitle'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
)
.then(async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.privilege ? '%' : 'localhost',
};
loading.value = true;
await updateMysqlAccess(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
loading.value = false;
form.privilege = form.oldPrivilege;
});
};
const onSaveAccess = () => {
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmAccessDialogRef.value!.acceptParams(params);
};
defineExpose({
acceptParams,
});
@@ -34,6 +34,7 @@
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="permission">
@@ -42,10 +43,13 @@
<el-option
v-if="form.from !== 'local'"
value="localhost"
:label="$t('terminal.localhost')"
:label="$t('terminal.localhost') + '(localhost)'"
/>
<el-option value="ip" :label="$t('database.permissionForIP')" />
</el-select>
<span v-if="form.from !== 'local'" class="input-help">
{{ $t('database.localhostHelper') }}
</span>
</el-form-item>
<el-form-item v-if="form.permission === 'ip'" prop="permissionIPs">
<el-input clearable :rows="3" type="textarea" v-model="form.permissionIPs" />
@@ -85,7 +89,7 @@ import { ElForm } from 'element-plus';
import { addMysqlDB } from '@/api/modules/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { checkIp, getRandomStr } from '@/utils/util';
import { getRandomStr } from '@/utils/util';
const loading = ref();
const createVisible = ref(false);
@@ -104,19 +108,10 @@ const form = reactive({
const rules = reactive({
name: [Rules.requiredInput, Rules.dbName],
username: [Rules.requiredInput, Rules.name],
password: [Rules.paramComplexity],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
permission: [Rules.requiredSelect],
permissionIPs: [{ validator: checkIPs, trigger: 'blur', required: true }],
permissionIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
function checkIPs(rule: any, value: any, callback: any) {
let ips = form.permissionIPs.split(',');
for (const item of ips) {
if (checkIp(item)) {
return callback(new Error(i18n.global.t('commons.rule.ip')));
}
}
callback();
}
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@@ -18,12 +18,8 @@
<el-input disabled v-model="changeForm.userName"></el-input>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input
type="password"
clearable
show-password
v-model="changeForm.password"
></el-input>
<el-input type="password" clearable show-password v-model="changeForm.password" />
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
</div>
<div v-if="changeForm.operation === 'privilege'">
@@ -33,10 +29,13 @@
<el-option
v-if="changeForm.from !== 'local'"
value="localhost"
:label="$t('terminal.localhost')"
:label="$t('terminal.localhost') + '(localhost)'"
/>
<el-option value="ip" :label="$t('database.permissionForIP')" />
</el-select>
<span v-if="changeForm.from !== 'local'" class="input-help">
{{ $t('database.localhostHelper') }}
</span>
</el-form-item>
<el-form-item v-if="changeForm.privilege === 'ip'" prop="privilegeIPs">
<el-input clearable :rows="3" type="textarea" v-model="changeForm.privilegeIPs" />
@@ -69,13 +68,14 @@ import { deleteCheckMysqlDB, updateMysqlAccess, updateMysqlPassword } from '@/ap
import DrawerHeader from '@/components/drawer-header/index.vue';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { checkIp } from '@/utils/util';
const loading = ref();
const changeVisible = ref(false);
type FormInstance = InstanceType<typeof ElForm>;
const changeFormRef = ref<FormInstance>();
const title = ref();
const oldPrivilege = ref();
const oldPrivilegeIPs = ref();
const changeForm = reactive({
id: 0,
from: '',
@@ -92,20 +92,10 @@ const changeForm = reactive({
const confirmDialogRef = ref();
const rules = reactive({
password: [Rules.paramComplexity],
privilegeIPs: [{ validator: checkIPs, trigger: 'blur', required: true }],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
privilegeIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
function checkIPs(rule: any, value: any, callback: any) {
let ips = changeForm.privilegeIPs.split(',');
for (const item of ips) {
if (checkIp(item)) {
return callback(new Error(i18n.global.t('commons.rule.ip')));
}
}
callback();
}
interface DialogProps {
id: number;
from: string;
@@ -136,6 +126,8 @@ const acceptParams = (params: DialogProps): void => {
changeForm.privilegeIPs = params.privilegeIPs;
changeForm.value = params.value;
changeVisible.value = true;
oldPrivilege.value = params.privilege;
oldPrivilegeIPs.value = params.privilegeIPs;
};
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -179,6 +171,10 @@ const submitChangeInfo = async (formEl: FormInstance | undefined) => {
}
return;
}
if (changeForm.privilege === oldPrivilege.value && changeForm.privilegeIPs === oldPrivilegeIPs.value) {
changeVisible.value = false;
return;
}
if (changeForm.privilege !== 'ip') {
param.value = changeForm.privilege;
} else {
@@ -4,9 +4,6 @@
<template #title>
<back-button name="MySQL" :header="props.database + ' ' + $t('commons.button.set')">
<template #buttons>
<el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf">
{{ $t('database.confChange') }}
</el-button>
<el-button
type="primary"
:disabled="mysqlStatus !== 'Running'"
@@ -15,6 +12,9 @@
>
{{ $t('database.currentStatus') }}
</el-button>
<el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf">
{{ $t('database.confChange') }}
</el-button>
<el-button
type="primary"
:disabled="mysqlStatus !== 'Running'"
@@ -159,7 +159,7 @@ const globalStore = GlobalStore();
const loading = ref(false);
const extensions = [javascript(), oneDark];
const activeName = ref('conf');
const activeName = ref('status');
const baseInfo = reactive({
name: '',
@@ -19,6 +19,7 @@
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model="form.password" />
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="superUser">
<el-checkbox v-model="form.superUser">{{ $t('database.pgSuperUser') }}</el-checkbox>
@@ -65,7 +66,7 @@ const confirmDialogRef = ref();
const rules = reactive({
username: [Rules.requiredInput, Rules.name],
password: [Rules.paramComplexity],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
interface DialogProps {
@@ -9,7 +9,7 @@
<template #header>
<DrawerHeader :header="$t('database.databaseConnInfo')" :back="handleClose" />
</template>
<el-form @submit.prevent v-loading="loading" ref="formRef" :model="form" label-position="top">
<el-form @submit.prevent v-loading="loading" ref="formRef" :rules="rules" :model="form" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('database.containerConn')" v-if="form.from === 'local'">
@@ -68,7 +68,13 @@
<el-divider border-style="dashed" />
<div v-if="form.from === 'local'">
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input type="text" readonly disabled v-model="form.username">
<el-input
type="text"
style="width: calc(100% - 60px)"
readonly
disabled
v-model="form.username"
>
<template #append>
<el-button-group>
<CopyButton :content="form.username" />
@@ -76,11 +82,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item
:label="$t('commons.login.password')"
:rules="Rules.paramComplexity"
prop="password"
>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input
style="width: calc(100% - 205px)"
type="password"
@@ -94,6 +96,7 @@
{{ $t('commons.button.random') }}
</el-button>
</el-button-group>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
</div>
<div v-if="form.from !== 'local'">
@@ -110,8 +113,6 @@
</el-row>
</el-form>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit" @cancel="loadPassword"></ConfirmDialog>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="dialogVisible = false">
@@ -131,7 +132,6 @@ import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { getDatabase, updatePostgresqlPassword } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { GetAppConnInfo } from '@/api/modules/app';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
@@ -157,7 +157,9 @@ const form = reactive({
remoteIP: '',
});
const confirmDialogRef = ref();
const rules = reactive({
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@@ -225,36 +227,36 @@ const loadPassword = async () => {
form.remoteIP = res.data.address;
};
const onSubmit = async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.password,
};
loading.value = true;
await updatePostgresqlPassword(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
ElMessageBox.confirm(
i18n.global.t('database.changeConnHelper', [i18n.global.t('commons.login.password')]),
i18n.global.t('commons.msg.infoTitle'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
).then(async () => {
let param = {
id: 0,
from: form.from,
type: form.type,
database: form.database,
value: form.password,
};
loading.value = true;
await updatePostgresqlPassword(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
});
};
@@ -25,6 +25,7 @@
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="superUser">
<el-checkbox v-model="form.superUser">{{ $t('database.pgSuperUser') }}</el-checkbox>
@@ -81,7 +82,7 @@ const form = reactive({
const rules = reactive({
name: [Rules.requiredInput, Rules.dbName],
username: [Rules.requiredInput, Rules.name],
password: [Rules.paramComplexity],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
type FormInstance = InstanceType<typeof ElForm>;
@@ -24,6 +24,7 @@
show-password
v-model="changeForm.password"
></el-input>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
</div>
</el-col>
@@ -71,7 +72,7 @@ const changeForm = reactive({
});
const confirmDialogRef = ref();
const rules = reactive({
password: [Rules.paramComplexity],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
interface DialogProps {
@@ -80,6 +80,7 @@
{{ $t('commons.button.random') }}
</el-button>
</el-button-group>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<div v-if="form.from !== 'local'">
@@ -118,6 +119,7 @@ import { MsgSuccess } from '@/utils/message';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { getRandomStr } from '@/utils/util';
import { getSettingInfo } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
const loading = ref(false);
@@ -135,21 +137,9 @@ const form = reactive({
remoteIP: '',
});
const rules = reactive({
password: [{ validator: checkPassword, trigger: 'blur' }],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
function checkPassword(rule: any, value: any, callback: any) {
if (form.password !== '') {
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9.%@!~_-]{4,126}[a-zA-Z0-9]{1}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.paramComplexity', ['.%@!~_-'])));
} else {
callback();
}
}
callback();
}
const confirmDialogRef = ref();
const emit = defineEmits(['checkExist', 'closeTerminal']);
@@ -2,9 +2,6 @@
<div v-show="settingShow" v-loading="loading">
<LayoutContent :title="database + ' ' + $t('commons.button.set')" :reload="true">
<template #buttons>
<el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')">
{{ $t('database.confChange') }}
</el-button>
<el-button
type="primary"
:disabled="redisStatus !== 'Running'"
@@ -13,6 +10,9 @@
>
{{ $t('database.currentStatus') }}
</el-button>
<el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')">
{{ $t('database.confChange') }}
</el-button>
<el-button
type="primary"
:disabled="redisStatus !== 'Running'"
@@ -170,7 +170,7 @@ const rules = reactive({
maxmemory: [Rules.number, checkNumberRange(0, 999999)],
});
const activeName = ref('conf');
const activeName = ref('status');
const statusRef = ref();
const persistenceRef = ref();
@@ -217,7 +217,7 @@ const acceptParams = (prop: DialogProps): void => {
redisStatus.value = prop.status;
database.value = prop.database;
settingShow.value = true;
loadConfFile();
changeTab('status');
};
const portRef = ref();
@@ -328,6 +328,7 @@ const handleClose = () => {
saveContent();
})
.finally(() => {
isEdit.value = false;
closeEditor();
});
} else {
@@ -234,7 +234,7 @@ const acceptParams = async (props: MoveProps) => {
type.value = props.type;
if (props.name && props.name != '') {
oldName.value = props.name;
const res = await CheckFile(props.path + '/' + props.name);
const res = await CheckFile(props.path + '/' + props.name, false);
if (res.data) {
changeName.value = true;
addForm.cover = false;
@@ -72,6 +72,7 @@
</div>
<el-form-item prop="name" class="no-border">
<el-input
name="username"
v-model.trim="loginForm.name"
:placeholder="$t('commons.login.username')"
class="form-input"
@@ -85,6 +86,7 @@
</el-form-item>
<el-form-item prop="password" class="no-border">
<el-input
name="password"
type="password"
clearable
v-model.trim="loginForm.password"
+2 -6
View File
@@ -59,12 +59,8 @@ let isComplexity = ref(false);
type FormInstance = InstanceType<typeof ElForm>;
const passFormRef = ref<FormInstance>();
const passRules = reactive({
oldPass: [Rules.noSpace, Rules.requiredInput],
newPass: [
Rules.requiredInput,
Rules.noSpace,
{ min: 6, message: i18n.global.t('commons.rule.commonPassword'), trigger: 'blur' },
],
oldPass: [Rules.requiredInput, Rules.noSpace],
newPass: [Rules.requiredInput, Rules.noSpace],
newPassComplexity: [Rules.requiredInput, Rules.noSpace, Rules.password],
rePass: [Rules.requiredInput, Rules.noSpace, { validator: checkPasswordSame, trigger: 'blur' }],
});
@@ -70,12 +70,8 @@ import { logOutApi } from '@/api/modules/auth';
const globalStore = GlobalStore();
const passFormRef = ref<FormInstance>();
const passRules = reactive({
oldPassword: [Rules.noSpace, Rules.requiredInput],
newPassword: [
Rules.requiredInput,
Rules.noSpace,
{ min: 6, message: i18n.global.t('commons.rule.commonPassword'), trigger: 'blur' },
],
oldPassword: [Rules.requiredInput, Rules.noSpace],
newPassword: [Rules.requiredInput, Rules.noSpace],
newPasswordComplexity: [Rules.requiredInput, Rules.noSpace, Rules.password],
retryPassword: [Rules.requiredInput, Rules.noSpace, { validator: checkPassword, trigger: 'blur' }],
});
@@ -60,8 +60,9 @@ import DrawerHeader from '@/components/drawer-header/index.vue';
import { snapshotImport } from '@/api/modules/setting';
import { getBackupList, getFilesFromBackup } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import router from '@/routers';
import { CheckFile } from '@/api/modules/files';
const drawerVisible = ref(false);
const loading = ref();
@@ -109,7 +110,12 @@ const checkDisable = (val: string) => {
return false;
};
const toFolder = async () => {
router.push({ path: '/hosts/files', query: { path: backupPath.value } });
const res = await CheckFile(backupPath.value, true);
if (res.data) {
router.push({ path: '/hosts/files', query: { path: backupPath.value } });
} else {
MsgError(i18n.global.t('file.noSuchFile'));
}
};
const submitImport = async (formEl: FormInstance | undefined) => {
@@ -97,6 +97,17 @@
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'ClouDNS'">
<el-form-item label="Auth ID" prop="authorization.authID">
<el-input v-model.trim="account.authorization['authID']"></el-input>
</el-form-item>
<el-form-item label="Sub Auth ID" prop="authorization.subAuthID">
<el-input v-model.trim="account.authorization['subAuthID']"></el-input>
</el-form-item>
<el-form-item label="Auth Password" prop="authorization.authPassword">
<el-input v-model.trim="account.authorization['authPassword']"></el-input>
</el-form-item>
</div>
<el-form-item
label="API Key"
prop="authorization.apiKey"
@@ -119,6 +130,14 @@
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'WestCN'">
<el-form-item label="Username" prop="authorization.username">
<el-input v-model.trim="account.authorization['username']"></el-input>
</el-form-item>
<el-form-item label="Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
@@ -137,7 +156,7 @@
import { CreateDnsAccount, UpdateDnsAccount } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { MsgSuccess, MsgError } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { DNSTypes } from '@/global/mimetype';
@@ -169,6 +188,8 @@ const rules = ref<any>({
clientID: [Rules.requiredInput],
email: [Rules.email],
password: [Rules.requiredInput],
authPassword: [Rules.requiredInput],
username: [Rules.requiredInput],
},
});
const account = ref({
@@ -217,6 +238,12 @@ const submit = async (formEl: FormInstance | undefined) => {
if (!valid) {
return;
}
if (account.value.type === 'ClouDNS') {
if (!account.value.authorization['authID'] && !account.value.authorization['subAuthID']) {
MsgError('Please input Auth ID or Sub Auth ID');
return;
}
}
loading.value = true;
if (accountData.value.mode === 'edit') {
+11 -15
View File
@@ -1,6 +1,8 @@
module github.com/1Panel-dev/1Panel
go 1.22.3
go 1.23
toolchain go1.23.7
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
@@ -21,7 +23,7 @@ require (
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.7.1
github.com/goh-chunlin/go-onedrive v1.1.1
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/jackc/pgx/v5 v5.5.4
@@ -50,8 +52,6 @@ require (
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cloudapp v1.0.1105
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105
github.com/tencentyun/cos-go-sdk-v5 v0.7.46
github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.32.0
@@ -68,7 +68,6 @@ require (
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -99,11 +98,9 @@ require (
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 // indirect
github.com/cloudflare/cloudflare-go v0.112.0 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd v1.7.27 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.3 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
@@ -115,7 +112,6 @@ require (
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
@@ -169,13 +165,14 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mark3labs/mcp-go v0.14.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/miekg/dns v1.1.62 // indirect
@@ -189,7 +186,6 @@ require (
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/mountinfo v0.7.1 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
@@ -202,11 +198,10 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
@@ -228,6 +223,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
@@ -278,10 +274,10 @@ require (
k8s.io/client-go v0.29.2 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.1 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.29.7 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
+34 -17
View File
@@ -392,8 +392,8 @@ github.com/goh-chunlin/go-onedrive v1.1.1 h1:HGtHk5iG0MZ92zYUtaY04czfZPBIJUr12Uu
github.com/goh-chunlin/go-onedrive v1.1.1/go.mod h1:N8qIGHD7tryO734epiBKk5oXcpGwxKET/u3LuBHciTs=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
@@ -474,8 +474,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -617,8 +617,8 @@ github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
@@ -652,6 +652,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.14.1 h1:NsieyFbuWQaeZSWSHPvJ5TwJdQwu+1jmivAIVljeouY=
github.com/mark3labs/mcp-go v0.14.1/go.mod h1:xBB350hekQsJAK7gJAii8bcEoWemboLm2mRm5/+KBaU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -668,8 +670,8 @@ github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebG
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
@@ -758,6 +760,8 @@ github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=
github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
@@ -960,8 +964,6 @@ github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+z
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cloudapp v1.0.1105 h1:ikgsQkFcKDzOJFbxIcSCdu7oj8GQBfwJohBpYHWeSco=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cloudapp v1.0.1105/go.mod h1:/GMcTQRH1+iDTX8RSl+G79doAWcabhpt+xmv3V9P3p0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 h1:lCs0dmezU6/8JcfNwEaam1Pm1RS/5MhXvNhf/X2y65s=
@@ -1243,7 +1245,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1512,14 +1513,30 @@ k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/A
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.7 h1:Puwf5TIYuOipbcRnpFnLlGlR03DKenw8ggf3ijnuNQ0=
modernc.org/sqlite v1.29.7/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=