mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-10 16:00:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142c9b9a71 | ||
|
|
f3c7603f6e | ||
|
|
db26da1700 | ||
|
|
d5ff59f4e7 | ||
|
|
85b88db99e | ||
|
|
f7053934b5 | ||
|
|
bf1339bb40 | ||
|
|
43c5e62c23 | ||
|
|
28634ce631 | ||
|
|
72263dfdda | ||
|
|
479d36e25f | ||
|
|
2f3ad3537e | ||
|
|
2dfe5096f0 | ||
|
|
4375722aef | ||
|
|
bde4d06842 | ||
|
|
17a486b2c4 | ||
|
|
8499c09571 | ||
|
|
7cb70f785a | ||
|
|
f3781320de | ||
|
|
869ee75020 | ||
|
|
1d5a26373d | ||
|
|
176fd0f8ad | ||
|
|
341a55ae63 | ||
|
|
fb87cf545f | ||
|
|
6db2a9352e | ||
|
|
21826bcc1a | ||
|
|
eedbc2e95c | ||
|
|
46763df7bb | ||
|
|
175162753e | ||
|
|
d114933fe9 | ||
|
|
432412e596 | ||
|
|
1b577e3d41 | ||
|
|
eda9641d35 | ||
|
|
6808e4892c | ||
|
|
0d71cd4db2 | ||
|
|
b6b3304a71 | ||
|
|
59a0a573ed | ||
|
|
e82d686940 | ||
|
|
b48bb88aa1 | ||
|
|
5ce9ec5b64 | ||
|
|
740f3549ad | ||
|
|
aa8a9e8b1e | ||
|
|
14218e3495 | ||
|
|
c526eb2962 | ||
|
|
1ff8292be4 | ||
|
|
d4394616af | ||
|
|
462ca6b373 | ||
|
|
657c3ad37a | ||
|
|
90d11a3b74 | ||
|
|
883f3db725 | ||
|
|
75da72c265 | ||
|
|
78eef15f5b | ||
|
|
735cf3cc71 | ||
|
|
99c5d9a8b2 | ||
|
|
3a0d2039d0 | ||
|
|
98e2d6390a | ||
|
|
975e7f9f58 | ||
|
|
77c90a26f8 | ||
|
|
17cb12df8b |
+3
-1
@@ -73,4 +73,6 @@ agent/.golangci.yml
|
||||
openspec
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
opencode.json
|
||||
opencode.json
|
||||
superpowers
|
||||
.worktrees/
|
||||
|
||||
@@ -53,7 +53,7 @@ bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
+136
-33
@@ -354,19 +354,19 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Browser config
|
||||
// @Summary Get Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentBrowserConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentBrowserConfig
|
||||
// @Param request body dto.AgentWecomConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentWecomConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/browser/get [post]
|
||||
func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
var req dto.AgentBrowserConfigReq
|
||||
// @Router /ai/agents/channel/wecom/get [post]
|
||||
func (b *BaseApi) GetAgentWecomConfig(c *gin.Context) {
|
||||
var req dto.AgentWecomConfigReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetBrowserConfig(req)
|
||||
data, err := agentService.GetWecomConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
@@ -375,19 +375,142 @@ func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent Browser config
|
||||
// @Summary Update Agent WeCom channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentBrowserConfigUpdateReq true "request"
|
||||
// @Param request body dto.AgentWecomConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/browser/update [post]
|
||||
func (b *BaseApi) UpdateAgentBrowserConfig(c *gin.Context) {
|
||||
var req dto.AgentBrowserConfigUpdateReq
|
||||
// @Router /ai/agents/channel/wecom/update [post]
|
||||
func (b *BaseApi) UpdateAgentWecomConfig(c *gin.Context) {
|
||||
var req dto.AgentWecomConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateBrowserConfig(req); err != nil {
|
||||
if err := agentService.UpdateWecomConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentQQBotConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentQQBotConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/qqbot/get [post]
|
||||
func (b *BaseApi) GetAgentQQBotConfig(c *gin.Context) {
|
||||
var req dto.AgentQQBotConfigReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetQQBotConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentQQBotConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/qqbot/update [post]
|
||||
func (b *BaseApi) UpdateAgentQQBotConfig(c *gin.Context) {
|
||||
var req dto.AgentQQBotConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateQQBotConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Install Agent plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginInstallReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/install [post]
|
||||
func (b *BaseApi) InstallAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginInstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.InstallPlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Check Agent plugin installation status
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginCheckReq true "request"
|
||||
// @Success 200 {object} dto.AgentPluginStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/check [post]
|
||||
func (b *BaseApi) CheckAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginCheckReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.CheckPlugin(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentSecurityConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentSecurityConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/security/get [post]
|
||||
func (b *BaseApi) GetAgentSecurityConfig(c *gin.Context) {
|
||||
var req dto.AgentSecurityConfigReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetSecurityConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentSecurityConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/security/update [post]
|
||||
func (b *BaseApi) UpdateAgentSecurityConfig(c *gin.Context) {
|
||||
var req dto.AgentSecurityConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateSecurityConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -435,26 +558,6 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Approve Agent Feishu pairing code
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentFeishuPairingApproveReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/feishu/approve [post]
|
||||
func (b *BaseApi) ApproveAgentFeishuPairing(c *gin.Context) {
|
||||
var req dto.AgentFeishuPairingApproveReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.ApproveFeishuPairing(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Approve Agent channel pairing code
|
||||
// @Accept json
|
||||
|
||||
@@ -430,6 +430,16 @@ func (b *BaseApi) Backup(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "container":
|
||||
if err := backupService.ContainerBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "compose":
|
||||
if err := backupService.ComposeBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -485,6 +495,16 @@ func (b *BaseApi) Recover(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "container":
|
||||
if err := backupService.ContainerRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "compose":
|
||||
if err := backupService.ComposeRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -525,6 +545,16 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "container":
|
||||
if err := backupService.ContainerRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "compose":
|
||||
if err := backupService.ComposeRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -53,6 +56,162 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) {
|
||||
helper.SuccessWithData(c, containerService.LoadUsers(req))
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary List container files
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {array} dto.ContainerFileInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/search [post]
|
||||
func (b *BaseApi) ListContainerFiles(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
files, err := containerService.ListContainerFiles(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, files)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Upload container file
|
||||
// @Accept multipart/form-data
|
||||
// @Param containerID formData string true "containerID"
|
||||
// @Param path formData string true "path"
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/upload [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器 [containerID] 上传文件到 [path]","formatEN":"Upload file to [path] in container [containerID]"}
|
||||
func (b *BaseApi) UploadContainerFile(c *gin.Context) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
containerIDs := form.Value["containerID"]
|
||||
paths := form.Value["path"]
|
||||
uploadFiles := form.File["file"]
|
||||
if len(containerIDs) == 0 || len(paths) == 0 || len(uploadFiles) == 0 {
|
||||
helper.BadRequest(c, errors.New("invalid container file upload params"))
|
||||
return
|
||||
}
|
||||
req := dto.ContainerFileReq{
|
||||
ContainerID: containerIDs[0],
|
||||
Path: paths[0],
|
||||
}
|
||||
for _, uploadFile := range uploadFiles {
|
||||
file, err := uploadFile.Open()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
err = containerService.UploadContainerFile(req, path.Base(uploadFile.Filename), uploadFile.Size, file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Get container file content
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {object} dto.ContainerFileContent
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/content [post]
|
||||
func (b *BaseApi) GetContainerFileContent(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
content, err := containerService.GetContainerFileContent(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, content)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Get container file size
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {int} size
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/size [post]
|
||||
func (b *BaseApi) GetContainerFileSize(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
size, err := containerService.GetContainerFileSize(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, size)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Delete container file
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileBatchDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/del [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器 [containerID] 文件 [paths]","formatEN":"Delete files [paths] in container [containerID]"}
|
||||
func (b *BaseApi) DeleteContainerFile(c *gin.Context) {
|
||||
var req dto.ContainerFileBatchDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := containerService.DeleteContainerFile(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Download container file
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/download [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载容器 [containerID] 文件 [path]","formatEN":"Download file [path] from container [containerID]"}
|
||||
func (b *BaseApi) DownloadContainerFile(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.ContainerID == "" || req.Path == "" {
|
||||
helper.BadRequest(c, errors.New("invalid container file download params"))
|
||||
return
|
||||
}
|
||||
reader, fileName, contentType, err := containerService.DownloadContainerFile(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
|
||||
c.DataFromReader(http.StatusOK, -1, contentType, reader, nil)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary List containers
|
||||
// @Accept json
|
||||
|
||||
+92
-43
@@ -3,32 +3,33 @@ package dto
|
||||
import "time"
|
||||
|
||||
type AgentCreateReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
AccountID uint `json:"accountId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Token string `json:"token"`
|
||||
TaskID string `json:"taskID"`
|
||||
Advanced bool `json:"advanced"`
|
||||
ContainerName string `json:"containerName"`
|
||||
AllowPort bool `json:"allowPort"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
CpuQuota float64 `json:"cpuQuota"`
|
||||
MemoryLimit float64 `json:"memoryLimit"`
|
||||
MemoryUnit string `json:"memoryUnit"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
EditCompose bool `json:"editCompose"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
AccountID uint `json:"accountId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Token string `json:"token"`
|
||||
TaskID string `json:"taskID"`
|
||||
Advanced bool `json:"advanced"`
|
||||
ContainerName string `json:"containerName"`
|
||||
AllowPort bool `json:"allowPort"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
CpuQuota float64 `json:"cpuQuota"`
|
||||
MemoryLimit float64 `json:"memoryLimit"`
|
||||
MemoryUnit string `json:"memoryUnit"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
EditCompose bool `json:"editCompose"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
@@ -194,10 +195,63 @@ type AgentTelegramConfig struct {
|
||||
|
||||
type AgentChannelPairingApproveReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom"`
|
||||
PairingCode string `json:"pairingCode" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWecomConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWecomConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open"`
|
||||
BotID string `json:"botId" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWecomConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
BotID string `json:"botId"`
|
||||
Secret string `json:"secret"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId" validate:"required"`
|
||||
ClientSecret string `json:"clientSecret" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentPluginInstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=qqbot wecom"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentPluginCheckReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=qqbot wecom"`
|
||||
}
|
||||
|
||||
type AgentPluginStatus struct {
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentDiscordConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
@@ -219,24 +273,17 @@ type AgentDiscordConfig struct {
|
||||
Proxy string `json:"proxy"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfigReq struct {
|
||||
type AgentSecurityConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Headless bool `json:"headless"`
|
||||
NoSandbox bool `json:"noSandbox"`
|
||||
DefaultProfile string `json:"defaultProfile" validate:"required"`
|
||||
type AgentSecurityConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ExecutablePath string `json:"executablePath"`
|
||||
Headless bool `json:"headless"`
|
||||
NoSandbox bool `json:"noSandbox"`
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
type AgentSecurityConfig struct {
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
}
|
||||
|
||||
type AgentOtherConfigReq struct {
|
||||
@@ -244,10 +291,12 @@ type AgentOtherConfigReq struct {
|
||||
}
|
||||
|
||||
type AgentOtherConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
}
|
||||
|
||||
type AgentOtherConfig struct {
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
}
|
||||
|
||||
@@ -65,10 +65,11 @@ type UploadForRecover struct {
|
||||
}
|
||||
|
||||
type CommonBackup struct {
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
Secret string `json:"secret"`
|
||||
StopBefore bool `json:"stopBefore"`
|
||||
TaskID string `json:"taskID"`
|
||||
FileName string `json:"fileName"`
|
||||
Args []string `json:"args"`
|
||||
@@ -77,7 +78,7 @@ type CommonBackup struct {
|
||||
}
|
||||
type CommonRecover struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
|
||||
@@ -48,6 +48,34 @@ type ContainerOptions struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type ContainerFileReq struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerFileBatchDeleteReq struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Paths []string `json:"paths" validate:"required,min=1,dive,required"`
|
||||
}
|
||||
|
||||
type ContainerFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"isDir"`
|
||||
IsLink bool `json:"isLink"`
|
||||
LinkTo string `json:"linkTo"`
|
||||
Size int64 `json:"size"`
|
||||
Mode string `json:"mode"`
|
||||
ModTime string `json:"modTime"`
|
||||
}
|
||||
|
||||
type ContainerFileContent struct {
|
||||
Content string `json:"content"`
|
||||
Size int64 `json:"size"`
|
||||
Truncated bool `json:"truncated"`
|
||||
IsBinary bool `json:"isBinary"`
|
||||
}
|
||||
|
||||
type ContainerStatus struct {
|
||||
Created int `json:"created"`
|
||||
Running int `json:"running"`
|
||||
|
||||
@@ -4,6 +4,7 @@ import "time"
|
||||
|
||||
type DashboardBase struct {
|
||||
WebsiteNumber int `json:"websiteNumber"`
|
||||
AgentNumber int `json:"agentNumber"`
|
||||
DatabaseNumber int `json:"databaseNumber"`
|
||||
CronjobNumber int `json:"cronjobNumber"`
|
||||
AppInstalledNumber int `json:"appInstalledNumber"`
|
||||
|
||||
+140
-130
@@ -20,16 +20,34 @@ type Meta struct {
|
||||
}
|
||||
|
||||
var catalog = map[string]Meta{
|
||||
"custom": {
|
||||
Key: "custom",
|
||||
DisplayName: "Custom",
|
||||
Sort: 10,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "CUSTOM_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{},
|
||||
},
|
||||
"ollama": {
|
||||
Key: "ollama",
|
||||
DisplayName: "Ollama",
|
||||
Sort: 10,
|
||||
Sort: 15,
|
||||
Enabled: true,
|
||||
},
|
||||
"vllm": {
|
||||
Key: "vllm",
|
||||
DisplayName: "vLLM",
|
||||
Sort: 20,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "VLLM_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{},
|
||||
},
|
||||
"deepseek": {
|
||||
Key: "deepseek",
|
||||
DisplayName: "DeepSeek",
|
||||
Sort: 20,
|
||||
Sort: 25,
|
||||
DefaultBaseURL: "https://api.deepseek.com/v1",
|
||||
EnvKey: "DEEPSEEK_API_KEY",
|
||||
Enabled: true,
|
||||
@@ -39,129 +57,10 @@ var catalog = map[string]Meta{
|
||||
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"},
|
||||
},
|
||||
},
|
||||
"custom": {
|
||||
Key: "custom",
|
||||
DisplayName: "Custom",
|
||||
Sort: 30,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "CUSTOM_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{},
|
||||
},
|
||||
"openai": {
|
||||
Key: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
Sort: 40,
|
||||
DefaultBaseURL: "https://api.openai.com/v1",
|
||||
EnvKey: "OPENAI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
|
||||
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
|
||||
{ID: "openai/gpt-4o", Name: "GPT-4o"},
|
||||
{ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"},
|
||||
{ID: "openai/gpt-5", Name: "GPT-5"},
|
||||
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
Key: "anthropic",
|
||||
DisplayName: "Anthropic",
|
||||
Sort: 50,
|
||||
DefaultBaseURL: "https://api.anthropic.com",
|
||||
EnvKey: "ANTHROPIC_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
|
||||
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"},
|
||||
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"},
|
||||
},
|
||||
},
|
||||
"gemini": {
|
||||
Key: "gemini",
|
||||
DisplayName: "Gemini",
|
||||
Sort: 60,
|
||||
DefaultBaseURL: "https://generativelanguage.googleapis.com",
|
||||
EnvKey: "GEMINI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "google/gemini-1.5-flash", Name: "Gemini 1.5 Flash"},
|
||||
{ID: "google/gemini-1.5-pro", Name: "Gemini 1.5 Pro"},
|
||||
{ID: "google/gemini-2.0-flash", Name: "Gemini 2.0 Flash"},
|
||||
{ID: "google/gemini-2.5-flash", Name: "Gemini 2.5 Flash"},
|
||||
{ID: "google/gemini-2.5-pro", Name: "Gemini 2.5 Pro"},
|
||||
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
|
||||
},
|
||||
},
|
||||
"minimax": {
|
||||
Key: "minimax",
|
||||
DisplayName: "MiniMax (CN)",
|
||||
Sort: 70,
|
||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
||||
EnvKey: "MINIMAX_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "minimax/MiniMax-M2.1", Name: "MiniMax M2.1"},
|
||||
{ID: "minimax/MiniMax-M2.1-lightning", Name: "MiniMax M2.1 Lightning"},
|
||||
},
|
||||
},
|
||||
"moonshot": {
|
||||
Key: "moonshot",
|
||||
DisplayName: "Moonshot (Global)",
|
||||
Sort: 80,
|
||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
||||
EnvKey: "MOONSHOT_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"kimi": {
|
||||
Key: "kimi",
|
||||
DisplayName: "Kimi (CN)",
|
||||
Sort: 90,
|
||||
DefaultBaseURL: "https://api.moonshot.cn/v1",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"zai": {
|
||||
Key: "zai",
|
||||
DisplayName: "Z.ai",
|
||||
Sort: 95,
|
||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
EnvKey: "ZAI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "zai/glm-5", Name: "GLM-5"},
|
||||
{ID: "zai/glm-4.7", Name: "GLM-4.7"},
|
||||
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash"},
|
||||
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX"},
|
||||
},
|
||||
},
|
||||
"kimi-coding": {
|
||||
Key: "kimi-coding",
|
||||
DisplayName: "Kimi Coding",
|
||||
Sort: 100,
|
||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
|
||||
},
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
Key: "bailian-coding-plan",
|
||||
DisplayName: "阿里云百炼 Coding Plan",
|
||||
Sort: 11,
|
||||
Sort: 30,
|
||||
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
|
||||
EnvKey: "QWEN_API_KEY",
|
||||
Enabled: true,
|
||||
@@ -179,7 +78,7 @@ var catalog = map[string]Meta{
|
||||
"ark-coding-plan": {
|
||||
Key: "ark-coding-plan",
|
||||
DisplayName: "方舟 Coding Plan",
|
||||
Sort: 12,
|
||||
Sort: 35,
|
||||
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
|
||||
EnvKey: "ARK_API_KEY",
|
||||
Enabled: true,
|
||||
@@ -192,13 +91,124 @@ var catalog = map[string]Meta{
|
||||
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking"},
|
||||
},
|
||||
},
|
||||
"qwen": {
|
||||
Key: "qwen",
|
||||
DisplayName: "Qwen",
|
||||
Sort: 110,
|
||||
DefaultBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
EnvKey: "QWEN_API_KEY",
|
||||
Enabled: false,
|
||||
"zai": {
|
||||
Key: "zai",
|
||||
DisplayName: "Z.ai",
|
||||
Sort: 40,
|
||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
EnvKey: "ZAI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "zai/glm-5", Name: "GLM-5"},
|
||||
{ID: "zai/glm-4.7", Name: "GLM-4.7"},
|
||||
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash"},
|
||||
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX"},
|
||||
},
|
||||
},
|
||||
"minimax": {
|
||||
Key: "minimax",
|
||||
DisplayName: "MiniMax (CN)",
|
||||
Sort: 45,
|
||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
||||
EnvKey: "MINIMAX_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
|
||||
},
|
||||
},
|
||||
"kimi": {
|
||||
Key: "kimi",
|
||||
DisplayName: "Kimi (CN)",
|
||||
Sort: 50,
|
||||
DefaultBaseURL: "https://api.moonshot.cn/v1",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"kimi-coding": {
|
||||
Key: "kimi-coding",
|
||||
DisplayName: "Kimi Coding",
|
||||
Sort: 51,
|
||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
|
||||
},
|
||||
},
|
||||
"openai": {
|
||||
Key: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
Sort: 55,
|
||||
DefaultBaseURL: "https://api.openai.com/v1",
|
||||
EnvKey: "OPENAI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
|
||||
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
|
||||
{ID: "openai/gpt-4o", Name: "GPT-4o"},
|
||||
{ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"},
|
||||
{ID: "openai/gpt-5", Name: "GPT-5"},
|
||||
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"},
|
||||
},
|
||||
},
|
||||
"openrouter": {
|
||||
Key: "openrouter",
|
||||
DisplayName: "OpenRouter",
|
||||
Sort: 56,
|
||||
DefaultBaseURL: "https://openrouter.ai/api/v1",
|
||||
EnvKey: "OPENROUTER_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "openrouter/free", Name: "openrouter/free"},
|
||||
{ID: "openrouter/auto", Name: "openrouter/auto"},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
Key: "anthropic",
|
||||
DisplayName: "Anthropic",
|
||||
Sort: 60,
|
||||
DefaultBaseURL: "https://api.anthropic.com",
|
||||
EnvKey: "ANTHROPIC_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
|
||||
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"},
|
||||
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"},
|
||||
},
|
||||
},
|
||||
"gemini": {
|
||||
Key: "gemini",
|
||||
DisplayName: "Gemini",
|
||||
Sort: 65,
|
||||
DefaultBaseURL: "https://generativelanguage.googleapis.com",
|
||||
EnvKey: "GEMINI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
|
||||
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
|
||||
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"},
|
||||
},
|
||||
},
|
||||
"moonshot": {
|
||||
Key: "moonshot",
|
||||
DisplayName: "Moonshot (Global)",
|
||||
Sort: 70,
|
||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
||||
EnvKey: "MOONSHOT_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenClawPatch struct {
|
||||
PrimaryModel string
|
||||
Models map[string]interface{}
|
||||
}
|
||||
|
||||
func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
if modelName == "" {
|
||||
return nil, fmt.Errorf("model is required")
|
||||
}
|
||||
modelID := modelName
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "deepseek":
|
||||
return buildDeepseekPatch(modelName, baseURL, apiKey), nil
|
||||
case "gemini":
|
||||
return buildGeminiPatch(modelName), nil
|
||||
case "moonshot", "kimi":
|
||||
return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil
|
||||
case "bailian-coding-plan":
|
||||
return buildBailianPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
case "ark-coding-plan":
|
||||
return buildArkPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
case "minimax":
|
||||
return buildMiniMaxPatch(modelID, baseURL, apiKey), nil
|
||||
case "custom", "vllm":
|
||||
return buildCustomPatch(provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
case "ollama":
|
||||
return buildOllamaPatch(modelName, modelID, apiType, baseURL), nil
|
||||
case "kimi-coding":
|
||||
return buildKimiCodingPatch(modelName, modelID, baseURL, apiKey), nil
|
||||
case "zai":
|
||||
return buildZaiPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
default:
|
||||
return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildGeminiPatch(modelName string) *OpenClawPatch {
|
||||
return &OpenClawPatch{PrimaryModel: modelName}
|
||||
}
|
||||
|
||||
func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch {
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: modelName,
|
||||
Models: providerModels("deepseek", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.deepseek.com/v1"), "openai-completions", map[string]interface{}{
|
||||
"id": "deepseek-chat",
|
||||
"name": "DeepSeek Chat",
|
||||
"reasoning": false,
|
||||
"input": []string{"text"},
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 8192,
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
|
||||
configProvider := provider
|
||||
primaryModel := modelName
|
||||
if provider == "kimi" {
|
||||
configProvider = "moonshot"
|
||||
primaryModel = "moonshot/" + modelID
|
||||
}
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: primaryModel,
|
||||
Models: providerModels(configProvider, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), "openai-completions", map[string]interface{}{
|
||||
"id": modelID,
|
||||
"name": modelID,
|
||||
"reasoning": strings.Contains(strings.ToLower(modelID), "thinking"),
|
||||
"input": []string{"text"},
|
||||
"contextWindow": 256000,
|
||||
"maxTokens": 8192,
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildBailianPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
|
||||
normalizedID := normalizeBailianCodingPlanModelID(modelID)
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID),
|
||||
Models: providerModels("bailian-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("bailian-coding-plan", baseURL), "openai-completions", map[string]interface{}{
|
||||
"id": normalizedID,
|
||||
"name": normalizedID,
|
||||
"reasoning": isReasoningModel(normalizedID),
|
||||
"input": []string{"text"},
|
||||
"contextWindow": fallbackInt(contextWindow, 256000),
|
||||
"maxTokens": fallbackInt(maxTokens, 8192),
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildArkPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
|
||||
normalizedID := normalizeArkCodingPlanModelID(modelID)
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: "ark-coding-plan/" + normalizedID,
|
||||
Models: providerModels("ark-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("ark-coding-plan", baseURL), "openai-completions", map[string]interface{}{
|
||||
"id": normalizedID,
|
||||
"name": normalizedID,
|
||||
"reasoning": isReasoningModel(normalizedID),
|
||||
"input": []string{"text"},
|
||||
"contextWindow": fallbackInt(contextWindow, 256000),
|
||||
"maxTokens": fallbackInt(maxTokens, 8192),
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildMiniMaxPatch(modelID, baseURL, apiKey string) *OpenClawPatch {
|
||||
normalizedID := normalizeMiniMaxModelID(modelID)
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: "minimax-portal/" + normalizedID,
|
||||
Models: providerModels("minimax-portal", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.minimaxi.com/anthropic"), "anthropic-messages", map[string]interface{}{
|
||||
"id": normalizedID,
|
||||
"name": strings.ReplaceAll(normalizedID, "-", " "),
|
||||
"reasoning": false,
|
||||
"input": []string{"text"},
|
||||
"contextWindow": 200000,
|
||||
"maxTokens": 8192,
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildCustomPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
|
||||
customModelID := normalizeCustomModel(modelName)
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: provider + "/" + customModelID,
|
||||
Models: providerModels(provider, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), apiType, map[string]interface{}{
|
||||
"id": customModelID,
|
||||
"name": customModelID,
|
||||
"reasoning": isReasoningModel(customModelID),
|
||||
"input": []string{"text"},
|
||||
"contextWindow": fallbackInt(contextWindow, 128000),
|
||||
"maxTokens": fallbackInt(maxTokens, 8192),
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildOllamaPatch(modelName, modelID, apiType, baseURL string) *OpenClawPatch {
|
||||
api := normalizeAPIType(apiType)
|
||||
if api != "openai-completions" && api != "openai-responses" {
|
||||
api = "openai-responses"
|
||||
}
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: modelName,
|
||||
Models: providerModels("ollama", "ollama", strings.TrimSpace(baseURL), api, map[string]interface{}{
|
||||
"id": modelID,
|
||||
"name": modelID,
|
||||
"reasoning": api != "openai-completions",
|
||||
"input": []string{"text"},
|
||||
"contextWindow": 160000,
|
||||
"maxTokens": 8192,
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildKimiCodingPatch(modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: modelName,
|
||||
Models: providerModels("kimi-coding", strings.TrimSpace(apiKey), withCatalogDefault("kimi-coding", baseURL), "anthropic-messages", map[string]interface{}{
|
||||
"id": modelID,
|
||||
"name": "Kimi for Coding",
|
||||
"reasoning": true,
|
||||
"input": []string{"text", "image"},
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 32768,
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildZaiPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: "zai/" + modelID,
|
||||
Models: providerModels("zai", strings.TrimSpace(apiKey), withCatalogDefault("zai", baseURL), "openai-completions", map[string]interface{}{
|
||||
"id": modelID,
|
||||
"name": zaiModelDisplayName(modelID),
|
||||
"reasoning": modelID == "glm-5",
|
||||
"input": []string{"text"},
|
||||
"contextWindow": fallbackInt(contextWindow, 204800),
|
||||
"maxTokens": fallbackInt(maxTokens, 131072),
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func buildGenericPatch(provider, modelName, modelID, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
|
||||
providerName := provider
|
||||
primaryModel := modelName
|
||||
if provider == "gemini" {
|
||||
providerName = "google"
|
||||
primaryModel = "google/" + modelID
|
||||
}
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: primaryModel,
|
||||
Models: providerModels(providerName, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), normalizeAPIType(apiType), map[string]interface{}{
|
||||
"id": modelID,
|
||||
"name": modelID,
|
||||
"reasoning": isReasoningModel(modelID),
|
||||
"input": []string{"text"},
|
||||
"contextWindow": fallbackInt(contextWindow, 256000),
|
||||
"maxTokens": fallbackInt(maxTokens, 8192),
|
||||
"cost": map[string]interface{}{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func providerModels(provider, apiKey, baseURL, api string, model map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"mode": "merge",
|
||||
"providers": map[string]interface{}{
|
||||
provider: map[string]interface{}{
|
||||
"apiKey": apiKey,
|
||||
"baseUrl": baseURL,
|
||||
"api": api,
|
||||
"models": []map[string]interface{}{model},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func withCatalogDefault(provider, baseURL string) string {
|
||||
if strings.TrimSpace(baseURL) != "" {
|
||||
return strings.TrimSpace(baseURL)
|
||||
}
|
||||
if defaultURL, ok := DefaultBaseURL(provider); ok {
|
||||
return defaultURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fallbackInt(value, fallback int) int {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func normalizeAPIType(apiType string) string {
|
||||
trim := strings.ToLower(strings.TrimSpace(apiType))
|
||||
if trim == "" {
|
||||
return "openai-completions"
|
||||
}
|
||||
return trim
|
||||
}
|
||||
|
||||
func normalizeCustomModel(modelName string) string {
|
||||
trim := strings.TrimSpace(modelName)
|
||||
trim = strings.TrimLeft(trim, "/")
|
||||
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 && strings.EqualFold(parts[0], "custom") {
|
||||
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
|
||||
}
|
||||
return trim
|
||||
}
|
||||
|
||||
func normalizeBailianCodingPlanModelID(modelID string) string {
|
||||
trim := strings.TrimSpace(modelID)
|
||||
switch strings.ToLower(trim) {
|
||||
case "minimax-m2.5", "minimax m2.5", "minimax/minimax-m2.5", "minimax/minimax m2.5":
|
||||
return "MiniMax/MiniMax-M2.5"
|
||||
default:
|
||||
return trim
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeArkCodingPlanModelID(modelID string) string {
|
||||
return strings.ToLower(strings.TrimSpace(modelID))
|
||||
}
|
||||
|
||||
func normalizeMiniMaxModelID(modelID string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(modelID)) {
|
||||
case "minimax-m2.1", "minimax m2.1", "minimax-m2.1-preview", "minimax-m2.1-latest":
|
||||
return "MiniMax-M2.1"
|
||||
case "minimax-m2.1-lightning", "minimax m2.1 lightning":
|
||||
return "MiniMax-M2.1-lightning"
|
||||
default:
|
||||
return modelID
|
||||
}
|
||||
}
|
||||
|
||||
func zaiModelDisplayName(modelID string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(modelID)) {
|
||||
case "glm-5":
|
||||
return "GLM-5"
|
||||
case "glm-4.7":
|
||||
return "GLM-4.7"
|
||||
case "glm-4.7-flash":
|
||||
return "GLM-4.7-Flash"
|
||||
case "glm-4.7-flashx":
|
||||
return "GLM-4.7-FlashX"
|
||||
default:
|
||||
return strings.TrimSpace(modelID)
|
||||
}
|
||||
}
|
||||
|
||||
func bailianPrimaryModelID(modelID string) string {
|
||||
trim := strings.TrimSpace(modelID)
|
||||
if trim == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(trim, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := strings.TrimSpace(parts[i])
|
||||
if part != "" {
|
||||
return part
|
||||
}
|
||||
}
|
||||
return trim
|
||||
}
|
||||
|
||||
func isReasoningModel(modelID string) bool {
|
||||
trim := strings.ToLower(strings.TrimSpace(modelID))
|
||||
return strings.Contains(trim, "reason") || strings.Contains(trim, "thinking")
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
type VerifyRequest struct {
|
||||
Method string
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func SkipVerification(key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "custom", "vllm", "ollama", "kimi-coding":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func VerifyAccount(provider, baseURL, apiKey string) error {
|
||||
req := BuildVerifyRequest(provider, baseURL, apiKey)
|
||||
var body *bytes.Buffer
|
||||
if len(req.Body) > 0 {
|
||||
body = bytes.NewBuffer(req.Body)
|
||||
} else {
|
||||
body = bytes.NewBuffer(nil)
|
||||
}
|
||||
httpReq, err := http.NewRequest(req.Method, req.URL, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range req.Headers {
|
||||
httpReq.Header.Set(key, value)
|
||||
}
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
|
||||
if err != nil {
|
||||
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
headers := map[string]string{}
|
||||
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
|
||||
|
||||
switch provider {
|
||||
case "anthropic", "kimi-coding":
|
||||
headers["x-api-key"] = apiKey
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
if strings.Contains(base, "/v1") {
|
||||
request.URL = base + "/models"
|
||||
} else {
|
||||
request.URL = base + "/v1/models"
|
||||
}
|
||||
case "gemini":
|
||||
request.Method = http.MethodPost
|
||||
if strings.Contains(base, "/v1beta") {
|
||||
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
|
||||
} else {
|
||||
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
|
||||
}
|
||||
headers["x-goog-api-key"] = apiKey
|
||||
headers["Content-Type"] = "application/json"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"contents": []map[string]interface{}{{
|
||||
"parts": []map[string]string{{
|
||||
"text": "Explain how AI works in a few words",
|
||||
}},
|
||||
}},
|
||||
})
|
||||
case "zai":
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
request.URL = base + "/models"
|
||||
case "bailian-coding-plan":
|
||||
request.Method = http.MethodPost
|
||||
if !strings.Contains(base, "/v1") {
|
||||
base = base + "/v1"
|
||||
}
|
||||
request.URL = base + "/chat/completions"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
headers["Content-Type"] = "application/json"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": "qwen3.5-plus",
|
||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
||||
"max_tokens": 1,
|
||||
})
|
||||
case "ark-coding-plan":
|
||||
request.Method = http.MethodPost
|
||||
if !strings.Contains(base, "/api/coding/v3") {
|
||||
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
|
||||
}
|
||||
request.URL = base + "/chat/completions"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
headers["Content-Type"] = "application/json"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": "doubao-seed-2.0-code",
|
||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
||||
"max_tokens": 1,
|
||||
})
|
||||
case "minimax":
|
||||
request.Method = http.MethodPost
|
||||
if !strings.Contains(base, "/v1") {
|
||||
base = base + "/v1"
|
||||
}
|
||||
request.URL = base + "/chat/completions"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
headers["Content-Type"] = "application/json"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": "MiniMax-M2.1",
|
||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
||||
"max_tokens": 1,
|
||||
})
|
||||
default:
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
if strings.Contains(base, "/v1") {
|
||||
request.URL = base + "/models"
|
||||
} else {
|
||||
request.URL = base + "/v1/models"
|
||||
}
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func mustJSON(value interface{}) []byte {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return []byte("{}")
|
||||
}
|
||||
return payload
|
||||
}
|
||||
@@ -80,9 +80,10 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
|
||||
}
|
||||
if !withAll && len(quicks) == 0 {
|
||||
return []model.QuickJump{
|
||||
{Name: "Agent", Title: "aiTools.agents.agents", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"},
|
||||
{Name: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
|
||||
{Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"},
|
||||
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: true, Router: "/cronjobs"},
|
||||
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: false, Router: "/cronjobs"},
|
||||
{Name: "AppInstalled", Title: "home.appInstalled", Recommend: 70, IsShow: true, Router: "/apps/installed"},
|
||||
}
|
||||
}
|
||||
|
||||
+890
-656
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"mime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -483,13 +484,14 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
if username == "" {
|
||||
username = req.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
|
||||
cfg := email.SMTPConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
Username: username,
|
||||
Password: req.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, req.DisplayName, req.Sender),
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
@@ -18,11 +24,6 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -423,25 +424,19 @@ func loadDiskUsage(alert dto.AlertDTO) {
|
||||
}
|
||||
if isAlertDue(newDate) {
|
||||
if strings.Contains(alert.Project, "all") {
|
||||
err = processAllDisks(alert)
|
||||
_ = processAllDisks(alert)
|
||||
} else {
|
||||
err = processSingleDisk(alert)
|
||||
_ = processSingleDisk(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadPanelLogin(alert dto.AlertDTO) {
|
||||
count, isAlert, err := alertUtil.CountRecentFailedLoginLogs(alert.Cycle, alert.Count)
|
||||
alertType := alert.Type
|
||||
quota := strconv.Itoa(count)
|
||||
quotaType := strconv.Itoa(int(alert.Cycle))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to count recent failed login logs: %v", err)
|
||||
}
|
||||
if isAlert {
|
||||
alertType = "panelLogin"
|
||||
quota = strconv.Itoa(count)
|
||||
quotaType = "panelLogin"
|
||||
params := []dto.Param{
|
||||
{
|
||||
Index: "1",
|
||||
@@ -454,7 +449,7 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
||||
Value: "",
|
||||
},
|
||||
}
|
||||
sendAlerts(alert, alertType, quota, quotaType, params)
|
||||
sendAlerts(alert, "panelLogin", strconv.Itoa(count), "panelLogin", params)
|
||||
}
|
||||
|
||||
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
|
||||
@@ -463,15 +458,13 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
||||
global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err)
|
||||
}
|
||||
if len(records) > 0 {
|
||||
quota = strings.Join(func() []string {
|
||||
quota := strings.Join(func() []string {
|
||||
var ips []string
|
||||
for _, r := range records {
|
||||
ips = append(ips, r.IP)
|
||||
}
|
||||
return ips
|
||||
}(), "\n")
|
||||
alertType = "panelIpLogin"
|
||||
quotaType = "panelIpLogin"
|
||||
params := []dto.Param{
|
||||
{
|
||||
Index: "1",
|
||||
@@ -484,22 +477,16 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
||||
Value: " IP ",
|
||||
},
|
||||
}
|
||||
sendAlerts(alert, alertType, quota, quotaType, params)
|
||||
sendAlerts(alert, "panelIpLogin", quota, "panelIpLogin", params)
|
||||
}
|
||||
}
|
||||
|
||||
func loadSSHLogin(alert dto.AlertDTO) {
|
||||
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
|
||||
alertType := alert.Type
|
||||
quota := strconv.Itoa(count)
|
||||
quotaType := strconv.Itoa(int(alert.Cycle))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
|
||||
}
|
||||
if isAlert {
|
||||
alertType = "sshLogin"
|
||||
quota = strconv.Itoa(count)
|
||||
quotaType = "sshLogin"
|
||||
params := []dto.Param{
|
||||
{
|
||||
Index: "1",
|
||||
@@ -512,7 +499,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
Value: "",
|
||||
},
|
||||
}
|
||||
sendAlerts(alert, alertType, quota, quotaType, params)
|
||||
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
||||
}
|
||||
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
|
||||
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
|
||||
@@ -520,9 +507,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
|
||||
}
|
||||
if len(records) > 0 {
|
||||
quota = strings.Join(records, "\n")
|
||||
alertType = "sshIpLogin"
|
||||
quotaType = "sshIpLogin"
|
||||
quota := strings.Join(records, "\n")
|
||||
params := []dto.Param{
|
||||
{
|
||||
Index: "1",
|
||||
@@ -535,7 +520,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
Value: " IP ",
|
||||
},
|
||||
}
|
||||
sendAlerts(alert, alertType, quota, quotaType, params)
|
||||
sendAlerts(alert, "sshIpLogin", quota, "sshIpLogin", params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,7 +879,7 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf(errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -907,7 +892,7 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
if err != nil {
|
||||
global.LOG.Errorf(err.Error())
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -516,6 +516,10 @@ func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (a
|
||||
}
|
||||
appInstall.Env = string(paramByte)
|
||||
|
||||
var maxSort int
|
||||
global.DB.Model(&model.AppInstall{}).Where("favorite = ?", false).Select("COALESCE(MAX(sort_order),0)").Scan(&maxSort)
|
||||
appInstall.SortOrder = maxSort + 1
|
||||
|
||||
if err = appInstallRepo.Create(context.Background(), appInstall); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
composeV2 "github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||
@@ -363,7 +362,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images, err := composeV2.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
|
||||
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -676,6 +675,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldVersion := install.Version
|
||||
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -752,7 +752,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if req.DockerCompose != "" {
|
||||
composeContent = []byte(req.DockerCompose)
|
||||
}
|
||||
images, err := composeV2.GetImagesFromDockerCompose(content, composeContent)
|
||||
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -802,6 +802,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
|
||||
var newCompose string
|
||||
if err = migrateOpenclawHTTPSUpgrade(&install, oldVersion, detail.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.DockerCompose == "" {
|
||||
newCompose, err = getUpgradeCompose(install, detail)
|
||||
if err != nil {
|
||||
@@ -911,7 +914,7 @@ func getContainerNames(install model.AppInstall) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true)
|
||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1133,7 +1136,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
||||
}
|
||||
|
||||
func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
|
||||
client, err := composeV2.NewDockerClient()
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -1169,7 +1172,7 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images, err := composeV2.GetImagesFromDockerCompose(envByte, []byte(appInstall.DockerCompose))
|
||||
images, err := docker.GetImagesFromDockerCompose(envByte, []byte(appInstall.DockerCompose))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ type IBackupService interface {
|
||||
|
||||
AppBackup(db dto.CommonBackup) (*model.BackupRecord, error)
|
||||
AppRecover(req dto.CommonRecover) error
|
||||
|
||||
ContainerBackup(req dto.CommonBackup) error
|
||||
ContainerRecover(req dto.CommonRecover) error
|
||||
ComposeBackup(req dto.CommonBackup) error
|
||||
ComposeRecover(req dto.CommonRecover) error
|
||||
}
|
||||
|
||||
func NewIBackupService() IBackupService {
|
||||
|
||||
@@ -0,0 +1,707 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type composeBackupFile struct {
|
||||
OriginalPath string `json:"originalPath"`
|
||||
FileName string `json:"fileName"`
|
||||
RelativePath string `json:"relativePath,omitempty"`
|
||||
BackupPath string `json:"backupPath"`
|
||||
}
|
||||
|
||||
type composeBackupMeta struct {
|
||||
ComposeName string `json:"composeName"`
|
||||
ComposePath string `json:"composePath"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Files []composeBackupFile `json:"files"`
|
||||
Containers []string `json:"containers"`
|
||||
}
|
||||
|
||||
type composeBackupContext struct {
|
||||
req dto.CommonBackup
|
||||
composeName string
|
||||
composePath string
|
||||
composeFiles []string
|
||||
composeDir string
|
||||
fileOp files.FileOp
|
||||
dockerClient *client.Client
|
||||
stopped bool
|
||||
backupDir string
|
||||
fileName string
|
||||
filePath string
|
||||
tmpDir string
|
||||
meta composeBackupMeta
|
||||
}
|
||||
|
||||
type composeRecoverContext struct {
|
||||
req dto.CommonRecover
|
||||
fileOp files.FileOp
|
||||
tmpDir string
|
||||
meta composeBackupMeta
|
||||
composeName string
|
||||
targetDir string
|
||||
composePath string
|
||||
enqueued bool
|
||||
}
|
||||
|
||||
func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
|
||||
timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
|
||||
fileName := req.FileName
|
||||
if fileName == "" {
|
||||
fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow)
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".tar.gz") {
|
||||
fileName += ".tar.gz"
|
||||
}
|
||||
itemDir := fmt.Sprintf("compose/%s", req.Name)
|
||||
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||
record := &model.BackupRecord{
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
SourceAccountIDs: "1",
|
||||
DownloadAccountID: 1,
|
||||
FileDir: itemDir,
|
||||
FileName: fileName,
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save compose backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
|
||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackupService) ComposeRecover(req dto.CommonRecover) error {
|
||||
return handleComposeRecover(req, nil)
|
||||
}
|
||||
|
||||
func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, backupDir, fileName string) error {
|
||||
composeCtx, err := newComposeBackupContext(req, backupDir, fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerNames, err := loadComposeContainerNames(composeCtx)
|
||||
if err != nil {
|
||||
composeCtx.close()
|
||||
return err
|
||||
}
|
||||
|
||||
backupTask := parentTask
|
||||
if backupTask == nil {
|
||||
backupTask, err = task.NewTaskWithOps(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.StopBefore {
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStop"), func(t *task.Task) error {
|
||||
return stepStopComposeForBackup(composeCtx)
|
||||
}, func(t *task.Task) {
|
||||
_ = stepStartComposeAfterBackup(composeCtx)
|
||||
}, 3, time.Hour)
|
||||
}
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupPrepare"), func(t *task.Task) error { return stepPrepareComposeBackup(composeCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupFiles"), func(t *task.Task) error { return stepBackupComposeFiles(composeCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupContainers"), func(t *task.Task) error { return nil }, nil, 3, time.Hour)
|
||||
for _, containerName := range containerNames {
|
||||
backupFileName := fmt.Sprintf("%s.tar.gz", sanitizeComposeFileName(containerName))
|
||||
backupFile := path.Join(composeCtx.tmpDir, "containers", backupFileName)
|
||||
if err := handleContainerBackup(containerName, backupTask, 0, path.Dir(backupFile), path.Base(backupFile), "", "", false); err != nil {
|
||||
return err
|
||||
}
|
||||
composeCtx.meta.Containers = append(composeCtx.meta.Containers, path.Join("containers", backupFileName))
|
||||
}
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupMeta"), func(t *task.Task) error { return stepWriteComposeBackupMeta(composeCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(task.GetTaskName(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error {
|
||||
return stepPackComposeBackup(composeCtx)
|
||||
}, nil, 3, time.Hour)
|
||||
if req.StopBefore {
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStart"), func(t *task.Task) error {
|
||||
return stepStartComposeAfterBackup(composeCtx)
|
||||
}, nil, 3, time.Hour)
|
||||
}
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupCleanup"), func(t *task.Task) error {
|
||||
composeCtx.close()
|
||||
return nil
|
||||
}, nil, 0, time.Hour)
|
||||
if parentTask != nil {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
defer composeCtx.close()
|
||||
if err := backupTask.Execute(); err != nil {
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadComposeContainerNames(composeCtx *composeBackupContext) ([]string, error) {
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
|
||||
containers, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
if len(item.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
names = append(names, strings.TrimPrefix(item.Names[0], "/"))
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func handleComposeRecover(req dto.CommonRecover, parentTask *task.Task) error {
|
||||
var recoverCtx *composeRecoverContext
|
||||
|
||||
recoverTask := parentTask
|
||||
var err error
|
||||
if recoverTask == nil {
|
||||
if isImportRecover(req) {
|
||||
taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Compose")
|
||||
recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
taskName := req.Name
|
||||
if taskName == "" {
|
||||
taskName = "compose"
|
||||
}
|
||||
recoverTask, err = task.NewTaskWithOps(taskName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeout := loadRecoverTimeout(req.Timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverPrepare"), func(t *task.Task) error {
|
||||
ctx, err := newComposeRecoverContext(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoverCtx = ctx
|
||||
if err := stepPrepareComposeRecover(recoverCtx); err != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, func(t *task.Task) {
|
||||
if recoverCtx != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
}
|
||||
}, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverExtract"), func(t *task.Task) error { return stepExtractComposeRecover(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverMeta"), func(t *task.Task) error {
|
||||
if err := stepLoadComposeRecoverMeta(recoverCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogName", map[string]interface{}{
|
||||
"name": recoverCtx.composeName,
|
||||
}))
|
||||
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogPath", map[string]interface{}{
|
||||
"backupPath": recoverCtx.meta.ComposePath,
|
||||
"targetDir": recoverCtx.targetDir,
|
||||
}))
|
||||
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogCount", map[string]interface{}{
|
||||
"files": len(recoverCtx.meta.Files),
|
||||
"containers": len(recoverCtx.meta.Containers),
|
||||
}))
|
||||
return nil
|
||||
}, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverFiles"), func(t *task.Task) error { return stepRestoreComposeFiles(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverContainers"), func(t *task.Task) error {
|
||||
if recoverCtx.enqueued {
|
||||
return nil
|
||||
}
|
||||
containerItems := make([]string, 0, len(recoverCtx.meta.Containers))
|
||||
for _, item := range recoverCtx.meta.Containers {
|
||||
backupItem := item
|
||||
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
|
||||
}
|
||||
if !recoverCtx.fileOp.Stat(filePath) {
|
||||
return fmt.Errorf("container backup file not found: %s", backupItem)
|
||||
}
|
||||
containerItems = append(containerItems, backupItem)
|
||||
}
|
||||
for _, backupItem := range containerItems {
|
||||
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
|
||||
}
|
||||
containerLabel := strings.TrimSuffix(path.Base(backupItem), ".tar.gz")
|
||||
containerReq := recoverCtx.req
|
||||
containerReq.Type = "container"
|
||||
containerReq.Name = containerLabel
|
||||
containerReq.DetailName = ""
|
||||
containerReq.File = filePath
|
||||
containerReq.Secret = ""
|
||||
if err := handleContainerRecover(containerReq, recoverTask); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverRecord"), func(t *task.Task) error {
|
||||
return stepSaveComposeRecord(recoverCtx)
|
||||
}, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverCleanup"), func(t *task.Task) error {
|
||||
if recoverCtx != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
}
|
||||
return nil
|
||||
}, nil, 0, timeout)
|
||||
recoverCtx.enqueued = true
|
||||
return nil
|
||||
}, nil, 3, timeout)
|
||||
if parentTask != nil {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func newComposeBackupContext(req dto.CommonBackup, backupDir, fileName string) (*composeBackupContext, error) {
|
||||
if req.Name == "" {
|
||||
return nil, fmt.Errorf("compose name is required")
|
||||
}
|
||||
dockerClient, err := dockerUtils.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
composePath, composeFiles, err := loadComposePathAndFiles(req.Name, dockerClient)
|
||||
if err != nil {
|
||||
_ = dockerClient.Close()
|
||||
return nil, err
|
||||
}
|
||||
filePath := path.Join(backupDir, fileName)
|
||||
tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz"))
|
||||
ctx := &composeBackupContext{
|
||||
req: req,
|
||||
composeName: req.Name,
|
||||
composePath: composePath,
|
||||
composeFiles: composeFiles,
|
||||
composeDir: path.Dir(composeFiles[0]),
|
||||
fileOp: files.NewFileOp(),
|
||||
dockerClient: dockerClient,
|
||||
backupDir: backupDir,
|
||||
fileName: fileName,
|
||||
filePath: filePath,
|
||||
tmpDir: tmpDir,
|
||||
meta: composeBackupMeta{
|
||||
ComposeName: req.Name,
|
||||
ComposePath: composePath,
|
||||
CreatedAt: time.Now().Format(constant.DateTimeLayout),
|
||||
Files: make([]composeBackupFile, 0),
|
||||
Containers: make([]string, 0),
|
||||
},
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (string, []string, error) {
|
||||
composeRecord, _ := composeRepo.GetRecord(repo.WithByName(composeName))
|
||||
if composeRecord.ID == 0 {
|
||||
composeRecord, _ = composeRepo.GetRecord(repo.WithByName(strings.ToLower(composeName)))
|
||||
}
|
||||
composePath := composeRecord.Path
|
||||
if composePath == "" {
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel))
|
||||
list, err := dockerClient.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return "", nil, fmt.Errorf("compose %s not found", composeName)
|
||||
}
|
||||
var targetContainer *container.Summary
|
||||
for i := range list {
|
||||
if strings.EqualFold(list[i].Labels[composeProjectLabel], composeName) {
|
||||
targetContainer = &list[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetContainer == nil {
|
||||
return "", nil, fmt.Errorf("compose %s not found", composeName)
|
||||
}
|
||||
config := targetContainer.Labels[composeConfigLabel]
|
||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||
composePath = config
|
||||
} else {
|
||||
composePath = workdir
|
||||
}
|
||||
}
|
||||
composeFiles := normalizeComposeFiles(composePath)
|
||||
if len(composeFiles) == 0 {
|
||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
||||
}
|
||||
return composePath, composeFiles, nil
|
||||
}
|
||||
|
||||
func normalizeComposeFiles(composePath string) []string {
|
||||
items := strings.Split(composePath, ",")
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
stat, err := os.Stat(item)
|
||||
if err == nil && stat.IsDir() {
|
||||
item = path.Join(item, "docker-compose.yml")
|
||||
}
|
||||
if _, err := os.Stat(item); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *composeBackupContext) close() {
|
||||
if c.dockerClient != nil {
|
||||
_ = c.dockerClient.Close()
|
||||
c.dockerClient = nil
|
||||
}
|
||||
if c.tmpDir != "" {
|
||||
_ = os.RemoveAll(c.tmpDir)
|
||||
c.tmpDir = ""
|
||||
}
|
||||
}
|
||||
|
||||
func stepPrepareComposeBackup(composeCtx *composeBackupContext) error {
|
||||
if err := os.MkdirAll(composeCtx.backupDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("mkdir %s failed, err: %v", composeCtx.backupDir, err)
|
||||
}
|
||||
_ = os.RemoveAll(composeCtx.tmpDir)
|
||||
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "compose_files"), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "containers"), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepStopComposeForBackup(composeCtx *composeBackupContext) error {
|
||||
if composeCtx.stopped {
|
||||
return nil
|
||||
}
|
||||
options := container.ListOptions{All: false}
|
||||
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
|
||||
runningList, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(runningList) == 0 {
|
||||
return nil
|
||||
}
|
||||
if stdout, err := compose.Operate(composeCtx.composePath, "stop"); err != nil {
|
||||
return fmt.Errorf("docker-compose stop failed, std: %s, err: %v", stdout, err)
|
||||
}
|
||||
composeCtx.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepStartComposeAfterBackup(composeCtx *composeBackupContext) error {
|
||||
if !composeCtx.stopped {
|
||||
return nil
|
||||
}
|
||||
if stdout, err := compose.Up(composeCtx.composePath); err != nil {
|
||||
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
||||
}
|
||||
composeCtx.stopped = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepBackupComposeFiles(composeCtx *composeBackupContext) error {
|
||||
for i, filePath := range composeCtx.composeFiles {
|
||||
backupName := fmt.Sprintf("%02d_%s", i, path.Base(filePath))
|
||||
backupPath := path.Join(composeCtx.tmpDir, "compose_files", backupName)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := composeCtx.fileOp.SaveFile(backupPath, string(content), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
relativePath := path.Base(filePath)
|
||||
if composeCtx.composeDir != "" {
|
||||
rel, relErr := filepath.Rel(composeCtx.composeDir, filePath)
|
||||
if relErr == nil {
|
||||
rel = filepath.ToSlash(rel)
|
||||
if rel != "" && rel != "." && !strings.HasPrefix(rel, "../") {
|
||||
relativePath = rel
|
||||
}
|
||||
}
|
||||
}
|
||||
composeCtx.meta.Files = append(composeCtx.meta.Files, composeBackupFile{
|
||||
OriginalPath: filePath,
|
||||
FileName: path.Base(filePath),
|
||||
RelativePath: relativePath,
|
||||
BackupPath: path.Join("compose_files", backupName),
|
||||
})
|
||||
}
|
||||
if len(composeCtx.composeFiles) != 0 {
|
||||
envPath := path.Join(path.Dir(composeCtx.composeFiles[0]), ".env")
|
||||
if composeCtx.fileOp.Stat(envPath) {
|
||||
envContent, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_files", ".env"), string(envContent), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepWriteComposeBackupMeta(composeCtx *composeBackupContext) error {
|
||||
metaBytes, err := json.MarshalIndent(composeCtx.meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_meta.json"), string(metaBytes), fs.ModePerm)
|
||||
}
|
||||
|
||||
func stepPackComposeBackup(composeCtx *composeBackupContext) error {
|
||||
return composeCtx.fileOp.TarGzCompressPro(true, composeCtx.tmpDir, composeCtx.filePath, composeCtx.req.Secret, "")
|
||||
}
|
||||
|
||||
func newComposeRecoverContext(req dto.CommonRecover) (*composeRecoverContext, error) {
|
||||
tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz"))
|
||||
ctx := &composeRecoverContext{
|
||||
req: req,
|
||||
fileOp: files.NewFileOp(),
|
||||
tmpDir: tmpDir,
|
||||
meta: composeBackupMeta{
|
||||
Files: make([]composeBackupFile, 0),
|
||||
Containers: make([]string, 0),
|
||||
},
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (c *composeRecoverContext) close() {
|
||||
if c.tmpDir != "" {
|
||||
_ = os.RemoveAll(c.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
func stepPrepareComposeRecover(recoverCtx *composeRecoverContext) error {
|
||||
if !recoverCtx.fileOp.Stat(recoverCtx.req.File) {
|
||||
return buserr.WithName("ErrFileNotFound", recoverCtx.req.File)
|
||||
}
|
||||
_ = os.RemoveAll(recoverCtx.tmpDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepExtractComposeRecover(recoverCtx *composeRecoverContext) error {
|
||||
return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret)
|
||||
}
|
||||
|
||||
func stepLoadComposeRecoverMeta(recoverCtx *composeRecoverContext) error {
|
||||
metaPath := path.Join(recoverCtx.tmpDir, "compose_meta.json")
|
||||
if !recoverCtx.fileOp.Stat(metaPath) {
|
||||
return fmt.Errorf("compose_meta.json not found in backup file")
|
||||
}
|
||||
metaBytes, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil {
|
||||
return fmt.Errorf("unmarshal compose_meta.json failed, err: %v", err)
|
||||
}
|
||||
recoverCtx.composeName = strings.TrimSpace(recoverCtx.req.Name)
|
||||
if recoverCtx.composeName == "" {
|
||||
recoverCtx.composeName = strings.TrimSpace(recoverCtx.meta.ComposeName)
|
||||
}
|
||||
if recoverCtx.composeName == "" {
|
||||
return fmt.Errorf("compose name not found in recover request or backup file")
|
||||
}
|
||||
recoverCtx.targetDir = resolveComposeRecoverTargetDir(recoverCtx.meta, recoverCtx.composeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveComposeRecoverTargetDir(meta composeBackupMeta, composeName string) string {
|
||||
composePath := strings.TrimSpace(meta.ComposePath)
|
||||
if composePath != "" {
|
||||
items := strings.Split(composePath, ",")
|
||||
for _, item := range items {
|
||||
p := strings.TrimSpace(item)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(path.Ext(p))
|
||||
if ext == ".yml" || ext == ".yaml" {
|
||||
return path.Dir(p)
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
return path.Join(global.Dir.DataDir, "docker/compose", composeName)
|
||||
}
|
||||
|
||||
func safeJoinWithinBase(baseDir, name string) (string, error) {
|
||||
base := filepath.Clean(baseDir)
|
||||
candidate := strings.TrimSpace(name)
|
||||
candidate = strings.ReplaceAll(candidate, "\\", "/")
|
||||
candidate = filepath.Clean(filepath.FromSlash(candidate))
|
||||
if candidate == "" || candidate == "." {
|
||||
return "", fmt.Errorf("invalid path: empty")
|
||||
}
|
||||
if filepath.IsAbs(candidate) {
|
||||
return "", fmt.Errorf("invalid path %q: absolute path is not allowed", name)
|
||||
}
|
||||
if candidate == ".." || strings.HasPrefix(candidate, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
|
||||
}
|
||||
resolved := filepath.Clean(filepath.Join(base, candidate))
|
||||
rel, err := filepath.Rel(base, resolved)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve path %q failed, err: %v", name, err)
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if recoverCtx.targetDir != "" {
|
||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||
}
|
||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
||||
for _, item := range recoverCtx.meta.Files {
|
||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||
}
|
||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
||||
continue
|
||||
}
|
||||
targetName := item.FileName
|
||||
if item.RelativePath != "" {
|
||||
targetName = item.RelativePath
|
||||
}
|
||||
if targetName == "" {
|
||||
targetName = path.Base(item.OriginalPath)
|
||||
}
|
||||
if targetName == "" {
|
||||
targetName = "docker-compose.yml"
|
||||
}
|
||||
targetPath, err := safeJoinWithinBase(recoverCtx.targetDir, targetName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
||||
}
|
||||
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
restored = append(restored, targetPath)
|
||||
}
|
||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||
if recoverCtx.fileOp.Stat(envPath) {
|
||||
envContent, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := recoverCtx.fileOp.SaveFile(path.Join(recoverCtx.targetDir, ".env"), string(envContent), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(restored) == 0 {
|
||||
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
||||
if !recoverCtx.fileOp.Stat(defaultPath) {
|
||||
return fmt.Errorf("compose file not found in backup data")
|
||||
}
|
||||
restored = append(restored, defaultPath)
|
||||
}
|
||||
sort.Strings(restored)
|
||||
recoverCtx.composePath = strings.Join(restored, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepSaveComposeRecord(recoverCtx *composeRecoverContext) error {
|
||||
if recoverCtx.composePath == "" {
|
||||
recoverCtx.composePath = path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
||||
}
|
||||
recordName := strings.ToLower(recoverCtx.composeName)
|
||||
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
|
||||
if record.ID == 0 {
|
||||
return composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: recoverCtx.composePath})
|
||||
}
|
||||
return composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": recoverCtx.composePath})
|
||||
}
|
||||
|
||||
func sanitizeComposeFileName(in string) string {
|
||||
name := strings.TrimSpace(in)
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
if name == "" {
|
||||
return "container"
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type containerBackupMeta struct {
|
||||
ContainerName string `json:"containerName"`
|
||||
ContainerID string `json:"containerID"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Image string `json:"image"`
|
||||
HostConfig *container.HostConfig `json:"hostConfig,omitempty"`
|
||||
Config *container.Config `json:"config,omitempty"`
|
||||
Mounts []containerMountBackup `json:"mounts"`
|
||||
}
|
||||
|
||||
type containerMountBackup struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Destination string `json:"destination"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
RW bool `json:"rw"`
|
||||
Propagation string `json:"propagation,omitempty"`
|
||||
BackupPath string `json:"backupPath,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type containerBackupContext struct {
|
||||
containerName string
|
||||
backupDir string
|
||||
fileName string
|
||||
secret string
|
||||
filePath string
|
||||
tmpDir string
|
||||
mountRoot string
|
||||
wasRunning bool
|
||||
stopped bool
|
||||
fileOp files.FileOp
|
||||
inspectInfo container.InspectResponse
|
||||
meta containerBackupMeta
|
||||
}
|
||||
|
||||
type containerRecoverContext struct {
|
||||
req dto.CommonRecover
|
||||
targetName string
|
||||
fileOp files.FileOp
|
||||
client *client.Client
|
||||
tmpDir string
|
||||
meta containerBackupMeta
|
||||
inspectInfo container.InspectResponse
|
||||
shouldStart bool
|
||||
createdContainerID string
|
||||
}
|
||||
|
||||
func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
|
||||
timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
|
||||
fileName := req.FileName
|
||||
if fileName == "" {
|
||||
fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow)
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".tar.gz") {
|
||||
fileName += ".tar.gz"
|
||||
}
|
||||
itemDir := fmt.Sprintf("container/%s", req.Name)
|
||||
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||
record := &model.BackupRecord{
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
SourceAccountIDs: "1",
|
||||
DownloadAccountID: 1,
|
||||
FileDir: itemDir,
|
||||
FileName: fileName,
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
|
||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackupService) ContainerRecover(req dto.CommonRecover) error {
|
||||
return handleContainerRecover(req, nil)
|
||||
}
|
||||
|
||||
func handleContainerBackup(containerName string, parentTask *task.Task, recordID uint, backupDir, fileName, taskID, secret string, stopBefore bool) error {
|
||||
var (
|
||||
err error
|
||||
backupTask *task.Task
|
||||
)
|
||||
backupCtx, err := newContainerBackupContext(containerName, backupDir, fileName, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupTask = parentTask
|
||||
if backupTask == nil {
|
||||
backupTask, err = task.NewTaskWithOps(containerName, task.TaskBackup, task.TaskScopeBackup, taskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if stopBefore {
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStop"), func(t *task.Task) error {
|
||||
return stepStopContainerForBackup(backupCtx)
|
||||
}, func(t *task.Task) {
|
||||
_ = stepStartContainerAfterBackup(backupCtx)
|
||||
}, 3, time.Hour)
|
||||
}
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupPrepare"), func(t *task.Task) error {
|
||||
t.Logf("------------------ %s ------------------", containerName)
|
||||
return stepPrepareContainerBackup(backupCtx)
|
||||
}, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupInspect"), func(t *task.Task) error { return stepBackupContainerInspect(backupCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMounts"), func(t *task.Task) error { return stepBackupContainerMounts(backupCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMeta"), func(t *task.Task) error { return stepWriteContainerMeta(backupCtx) }, nil, 3, time.Hour)
|
||||
backupTask.AddSubTaskWithOps(task.GetTaskName(containerName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return stepPackContainerBackup(backupCtx) }, nil, 3, time.Hour)
|
||||
if stopBefore {
|
||||
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStart"), func(t *task.Task) error {
|
||||
return stepStartContainerAfterBackup(backupCtx)
|
||||
}, nil, 3, time.Hour)
|
||||
}
|
||||
if parentTask != nil {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
defer backupCtx.close()
|
||||
if err := backupTask.Execute(); err != nil {
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleContainerRecover(req dto.CommonRecover, parentTask *task.Task) error {
|
||||
var (
|
||||
err error
|
||||
recoverTask *task.Task
|
||||
recoverCtx *containerRecoverContext
|
||||
)
|
||||
recoverTask = parentTask
|
||||
if recoverTask == nil {
|
||||
if isImportRecover(req) {
|
||||
taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Container")
|
||||
recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
recoverTask, err = task.NewTaskWithOps("container", task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeout := loadRecoverTimeout(req.Timeout)
|
||||
logName := strings.TrimSpace(req.Name)
|
||||
if logName == "" && req.File != "" {
|
||||
logName = strings.TrimSuffix(path.Base(req.File), ".tar.gz")
|
||||
}
|
||||
if logName == "" {
|
||||
logName = "container"
|
||||
}
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverPrepare"), func(t *task.Task) error {
|
||||
ctx, err := newContainerRecoverContext(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoverCtx = ctx
|
||||
t.Logf("------------------ %s ------------------", logName)
|
||||
if err := stepPrepareContainerRecover(recoverCtx); err != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, func(t *task.Task) {
|
||||
if recoverCtx != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
}
|
||||
}, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverExtract"), func(t *task.Task) error { return stepExtractContainerRecover(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverParse"), func(t *task.Task) error { return stepLoadContainerRecoverData(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCreate"), func(t *task.Task) error { return stepRecreateContainer(recoverCtx, t) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverMounts"), func(t *task.Task) error { return stepRestoreContainerMounts(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverStart"), func(t *task.Task) error { return stepStartRecoveredContainer(recoverCtx) }, nil, 3, timeout)
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCleanup"), func(t *task.Task) error {
|
||||
if recoverCtx != nil {
|
||||
recoverCtx.close()
|
||||
recoverCtx = nil
|
||||
}
|
||||
return nil
|
||||
}, nil, 0, timeout)
|
||||
if parentTask != nil {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRecoverTimeout(timeout int) time.Duration {
|
||||
switch timeout {
|
||||
case -1:
|
||||
return 0
|
||||
case 0:
|
||||
return 3 * time.Hour
|
||||
default:
|
||||
return time.Duration(timeout) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
func isImportRecover(req dto.CommonRecover) bool {
|
||||
return req.BackupRecordID == 0
|
||||
}
|
||||
|
||||
func newContainerBackupContext(containerName, backupDir, fileName, secret string) (*containerBackupContext, error) {
|
||||
dockerClient, err := dockerUtils.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = dockerClient.Close()
|
||||
}()
|
||||
inspectInfo, err := dockerClient.ContainerInspect(context.Background(), containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filePath := path.Join(backupDir, fileName)
|
||||
tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz"))
|
||||
backupCtx := &containerBackupContext{
|
||||
containerName: containerName,
|
||||
backupDir: backupDir,
|
||||
fileName: fileName,
|
||||
secret: secret,
|
||||
filePath: filePath,
|
||||
tmpDir: tmpDir,
|
||||
mountRoot: path.Join(tmpDir, "mounts"),
|
||||
wasRunning: inspectInfo.State != nil && inspectInfo.State.Running,
|
||||
fileOp: files.NewFileOp(),
|
||||
inspectInfo: inspectInfo,
|
||||
meta: containerBackupMeta{
|
||||
ContainerName: containerName,
|
||||
ContainerID: inspectInfo.ID,
|
||||
CreatedAt: time.Now().Format(constant.DateTimeLayout),
|
||||
Image: inspectInfo.Config.Image,
|
||||
HostConfig: inspectInfo.HostConfig,
|
||||
Config: inspectInfo.Config,
|
||||
Mounts: make([]containerMountBackup, 0),
|
||||
},
|
||||
}
|
||||
return backupCtx, nil
|
||||
}
|
||||
|
||||
func newContainerRecoverContext(req dto.CommonRecover) (*containerRecoverContext, error) {
|
||||
dockerClient, err := dockerUtils.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz"))
|
||||
ctx := &containerRecoverContext{
|
||||
req: req,
|
||||
targetName: req.Name,
|
||||
fileOp: files.NewFileOp(),
|
||||
client: dockerClient,
|
||||
tmpDir: tmpDir,
|
||||
meta: containerBackupMeta{
|
||||
Mounts: make([]containerMountBackup, 0),
|
||||
},
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (c *containerBackupContext) close() {
|
||||
if c.tmpDir != "" {
|
||||
_ = os.RemoveAll(c.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *containerRecoverContext) close() {
|
||||
if c.client != nil {
|
||||
_ = c.client.Close()
|
||||
}
|
||||
if c.tmpDir != "" {
|
||||
_ = os.RemoveAll(c.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
func stepPrepareContainerBackup(backupCtx *containerBackupContext) error {
|
||||
if err := os.MkdirAll(backupCtx.backupDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("mkdir %s failed, err: %v", backupCtx.backupDir, err)
|
||||
}
|
||||
_ = os.RemoveAll(backupCtx.tmpDir)
|
||||
if err := os.MkdirAll(backupCtx.mountRoot, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepStopContainerForBackup(backupCtx *containerBackupContext) error {
|
||||
if !backupCtx.wasRunning || backupCtx.stopped {
|
||||
return nil
|
||||
}
|
||||
dockerClient, err := dockerUtils.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = dockerClient.Close()
|
||||
}()
|
||||
if err := dockerClient.ContainerStop(context.Background(), backupCtx.inspectInfo.ID, container.StopOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
backupCtx.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepStartContainerAfterBackup(backupCtx *containerBackupContext) error {
|
||||
if !backupCtx.stopped {
|
||||
return nil
|
||||
}
|
||||
dockerClient, err := dockerUtils.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = dockerClient.Close()
|
||||
}()
|
||||
if err := dockerClient.ContainerStart(context.Background(), backupCtx.inspectInfo.ID, container.StartOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
backupCtx.stopped = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepBackupContainerInspect(backupCtx *containerBackupContext) error {
|
||||
inspectBytes, err := json.MarshalIndent(backupCtx.inspectInfo, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "inspect.json"), string(inspectBytes), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if backupCtx.inspectInfo.NetworkSettings != nil {
|
||||
networkBytes, err := json.MarshalIndent(backupCtx.inspectInfo.NetworkSettings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "network.json"), string(networkBytes), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepBackupContainerMounts(backupCtx *containerBackupContext) error {
|
||||
var (
|
||||
dockerClient *client.Client
|
||||
clientErr error
|
||||
)
|
||||
ensureClient := func() (*client.Client, error) {
|
||||
if dockerClient != nil || clientErr != nil {
|
||||
return dockerClient, clientErr
|
||||
}
|
||||
dockerClient, clientErr = dockerUtils.NewDockerClient()
|
||||
return dockerClient, clientErr
|
||||
}
|
||||
defer func() {
|
||||
if dockerClient != nil {
|
||||
_ = dockerClient.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for i, item := range backupCtx.inspectInfo.Mounts {
|
||||
mountMeta := containerMountBackup{
|
||||
Type: string(item.Type),
|
||||
Name: item.Name,
|
||||
Source: item.Source,
|
||||
Destination: item.Destination,
|
||||
Mode: item.Mode,
|
||||
RW: item.RW,
|
||||
Propagation: string(item.Propagation),
|
||||
Status: "skipped",
|
||||
}
|
||||
|
||||
mountDirName := fmt.Sprintf("%02d_%s", i, sanitizeContainerMountName(item.Destination))
|
||||
mountDir := path.Join(backupCtx.mountRoot, mountDirName)
|
||||
mountMeta.BackupPath = path.Join("mounts", mountDirName, "data")
|
||||
|
||||
switch item.Type {
|
||||
case mount.TypeBind, mount.TypeVolume:
|
||||
if item.Source == "" {
|
||||
mountMeta.Message = "empty source"
|
||||
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
|
||||
continue
|
||||
}
|
||||
sourceInfo, statErr := os.Stat(item.Source)
|
||||
if statErr != nil {
|
||||
mountMeta.Message = statErr.Error()
|
||||
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
|
||||
continue
|
||||
}
|
||||
dataDir := path.Join(mountDir, "data")
|
||||
if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if sourceInfo.IsDir() {
|
||||
if err := backupCtx.fileOp.CopyDirWithNewName(item.Source, dataDir, "."); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := backupCtx.fileOp.CopyFile(item.Source, dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if item.Type == mount.TypeVolume && item.Name != "" {
|
||||
cli, err := ensureClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volumeInfo, volumeErr := cli.VolumeInspect(context.Background(), item.Name)
|
||||
if volumeErr == nil {
|
||||
volumeBytes, volumeMarshalErr := json.MarshalIndent(volumeInfo, "", " ")
|
||||
if volumeMarshalErr == nil {
|
||||
_ = backupCtx.fileOp.SaveFile(path.Join(mountDir, "volume.json"), string(volumeBytes), fs.ModePerm)
|
||||
}
|
||||
}
|
||||
}
|
||||
mountMeta.Status = "backed_up"
|
||||
default:
|
||||
mountMeta.Message = "mount type not supported for data backup"
|
||||
}
|
||||
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepWriteContainerMeta(backupCtx *containerBackupContext) error {
|
||||
metaBytes, err := json.MarshalIndent(backupCtx.meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "meta.json"), string(metaBytes), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepPackContainerBackup(backupCtx *containerBackupContext) error {
|
||||
if err := backupCtx.fileOp.TarGzCompressPro(true, backupCtx.tmpDir, backupCtx.filePath, backupCtx.secret, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepPrepareContainerRecover(recoverCtx *containerRecoverContext) error {
|
||||
if !recoverCtx.fileOp.Stat(recoverCtx.req.File) {
|
||||
return buserr.WithName("ErrFileNotFound", recoverCtx.req.File)
|
||||
}
|
||||
_ = os.RemoveAll(recoverCtx.tmpDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepExtractContainerRecover(recoverCtx *containerRecoverContext) error {
|
||||
return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret)
|
||||
}
|
||||
|
||||
func stepLoadContainerRecoverData(recoverCtx *containerRecoverContext) error {
|
||||
if err := loadContainerRecoverMeta(recoverCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadContainerRecoverInspect(recoverCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
if recoverCtx.targetName == "" {
|
||||
recoverCtx.targetName = strings.TrimPrefix(recoverCtx.inspectInfo.Name, "/")
|
||||
}
|
||||
if recoverCtx.targetName == "" {
|
||||
recoverCtx.targetName = recoverCtx.meta.ContainerName
|
||||
}
|
||||
if recoverCtx.targetName == "" {
|
||||
return fmt.Errorf("container name not found in recover request or backup file")
|
||||
}
|
||||
if recoverCtx.inspectInfo.Config == nil {
|
||||
recoverCtx.inspectInfo.Config = recoverCtx.meta.Config
|
||||
}
|
||||
if recoverCtx.inspectInfo.HostConfig == nil {
|
||||
recoverCtx.inspectInfo.HostConfig = recoverCtx.meta.HostConfig
|
||||
}
|
||||
if recoverCtx.inspectInfo.Config == nil {
|
||||
return fmt.Errorf("container config not found in backup file")
|
||||
}
|
||||
if recoverCtx.inspectInfo.HostConfig == nil {
|
||||
recoverCtx.inspectInfo.HostConfig = &container.HostConfig{}
|
||||
}
|
||||
recoverCtx.shouldStart = recoverCtx.inspectInfo.State != nil && recoverCtx.inspectInfo.State.Running
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadContainerRecoverMeta(recoverCtx *containerRecoverContext) error {
|
||||
metaPath := path.Join(recoverCtx.tmpDir, "meta.json")
|
||||
if !recoverCtx.fileOp.Stat(metaPath) {
|
||||
return nil
|
||||
}
|
||||
metaBytes, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil {
|
||||
return fmt.Errorf("unmarshal meta.json failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadContainerRecoverInspect(recoverCtx *containerRecoverContext) error {
|
||||
inspectPath := path.Join(recoverCtx.tmpDir, "inspect.json")
|
||||
if !recoverCtx.fileOp.Stat(inspectPath) {
|
||||
return fmt.Errorf("inspect.json not found in backup file")
|
||||
}
|
||||
inspectBytes, err := os.ReadFile(inspectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(inspectBytes, &recoverCtx.inspectInfo); err != nil {
|
||||
return fmt.Errorf("unmarshal inspect.json failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.Task) error {
|
||||
ctx := context.Background()
|
||||
if err := ensureContainerRecoverNetworks(recoverCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureContainerRecoverVolumes(recoverCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := cloneContainerConfig(recoverCtx.inspectInfo.Config)
|
||||
hostConfig := cloneContainerHostConfig(recoverCtx.inspectInfo.HostConfig)
|
||||
if config.Image == "" {
|
||||
config.Image = recoverCtx.meta.Image
|
||||
}
|
||||
if config.Image == "" {
|
||||
return fmt.Errorf("container image not found in backup file")
|
||||
}
|
||||
if !checkImageExist(recoverCtx.client, config.Image) {
|
||||
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := recoverCtx.client.ContainerInspect(ctx, recoverCtx.targetName); err == nil {
|
||||
if err := recoverCtx.client.ContainerRemove(ctx, recoverCtx.targetName, container.RemoveOptions{Force: true, RemoveVolumes: false}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !client.IsErrNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
networkConfig, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
|
||||
removeBridgeDriverIPAM(recoverCtx.client, networkConfig, extraNetworks)
|
||||
createRes, err := recoverCtx.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, recoverCtx.targetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoverCtx.createdContainerID = createRes.ID
|
||||
|
||||
extraNames := make([]string, 0, len(extraNetworks))
|
||||
for name := range extraNetworks {
|
||||
extraNames = append(extraNames, name)
|
||||
}
|
||||
sort.Strings(extraNames)
|
||||
for _, item := range extraNames {
|
||||
if err := recoverCtx.client.NetworkConnect(ctx, item, recoverCtx.createdContainerID, extraNetworks[item]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeBridgeDriverIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||
if primary != nil {
|
||||
removeBridgeDriverIPAMFromEndpoints(cli, primary.EndpointsConfig)
|
||||
}
|
||||
removeBridgeDriverIPAMFromEndpoints(cli, extras)
|
||||
}
|
||||
|
||||
func removeBridgeDriverIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
|
||||
for netName, endpoint := range endpoints {
|
||||
if endpoint == nil || endpoint.IPAMConfig == nil {
|
||||
continue
|
||||
}
|
||||
info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Driver == "bridge" {
|
||||
endpoint.IPAMConfig = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureContainerRecoverNetworks(recoverCtx *containerRecoverContext) error {
|
||||
if recoverCtx.inspectInfo.NetworkSettings == nil {
|
||||
return nil
|
||||
}
|
||||
for netName := range recoverCtx.inspectInfo.NetworkSettings.Networks {
|
||||
if netName == "" || netName == "bridge" || netName == "host" || netName == "none" {
|
||||
continue
|
||||
}
|
||||
if _, err := recoverCtx.client.NetworkInspect(context.Background(), netName, network.InspectOptions{}); err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
return err
|
||||
}
|
||||
if _, err := recoverCtx.client.NetworkCreate(context.Background(), netName, network.CreateOptions{Driver: "bridge"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureContainerRecoverVolumes(recoverCtx *containerRecoverContext) error {
|
||||
for _, item := range recoverCtx.meta.Mounts {
|
||||
if item.Type != string(mount.TypeVolume) || item.Name == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := recoverCtx.client.VolumeInspect(context.Background(), item.Name); err == nil {
|
||||
continue
|
||||
} else if !client.IsErrNotFound(err) {
|
||||
return err
|
||||
}
|
||||
createOptions := volume.CreateOptions{Name: item.Name}
|
||||
if item.BackupPath != "" {
|
||||
volumeMetaPath := path.Join(recoverCtx.tmpDir, path.Dir(item.BackupPath), "volume.json")
|
||||
if recoverCtx.fileOp.Stat(volumeMetaPath) {
|
||||
volumeBytes, readErr := os.ReadFile(volumeMetaPath)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
var volumeInfo volume.Volume
|
||||
if unmarshalErr := json.Unmarshal(volumeBytes, &volumeInfo); unmarshalErr != nil {
|
||||
return unmarshalErr
|
||||
}
|
||||
if volumeInfo.Driver != "" {
|
||||
createOptions.Driver = volumeInfo.Driver
|
||||
}
|
||||
if len(volumeInfo.Options) != 0 {
|
||||
createOptions.DriverOpts = volumeInfo.Options
|
||||
}
|
||||
if len(volumeInfo.Labels) != 0 {
|
||||
createOptions.Labels = volumeInfo.Labels
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := recoverCtx.client.VolumeCreate(context.Background(), createOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettings, hostConfig *container.HostConfig) (*network.NetworkingConfig, map[string]*network.EndpointSettings) {
|
||||
extraNetworks := make(map[string]*network.EndpointSettings)
|
||||
if hostConfig != nil {
|
||||
networkMode := string(hostConfig.NetworkMode)
|
||||
if networkMode == "host" || networkMode == "none" {
|
||||
return nil, extraNetworks
|
||||
}
|
||||
}
|
||||
if networkSettings == nil || len(networkSettings.Networks) == 0 {
|
||||
return nil, extraNetworks
|
||||
}
|
||||
|
||||
primaryName := ""
|
||||
if hostConfig != nil {
|
||||
networkMode := string(hostConfig.NetworkMode)
|
||||
if networkMode != "" && networkMode != "default" && networkMode != "bridge" {
|
||||
if _, ok := networkSettings.Networks[networkMode]; ok {
|
||||
primaryName = networkMode
|
||||
}
|
||||
}
|
||||
}
|
||||
if primaryName == "" {
|
||||
if _, ok := networkSettings.Networks["bridge"]; ok {
|
||||
primaryName = "bridge"
|
||||
} else {
|
||||
names := make([]string, 0, len(networkSettings.Networks))
|
||||
for name := range networkSettings.Networks {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if len(names) > 0 {
|
||||
primaryName = names[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config := &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}
|
||||
for name, endpoint := range networkSettings.Networks {
|
||||
if name == "host" || name == "none" {
|
||||
continue
|
||||
}
|
||||
endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
|
||||
if endpoint.IPAMConfig != nil {
|
||||
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
||||
IPv4Address: endpoint.IPAMConfig.IPv4Address,
|
||||
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
||||
}
|
||||
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
|
||||
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
||||
IPv4Address: endpoint.IPAddress,
|
||||
IPv6Address: endpoint.GlobalIPv6Address,
|
||||
}
|
||||
}
|
||||
if name == primaryName {
|
||||
config.EndpointsConfig[name] = endpointSetting
|
||||
} else {
|
||||
extraNetworks[name] = endpointSetting
|
||||
}
|
||||
}
|
||||
if len(config.EndpointsConfig) == 0 {
|
||||
return nil, extraNetworks
|
||||
}
|
||||
return config, extraNetworks
|
||||
}
|
||||
|
||||
func cloneContainerConfig(config *container.Config) *container.Config {
|
||||
if config == nil {
|
||||
return &container.Config{}
|
||||
}
|
||||
item := *config
|
||||
if len(config.Env) != 0 {
|
||||
item.Env = append([]string(nil), config.Env...)
|
||||
}
|
||||
if len(config.Cmd) != 0 {
|
||||
item.Cmd = append([]string(nil), config.Cmd...)
|
||||
}
|
||||
if len(config.Entrypoint) != 0 {
|
||||
item.Entrypoint = append([]string(nil), config.Entrypoint...)
|
||||
}
|
||||
if len(config.Labels) != 0 {
|
||||
labels := make(map[string]string, len(config.Labels))
|
||||
for key, val := range config.Labels {
|
||||
labels[key] = val
|
||||
}
|
||||
item.Labels = labels
|
||||
}
|
||||
if len(config.Volumes) != 0 {
|
||||
volumes := make(map[string]struct{}, len(config.Volumes))
|
||||
for key, val := range config.Volumes {
|
||||
volumes[key] = val
|
||||
}
|
||||
item.Volumes = volumes
|
||||
}
|
||||
return &item
|
||||
}
|
||||
|
||||
func cloneContainerHostConfig(hostConfig *container.HostConfig) *container.HostConfig {
|
||||
if hostConfig == nil {
|
||||
return &container.HostConfig{}
|
||||
}
|
||||
item := *hostConfig
|
||||
if len(hostConfig.Binds) != 0 {
|
||||
item.Binds = append([]string(nil), hostConfig.Binds...)
|
||||
}
|
||||
if len(hostConfig.DNS) != 0 {
|
||||
item.DNS = append([]string(nil), hostConfig.DNS...)
|
||||
}
|
||||
if len(hostConfig.ExtraHosts) != 0 {
|
||||
item.ExtraHosts = append([]string(nil), hostConfig.ExtraHosts...)
|
||||
}
|
||||
if len(hostConfig.Mounts) != 0 {
|
||||
item.Mounts = append([]mount.Mount(nil), hostConfig.Mounts...)
|
||||
}
|
||||
return &item
|
||||
}
|
||||
|
||||
func stepRestoreContainerMounts(recoverCtx *containerRecoverContext) error {
|
||||
currentContainer := recoverCtx.createdContainerID
|
||||
if currentContainer == "" {
|
||||
currentContainer = recoverCtx.targetName
|
||||
}
|
||||
currentInspect, err := recoverCtx.client.ContainerInspect(context.Background(), currentContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentMounts := make(map[string]container.MountPoint, len(currentInspect.Mounts))
|
||||
for _, item := range currentInspect.Mounts {
|
||||
currentMounts[item.Destination] = item
|
||||
}
|
||||
|
||||
for _, item := range recoverCtx.meta.Mounts {
|
||||
if item.Status != "backed_up" || item.BackupPath == "" || !item.RW {
|
||||
continue
|
||||
}
|
||||
backupPath := path.Join(recoverCtx.tmpDir, item.BackupPath)
|
||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
||||
continue
|
||||
}
|
||||
sourcePath := item.Source
|
||||
if currentMount, ok := currentMounts[item.Destination]; ok {
|
||||
if currentMount.Source != "" {
|
||||
sourcePath = currentMount.Source
|
||||
}
|
||||
if item.Type == string(mount.TypeVolume) && item.Name == "" {
|
||||
item.Name = currentMount.Name
|
||||
}
|
||||
}
|
||||
if sourcePath == "" && item.Type == string(mount.TypeVolume) && item.Name != "" {
|
||||
volumeInfo, volumeErr := recoverCtx.client.VolumeInspect(context.Background(), item.Name)
|
||||
if volumeErr != nil {
|
||||
return volumeErr
|
||||
}
|
||||
sourcePath = volumeInfo.Mountpoint
|
||||
}
|
||||
if sourcePath == "" {
|
||||
continue
|
||||
}
|
||||
if err := restoreContainerMountData(recoverCtx.fileOp, backupPath, sourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreContainerMountData(fileOp files.FileOp, backupPath, sourcePath string) error {
|
||||
if sourcePath == "/" {
|
||||
return fmt.Errorf("invalid mount source path /")
|
||||
}
|
||||
entries, err := os.ReadDir(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entries) == 1 && !entries[0].IsDir() && entries[0].Name() == path.Base(sourcePath) {
|
||||
if err := os.MkdirAll(path.Dir(sourcePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.RemoveAll(sourcePath)
|
||||
if err := fileOp.CopyFile(path.Join(backupPath, entries[0].Name()), path.Dir(sourcePath)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = os.RemoveAll(sourcePath)
|
||||
if err := os.MkdirAll(sourcePath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyDirWithNewName(backupPath, sourcePath, "."); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepStartRecoveredContainer(recoverCtx *containerRecoverContext) error {
|
||||
if !recoverCtx.shouldStart {
|
||||
return nil
|
||||
}
|
||||
containerID := recoverCtx.createdContainerID
|
||||
if containerID == "" {
|
||||
containerID = recoverCtx.targetName
|
||||
}
|
||||
return recoverCtx.client.ContainerStart(context.Background(), containerID, container.StartOptions{})
|
||||
}
|
||||
|
||||
func sanitizeContainerMountName(in string) string {
|
||||
name := strings.TrimSpace(in)
|
||||
name = strings.Trim(name, "/")
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
if name == "" {
|
||||
return "root"
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -40,6 +43,7 @@ import (
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/gin-gonic/gin"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@@ -89,6 +93,12 @@ type IContainerService interface {
|
||||
Prune(req dto.ContainerPrune) error
|
||||
|
||||
LoadUsers(req dto.OperationWithName) []string
|
||||
ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error)
|
||||
UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error
|
||||
GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error)
|
||||
GetContainerFileSize(req dto.ContainerFileReq) (int64, error)
|
||||
DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error
|
||||
DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error)
|
||||
|
||||
StreamLogs(ctx *gin.Context, params dto.StreamLog)
|
||||
}
|
||||
@@ -1166,6 +1176,444 @@ func (u *ContainerService) LoadUsers(req dto.OperationWithName) []string {
|
||||
return users
|
||||
}
|
||||
|
||||
func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error) {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isDir := stat.Mode.IsDir()
|
||||
isLink := stat.Mode&os.ModeSymlink != 0
|
||||
if isLink && !isDir {
|
||||
linkDir, linkErr := isContainerDir(cli, req.ContainerID, req.Path)
|
||||
if linkErr == nil {
|
||||
isDir = linkDir
|
||||
}
|
||||
}
|
||||
if !isDir {
|
||||
return []dto.ContainerFileInfo{toContainerFileInfo(req.Path, stat, isDir)}, nil
|
||||
}
|
||||
|
||||
output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
files := make([]dto.ContainerFileInfo, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
name := strings.TrimSpace(line)
|
||||
if len(name) == 0 || name == "." || name == ".." {
|
||||
continue
|
||||
}
|
||||
childPath := req.Path
|
||||
if childPath == "/" {
|
||||
childPath = "/" + name
|
||||
} else {
|
||||
childPath = strings.TrimSuffix(childPath, "/") + "/" + name
|
||||
}
|
||||
childStat, statErr := cli.ContainerStatPath(ctx, req.ContainerID, childPath)
|
||||
if statErr != nil {
|
||||
continue
|
||||
}
|
||||
childIsDir := childStat.Mode.IsDir()
|
||||
if childStat.Mode&os.ModeSymlink != 0 && !childIsDir {
|
||||
linkDir, linkErr := isContainerDir(cli, req.ContainerID, childPath)
|
||||
if linkErr == nil {
|
||||
childIsDir = linkDir
|
||||
}
|
||||
}
|
||||
files = append(files, toContainerFileInfo(childPath, childStat, childIsDir))
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].IsDir != files[j].IsDir {
|
||||
return files[i].IsDir
|
||||
}
|
||||
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error {
|
||||
for _, item := range req.Paths {
|
||||
if strings.TrimSpace(item) == "/" {
|
||||
return buserr.New("ErrPathNotDelete")
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
command := []string{"rm", "-rf", "--"}
|
||||
command = append(command, req.Paths...)
|
||||
_, err = runContainerCommand(cli, req.ContainerID, command)
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *ContainerService) UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
safeName := path.Base(fileName)
|
||||
if safeName == "." || safeName == "/" || len(safeName) == 0 {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
if _, mkErr := runContainerCommand(cli, req.ContainerID, []string{"mkdir", "-p", "--", req.Path}); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
stat, err = cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !stat.Mode.IsDir() {
|
||||
return fmt.Errorf("path %s is not directory", req.Path)
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
tw := tar.NewWriter(pipeWriter)
|
||||
header := &tar.Header{
|
||||
Name: safeName,
|
||||
Mode: 0644,
|
||||
Size: fileSize,
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
_ = tw.Close()
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(tw, file); err != nil {
|
||||
_ = tw.Close()
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
_ = pipeWriter.Close()
|
||||
writeErr <- nil
|
||||
}()
|
||||
|
||||
err = cli.CopyToContainer(ctx, req.ContainerID, req.Path, pipeReader, container.CopyToContainerOptions{
|
||||
CopyUIDGID: true,
|
||||
})
|
||||
if err != nil {
|
||||
_ = pipeReader.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
<-writeErr
|
||||
return err
|
||||
}
|
||||
if err := <-writeErr; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error) {
|
||||
if len(req.Path) == 0 {
|
||||
return nil, buserr.New("ErrInvalidChar")
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stat.Mode.IsDir() {
|
||||
return nil, fmt.Errorf("path %s is directory", req.Path)
|
||||
}
|
||||
|
||||
content := &dto.ContainerFileContent{Size: stat.Size}
|
||||
headBytes, err := runContainerCommandRaw(cli, req.ContainerID, []string{"head", "-c", "4096", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bytes.IndexByte(headBytes, 0) >= 0 {
|
||||
content.IsBinary = true
|
||||
return content, nil
|
||||
}
|
||||
|
||||
const inlinePreviewMax = 512 * 1024
|
||||
if stat.Size <= inlinePreviewMax {
|
||||
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"cat", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Content = string(raw)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"tail", "-n", "300", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Content = string(raw)
|
||||
content.Truncated = true
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64, error) {
|
||||
if len(req.Path) == 0 {
|
||||
return 0, buserr.New("ErrInvalidChar")
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !stat.Mode.IsDir() {
|
||||
return stat.Size, nil
|
||||
}
|
||||
output, err := runContainerCommand(cli, req.ContainerID, []string{"du", "-sb", "--", req.Path})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
parts := strings.Fields(output)
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("invalid du output")
|
||||
}
|
||||
size, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error) {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
fileName := stat.Name
|
||||
if len(fileName) == 0 {
|
||||
fileName = "container-file"
|
||||
}
|
||||
if stat.Mode.IsDir() {
|
||||
if _, err := runContainerCommand(cli, req.ContainerID, []string{"sh", "-c", "command -v tar >/dev/null 2>&1"}); err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", fmt.Errorf("tar command not found in container")
|
||||
}
|
||||
|
||||
targetPath := path.Clean(req.Path)
|
||||
parentPath := path.Dir(targetPath)
|
||||
targetName := path.Base(targetPath)
|
||||
if parentPath == "." || parentPath == "" {
|
||||
parentPath = "/"
|
||||
}
|
||||
tarStream, err := runContainerCommandStream(cli, req.ContainerID, []string{
|
||||
"tar", "-czf", "-", "-C", parentPath, "--", targetName,
|
||||
})
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".tar.gz") {
|
||||
fileName += ".tar.gz"
|
||||
}
|
||||
return &closeHookReader{
|
||||
ReadCloser: tarStream,
|
||||
onClose: cli.Close,
|
||||
}, fileName, "application/gzip", nil
|
||||
}
|
||||
|
||||
fileStream, err := runContainerCommandStream(cli, req.ContainerID, []string{"cat", "--", req.Path})
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
return &closeHookReader{
|
||||
ReadCloser: fileStream,
|
||||
onClose: cli.Close,
|
||||
}, fileName, "application/octet-stream", nil
|
||||
}
|
||||
|
||||
func runContainerCommand(cli *client.Client, containerID string, command []string) (string, error) {
|
||||
raw, err := runContainerCommandRaw(cli, containerID, command)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(raw)), nil
|
||||
}
|
||||
|
||||
type closeHookReader struct {
|
||||
io.ReadCloser
|
||||
onClose func() error
|
||||
}
|
||||
|
||||
func (r *closeHookReader) Close() error {
|
||||
var closeErr error
|
||||
if r.ReadCloser != nil {
|
||||
closeErr = r.ReadCloser.Close()
|
||||
}
|
||||
if r.onClose != nil {
|
||||
if err := r.onClose(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func runContainerCommandRaw(cli *client.Client, containerID string, command []string) ([]byte, error) {
|
||||
ctx := context.Background()
|
||||
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer hijack.Close()
|
||||
|
||||
raw, err := io.ReadAll(hijack.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
if _, err := stdcopy.StdCopy(&stdout, &stderr, bytes.NewReader(raw)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := strings.TrimSpace(stdout.String())
|
||||
errorOutput := strings.TrimSpace(stderr.String())
|
||||
info, err := cli.ContainerExecInspect(ctx, resp.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.ExitCode != 0 {
|
||||
if len(errorOutput) != 0 {
|
||||
return nil, fmt.Errorf("%s", errorOutput)
|
||||
}
|
||||
if len(output) == 0 {
|
||||
return nil, fmt.Errorf("container command failed with exit code %d", info.ExitCode)
|
||||
}
|
||||
return nil, fmt.Errorf("%s", output)
|
||||
}
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func runContainerCommandStream(cli *client.Client, containerID string, command []string) (io.ReadCloser, error) {
|
||||
ctx := context.Background()
|
||||
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
go func() {
|
||||
defer hijack.Close()
|
||||
var stderr bytes.Buffer
|
||||
_, copyErr := stdcopy.StdCopy(pipeWriter, &stderr, hijack.Reader)
|
||||
if copyErr != nil {
|
||||
_ = pipeWriter.CloseWithError(copyErr)
|
||||
return
|
||||
}
|
||||
info, inspectErr := cli.ContainerExecInspect(ctx, resp.ID)
|
||||
if inspectErr != nil {
|
||||
_ = pipeWriter.CloseWithError(inspectErr)
|
||||
return
|
||||
}
|
||||
if info.ExitCode != 0 {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if len(msg) == 0 {
|
||||
msg = fmt.Sprintf("container command failed with exit code %d", info.ExitCode)
|
||||
}
|
||||
_ = pipeWriter.CloseWithError(fmt.Errorf("%s", msg))
|
||||
return
|
||||
}
|
||||
_ = pipeWriter.Close()
|
||||
}()
|
||||
return pipeReader, nil
|
||||
}
|
||||
|
||||
func toContainerFileInfo(filePath string, stat container.PathStat, isDir bool) dto.ContainerFileInfo {
|
||||
name := stat.Name
|
||||
if len(name) == 0 {
|
||||
items := strings.Split(strings.TrimSuffix(filePath, "/"), "/")
|
||||
name = items[len(items)-1]
|
||||
}
|
||||
isLink := stat.Mode&os.ModeSymlink != 0
|
||||
return dto.ContainerFileInfo{
|
||||
Name: name,
|
||||
Path: filePath,
|
||||
IsDir: isDir,
|
||||
IsLink: isLink,
|
||||
LinkTo: stat.LinkTarget,
|
||||
Size: stat.Size,
|
||||
Mode: stat.Mode.String(),
|
||||
ModTime: stat.Mtime.Format(constant.DateTimeLayout),
|
||||
}
|
||||
}
|
||||
|
||||
func isContainerDir(cli *client.Client, containerID, targetPath string) (bool, error) {
|
||||
_, err := runContainerCommand(cli, containerID, []string{
|
||||
"sh", "-c", "[ -d \"$1\" ]", "sh", targetPath,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func stringsToMap(list []string) map[string]string {
|
||||
var labelMap = make(map[string]string)
|
||||
for _, label := range list {
|
||||
|
||||
@@ -284,7 +284,7 @@ func (u *DashboardService) LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher,
|
||||
return data, err
|
||||
}
|
||||
|
||||
showList, err := launcherRepo.ListName()
|
||||
showList, _ := launcherRepo.ListName()
|
||||
defaultList, err := appRepo.GetTopRecommend()
|
||||
if err != nil {
|
||||
return data, nil
|
||||
@@ -361,6 +361,9 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
||||
_ = copier.Copy(&item, &quick)
|
||||
list = append(list, item)
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].Recommend < list[j].Recommend
|
||||
})
|
||||
return list
|
||||
}
|
||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
@@ -612,6 +615,9 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
website, _ := websiteRepo.GetBy()
|
||||
base.WebsiteNumber = len(website)
|
||||
|
||||
agents, _ := agentRepo.List()
|
||||
base.AgentNumber = len(agents)
|
||||
|
||||
postgresqlDbs, _ := postgresqlRepo.List()
|
||||
mysqlDbs, _ := mysqlRepo.List()
|
||||
base.DatabaseNumber = len(mysqlDbs) + len(postgresqlDbs)
|
||||
@@ -625,6 +631,8 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
switch quicks[i].Name {
|
||||
case "Agent":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||
case "Website":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.WebsiteNumber)
|
||||
case "Database":
|
||||
@@ -638,7 +646,7 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
_ = copier.Copy(&item, quicks[i])
|
||||
base.QuickJumps = append(base.QuickJumps, item)
|
||||
}
|
||||
sort.Slice(quicks, func(i, j int) bool {
|
||||
return quicks[i].Recommend < quicks[j].Recommend
|
||||
sort.Slice(base.QuickJumps, func(i, j int) bool {
|
||||
return base.QuickJumps[i].Recommend < base.QuickJumps[j].Recommend
|
||||
})
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
|
||||
logFileRes *dto.LogFileRes
|
||||
)
|
||||
if stat.Size() > files.MaxReadFileSize {
|
||||
lines, err = files.TailFromEnd(logFilePath, req.PageSize)
|
||||
lines, _ = files.TailFromEnd(logFilePath, req.PageSize)
|
||||
isEndOfFile = true
|
||||
scope = "tail"
|
||||
} else {
|
||||
|
||||
@@ -289,11 +289,11 @@ func (s *IptablesService) LoadChainStatus(req dto.OperationWithName) dto.Iptable
|
||||
}
|
||||
switch req.Name {
|
||||
case iptables.Chain1PanelBasic:
|
||||
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
||||
case iptables.Chain1PanelInput:
|
||||
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
||||
case iptables.Chain1PanelOutput:
|
||||
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
|
||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -459,12 +459,7 @@ func addProxy(server *model.McpServer) {
|
||||
} else {
|
||||
proxyPath = server.StreamableHttpPath
|
||||
}
|
||||
safePass, err := nginx.NginxSafeString(fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath), nginx.ModeURL)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("mcp add proxy failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
location.UpdateDirective("proxy_pass", []string{safePass})
|
||||
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
|
||||
location.ChangePath("^~", proxyPath)
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err))
|
||||
@@ -522,12 +517,7 @@ func addMCPProxy(websiteID uint) error {
|
||||
} else {
|
||||
proxyPath = server.StreamableHttpPath
|
||||
}
|
||||
safePass, err := nginx.NginxSafeString(fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath), nginx.ModeURL)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("mcp add proxy failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("proxy_pass", []string{safePass})
|
||||
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
|
||||
location.ChangePath("^~", proxyPath)
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return buserr.WithErr("ErrUpdateBuWebsite", err)
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/subosito/gotenv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
@@ -241,7 +240,7 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
|
||||
return err
|
||||
}
|
||||
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
|
||||
cmdMgr := cmd2.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
|
||||
if err = cmdMgr.RunBashCf("docker compose -f %s build", nginxInstall.GetComposePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
||||
@@ -295,12 +294,7 @@ func (w WebsiteService) UpdatePathAuthBasic(req request.NginxPathAuthUpdate) err
|
||||
config.FilePath = confPath
|
||||
directives := config.Directives
|
||||
location, _ := directives[0].(*components.Location)
|
||||
safePass, err := nginx.NginxSafeString(fmt.Sprintf("/www/sites/%s/path_auth/pass/%s", website.Alias, fmt.Sprintf("%s.pass", req.Name)), nginx.ModePath)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("invalid auth_basic_user_file path, err: %v", err)
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("auth_basic_user_file", []string{safePass})
|
||||
location.UpdateDirective("auth_basic_user_file", []string{fmt.Sprintf("/www/sites/%s/path_auth/pass/%s", website.Alias, fmt.Sprintf("%s.pass", req.Name))})
|
||||
location.ChangePath("~*", fmt.Sprintf("^%s", req.Path))
|
||||
var passwdHash []byte
|
||||
passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
|
||||
@@ -27,15 +27,15 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
|
||||
switch req.Operate {
|
||||
case "delete":
|
||||
return w.DeleteProxy(request.WebsiteProxyDel{
|
||||
ID: req.ID,
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
})
|
||||
case "disable":
|
||||
fallthrough
|
||||
case "enable":
|
||||
return w.UpdateProxyStatus(request.WebsiteProxyStatusUpdate{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Status: req.Operate,
|
||||
})
|
||||
}
|
||||
@@ -114,16 +114,8 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
|
||||
err = errors.New("invalid proxy config, no location found")
|
||||
return
|
||||
}
|
||||
safePass, err := nginx.NginxSafeString(req.ProxyPass, nginx.ModeURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
safeHeader, err := nginx.NginxSafeString(req.ProxyHost, nginx.ModeHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("proxy_pass", []string{safePass})
|
||||
location.UpdateDirective("proxy_set_header", []string{"Host", safeHeader})
|
||||
applyLocationProxyPass(location, req.ProxyPass, &req.SNI, req.ProxySSLName)
|
||||
location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost})
|
||||
location.ChangePath(req.Modifier, req.Match)
|
||||
// Server Cache Settings
|
||||
if req.Cache {
|
||||
@@ -151,37 +143,17 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
|
||||
} else {
|
||||
location.RemoveSubFilter()
|
||||
}
|
||||
// SSL Settings
|
||||
if req.SNI {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
|
||||
if req.ProxySSLName != "" {
|
||||
location.UpdateDirective("proxy_ssl_name", []string{req.ProxySSLName})
|
||||
}
|
||||
} else {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
|
||||
}
|
||||
// Explicit SNI configuration still takes precedence over the automatic HTTPS upstream default.
|
||||
// CORS Settings
|
||||
if req.Cors {
|
||||
safeAllowOrigins, err := nginx.NginxSafeString(req.AllowOrigins, nginx.ModeGeneric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", safeAllowOrigins, "always"})
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"})
|
||||
if req.AllowMethods != "" {
|
||||
safeAllowMethods, err := nginx.NginxSafeString(req.AllowMethods, nginx.ModeAllowMethods)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", safeAllowMethods, "always"})
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", req.AllowMethods, "always"})
|
||||
} else {
|
||||
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Methods"})
|
||||
}
|
||||
if req.AllowHeaders != "" {
|
||||
safeAllowHeaders, err := nginx.NginxSafeString(req.AllowHeaders, nginx.ModeGeneric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", safeAllowHeaders, "always"})
|
||||
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", req.AllowHeaders, "always"})
|
||||
} else {
|
||||
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Headers"})
|
||||
}
|
||||
|
||||
@@ -47,6 +47,30 @@ func handleChineseDomain(domain string) (string, error) {
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func isHTTPSProxyPass(proxyPass string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(proxyPass)), "https://")
|
||||
}
|
||||
|
||||
func applyLocationProxyPass(location *components.Location, proxyPass string, sni *bool, proxySSLName string) {
|
||||
location.UpdateDirective("proxy_pass", []string{proxyPass})
|
||||
|
||||
enableSNI := isHTTPSProxyPass(proxyPass)
|
||||
if sni != nil {
|
||||
enableSNI = enableSNI && *sni
|
||||
}
|
||||
if enableSNI {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
|
||||
} else {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
|
||||
}
|
||||
|
||||
sslName := "$proxy_host"
|
||||
if proxySSLName != "" {
|
||||
sslName = proxySSLName
|
||||
}
|
||||
location.UpdateDirective("proxy_ssl_name", []string{sslName})
|
||||
}
|
||||
|
||||
func createIndexFile(website *model.Website, runtime *model.Runtime) error {
|
||||
var (
|
||||
indexPath string
|
||||
@@ -122,7 +146,7 @@ func createProxyFile(website *model.Website) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
location.ChangePath("^~", "/")
|
||||
location.UpdateDirective("proxy_pass", []string{website.Proxy})
|
||||
applyLocationProxyPass(location, website.Proxy, nil, "")
|
||||
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
|
||||
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return buserr.WithErr("ErrUpdateBuWebsite", err)
|
||||
|
||||
@@ -275,7 +275,8 @@ func (t *Task) Execute() error {
|
||||
}
|
||||
var err error
|
||||
t.Log(i18n.GetWithName("TaskStart", t.Name))
|
||||
for _, subTask := range t.SubTasks {
|
||||
for i := 0; i < len(t.SubTasks); i++ {
|
||||
subTask := t.SubTasks[i]
|
||||
t.Task.CurrentStep = subTask.StepAlias
|
||||
t.updateTask(t.Task)
|
||||
if err = subTask.Execute(); err == nil {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package docs
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed x-log.json
|
||||
var XLogJson []byte
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package nginx_conf
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ ErrAgentApiKeyRequired: 'API key is required'
|
||||
ErrAgentComposeRequired: 'Compose content is required'
|
||||
ErrAgentIDRequired: 'Agent ID is required'
|
||||
ErrAgentAccountIDRequired: 'Account ID is required'
|
||||
ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Professional Edition for unlimited agents'
|
||||
|
||||
#backup
|
||||
Localhost: 'Local'
|
||||
@@ -231,6 +232,36 @@ BuildCache: 'Build Cache'
|
||||
Volume: 'Volume'
|
||||
Network: "Network"
|
||||
PruneStart: 'Cleaning in progress'
|
||||
ContainerBackupPrepare: 'Prepare container backup workspace'
|
||||
ContainerBackupStop: 'Stop container before backup'
|
||||
ContainerBackupInspect: 'Backup container inspect and network'
|
||||
ContainerBackupMounts: 'Backup container mounts'
|
||||
ContainerBackupMeta: 'Write container backup metadata'
|
||||
ContainerBackupStart: 'Start container after backup'
|
||||
ContainerRecoverPrepare: 'Prepare container restore workspace'
|
||||
ContainerRecoverExtract: 'Extract container backup archive'
|
||||
ContainerRecoverParse: 'Load container backup metadata'
|
||||
ContainerRecoverCreate: 'Recreate container'
|
||||
ContainerRecoverMounts: 'Restore container mounts'
|
||||
ContainerRecoverStart: 'Start restored container'
|
||||
ContainerRecoverCleanup: 'Cleanup container restore workspace'
|
||||
ComposeBackupPrepare: 'Prepare compose backup workspace'
|
||||
ComposeBackupStop: 'Stop compose before backup'
|
||||
ComposeBackupFiles: 'Backup compose files'
|
||||
ComposeBackupContainers: 'Backup compose containers'
|
||||
ComposeBackupMeta: 'Write compose backup metadata'
|
||||
ComposeBackupStart: 'Start compose after backup'
|
||||
ComposeBackupCleanup: 'Cleanup compose backup workspace'
|
||||
ComposeRecoverPrepare: 'Prepare compose restore workspace'
|
||||
ComposeRecoverExtract: 'Extract compose backup archive'
|
||||
ComposeRecoverMeta: 'Load compose backup metadata'
|
||||
ComposeRecoverMetaLogName: 'Compose restore meta: name={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Compose restore meta: backupPath={{ .backupPath }} targetDir={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Compose restore meta: files={{ .files }} containers={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Restore compose files'
|
||||
ComposeRecoverContainers: 'Restore compose containers'
|
||||
ComposeRecoverRecord: 'Save compose record'
|
||||
ComposeRecoverCleanup: 'Cleanup compose restore workspace'
|
||||
|
||||
#runtime
|
||||
ErrFileNotExist: 'Source file not found: {{ .detail }}'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'Clave API requerida'
|
||||
ErrAgentComposeRequired: 'Contenido compose requerido'
|
||||
ErrAgentIDRequired: 'ID de agente requerido'
|
||||
ErrAgentAccountIDRequired: 'ID de cuenta requerido'
|
||||
ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualice a la edición Professional para eliminar el límite'
|
||||
Localhost: 'Máquina local'
|
||||
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
|
||||
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'Caché de compilación'
|
||||
Volume: 'Volumen de almacenamiento'
|
||||
Network: 'Red'
|
||||
PruneStart: 'Limpiando en progreso espere...'
|
||||
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
|
||||
ContainerBackupStop: 'Detener contenedor antes del respaldo'
|
||||
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
|
||||
ContainerBackupMounts: 'Respaldar montajes del contenedor'
|
||||
ContainerBackupMeta: 'Escribir metadatos del respaldo del contenedor'
|
||||
ContainerBackupStart: 'Iniciar contenedor después del respaldo'
|
||||
ContainerRecoverPrepare: 'Preparar espacio de restauración del contenedor'
|
||||
ContainerRecoverExtract: 'Extraer archivo de respaldo del contenedor'
|
||||
ContainerRecoverParse: 'Cargar metadatos del respaldo del contenedor'
|
||||
ContainerRecoverCreate: 'Recrear contenedor'
|
||||
ContainerRecoverMounts: 'Restaurar montajes del contenedor'
|
||||
ContainerRecoverStart: 'Iniciar contenedor restaurado'
|
||||
ContainerRecoverCleanup: 'Limpiar espacio de restauracion del contenedor'
|
||||
ComposeBackupPrepare: 'Preparar espacio de respaldo del compose'
|
||||
ComposeBackupStop: 'Detener compose antes del respaldo'
|
||||
ComposeBackupFiles: 'Respaldar archivos de compose'
|
||||
ComposeBackupContainers: 'Respaldar contenedores del compose'
|
||||
ComposeBackupMeta: 'Escribir metadatos del respaldo de compose'
|
||||
ComposeBackupStart: 'Iniciar compose después del respaldo'
|
||||
ComposeBackupCleanup: 'Limpiar espacio de respaldo del compose'
|
||||
ComposeRecoverPrepare: 'Preparar espacio de restauración del compose'
|
||||
ComposeRecoverExtract: 'Extraer archivo de respaldo de compose'
|
||||
ComposeRecoverMeta: 'Cargar metadatos del respaldo de compose'
|
||||
ComposeRecoverMetaLogName: 'Metadatos de restauracion de compose: nombre={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Metadatos de restauracion de compose: rutaRespaldo={{ .backupPath }} rutaDestino={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Metadatos de restauracion de compose: archivos={{ .files }} contenedores={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Restaurar archivos de compose'
|
||||
ComposeRecoverContainers: 'Restaurar contenedores del compose'
|
||||
ComposeRecoverRecord: 'Guardar registro de compose'
|
||||
ComposeRecoverCleanup: 'Limpiar espacio de restauracion de compose'
|
||||
ErrFileNotExist: 'El archivo {{ .detail }} no existe, verifique la integridad'
|
||||
ErrImageBuildErr: 'Fallo al construir la imagen'
|
||||
ErrImageExist: 'La imagen ya existe Modifique el nombre de la imagen.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'APIキーが必要です'
|
||||
ErrAgentComposeRequired: 'Composeが必要です'
|
||||
ErrAgentIDRequired: 'エージェントIDが必要です'
|
||||
ErrAgentAccountIDRequired: 'アカウントIDが必要です'
|
||||
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Professional Edition にアップグレードすると数の制限はありません'
|
||||
Localhost: 'ローカルマシン'
|
||||
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
|
||||
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'ビルドキャッシュ'
|
||||
Volume: 'ストレージボリューム'
|
||||
Network: 'ネットワーク'
|
||||
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
|
||||
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
|
||||
ContainerBackupStop: 'バックアップ前にコンテナを停止'
|
||||
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
|
||||
ContainerBackupMounts: 'コンテナのマウントデータをバックアップ'
|
||||
ContainerBackupMeta: 'コンテナバックアップのメタデータを書き込み'
|
||||
ContainerBackupStart: 'バックアップ後にコンテナを起動'
|
||||
ContainerRecoverPrepare: 'コンテナ復元作業領域を準備'
|
||||
ContainerRecoverExtract: 'コンテナバックアップを展開'
|
||||
ContainerRecoverParse: 'コンテナバックアップメタデータを読み込み'
|
||||
ContainerRecoverCreate: 'コンテナを再作成'
|
||||
ContainerRecoverMounts: 'コンテナマウントを復元'
|
||||
ContainerRecoverStart: '復元済みコンテナを起動'
|
||||
ContainerRecoverCleanup: 'コンテナ復元作業領域をクリーンアップ'
|
||||
ComposeBackupPrepare: 'Compose バックアップ作業領域を準備'
|
||||
ComposeBackupStop: 'バックアップ前に Compose を停止'
|
||||
ComposeBackupFiles: 'Compose ファイルをバックアップ'
|
||||
ComposeBackupContainers: 'Compose コンテナをバックアップ'
|
||||
ComposeBackupMeta: 'Compose バックアップのメタデータを書き込み'
|
||||
ComposeBackupStart: 'バックアップ後に Compose を起動'
|
||||
ComposeBackupCleanup: 'Compose バックアップ作業領域をクリーンアップ'
|
||||
ComposeRecoverPrepare: 'Compose 復元作業領域を準備'
|
||||
ComposeRecoverExtract: 'Compose バックアップを展開'
|
||||
ComposeRecoverMeta: 'Compose バックアップメタデータを読み込み'
|
||||
ComposeRecoverMetaLogName: 'Compose 復元メタ情報: 名前={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Compose 復元メタ情報: バックアップパス={{ .backupPath }} 復元先={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Compose 復元メタ情報: ファイル={{ .files }} コンテナ={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Compose ファイルを復元'
|
||||
ComposeRecoverContainers: 'Compose コンテナを復元'
|
||||
ComposeRecoverRecord: 'Compose 記録を保存'
|
||||
ComposeRecoverCleanup: 'Compose 復元作業領域をクリーンアップ'
|
||||
ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。'
|
||||
ErrImageBuildErr: 'イメージのビルドに失敗しました'
|
||||
ErrImageExist: 'イメージはすでに存在します!イメージ名を変更してください。'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'API 키 필요'
|
||||
ErrAgentComposeRequired: 'Compose 내용 필요'
|
||||
ErrAgentIDRequired: '에이전트 ID 필요'
|
||||
ErrAgentAccountIDRequired: '계정 ID 필요'
|
||||
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 생성할 수 있으며, Professional 에디션으로 업그레이드하면 수량 제한이 없습니다'
|
||||
Localhost: '로컬 머신'
|
||||
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
|
||||
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: '빌드 캐시'
|
||||
Volume: '스토리지 볼륨'
|
||||
Network: '네트워크'
|
||||
PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
|
||||
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
|
||||
ContainerBackupStop: '백업 전에 컨테이너 중지'
|
||||
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
|
||||
ContainerBackupMounts: '컨테이너 마운트 데이터 백업'
|
||||
ContainerBackupMeta: '컨테이너 백업 메타데이터 작성'
|
||||
ContainerBackupStart: '백업 후 컨테이너 시작'
|
||||
ContainerRecoverPrepare: '컨테이너 복원 작업 공간 준비'
|
||||
ContainerRecoverExtract: '컨테이너 백업 파일 압축 해제'
|
||||
ContainerRecoverParse: '컨테이너 백업 메타데이터 로드'
|
||||
ContainerRecoverCreate: '컨테이너 재생성'
|
||||
ContainerRecoverMounts: '컨테이너 마운트 데이터 복원'
|
||||
ContainerRecoverStart: '복원된 컨테이너 시작'
|
||||
ContainerRecoverCleanup: '컨테이너 복원 작업 공간 정리'
|
||||
ComposeBackupPrepare: 'Compose 백업 작업 공간 준비'
|
||||
ComposeBackupStop: '백업 전에 Compose 중지'
|
||||
ComposeBackupFiles: 'Compose 파일 백업'
|
||||
ComposeBackupContainers: 'Compose 컨테이너 백업'
|
||||
ComposeBackupMeta: 'Compose 백업 메타데이터 작성'
|
||||
ComposeBackupStart: '백업 후 Compose 시작'
|
||||
ComposeBackupCleanup: 'Compose 백업 작업 공간 정리'
|
||||
ComposeRecoverPrepare: 'Compose 복원 작업 공간 준비'
|
||||
ComposeRecoverExtract: 'Compose 백업 파일 압축 해제'
|
||||
ComposeRecoverMeta: 'Compose 백업 메타데이터 로드'
|
||||
ComposeRecoverMetaLogName: 'Compose 복원 메타: 이름={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Compose 복원 메타: 백업경로={{ .backupPath }} 대상경로={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Compose 복원 메타: 파일={{ .files }} 컨테이너={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Compose 파일 복원'
|
||||
ComposeRecoverContainers: 'Compose 컨테이너 복원'
|
||||
ComposeRecoverRecord: 'Compose 기록 저장'
|
||||
ComposeRecoverCleanup: 'Compose 복원 작업 공간 정리'
|
||||
ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다 소스 파일의 무결성을 확인하세요'
|
||||
ErrImageBuildErr: '이미지 빌드 실패'
|
||||
ErrImageExist: '이미지가 이미 존재합니다 이미지 이름을 수정하세요.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'Kunci API diperlukan'
|
||||
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
|
||||
ErrAgentIDRequired: 'ID ejen diperlukan'
|
||||
ErrAgentAccountIDRequired: 'ID akaun diperlukan'
|
||||
ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke edisi Professional untuk tiada had bilangan'
|
||||
Localhost: 'Mesin Tempatan'
|
||||
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
|
||||
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'Cache binaan'
|
||||
Volume: 'Jilid storan'
|
||||
Network: 'Rangkaian'
|
||||
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
|
||||
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
|
||||
ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
|
||||
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
|
||||
ContainerBackupMounts: 'Sandarkan mount kontena'
|
||||
ContainerBackupMeta: 'Tulis metadata sandaran kontena'
|
||||
ContainerBackupStart: 'Mulakan kontena selepas sandaran'
|
||||
ContainerRecoverPrepare: 'Sediakan ruang kerja pemulihan kontena'
|
||||
ContainerRecoverExtract: 'Ekstrak arkib sandaran kontena'
|
||||
ContainerRecoverParse: 'Muat metadata sandaran kontena'
|
||||
ContainerRecoverCreate: 'Cipta semula kontena'
|
||||
ContainerRecoverMounts: 'Pulihkan mount kontena'
|
||||
ContainerRecoverStart: 'Mulakan kontena dipulihkan'
|
||||
ContainerRecoverCleanup: 'Bersihkan ruang kerja pemulihan kontena'
|
||||
ComposeBackupPrepare: 'Sediakan ruang kerja sandaran compose'
|
||||
ComposeBackupStop: 'Hentikan compose sebelum sandaran'
|
||||
ComposeBackupFiles: 'Sandarkan fail compose'
|
||||
ComposeBackupContainers: 'Sandarkan kontena compose'
|
||||
ComposeBackupMeta: 'Tulis metadata sandaran compose'
|
||||
ComposeBackupStart: 'Mulakan compose selepas sandaran'
|
||||
ComposeBackupCleanup: 'Bersihkan ruang kerja sandaran compose'
|
||||
ComposeRecoverPrepare: 'Sediakan ruang kerja pemulihan compose'
|
||||
ComposeRecoverExtract: 'Ekstrak arkib sandaran compose'
|
||||
ComposeRecoverMeta: 'Muat metadata sandaran compose'
|
||||
ComposeRecoverMetaLogName: 'Metadata pemulihan compose: nama={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Metadata pemulihan compose: laluanSandaran={{ .backupPath }} laluanSasaran={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Metadata pemulihan compose: fail={{ .files }} kontena={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Pulihkan fail compose'
|
||||
ComposeRecoverContainers: 'Pulihkan kontena compose'
|
||||
ComposeRecoverRecord: 'Simpan rekod compose'
|
||||
ComposeRecoverCleanup: 'Bersihkan ruang kerja pemulihan compose'
|
||||
ErrFileNotExist: 'Fail {{ .detail }} tidak wujud Sila semak integriti fail sumber'
|
||||
ErrImageBuildErr: 'Pembinaan imej gagal'
|
||||
ErrImageExist: 'Imej sudah wujud Sila ubah nama imej.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'Chave API obrigatória'
|
||||
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
|
||||
ErrAgentIDRequired: 'ID do agente obrigatório'
|
||||
ErrAgentAccountIDRequired: 'ID da conta obrigatório'
|
||||
ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Atualize para a edição Professional para remover o limite'
|
||||
Localhost: 'Máquina Local'
|
||||
ErrBackupInUsed: 'Conta de backup em uso por tarefa'
|
||||
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'Cache de construção'
|
||||
Volume: 'Volume de armazenamento'
|
||||
Network: 'Rede'
|
||||
PruneStart: 'Limpando em andamento aguarde...'
|
||||
ContainerBackupPrepare: 'Preparar área de backup do contêiner'
|
||||
ContainerBackupStop: 'Parar contêiner antes do backup'
|
||||
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
|
||||
ContainerBackupMounts: 'Fazer backup dos mounts do contêiner'
|
||||
ContainerBackupMeta: 'Escrever metadados do backup do contêiner'
|
||||
ContainerBackupStart: 'Iniciar contêiner após o backup'
|
||||
ContainerRecoverPrepare: 'Preparar área de restauração do contêiner'
|
||||
ContainerRecoverExtract: 'Extrair arquivo de backup do contêiner'
|
||||
ContainerRecoverParse: 'Carregar metadados do backup do contêiner'
|
||||
ContainerRecoverCreate: 'Recriar contêiner'
|
||||
ContainerRecoverMounts: 'Restaurar mounts do contêiner'
|
||||
ContainerRecoverStart: 'Iniciar contêiner restaurado'
|
||||
ContainerRecoverCleanup: 'Limpar área de restauração do contêiner'
|
||||
ComposeBackupPrepare: 'Preparar área de backup do compose'
|
||||
ComposeBackupStop: 'Parar compose antes do backup'
|
||||
ComposeBackupFiles: 'Fazer backup dos arquivos do compose'
|
||||
ComposeBackupContainers: 'Fazer backup dos contêineres do compose'
|
||||
ComposeBackupMeta: 'Escrever metadados do backup do compose'
|
||||
ComposeBackupStart: 'Iniciar compose após o backup'
|
||||
ComposeBackupCleanup: 'Limpar área de backup do compose'
|
||||
ComposeRecoverPrepare: 'Preparar área de restauração do compose'
|
||||
ComposeRecoverExtract: 'Extrair arquivo de backup do compose'
|
||||
ComposeRecoverMeta: 'Carregar metadados do backup do compose'
|
||||
ComposeRecoverMetaLogName: 'Metadados da restauracao do compose: nome={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Metadados da restauracao do compose: caminhoBackup={{ .backupPath }} caminhoDestino={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Metadados da restauracao do compose: arquivos={{ .files }} containers={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Restaurar arquivos do compose'
|
||||
ComposeRecoverContainers: 'Restaurar contêineres do compose'
|
||||
ComposeRecoverRecord: 'Salvar registro do compose'
|
||||
ComposeRecoverCleanup: 'Limpar área de restauração do compose'
|
||||
ErrFileNotExist: 'O arquivo {{ .detail }} não existe Verifique a integridade do arquivo de origem'
|
||||
ErrImageBuildErr: 'Falha na criação da imagem'
|
||||
ErrImageExist: 'A imagem já existe, modifique o nome da imagem.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'Нужен API-ключ'
|
||||
ErrAgentComposeRequired: 'Нужен compose'
|
||||
ErrAgentIDRequired: 'Нужен ID агента'
|
||||
ErrAgentAccountIDRequired: 'Нужен ID аккаунта'
|
||||
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Перейдите на Professional Edition, чтобы снять ограничение'
|
||||
Localhost: 'Локальная машина'
|
||||
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
|
||||
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'Кэш сборки'
|
||||
Volume: 'Том хранилища'
|
||||
Network: 'Сеть'
|
||||
PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
|
||||
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
|
||||
ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
|
||||
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
|
||||
ContainerBackupMounts: 'Сделать резервную копию точек монтирования контейнера'
|
||||
ContainerBackupMeta: 'Записать метаданные резервной копии контейнера'
|
||||
ContainerBackupStart: 'Запустить контейнер после резервного копирования'
|
||||
ContainerRecoverPrepare: 'Подготовить рабочую область восстановления контейнера'
|
||||
ContainerRecoverExtract: 'Распаковать архив резервной копии контейнера'
|
||||
ContainerRecoverParse: 'Загрузить метаданные резервной копии контейнера'
|
||||
ContainerRecoverCreate: 'Пересоздать контейнер'
|
||||
ContainerRecoverMounts: 'Восстановить точки монтирования контейнера'
|
||||
ContainerRecoverStart: 'Запустить восстановленный контейнер'
|
||||
ContainerRecoverCleanup: 'Очистить рабочую область восстановления контейнера'
|
||||
ComposeBackupPrepare: 'Подготовить рабочую область резервной копии compose'
|
||||
ComposeBackupStop: 'Остановить compose перед резервным копированием'
|
||||
ComposeBackupFiles: 'Сделать резервную копию файлов compose'
|
||||
ComposeBackupContainers: 'Сделать резервную копию контейнеров compose'
|
||||
ComposeBackupMeta: 'Записать метаданные резервной копии compose'
|
||||
ComposeBackupStart: 'Запустить compose после резервного копирования'
|
||||
ComposeBackupCleanup: 'Очистить рабочую область резервного копирования compose'
|
||||
ComposeRecoverPrepare: 'Подготовить рабочую область восстановления compose'
|
||||
ComposeRecoverExtract: 'Распаковать архив резервной копии compose'
|
||||
ComposeRecoverMeta: 'Загрузить метаданные резервной копии compose'
|
||||
ComposeRecoverMetaLogName: 'Метаданные восстановления compose: имя={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Метаданные восстановления compose: путьБэкапа={{ .backupPath }} путьНазначения={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Метаданные восстановления compose: файлов={{ .files }} контейнеров={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Восстановить файлы compose'
|
||||
ComposeRecoverContainers: 'Восстановить контейнеры compose'
|
||||
ComposeRecoverRecord: 'Сохранить запись compose'
|
||||
ComposeRecoverCleanup: 'Очистить рабочую область восстановления compose'
|
||||
ErrFileNotExist: 'Файл {{ .detail }} не существует Проверьте целостность исходного файла'
|
||||
ErrImageBuildErr: 'Сборка образа не удалась'
|
||||
ErrImageExist: 'Изображение уже существует Пожалуйста, измените имя изображения.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'API anahtarı gerekli'
|
||||
ErrAgentComposeRequired: 'Compose içeriği gerekli'
|
||||
ErrAgentIDRequired: 'Ajans ID gerekli'
|
||||
ErrAgentAccountIDRequired: 'Hesap ID gerekli'
|
||||
ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırı kaldırmak için Professional sürüme yükseltin'
|
||||
Localhost: 'Yerel Makine'
|
||||
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
|
||||
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: 'Derleme önbelleği'
|
||||
Volume: 'Depolama hacmi'
|
||||
Network: 'Ağ'
|
||||
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
|
||||
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
|
||||
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
|
||||
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
|
||||
ContainerBackupMounts: 'Konteyner mount verilerini yedekle'
|
||||
ContainerBackupMeta: 'Konteyner yedekleme meta verisini yaz'
|
||||
ContainerBackupStart: 'Yedeklemeden sonra konteyneri başlat'
|
||||
ContainerRecoverPrepare: 'Konteyner geri yükleme çalışma alanını hazırla'
|
||||
ContainerRecoverExtract: 'Konteyner yedek arşivini çıkar'
|
||||
ContainerRecoverParse: 'Konteyner yedek meta verisini yükle'
|
||||
ContainerRecoverCreate: 'Konteyneri yeniden oluştur'
|
||||
ContainerRecoverMounts: 'Konteyner mount verilerini geri yükle'
|
||||
ContainerRecoverStart: 'Geri yüklenen konteyneri başlat'
|
||||
ContainerRecoverCleanup: 'Konteyner geri yükleme çalışma alanını temizle'
|
||||
ComposeBackupPrepare: 'Compose yedekleme çalışma alanını hazırla'
|
||||
ComposeBackupStop: 'Yedeklemeden önce compose’u durdur'
|
||||
ComposeBackupFiles: 'Compose dosyalarını yedekle'
|
||||
ComposeBackupContainers: 'Compose konteynerlerini yedekle'
|
||||
ComposeBackupMeta: 'Compose yedek meta verisini yaz'
|
||||
ComposeBackupStart: 'Yedeklemeden sonra compose’u başlat'
|
||||
ComposeBackupCleanup: 'Compose yedekleme çalışma alanını temizle'
|
||||
ComposeRecoverPrepare: 'Compose geri yükleme çalışma alanını hazırla'
|
||||
ComposeRecoverExtract: 'Compose yedek arşivini çıkar'
|
||||
ComposeRecoverMeta: 'Compose yedek meta verisini yükle'
|
||||
ComposeRecoverMetaLogName: 'Compose geri yükleme meta: ad={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: 'Compose geri yükleme meta: yedekYolu={{ .backupPath }} hedefYol={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: 'Compose geri yükleme meta: dosya={{ .files }} konteyner={{ .containers }}'
|
||||
ComposeRecoverFiles: 'Compose dosyalarını geri yükle'
|
||||
ComposeRecoverContainers: 'Compose konteynerlerini geri yükle'
|
||||
ComposeRecoverRecord: 'Compose kaydını sakla'
|
||||
ComposeRecoverCleanup: 'Compose geri yükleme çalışma alanını temizle'
|
||||
ErrFileNotExist: '{{ .detail }} dosyası mevcut değil Lütfen kaynak dosyanın bütünlüğünü kontrol edin'
|
||||
ErrImageBuildErr: 'Image yapı başarısız'
|
||||
ErrImageExist: 'Görüntü zaten var Lütfen görüntü adını değiştirin.'
|
||||
|
||||
@@ -45,6 +45,7 @@ ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
|
||||
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
|
||||
ErrAgentIDRequired: '智能體 ID 不可為空,請重試'
|
||||
ErrAgentAccountIDRequired: '帳號 ID 不可為空,請重試'
|
||||
ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個智能體,升級至專業版則無數量限制'
|
||||
Localhost: '本機'
|
||||
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
|
||||
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
|
||||
@@ -208,6 +209,36 @@ BuildCache: '構建快取'
|
||||
Volume: '磁碟區'
|
||||
Network: '網路'
|
||||
PruneStart: '清理中,請稍候...'
|
||||
ContainerBackupPrepare: '準備容器備份工作目錄'
|
||||
ContainerBackupStop: '備份前停止容器'
|
||||
ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
|
||||
ContainerBackupMounts: '備份容器掛載資料'
|
||||
ContainerBackupMeta: '寫入容器備份中繼資訊'
|
||||
ContainerBackupStart: '備份後啟動容器'
|
||||
ContainerRecoverPrepare: '準備容器還原工作目錄'
|
||||
ContainerRecoverExtract: '解壓容器備份檔案'
|
||||
ContainerRecoverParse: '載入容器備份中繼資訊'
|
||||
ContainerRecoverCreate: '重建容器'
|
||||
ContainerRecoverMounts: '還原容器掛載資料'
|
||||
ContainerRecoverStart: '啟動已還原容器'
|
||||
ContainerRecoverCleanup: '清理容器還原工作目錄'
|
||||
ComposeBackupPrepare: '準備編排備份工作目錄'
|
||||
ComposeBackupStop: '備份前停止編排'
|
||||
ComposeBackupFiles: '備份編排檔案'
|
||||
ComposeBackupContainers: '備份編排容器'
|
||||
ComposeBackupMeta: '寫入編排備份中繼資訊'
|
||||
ComposeBackupStart: '備份後啟動編排'
|
||||
ComposeBackupCleanup: '清理編排備份工作目錄'
|
||||
ComposeRecoverPrepare: '準備編排還原工作目錄'
|
||||
ComposeRecoverExtract: '解壓編排備份檔案'
|
||||
ComposeRecoverMeta: '載入編排備份中繼資訊'
|
||||
ComposeRecoverMetaLogName: '編排還原中繼資訊:名稱={{ .name }}'
|
||||
ComposeRecoverMetaLogPath: '編排還原中繼資訊:備份路徑={{ .backupPath }} 目標路徑={{ .targetDir }}'
|
||||
ComposeRecoverMetaLogCount: '編排還原中繼資訊:檔案數={{ .files }} 容器數={{ .containers }}'
|
||||
ComposeRecoverFiles: '還原編排檔案'
|
||||
ComposeRecoverContainers: '還原編排容器'
|
||||
ComposeRecoverRecord: '保存編排記錄'
|
||||
ComposeRecoverCleanup: '清理編排還原工作目錄'
|
||||
ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!'
|
||||
ErrImageBuildErr: '映像build 失敗'
|
||||
ErrImageExist: '映像已存在!請修改映像名稱。'
|
||||
|
||||
@@ -50,6 +50,7 @@ ErrAgentApiKeyRequired: "API Key 不能为空"
|
||||
ErrAgentComposeRequired: "自定义编排内容不能为空"
|
||||
ErrAgentIDRequired: "智能体 ID 不能为空"
|
||||
ErrAgentAccountIDRequired: "账号 ID 不能为空"
|
||||
ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至专业版无数量限制"
|
||||
|
||||
#backup
|
||||
Localhost: '本机'
|
||||
@@ -231,6 +232,36 @@ BuildCache: "构建缓存"
|
||||
Volume: "存储卷"
|
||||
Network: "网络"
|
||||
PruneStart: "清理中,请稍候..."
|
||||
ContainerBackupPrepare: "准备容器备份工作目录"
|
||||
ContainerBackupStop: "备份前停止容器"
|
||||
ContainerBackupInspect: "备份容器 Inspect 与网络信息"
|
||||
ContainerBackupMounts: "备份容器挂载数据"
|
||||
ContainerBackupMeta: "写入容器备份元信息"
|
||||
ContainerBackupStart: "备份后启动容器"
|
||||
ContainerRecoverPrepare: "准备容器恢复工作目录"
|
||||
ContainerRecoverExtract: "解压容器备份文件"
|
||||
ContainerRecoverParse: "加载容器备份元信息"
|
||||
ContainerRecoverCreate: "重建容器"
|
||||
ContainerRecoverMounts: "恢复容器挂载数据"
|
||||
ContainerRecoverStart: "启动已恢复容器"
|
||||
ContainerRecoverCleanup: "清理容器恢复工作目录"
|
||||
ComposeBackupPrepare: "准备编排备份工作目录"
|
||||
ComposeBackupStop: "备份前停止编排"
|
||||
ComposeBackupFiles: "备份编排文件"
|
||||
ComposeBackupContainers: "备份编排容器"
|
||||
ComposeBackupMeta: "写入编排备份元信息"
|
||||
ComposeBackupStart: "备份后启动编排"
|
||||
ComposeBackupCleanup: "清理编排备份工作目录"
|
||||
ComposeRecoverPrepare: "准备编排恢复工作目录"
|
||||
ComposeRecoverExtract: "解压编排备份文件"
|
||||
ComposeRecoverMeta: "加载编排备份元信息"
|
||||
ComposeRecoverMetaLogName: "编排恢复元信息:名称={{ .name }}"
|
||||
ComposeRecoverMetaLogPath: "编排恢复元信息:备份路径={{ .backupPath }} 目标路径={{ .targetDir }}"
|
||||
ComposeRecoverMetaLogCount: "编排恢复元信息:文件数={{ .files }} 容器数={{ .containers }}"
|
||||
ComposeRecoverFiles: "恢复编排文件"
|
||||
ComposeRecoverContainers: "恢复编排容器"
|
||||
ComposeRecoverRecord: "保存编排记录"
|
||||
ComposeRecoverCleanup: "清理编排恢复工作目录"
|
||||
|
||||
#runtime
|
||||
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
|
||||
|
||||
@@ -43,6 +43,7 @@ func InitAgentDB() {
|
||||
migrations.InitLocalSSHShow,
|
||||
migrations.InitRecordStatus,
|
||||
migrations.AddShowNameForQuickJump,
|
||||
migrations.AddAgentQuickJump,
|
||||
migrations.AddTimeoutForClam,
|
||||
migrations.UpdateCronjobSpec,
|
||||
migrations.UpdateWebsiteSSLAddColumn,
|
||||
|
||||
@@ -589,6 +589,67 @@ var AddShowNameForQuickJump = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var AddAgentQuickJump = &gormigrate.Migration{
|
||||
ID: "20260312-add-agent-quick-jump",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.QuickJump{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var quicks []model.QuickJump
|
||||
if err := tx.Find(&quicks).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
cronjob *model.QuickJump
|
||||
database *model.QuickJump
|
||||
showList []*model.QuickJump
|
||||
)
|
||||
for i := range quicks {
|
||||
switch quicks[i].Name {
|
||||
case "Cronjob":
|
||||
cronjob = &quicks[i]
|
||||
case "Database":
|
||||
database = &quicks[i]
|
||||
}
|
||||
if quicks[i].IsShow {
|
||||
showList = append(showList, &quicks[i])
|
||||
}
|
||||
}
|
||||
|
||||
showCount := len(showList)
|
||||
updatedIDs := make(map[uint]struct{})
|
||||
kickedCronjob := false
|
||||
if showCount >= 4 && cronjob != nil && cronjob.IsShow {
|
||||
cronjob.IsShow = false
|
||||
updatedIDs[cronjob.ID] = struct{}{}
|
||||
kickedCronjob = true
|
||||
}
|
||||
if !kickedCronjob && showCount >= 4 && database != nil && database.IsShow {
|
||||
database.IsShow = false
|
||||
updatedIDs[database.ID] = struct{}{}
|
||||
}
|
||||
|
||||
for _, item := range quicks {
|
||||
if _, ok := updatedIDs[item.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Update("is_show", item.IsShow).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Create(&model.QuickJump{
|
||||
Name: "Agent",
|
||||
Title: "aiTools.agents.agents",
|
||||
Recommend: 1,
|
||||
IsShow: true,
|
||||
Router: "/ai/agents/agent",
|
||||
}).Error
|
||||
},
|
||||
}
|
||||
|
||||
var AddTimeoutForClam = &gormigrate.Migration{
|
||||
ID: "20250922-add-timeout-for-clam",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
@@ -21,6 +21,7 @@ func Routers() *gin.Engine {
|
||||
if !global.IsMaster {
|
||||
PrivateGroup.Use(middleware.Certificate())
|
||||
}
|
||||
PrivateGroup.Use(middleware.OperationResolveMeta())
|
||||
for _, router := range rou.RouterGroupApp {
|
||||
router.InitRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/docs"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
headerNeedOperationResolve = "X-Need-Op-Resolve"
|
||||
headerOperationResolved = "X-Op-Resolved"
|
||||
)
|
||||
|
||||
var (
|
||||
logMetaOnce sync.Once
|
||||
logMetaData map[string]operationMeta
|
||||
logMetaLoadErr error
|
||||
)
|
||||
|
||||
type operationMeta struct {
|
||||
BodyKeys []string `json:"bodyKeys"`
|
||||
BeforeFunctions []functionInfo `json:"beforeFunctions"`
|
||||
}
|
||||
|
||||
type functionInfo struct {
|
||||
InputColumn string `json:"input_column"`
|
||||
InputValue string `json:"input_value"`
|
||||
IsList bool `json:"isList"`
|
||||
DB string `json:"db"`
|
||||
OutputColumn string `json:"output_column"`
|
||||
OutputValue string `json:"output_value"`
|
||||
}
|
||||
|
||||
func OperationResolveMeta() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader(headerNeedOperationResolve) != "1" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
metaMap, err := loadOperationMeta()
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
|
||||
meta, ok := metaMap[reqPath]
|
||||
if !ok || len(meta.BeforeFunctions) == 0 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
values := make(map[string]interface{})
|
||||
if len(meta.BodyKeys) > 0 {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err == nil {
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
bodyMap := make(map[string]interface{})
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
bodyMap, _ = parseMultipart(body, c.Request.Header.Get("Content-Type"))
|
||||
} else {
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.UseNumber()
|
||||
_ = decoder.Decode(&bodyMap)
|
||||
}
|
||||
for _, key := range meta.BodyKeys {
|
||||
if value, ok := bodyMap[key]; ok {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolved, err := resolveOperationValues(reqPath, values, meta.BeforeFunctions)
|
||||
if err == nil && len(resolved) > 0 {
|
||||
if data, err := json.Marshal(resolved); err == nil {
|
||||
c.Header(headerOperationResolved, base64.RawURLEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func loadOperationMeta() (map[string]operationMeta, error) {
|
||||
logMetaOnce.Do(func() {
|
||||
logMetaData = make(map[string]operationMeta)
|
||||
logMetaLoadErr = json.Unmarshal(docs.XLogJson, &logMetaData)
|
||||
})
|
||||
return logMetaData, logMetaLoadErr
|
||||
}
|
||||
|
||||
func parseMultipart(formData []byte, contentType string) (map[string]interface{}, error) {
|
||||
d, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil || d != "multipart/form-data" {
|
||||
return nil, http.ErrNotMultipart
|
||||
}
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok {
|
||||
return nil, http.ErrMissingBoundary
|
||||
}
|
||||
reader := multipart.NewReader(bytes.NewReader(formData), boundary)
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
f, err := reader.ReadForm(32 << 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range f.Value {
|
||||
if len(v) > 0 {
|
||||
ret[k] = v[0]
|
||||
}
|
||||
}
|
||||
for k, v := range f.File {
|
||||
if len(v) > 0 {
|
||||
ret[k] = v[0].Filename
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func resolveOperationValues(pathItem string, values map[string]interface{}, beforeFunctions []functionInfo) (map[string]string, error) {
|
||||
dbItem, err := newResolveDB(pathItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeResolveDB(dbItem)
|
||||
|
||||
resolved := make(map[string]string)
|
||||
for _, funcs := range beforeFunctions {
|
||||
if !isSafeIdentifier(funcs.DB) || !isSafeIdentifier(funcs.InputColumn) || !isSafeIdentifier(funcs.OutputColumn) {
|
||||
continue
|
||||
}
|
||||
for key, value := range values {
|
||||
if funcs.InputValue != key {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
if funcs.IsList {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
} else {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
}
|
||||
outputValue := strings.Join(names, ",")
|
||||
resolved[funcs.OutputValue] = outputValue
|
||||
values[funcs.OutputValue] = outputValue
|
||||
break
|
||||
}
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func newResolveDB(pathItem string) (*gorm.DB, error) {
|
||||
dbFile := ""
|
||||
switch {
|
||||
case strings.HasPrefix(pathItem, "/core"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/core.db")
|
||||
case strings.HasPrefix(pathItem, "/xpack"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db")
|
||||
default:
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/agent.db")
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(4)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
sqlDB.SetConnMaxIdleTime(15 * time.Minute)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func closeResolveDB(db *gorm.DB) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
|
||||
func isSafeIdentifier(val string) bool {
|
||||
return re.GetRegex(re.SQLIdentifierPattern).MatchString(val)
|
||||
}
|
||||
@@ -53,13 +53,18 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
|
||||
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
|
||||
aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig)
|
||||
aiToolsRouter.POST("/agents/channel/feishu/approve", baseApi.ApproveAgentFeishuPairing)
|
||||
aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig)
|
||||
aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig)
|
||||
aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig)
|
||||
aiToolsRouter.POST("/agents/channel/discord/update", baseApi.UpdateAgentDiscordConfig)
|
||||
aiToolsRouter.POST("/agents/browser/get", baseApi.GetAgentBrowserConfig)
|
||||
aiToolsRouter.POST("/agents/browser/update", baseApi.UpdateAgentBrowserConfig)
|
||||
aiToolsRouter.POST("/agents/channel/wecom/get", baseApi.GetAgentWecomConfig)
|
||||
aiToolsRouter.POST("/agents/channel/wecom/update", baseApi.UpdateAgentWecomConfig)
|
||||
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
|
||||
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
|
||||
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/plugin/check", baseApi.CheckAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/security/get", baseApi.GetAgentSecurityConfig)
|
||||
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
|
||||
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
|
||||
aiToolsRouter.POST("/agents/other/update", baseApi.UpdateAgentOtherConfig)
|
||||
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
|
||||
|
||||
@@ -35,6 +35,12 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
baRouter.POST("/prune", baseApi.ContainerPrune)
|
||||
|
||||
baRouter.POST("/users", baseApi.LoadContainerUsers)
|
||||
baRouter.POST("/files/search", baseApi.ListContainerFiles)
|
||||
baRouter.POST("/files/upload", baseApi.UploadContainerFile)
|
||||
baRouter.POST("/files/content", baseApi.GetContainerFileContent)
|
||||
baRouter.POST("/files/size", baseApi.GetContainerFileSize)
|
||||
baRouter.POST("/files/del", baseApi.DeleteContainerFile)
|
||||
baRouter.POST("/files/download", baseApi.DownloadContainerFile)
|
||||
|
||||
baRouter.GET("/repo", baseApi.ListRepo)
|
||||
baRouter.POST("/repo/status", baseApi.CheckRepoStatus)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/jinzhu/copier"
|
||||
"mime"
|
||||
network "net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -71,13 +72,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
|
||||
if username == "" {
|
||||
username = emailInfo.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailInfo.DisplayName)
|
||||
smtpConfig := email.SMTPConfig{
|
||||
Host: emailInfo.Host,
|
||||
Port: emailInfo.Port,
|
||||
Sender: emailInfo.Sender,
|
||||
Username: username,
|
||||
Password: emailInfo.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, emailInfo.DisplayName, emailInfo.Sender),
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailInfo.Sender),
|
||||
Encryption: emailInfo.Encryption,
|
||||
Recipient: emailInfo.Recipient,
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ type authorizer struct {
|
||||
defAuth Authenticator
|
||||
}
|
||||
|
||||
type preemptiveAuthorizer struct {
|
||||
auth Authenticator
|
||||
}
|
||||
|
||||
type authShim struct {
|
||||
factory AuthFactory
|
||||
body io.Reader
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -139,18 +140,27 @@ func getComposeImagesByCommand(filePath string) ([]string, error) {
|
||||
}
|
||||
base, extra := getComposeBaseCmd()
|
||||
args := append(extra, strings.Fields(loadFiles(filePath))...)
|
||||
args = append(args, "config", "--images")
|
||||
args = append(args, "config", "--format", "json", "--no-normalize")
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).
|
||||
RunWithStdout(base, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run compose config --images failed, std: %s, err: %v", stdout, err)
|
||||
return nil, fmt.Errorf("run compose config --format json --no-normalize failed, std: %s, err: %v", stdout, err)
|
||||
}
|
||||
|
||||
var composeConfig struct {
|
||||
Services map[string]struct {
|
||||
Image string `json:"image"`
|
||||
} `json:"services"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(stdout), &composeConfig); err != nil {
|
||||
return nil, fmt.Errorf("parse compose config json failed, std: %s, err: %v", stdout, err)
|
||||
}
|
||||
|
||||
var images []string
|
||||
seen := make(map[string]struct{})
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
image := strings.TrimSpace(line)
|
||||
if len(image) == 0 {
|
||||
for _, service := range composeConfig.Services {
|
||||
image := strings.TrimSpace(service.Image)
|
||||
if image == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[image]; ok {
|
||||
@@ -160,7 +170,7 @@ func getComposeImagesByCommand(filePath string) ([]string, error) {
|
||||
images = append(images, image)
|
||||
}
|
||||
if len(images) == 0 {
|
||||
return nil, errors.New("no images found from compose config")
|
||||
return nil, errors.New("no images found from compose config json")
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func LoadServiceName(keyword string) (string, error) {
|
||||
}
|
||||
|
||||
processedName := loadProcessedName(client.Name(), keyword)
|
||||
exist, err := client.IsExist(processedName)
|
||||
exist, _ := client.IsExist(processedName)
|
||||
if exist {
|
||||
return processedName, nil
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
@@ -301,7 +303,11 @@ func (c Client) BuildImageWithProcessAndOptions(task *task.Task, tar io.ReadClos
|
||||
}
|
||||
|
||||
func (c Client) PullImageWithProcess(task *task.Task, imageName string) error {
|
||||
return c.PullImageWithProcessAndOptions(task, imageName, image.PullOptions{})
|
||||
options := image.PullOptions{}
|
||||
if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok {
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
return c.PullImageWithProcessAndOptions(task, imageName, options)
|
||||
}
|
||||
|
||||
func logProcess(progress map[string]interface{}, task *task.Task) {
|
||||
@@ -319,8 +325,99 @@ func PullImage(imageName string) error {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
if _, err := cli.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil {
|
||||
options := image.PullOptions{}
|
||||
if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok {
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
if _, err := cli.ImagePull(context.Background(), imageName, options); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) {
|
||||
registryHost, hasRegistry := extractRegistryHost(imageName)
|
||||
cfg := config.LoadDefaultConfigFile(io.Discard)
|
||||
if cfg == nil {
|
||||
return "", false
|
||||
}
|
||||
candidates := make([]string, 0)
|
||||
if hasRegistry {
|
||||
candidates = append(candidates, registryHost, "https://"+registryHost, "http://"+registryHost)
|
||||
}
|
||||
if !hasRegistry || isDockerHubRegistry(registryHost) {
|
||||
candidates = append(candidates,
|
||||
"https://index.docker.io/v1/",
|
||||
"index.docker.io",
|
||||
"docker.io",
|
||||
"registry-1.docker.io",
|
||||
"https://registry-1.docker.io",
|
||||
)
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for _, key := range candidates {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
auth, err := cfg.GetAuthConfig(key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if auth.Username == "" && auth.Password == "" && auth.Auth == "" && auth.IdentityToken == "" && auth.RegistryToken == "" {
|
||||
continue
|
||||
}
|
||||
authStr, err := registry.EncodeAuthConfig(registry.AuthConfig{
|
||||
Username: auth.Username,
|
||||
Password: auth.Password,
|
||||
Auth: auth.Auth,
|
||||
Email: auth.Email,
|
||||
ServerAddress: auth.ServerAddress,
|
||||
IdentityToken: auth.IdentityToken,
|
||||
RegistryToken: auth.RegistryToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return authStr, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isDockerHubRegistry(host string) bool {
|
||||
switch normalizeRegistryHost(host) {
|
||||
case "docker.io", "index.docker.io", "registry-1.docker.io":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractRegistryHost(imageName string) (string, bool) {
|
||||
parts := strings.Split(imageName, "/")
|
||||
if len(parts) < 2 {
|
||||
return "", false
|
||||
}
|
||||
first := parts[0]
|
||||
if strings.Contains(first, ".") || strings.Contains(first, ":") || first == "localhost" {
|
||||
return normalizeRegistryHost(first), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func normalizeRegistryHost(registryKey string) string {
|
||||
key := strings.TrimSpace(registryKey)
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
key = strings.TrimPrefix(key, "http://")
|
||||
key = strings.TrimPrefix(key, "https://")
|
||||
key = strings.Trim(key, "/")
|
||||
if strings.Contains(key, "/") {
|
||||
key = strings.SplitN(key, "/", 2)[0]
|
||||
}
|
||||
return strings.ToLower(key)
|
||||
}
|
||||
|
||||
@@ -120,8 +120,7 @@ func parseRecipients(recipient string) []string {
|
||||
|
||||
func buildMessage(config SMTPConfig, message EmailMessage, toList []string) (string, error) {
|
||||
headers := make(map[string]string)
|
||||
encodedFrom := mime.BEncoding.Encode("UTF-8", config.From)
|
||||
headers["From"] = encodedFrom
|
||||
headers["From"] = config.From
|
||||
encodedSubject := mime.BEncoding.Encode("UTF-8", message.Subject)
|
||||
headers["Subject"] = encodedSubject
|
||||
headers["To"] = strings.Join(toList, ",")
|
||||
|
||||
Vendored
+17
-4
@@ -31,10 +31,10 @@ func Marshal(envMap map[string]string) (string, error) {
|
||||
for k, v := range envMap {
|
||||
if d, err := strconv.Atoi(v); err == nil && !isStartWithZero(v) {
|
||||
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
|
||||
} else if hasEvenDoubleQuotes(v) {
|
||||
} else if shouldUseSingleQuotes(v) {
|
||||
lines = append(lines, fmt.Sprintf(`%s='%s'`, k, v))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, v))
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, escapeDoubleQuotedEnvValue(v)))
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
@@ -88,10 +88,10 @@ func MarshalWithOrder(envMap map[string]string, orders []string) (string, error)
|
||||
func formatEnvLine(k, v string) string {
|
||||
if d, err := strconv.Atoi(v); err == nil && !isStartWithZero(v) {
|
||||
return fmt.Sprintf(`%s=%d`, k, d)
|
||||
} else if hasEvenDoubleQuotes(v) {
|
||||
} else if shouldUseSingleQuotes(v) {
|
||||
return fmt.Sprintf(`%s='%s'`, k, v)
|
||||
} else {
|
||||
return fmt.Sprintf(`%s="%s"`, k, v)
|
||||
return fmt.Sprintf(`%s="%s"`, k, escapeDoubleQuotedEnvValue(v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +123,16 @@ func hasEvenDoubleQuotes(s string) bool {
|
||||
}
|
||||
return count%2 == 0
|
||||
}
|
||||
|
||||
func shouldUseSingleQuotes(s string) bool {
|
||||
return hasEvenDoubleQuotes(s) && !strings.Contains(s, `\`) && !strings.Contains(s, `'`)
|
||||
}
|
||||
|
||||
func escapeDoubleQuotedEnvValue(s string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`"`, `\"`,
|
||||
"\n", `\n`,
|
||||
)
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
|
||||
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
|
||||
}
|
||||
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
|
||||
if err := cmd.RunDefaultBashCf(mvCommand); err != nil {
|
||||
if err := cmd.RunDefaultBashC(mvCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
)
|
||||
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
ModeGeneric Mode = iota
|
||||
ModeHost
|
||||
ModeURL
|
||||
ModePath
|
||||
ModeAllowMethods
|
||||
)
|
||||
|
||||
func NginxSafeString(input string, mode Mode) (string, error) {
|
||||
if input == "" {
|
||||
return "", errors.New("empty value not allowed")
|
||||
}
|
||||
|
||||
for _, r := range input {
|
||||
if unicode.IsControl(r) {
|
||||
return "", errors.New("control characters not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.ContainsAny(input, ";\n\r{}#`$") {
|
||||
return "", errors.New("illegal nginx syntax characters")
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeHost:
|
||||
return validateHost(input)
|
||||
case ModeURL:
|
||||
return validateURL(input)
|
||||
case ModePath:
|
||||
return validatePath(input)
|
||||
case ModeAllowMethods:
|
||||
return validateAllowMethods(input)
|
||||
default:
|
||||
return input, nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateHost(host string) (string, error) {
|
||||
if !re.GetRegex(re.NginxHostPattern).MatchString(host) {
|
||||
return "", errors.New("invalid host format")
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func validateURL(raw string) (string, error) {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return "", errors.New("invalid url")
|
||||
}
|
||||
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return "", errors.New("unsupported scheme")
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return "", errors.New("missing host")
|
||||
}
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func validatePath(p string) (string, error) {
|
||||
if !re.GetRegex(re.NginxPathPattern).MatchString(p) {
|
||||
return "", errors.New("invalid path")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
var allowedMethods = map[string]bool{
|
||||
"GET": true,
|
||||
"POST": true,
|
||||
"PUT": true,
|
||||
"DELETE": true,
|
||||
"PATCH": true,
|
||||
"OPTIONS": true,
|
||||
"HEAD": true,
|
||||
}
|
||||
|
||||
func validateAllowMethods(input string) (string, error) {
|
||||
parts := strings.Split(input, ",")
|
||||
for i, method := range parts {
|
||||
method = strings.TrimSpace(strings.ToUpper(method))
|
||||
if !allowedMethods[method] {
|
||||
return "", errors.New("invalid HTTP method: " + method)
|
||||
}
|
||||
parts[i] = method
|
||||
}
|
||||
return strings.Join(parts, ", "), nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -99,6 +100,13 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
|
||||
|
||||
func (r *Local) Delete(info DeleteInfo) error {
|
||||
if len(info.Name) != 0 {
|
||||
inUse, err := r.isDatabaseInUse(info.Name, info.Timeout)
|
||||
if err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("check database connections failed, err: %v", err)
|
||||
}
|
||||
if inUse && !info.ForceDelete {
|
||||
return buserr.WithDetail("ErrInUsed", info.Name, nil)
|
||||
}
|
||||
dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
|
||||
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("drop database failed, err: %v", err)
|
||||
@@ -203,6 +211,30 @@ func (r *Local) SyncDB() ([]SyncDBInfo, error) {
|
||||
|
||||
func (r *Local) Close() {}
|
||||
|
||||
func (r *Local) isDatabaseInUse(name string, timeout uint) (bool, error) {
|
||||
escapedName := strings.ReplaceAll(name, "'", "''")
|
||||
checkSQL := fmt.Sprintf(
|
||||
"SELECT COUNT(*) FROM pg_stat_activity WHERE datname='%s' AND pid <> pg_backend_pid()",
|
||||
escapedName,
|
||||
)
|
||||
lines, err := r.ExecSQLForRows(checkSQL, timeout)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, line := range lines {
|
||||
countStr := strings.TrimSpace(line)
|
||||
if len(countStr) == 0 {
|
||||
continue
|
||||
}
|
||||
count, parseErr := strconv.Atoi(countStr)
|
||||
if parseErr != nil {
|
||||
return false, parseErr
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *Local) ExecSQL(command string, timeout uint) error {
|
||||
itemCommand := r.PrefixCommand[:]
|
||||
itemCommand = append(itemCommand, command)
|
||||
|
||||
@@ -95,6 +95,13 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
|
||||
|
||||
func (r *Remote) Delete(info DeleteInfo) error {
|
||||
if len(info.Name) != 0 {
|
||||
inUse, err := r.isDatabaseInUse(info.Name, info.Timeout)
|
||||
if err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("check database connections failed, err: %v", err)
|
||||
}
|
||||
if inUse && !info.ForceDelete {
|
||||
return buserr.WithDetail("ErrInUsed", info.Name, nil)
|
||||
}
|
||||
dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
|
||||
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("drop database failed, err: %v", err)
|
||||
@@ -107,6 +114,24 @@ func (r *Remote) Delete(info DeleteInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) isDatabaseInUse(name string, timeout uint) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var count int
|
||||
if err := r.Client.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT COUNT(*) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()",
|
||||
name,
|
||||
).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return false, buserr.New("ErrExecTimeOut")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *Remote) ChangePrivileges(info Privileges) error {
|
||||
super := "SUPERUSER"
|
||||
if !info.SuperUser {
|
||||
|
||||
@@ -127,7 +127,6 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64, []float64) {
|
||||
}
|
||||
|
||||
c.cachedTotalUsage = totalUsage
|
||||
c.cachedPerCore = c.cachedPerCore
|
||||
c.cachedDetailedPercent = detailedPercent
|
||||
c.lastTotalStat = &curTotal
|
||||
c.lastDetailStat = &curDetail
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
AnsiEscapePattern = "\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>"
|
||||
RecycleBinFilePattern = `_1p_file_1p_(.+)_p_(\d+)_(\d+)`
|
||||
OrderByValidationPattern = `^[a-zA-Z_][a-zA-Z0-9_]*$`
|
||||
SQLIdentifierPattern = `^[A-Za-z_][A-Za-z0-9_]*$`
|
||||
NginxHostPattern = `^[a-zA-Z0-9.-]+(:[0-9]+)?$`
|
||||
NginxPathPattern = `^/[a-zA-Z0-9._/\-]*$`
|
||||
)
|
||||
@@ -60,6 +61,7 @@ func Init() {
|
||||
AnsiEscapePattern,
|
||||
RecycleBinFilePattern,
|
||||
OrderByValidationPattern,
|
||||
SQLIdentifierPattern,
|
||||
NginxHostPattern,
|
||||
NginxPathPattern,
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ import (
|
||||
|
||||
var Orders = make(map[uint]*acme.Order)
|
||||
|
||||
type domainError struct {
|
||||
Domain string
|
||||
Error error
|
||||
}
|
||||
|
||||
type zeroSSLRes struct {
|
||||
Success bool `json:"success"`
|
||||
EabKid string `json:"eab_kid"`
|
||||
|
||||
@@ -39,6 +39,10 @@ func IsUseCustomApp() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func IsXpack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type SettingInfo struct {
|
||||
Theme string `json:"theme"`
|
||||
MenuTabs string `json:"menuTabs"`
|
||||
Language string `json:"language"`
|
||||
DocSource string `json:"docSource"`
|
||||
|
||||
ServerPort string `json:"serverPort"`
|
||||
SSL string `json:"ssl"`
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -82,6 +83,15 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
|
||||
for _, set := range setting {
|
||||
settingMap[set.Key] = set.Value
|
||||
}
|
||||
if hideMenu, ok := settingMap["HideMenu"]; ok && len(hideMenu) > 0 {
|
||||
var menus []dto.ShowMenu
|
||||
if err := json.Unmarshal([]byte(hideMenu), &menus); err == nil {
|
||||
sortShowMenus(menus)
|
||||
if sortedBytes, err := json.Marshal(menus); err == nil {
|
||||
settingMap["HideMenu"] = string(sortedBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
var info dto.SettingInfo
|
||||
arr, err := json.Marshal(settingMap)
|
||||
if err != nil {
|
||||
@@ -103,6 +113,20 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
|
||||
return &info, err
|
||||
}
|
||||
|
||||
func sortShowMenus(menus []dto.ShowMenu) {
|
||||
for i := range menus {
|
||||
if len(menus[i].Children) > 0 {
|
||||
sortShowMenus(menus[i].Children)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(menus, func(i, j int) bool {
|
||||
if menus[i].Sort == menus[j].Sort {
|
||||
return menus[i].ID < menus[j].ID
|
||||
}
|
||||
return menus[i].Sort < menus[j].Sort
|
||||
})
|
||||
}
|
||||
|
||||
func (u *SettingService) Update(key, value string) error {
|
||||
oldVal, err := settingRepo.Get(repo.WithByKey(key))
|
||||
if err != nil {
|
||||
|
||||
@@ -286,8 +286,21 @@ type noteDetailHelper struct {
|
||||
}
|
||||
|
||||
func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
|
||||
docSource, _ := settingRepo.GetValueByKey("DocSource")
|
||||
lang, _ := settingRepo.GetValueByKey("Language")
|
||||
var notes []dto.ReleasesNotes
|
||||
resp, err := req_helper.HandleGet("https://1panel.cn/docs/v2/search/search_index.json")
|
||||
url := "https://1panel.cn/docs/v2/search/search_index.json"
|
||||
useIntlDocs := false
|
||||
lang = strings.ToLower(strings.TrimSpace(lang))
|
||||
if docSource == "withByRegion" {
|
||||
useIntlDocs = global.CONF.Base.Edition == "intl"
|
||||
} else {
|
||||
useIntlDocs = lang != "zh"
|
||||
}
|
||||
if useIntlDocs {
|
||||
url = "https://docs.1panel.pro/v2/search/search_index.json"
|
||||
}
|
||||
resp, err := req_helper.HandleGet(url)
|
||||
if err != nil {
|
||||
return notes, err
|
||||
}
|
||||
@@ -321,13 +334,13 @@ func analyzeDoc(version, content string) dto.ReleasesNotes {
|
||||
}
|
||||
item.CreatedAt = strings.ReplaceAll(strings.TrimSpace(parts[1]), "</p>", "")
|
||||
for i := 1; i < len(parts); i++ {
|
||||
if strings.Contains(parts[i], "问题修复") {
|
||||
if strings.Contains(parts[i], "问题修复") || strings.Contains(parts[i], "Bug Fixes") {
|
||||
item.FixCount = strings.Count(parts[i], "<li>")
|
||||
}
|
||||
if strings.Contains(parts[i], "新增功能") {
|
||||
if strings.Contains(parts[i], "新增功能") || strings.Contains(parts[i], "New Features") {
|
||||
item.NewCount = strings.Count(parts[i], "<li>")
|
||||
}
|
||||
if strings.Contains(parts[i], "功能优化") {
|
||||
if strings.Contains(parts[i], "功能优化") || strings.Contains(parts[i], "Improvements") {
|
||||
item.OptimizationCount = strings.Count(parts[i], "<li>")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +523,79 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/update": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigUpdateReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Update Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/telegram/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -739,6 +812,79 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/check": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginCheckReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Check Agent plugin installation status",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/install": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginInstallReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Install Agent plugin",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/providers": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -4095,6 +4241,225 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/content": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileContent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file content",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileBatchDeleteReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Download container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ContainerFileInfo"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "List container files",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/size": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file size",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "containerID",
|
||||
"in": "formData",
|
||||
"name": "containerID",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "path",
|
||||
"in": "formData",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "file",
|
||||
"in": "formData",
|
||||
"name": "file",
|
||||
"required": true,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Upload container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/image": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -23746,6 +24111,104 @@ const docTemplate = `{
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginCheckReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginInstallReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"taskID": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"taskID",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginStatus": {
|
||||
"properties": {
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfig": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigUpdateReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"appId",
|
||||
"clientSecret"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentTelegramConfig": {
|
||||
"properties": {
|
||||
"botToken": {
|
||||
@@ -25035,6 +25498,86 @@ const docTemplate = `{
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileBatchDeleteReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"paths": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileContent": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBinary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileInfo": {
|
||||
"properties": {
|
||||
"isDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isLink": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"linkTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"modTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerItemStats": {
|
||||
"properties": {
|
||||
"buildCacheReclaimable": {
|
||||
|
||||
@@ -519,6 +519,79 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/update": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigUpdateReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Update Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/telegram/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -735,6 +808,79 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/check": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginCheckReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Check Agent plugin installation status",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/install": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginInstallReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Install Agent plugin",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/providers": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -4091,6 +4237,225 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/content": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileContent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file content",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileBatchDeleteReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Download container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ContainerFileInfo"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "List container files",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/size": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file size",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "containerID",
|
||||
"in": "formData",
|
||||
"name": "containerID",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "path",
|
||||
"in": "formData",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "file",
|
||||
"in": "formData",
|
||||
"name": "file",
|
||||
"required": true,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Upload container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/image": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -23742,6 +24107,104 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginCheckReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginInstallReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"taskID": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"taskID",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginStatus": {
|
||||
"properties": {
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfig": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigUpdateReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"appId",
|
||||
"clientSecret"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentTelegramConfig": {
|
||||
"properties": {
|
||||
"botToken": {
|
||||
@@ -25031,6 +25494,86 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileBatchDeleteReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"paths": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileContent": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBinary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileInfo": {
|
||||
"properties": {
|
||||
"isDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isLink": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"linkTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"modTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerItemStats": {
|
||||
"properties": {
|
||||
"buildCacheReclaimable": {
|
||||
|
||||
@@ -62,7 +62,10 @@ func TestGenerateXlog(t *testing.T) {
|
||||
panic(fmt.Sprintf("json marshal for new file failed, err: %v", err))
|
||||
}
|
||||
if err := os.WriteFile("x-log.json", newJson, 0640); err != nil {
|
||||
panic(fmt.Sprintf("write new swagger.json failed, err: %v", err))
|
||||
panic(fmt.Sprintf("write core x-log.json failed, err: %v", err))
|
||||
}
|
||||
if err := os.WriteFile(workDir+"/agent/cmd/server/docs/x-log.json", newJson, 0640); err != nil {
|
||||
panic(fmt.Sprintf("write agent x-log.json failed, err: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,36 @@
|
||||
"formatZH": "docker 服务 [operation]",
|
||||
"formatEN": "[operation] docker service"
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除容器 [containerID] 文件 [paths]",
|
||||
"formatEN": "Delete files [paths] in container [containerID]"
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载容器 [containerID] 文件 [path]",
|
||||
"formatEN": "Download file [path] from container [containerID]"
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器 [containerID] 上传文件到 [path]",
|
||||
"formatEN": "Upload file to [path] in container [containerID]"
|
||||
},
|
||||
"/containers/image/build": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
|
||||
@@ -64,6 +64,7 @@ var WebUrlMap = map[string]struct{}{
|
||||
"/ai/gpu/history": {},
|
||||
"/ai/mcp": {},
|
||||
"/ai/model/tensorrt": {},
|
||||
"/ai/model/vllm": {},
|
||||
"/ai/model/ollama": {},
|
||||
"/ai/agents/agent": {},
|
||||
"/ai/agents/model": {},
|
||||
|
||||
@@ -17,7 +17,7 @@ func LoadMenus() string {
|
||||
{ID: "3", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model", Sort: 300,
|
||||
Children: []dto.ShowMenu{
|
||||
{ID: "44", Disabled: false, Title: "aiTools.agents.agents", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "OllamaModel", Path: "/ai/model", Sort: 100},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.localModel", IsShow: true, Label: "OllamaModel", Path: "/ai/model", Sort: 100},
|
||||
{ID: "42", Disabled: false, Title: "menu.mcp", IsShow: true, Label: "MCPServer", Path: "/ai/mcp", Sort: 200},
|
||||
{ID: "43", Disabled: false, Title: "aiTools.gpu.gpu", IsShow: true, Label: "GPU", Path: "/ai/gpu", Sort: 300},
|
||||
}},
|
||||
|
||||
@@ -33,6 +33,8 @@ func Init() {
|
||||
migrations.UpdateAiAgentsMenu,
|
||||
migrations.AddDashboardCarouselSetting,
|
||||
migrations.AddEditionSetting,
|
||||
migrations.UpdateAiLocalModelMenuTitle,
|
||||
migrations.AddDocSourceSetting,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -70,6 +70,9 @@ var InitSetting = &gormigrate.Migration{
|
||||
if err := tx.Create(&model.Setting{Key: "Language", Value: language}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "DocSource", Value: "withByRegion"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "SessionTimeout", Value: "86400"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -834,3 +837,32 @@ var AddEditionSetting = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddDocSourceSetting = &gormigrate.Migration{
|
||||
ID: "20260316-add-doc-source-setting",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var setting model.Setting
|
||||
if err := tx.Where("key = ?", "DocSource").First(&setting).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return tx.Create(&model.Setting{Key: "DocSource", Value: "withByRegion"}).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
if setting.Value == "" {
|
||||
return tx.Model(&model.Setting{}).Where("key = ?", "DocSource").Update("value", "withByRegion").Error
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateAiLocalModelMenuTitle = &gormigrate.Migration{
|
||||
ID: "20260307-update-ai-local-model-menu-title",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(
|
||||
`UPDATE settings
|
||||
SET value = REPLACE(value, 'aiTools.model.model', 'aiTools.model.localModel')
|
||||
WHERE key = 'HideMenu'
|
||||
AND value LIKE '%aiTools.model.model%'`,
|
||||
).Error
|
||||
},
|
||||
}
|
||||
|
||||
+173
-56
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -25,8 +26,15 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
headerNeedOperationResolve = "X-Need-Op-Resolve"
|
||||
headerOperationResolved = "X-Op-Resolved"
|
||||
)
|
||||
|
||||
func OperationLog() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Request.Header.Del(headerNeedOperationResolve)
|
||||
|
||||
if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet {
|
||||
c.Next()
|
||||
return
|
||||
@@ -70,7 +78,9 @@ func OperationLog() gin.HandlerFunc {
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
bodyMap, _ = parseMultipart(body, c.Request.Header.Get("Content-Type"))
|
||||
} else {
|
||||
_ = json.Unmarshal(body, &bodyMap)
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.UseNumber()
|
||||
_ = decoder.Decode(&bodyMap)
|
||||
}
|
||||
for _, key := range operationDic.BodyKeys {
|
||||
if _, ok := bodyMap[key]; ok {
|
||||
@@ -78,61 +88,42 @@ func OperationLog() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(operationDic.BeforeFunctions) != 0 {
|
||||
dbItem, err := newDB(record.Path)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
for _, funcs := range operationDic.BeforeFunctions {
|
||||
for key, value := range formatMap {
|
||||
if funcs.InputValue == key {
|
||||
var names []string
|
||||
if funcs.IsList {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
} else {
|
||||
_ = dbItem.Raw(fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn), value).Scan(&names)
|
||||
}
|
||||
formatMap[funcs.OutputValue] = strings.Join(names, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
closeDB(dbItem)
|
||||
needAgentResolve := len(operationDic.BeforeFunctions) != 0 && len(currentNode) != 0 && currentNode != "local" && !strings.HasPrefix(record.Path, "/core")
|
||||
allowCoreFallback := strings.HasPrefix(record.Path, "/core/xpack") || !willProxy(c.Request.URL.Path, currentNode) || len(currentNode) == 0 || currentNode == "local"
|
||||
if needAgentResolve {
|
||||
c.Request.Header.Set(headerNeedOperationResolve, "1")
|
||||
defer func() {
|
||||
c.Request.Header.Del(headerNeedOperationResolve)
|
||||
}()
|
||||
}
|
||||
for key, value := range formatMap {
|
||||
if strings.Contains(operationDic.FormatEN, "["+key+"]") {
|
||||
t := reflect.TypeOf(value)
|
||||
if t.Kind() != reflect.Array && t.Kind() != reflect.Slice {
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
} else {
|
||||
val := reflect.ValueOf(value)
|
||||
length := val.Len()
|
||||
|
||||
var elements []string
|
||||
for i := 0; i < length; i++ {
|
||||
element := val.Index(i).Interface().(string)
|
||||
elements = append(elements, element)
|
||||
}
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(elements, ",")))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(elements, ",")))
|
||||
}
|
||||
}
|
||||
}
|
||||
record.DetailEN = strings.ReplaceAll(operationDic.FormatEN, "[]", "")
|
||||
record.DetailZH = strings.ReplaceAll(operationDic.FormatZH, "[]", "")
|
||||
|
||||
writer := responseBodyWriter{
|
||||
ResponseWriter: c.Writer,
|
||||
body: &bytes.Buffer{},
|
||||
captureBody: shouldCaptureResponseBody(c.Request.URL.Path),
|
||||
}
|
||||
c.Writer = writer
|
||||
c.Writer = &writer
|
||||
now := time.Now()
|
||||
|
||||
c.Next()
|
||||
|
||||
if len(operationDic.BeforeFunctions) != 0 {
|
||||
if needAgentResolve {
|
||||
mergeResolvedData(writer.resolvedHeader, formatMap)
|
||||
}
|
||||
|
||||
if allowCoreFallback && !hasAllResolvedData(formatMap, operationDic.BeforeFunctions) {
|
||||
dbItem, err := newDB(record.Path)
|
||||
if err == nil {
|
||||
resolveByDB(dbItem, formatMap, operationDic.BeforeFunctions)
|
||||
closeDB(dbItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
fillOperationDetail(&operationDic, formatMap)
|
||||
record.DetailEN = strings.ReplaceAll(operationDic.FormatEN, "[]", "")
|
||||
record.DetailZH = strings.ReplaceAll(operationDic.FormatZH, "[]", "")
|
||||
|
||||
datas := writer.body.Bytes()
|
||||
logRepo := repo.NewILogRepo()
|
||||
if c.Request.Header.Get("Content-Encoding") == "gzip" {
|
||||
@@ -153,12 +144,24 @@ func OperationLog() gin.HandlerFunc {
|
||||
datas, _ = io.ReadAll(reader)
|
||||
}
|
||||
var res response
|
||||
_ = json.Unmarshal(datas, &res)
|
||||
if res.Code == 200 {
|
||||
record.Status = constant.StatusSuccess
|
||||
contentType := strings.ToLower(c.Writer.Header().Get("Content-Type"))
|
||||
isJSONResponse := strings.Contains(contentType, "application/json")
|
||||
if isJSONResponse {
|
||||
_ = json.Unmarshal(datas, &res)
|
||||
if res.Code == 200 {
|
||||
record.Status = constant.StatusSuccess
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = res.Message
|
||||
}
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = res.Message
|
||||
statusCode := c.Writer.Status()
|
||||
if statusCode >= 200 && statusCode < 400 {
|
||||
record.Status = constant.StatusSuccess
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = http.StatusText(statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
latency := time.Since(now)
|
||||
@@ -170,6 +173,30 @@ func OperationLog() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func fillOperationDetail(operationDic *operationJson, formatMap map[string]interface{}) {
|
||||
for key, value := range formatMap {
|
||||
if !strings.Contains(operationDic.FormatEN, "["+key+"]") {
|
||||
continue
|
||||
}
|
||||
t := reflect.TypeOf(value)
|
||||
if t == nil || (t.Kind() != reflect.Array && t.Kind() != reflect.Slice) {
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
continue
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(value)
|
||||
length := val.Len()
|
||||
elements := make([]string, 0, length)
|
||||
for i := 0; i < length; i++ {
|
||||
elements = append(elements, fmt.Sprintf("%v", val.Index(i).Interface()))
|
||||
}
|
||||
replaced := fmt.Sprintf("[%v]", strings.Join(elements, ","))
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", replaced)
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", replaced)
|
||||
}
|
||||
}
|
||||
|
||||
type operationJson struct {
|
||||
API string `json:"api"`
|
||||
Method string `json:"method"`
|
||||
@@ -195,14 +222,44 @@ type response struct {
|
||||
|
||||
type responseBodyWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
body *bytes.Buffer
|
||||
resolvedHeader string
|
||||
captureBody bool
|
||||
}
|
||||
|
||||
func (r responseBodyWriter) Write(b []byte) (int, error) {
|
||||
r.body.Write(b)
|
||||
func (r *responseBodyWriter) sanitizeResolvedHeader() {
|
||||
if len(r.resolvedHeader) == 0 {
|
||||
r.resolvedHeader = r.ResponseWriter.Header().Get(headerOperationResolved)
|
||||
}
|
||||
r.ResponseWriter.Header().Del(headerOperationResolved)
|
||||
}
|
||||
|
||||
func (r *responseBodyWriter) WriteHeader(code int) {
|
||||
r.sanitizeResolvedHeader()
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (r *responseBodyWriter) WriteHeaderNow() {
|
||||
r.sanitizeResolvedHeader()
|
||||
r.ResponseWriter.WriteHeaderNow()
|
||||
}
|
||||
|
||||
func (r *responseBodyWriter) Write(b []byte) (int, error) {
|
||||
r.sanitizeResolvedHeader()
|
||||
if r.captureBody {
|
||||
r.body.Write(b)
|
||||
}
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func shouldCaptureResponseBody(reqPath string) bool {
|
||||
reqPath = strings.ToLower(reqPath)
|
||||
if strings.Contains(reqPath, "download") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loadLogInfo(path string) string {
|
||||
path = replaceStr(path, "/api/v2", "/core", "/xpack")
|
||||
if !strings.Contains(path, "/") {
|
||||
@@ -218,10 +275,10 @@ func loadLogInfo(path string) string {
|
||||
func newDB(pathItem string) (*gorm.DB, error) {
|
||||
dbFile := ""
|
||||
switch {
|
||||
case strings.HasPrefix(pathItem, "/core/xpack") || strings.HasPrefix(pathItem, "/xpack"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db")
|
||||
case strings.HasPrefix(pathItem, "/core"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/core.db")
|
||||
case strings.HasPrefix(pathItem, "/xpack"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db")
|
||||
default:
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/agent.db")
|
||||
}
|
||||
@@ -248,6 +305,26 @@ func closeDB(db *gorm.DB) {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
|
||||
func resolveByDB(dbItem *gorm.DB, values map[string]interface{}, beforeFunctions []functionInfo) {
|
||||
for _, funcs := range beforeFunctions {
|
||||
for key, value := range values {
|
||||
if funcs.InputValue != key {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
if funcs.IsList {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
} else {
|
||||
sql := fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
}
|
||||
values[funcs.OutputValue] = strings.Join(names, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceStr(val string, rep ...string) string {
|
||||
for _, item := range rep {
|
||||
val = strings.ReplaceAll(val, item, "")
|
||||
@@ -284,3 +361,43 @@ func parseMultipart(formData []byte, contentType string) (map[string]interface{}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func mergeResolvedData(headerVal string, values map[string]interface{}) {
|
||||
if len(headerVal) == 0 {
|
||||
return
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(headerVal)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resolved := make(map[string]string)
|
||||
if err := json.Unmarshal(data, &resolved); err != nil {
|
||||
return
|
||||
}
|
||||
for key, value := range resolved {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func hasAllResolvedData(values map[string]interface{}, beforeFunctions []functionInfo) bool {
|
||||
for _, item := range beforeFunctions {
|
||||
if _, ok := values[item.OutputValue]; ok {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func willProxy(reqPath, currentNode string) bool {
|
||||
if strings.HasPrefix(reqPath, "/1panel/swagger") || !strings.HasPrefix(reqPath, "/api/v2") {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(reqPath, "/api/v2/core") && !strings.HasPrefix(reqPath, "/api/v2/core/xpack") {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(reqPath, "/api/v2/core") && (currentNode == "local" || len(currentNode) == 0) {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -241,6 +241,7 @@ export namespace AI {
|
||||
appVersion: string;
|
||||
webUIPort: number;
|
||||
bridgePort?: number;
|
||||
allowedOrigins?: string[];
|
||||
agentType: 'openclaw' | 'copaw';
|
||||
provider?: string;
|
||||
model?: string;
|
||||
@@ -428,10 +429,63 @@ export namespace AI {
|
||||
|
||||
export interface AgentChannelPairingApproveReq {
|
||||
agentId: number;
|
||||
type: 'feishu' | 'telegram' | 'discord';
|
||||
type: 'feishu' | 'telegram' | 'discord' | 'wecom';
|
||||
pairingCode: string;
|
||||
}
|
||||
|
||||
export interface AgentWecomConfigReq {
|
||||
agentId: number;
|
||||
}
|
||||
|
||||
export interface AgentWecomConfig {
|
||||
enabled: boolean;
|
||||
dmPolicy: 'pairing' | 'open';
|
||||
botId: string;
|
||||
secret: string;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export interface AgentWecomConfigUpdateReq {
|
||||
agentId: number;
|
||||
enabled: boolean;
|
||||
dmPolicy: 'pairing' | 'open';
|
||||
botId: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface AgentQQBotConfigReq {
|
||||
agentId: number;
|
||||
}
|
||||
|
||||
export interface AgentQQBotConfig {
|
||||
enabled: boolean;
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export interface AgentQQBotConfigUpdateReq {
|
||||
agentId: number;
|
||||
enabled: boolean;
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
export interface AgentPluginInstallReq {
|
||||
agentId: number;
|
||||
type: 'qqbot' | 'wecom';
|
||||
taskID: string;
|
||||
}
|
||||
|
||||
export interface AgentPluginCheckReq {
|
||||
agentId: number;
|
||||
type: 'qqbot' | 'wecom';
|
||||
}
|
||||
|
||||
export interface AgentPluginStatus {
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export interface AgentDiscordConfigReq {
|
||||
agentId: number;
|
||||
}
|
||||
@@ -453,24 +507,17 @@ export namespace AI {
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfigReq {
|
||||
export interface AgentSecurityConfigReq {
|
||||
agentId: number;
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfig {
|
||||
enabled: boolean;
|
||||
executablePath: string;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
defaultProfile: string;
|
||||
export interface AgentSecurityConfig {
|
||||
allowedOrigins: string[];
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfigUpdateReq {
|
||||
export interface AgentSecurityConfigUpdateReq {
|
||||
agentId: number;
|
||||
enabled: boolean;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
defaultProfile: string;
|
||||
allowedOrigins: string[];
|
||||
}
|
||||
|
||||
export interface AgentOtherConfigReq {
|
||||
@@ -479,10 +526,12 @@ export namespace AI {
|
||||
|
||||
export interface AgentOtherConfig {
|
||||
userTimezone: string;
|
||||
browserEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface AgentOtherConfigUpdateReq {
|
||||
agentId: number;
|
||||
userTimezone: string;
|
||||
browserEnabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export namespace Backup {
|
||||
detailName: string;
|
||||
secret: string;
|
||||
taskID: string;
|
||||
stopBefore?: boolean;
|
||||
}
|
||||
export interface Recover {
|
||||
downloadAccountID: number;
|
||||
|
||||
@@ -56,6 +56,26 @@ export namespace Container {
|
||||
name: string;
|
||||
state: string;
|
||||
}
|
||||
export interface ContainerFileReq {
|
||||
containerID: string;
|
||||
path: string;
|
||||
}
|
||||
export interface ContainerFileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
isDir: boolean;
|
||||
isLink: boolean;
|
||||
linkTo: string;
|
||||
size: number;
|
||||
mode: string;
|
||||
modTime: string;
|
||||
}
|
||||
export interface ContainerFileContent {
|
||||
content: string;
|
||||
size: number;
|
||||
truncated: boolean;
|
||||
isBinary: boolean;
|
||||
}
|
||||
export interface ResourceLimit {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
|
||||
@@ -21,6 +21,7 @@ export namespace Setting {
|
||||
theme: string;
|
||||
menuTabs: string;
|
||||
language: string;
|
||||
docSource: string;
|
||||
defaultIO: string;
|
||||
defaultNetwork: string;
|
||||
lastCleanTime: string;
|
||||
|
||||
@@ -145,10 +145,6 @@ export const updateAgentFeishuConfig = (req: AI.AgentFeishuConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/channel/feishu/update`, req);
|
||||
};
|
||||
|
||||
export const approveAgentFeishuPairing = (req: AI.AgentFeishuPairingApproveReq) => {
|
||||
return http.post(`/ai/agents/channel/feishu/approve`, req);
|
||||
};
|
||||
|
||||
export const getAgentTelegramConfig = (req: AI.AgentTelegramConfigReq) => {
|
||||
return http.post<AI.AgentTelegramConfig>(`/ai/agents/channel/telegram/get`, req);
|
||||
};
|
||||
@@ -165,12 +161,36 @@ export const updateAgentDiscordConfig = (req: AI.AgentDiscordConfigUpdateReq) =>
|
||||
return http.post(`/ai/agents/channel/discord/update`, req);
|
||||
};
|
||||
|
||||
export const getAgentBrowserConfig = (req: AI.AgentBrowserConfigReq) => {
|
||||
return http.post<AI.AgentBrowserConfig>(`/ai/agents/browser/get`, req);
|
||||
export const getAgentWecomConfig = (req: AI.AgentWecomConfigReq) => {
|
||||
return http.post<AI.AgentWecomConfig>(`/ai/agents/channel/wecom/get`, req);
|
||||
};
|
||||
|
||||
export const updateAgentBrowserConfig = (req: AI.AgentBrowserConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/browser/update`, req);
|
||||
export const updateAgentWecomConfig = (req: AI.AgentWecomConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/channel/wecom/update`, req);
|
||||
};
|
||||
|
||||
export const getAgentQQBotConfig = (req: AI.AgentQQBotConfigReq) => {
|
||||
return http.post<AI.AgentQQBotConfig>(`/ai/agents/channel/qqbot/get`, req);
|
||||
};
|
||||
|
||||
export const updateAgentQQBotConfig = (req: AI.AgentQQBotConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/channel/qqbot/update`, req);
|
||||
};
|
||||
|
||||
export const installAgentPlugin = (req: AI.AgentPluginInstallReq) => {
|
||||
return http.post(`/ai/agents/plugin/install`, req);
|
||||
};
|
||||
|
||||
export const checkAgentPlugin = (req: AI.AgentPluginCheckReq) => {
|
||||
return http.post<AI.AgentPluginStatus>(`/ai/agents/plugin/check`, req);
|
||||
};
|
||||
|
||||
export const getAgentSecurityConfig = (req: AI.AgentSecurityConfigReq) => {
|
||||
return http.post<AI.AgentSecurityConfig>(`/ai/agents/security/get`, req);
|
||||
};
|
||||
|
||||
export const updateAgentSecurityConfig = (req: AI.AgentSecurityConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/security/update`, req);
|
||||
};
|
||||
|
||||
export const getAgentOtherConfig = (req: AI.AgentOtherConfigReq) => {
|
||||
|
||||
@@ -52,8 +52,9 @@ export const handleRecover = (params: Backup.Recover, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.post(`/backups/recover${query}`, params, TimeoutEnum.T_10M);
|
||||
};
|
||||
export const handleRecoverByUpload = (params: Backup.Recover) => {
|
||||
return http.post(`/backups/recover/byupload`, params, TimeoutEnum.T_10M);
|
||||
export const handleRecoverByUpload = (params: Backup.Recover, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.post(`/backups/recover/byupload${query}`, params, TimeoutEnum.T_10M);
|
||||
};
|
||||
export const downloadBackupRecord = (params: Backup.RecordDownload, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
@@ -67,8 +68,9 @@ export const updateRecordDescription = (id: Number, description: String, node?:
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.post(`/backups/record/description/update${query}`, { id: id, description: description });
|
||||
};
|
||||
export const uploadByRecover = (filePath: string, targetDir: String) => {
|
||||
return http.post(`/backups/upload`, { filePath: filePath, targetDir: targetDir });
|
||||
export const uploadByRecover = (filePath: string, targetDir: String, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.post(`/backups/upload${query}`, { filePath: filePath, targetDir: targetDir });
|
||||
};
|
||||
export const searchBackupRecords = (params: Backup.SearchBackupRecord, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
|
||||
@@ -15,6 +15,30 @@ export const listContainerByImage = (image: string) => {
|
||||
export const loadContainerUsers = (name: string) => {
|
||||
return http.post<Array<string>>(`/containers/users`, { name: name });
|
||||
};
|
||||
export const listContainerFiles = (params: Container.ContainerFileReq) => {
|
||||
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const uploadContainerFile = (params: FormData) => {
|
||||
return http.upload(`/containers/files/upload`, params, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: TimeoutEnum.T_5M,
|
||||
});
|
||||
};
|
||||
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
|
||||
return http.post<Container.ContainerFileContent>(`/containers/files/content`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const getContainerFileSize = (params: Container.ContainerFileReq) => {
|
||||
return http.post<number>(`/containers/files/size`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const deleteContainerFile = (params: { containerID: string; paths: string[] }) => {
|
||||
return http.post(`/containers/files/del`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const downloadContainerFile = (params: { containerID: string; path: string }) => {
|
||||
return http.download<BlobPart>(`/containers/files/download`, params, {
|
||||
responseType: 'blob',
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
});
|
||||
};
|
||||
export const loadContainerStatus = () => {
|
||||
return http.get<Container.ContainerStatus>(`/containers/status`);
|
||||
};
|
||||
|
||||
@@ -60,8 +60,9 @@ export const listAppNodes = () => {
|
||||
};
|
||||
|
||||
// agent
|
||||
export const loadBaseDir = () => {
|
||||
return http.get<string>(`/settings/basedir`);
|
||||
export const loadBaseDir = (node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.get<string>(`/settings/basedir${query}`);
|
||||
};
|
||||
export const loadDaemonJsonPath = () => {
|
||||
return http.get<string>(`/settings/daemonjson`, {});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 175.35 235.42" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ff9d4d" d="M150.41,91.07c-7.76-7.58-16.99-13.51-27.67-17.84-10.69-4.33-22.28-6.5-34.76-6.5s-24.39,2.21-35.07,6.64c-10.69,4.43-19.95,10.48-27.81,18.13-7.86,7.68-14.01,16.58-18.44,26.77-4.44,10.16-6.66,21.11-6.66,32.8v.59c0,11.71,2.21,22.63,6.66,32.82,4.43,10.16,10.53,19.04,18.29,26.6,7.75,7.55,16.98,13.51,27.65,17.84,10.69,4.33,22.28,6.5,34.78,6.5s24.38-2.21,35.06-6.64c10.69-4.43,19.95-10.48,27.81-18.13,7.86-7.68,14.01-16.58,18.45-26.77,4.43-10.16,6.64-21.11,6.64-32.8v-.62c0-11.69-2.21-22.61-6.64-32.8-4.44-10.19-10.54-19.04-18.29-26.6ZM130,151.67c0,6.05-.96,11.76-2.88,17.1s-4.69,10.01-8.3,14.05c-3.63,4.03-8.02,7.21-13.15,9.52-5.14,2.31-11.05,3.47-17.69,3.47s-12.25-1.21-17.38-3.62c-5.14-2.41-9.63-5.68-13.46-9.82s-6.75-8.88-8.77-14.22c-2.02-5.34-3.01-11.02-3.01-17.07v-.62c0-6.03.95-11.74,2.87-17.07,1.92-5.34,4.69-10.01,8.32-14.05s8.01-7.21,13.15-9.52c5.14-2.34,11.03-3.49,17.69-3.49,6.24,0,11.98,1.21,17.22,3.64,5.24,2.41,9.78,5.68,13.61,9.82,3.83,4.13,6.75,8.88,8.77,14.22,2.02,5.34,3.03,11.02,3.03,17.07v.59Z" />
|
||||
<path fill="#ff9d4d" d="M71.01,38.34c2.06,2.01,4.51,3.59,7.35,4.74,2.84,1.15,5.92,1.73,9.24,1.73s6.48-.59,9.31-1.76c2.84-1.18,5.3-2.78,7.39-4.82,2.09-2.04,3.72-4.41,4.9-7.11,1.18-2.7,1.76-5.61,1.76-8.71v-.16c0-3.1-.59-6.01-1.76-8.71-1.18-2.71-2.8-5.06-4.86-7.07-2.06-2.01-4.51-3.59-7.35-4.74-2.84-1.15-5.92-1.73-9.24-1.73s-6.48.59-9.32,1.76c-2.84,1.18-5.3,2.78-7.39,4.82-2.09,2.04-3.72,4.4-4.9,7.11-1.18,2.7-1.77,5.61-1.77,8.71v.16c0,3.11.59,6.01,1.77,8.72,1.18,2.7,2.8,5.06,4.86,7.06Z" />
|
||||
<path fill="#ff9d4d" d="M14.35,54.93c2.06,2.01,4.51,3.59,7.35,4.74,2.84,1.15,5.92,1.73,9.24,1.73s6.48-.59,9.31-1.77c2.84-1.18,5.3-2.78,7.39-4.82,2.09-2.04,3.72-4.4,4.9-7.11,1.18-2.7,1.77-5.61,1.77-8.71v-.16c0-3.1-.59-6-1.77-8.71-1.18-2.71-2.8-5.06-4.86-7.06-2.06-2.01-4.51-3.59-7.35-4.74-2.84-1.15-5.92-1.73-9.24-1.73s-6.48.59-9.32,1.77c-2.84,1.18-5.3,2.78-7.39,4.82-2.09,2.04-3.72,4.41-4.9,7.11-1.18,2.7-1.77,5.61-1.77,8.71v.16c0,3.11.59,6.01,1.77,8.72,1.18,2.7,2.8,5.06,4.86,7.07Z" />
|
||||
<path fill="#ff9d4d" d="M127.67,54.93c2.06,2.01,4.51,3.59,7.35,4.74,2.84,1.15,5.92,1.73,9.24,1.73s6.48-.59,9.31-1.77c2.84-1.18,5.3-2.78,7.39-4.82,2.09-2.04,3.72-4.4,4.9-7.11,1.18-2.7,1.76-5.61,1.76-8.71v-.16c0-3.1-.59-6-1.76-8.71-1.18-2.71-2.8-5.06-4.86-7.06-2.06-2.01-4.51-3.59-7.35-4.74-2.84-1.15-5.92-1.73-9.23-1.73s-6.48.59-9.32,1.77c-2.84,1.18-5.3,2.78-7.39,4.82-2.09,2.04-3.72,4.41-4.9,7.11-1.18,2.7-1.77,5.61-1.77,8.71v.16c0,3.11.59,6.01,1.77,8.72,1.18,2.7,2.8,5.06,4.86,7.07Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="openclaw-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ff4d4d" />
|
||||
<stop offset="100%" stop-color="#991b1b" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#openclaw-gradient)" />
|
||||
<path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#openclaw-gradient)" />
|
||||
<path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#openclaw-gradient)" />
|
||||
<path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round" />
|
||||
<path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round" />
|
||||
<circle cx="45" cy="35" r="6" fill="#050810" />
|
||||
<circle cx="75" cy="35" r="6" fill="#050810" />
|
||||
<circle cx="46" cy="34" r="2.5" fill="#00e5cc" />
|
||||
<circle cx="76" cy="34" r="2.5" fill="#00e5cc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -27,7 +27,11 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button type="primary" :disabled="status && status != 'Running'" @click="onBackup()">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="status && status.toLowerCase() != 'running'"
|
||||
@click="onBackup()"
|
||||
>
|
||||
{{ $t('commons.button.backup') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||
@@ -106,6 +110,16 @@
|
||||
{{ $t('commons.msg.' + (isBackup ? 'backupHelper' : 'recoverHelper'), [name + '( ' + detailName + ' )']) }}
|
||||
</el-alert>
|
||||
<el-form class="mt-5" ref="backupForm" @submit.prevent label-position="top" v-loading="loading">
|
||||
<el-form-item v-if="isBackup && (type === 'container' || type === 'compose')">
|
||||
<el-checkbox v-model="stopBefore">
|
||||
{{
|
||||
type === 'container'
|
||||
? $t('container.stopContainerBeforeBackup')
|
||||
: $t('container.stopComposeBeforeBackup')
|
||||
}}
|
||||
</el-checkbox>
|
||||
<span class="input-help">{{ $t('container.stopBeforeBackupHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.compressPassword')">
|
||||
<el-input v-model="secret" :placeholder="$t('setting.backupRecoverMessage')" />
|
||||
</el-form-item>
|
||||
@@ -179,6 +193,8 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { mysqlArgs } from '@/views/cronjob/cronjob/helper';
|
||||
const { currentNode } = useGlobalStore();
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const PushApp = defineAsyncComponent(async () => {
|
||||
const modules = import.meta.glob('@/xpack/views/appstore/push-app/index.vue');
|
||||
const loader = modules['/src/xpack/views/appstore/push-app/index.vue'];
|
||||
@@ -213,6 +229,7 @@ const description = ref();
|
||||
const timeoutItem = ref(30);
|
||||
const timeoutUnit = ref('m');
|
||||
const node = ref();
|
||||
const stopBefore = ref(false);
|
||||
|
||||
const open = ref();
|
||||
const isBackup = ref();
|
||||
@@ -243,6 +260,7 @@ const acceptParams = (params: DialogProps): void => {
|
||||
};
|
||||
const handleClose = () => {
|
||||
backupVisible.value = false;
|
||||
emit('close');
|
||||
};
|
||||
const handleBackupClose = () => {
|
||||
open.value = false;
|
||||
@@ -324,6 +342,7 @@ const backup = async () => {
|
||||
taskID: taskID,
|
||||
description: description.value,
|
||||
args: args.value,
|
||||
stopBefore: stopBefore.value,
|
||||
};
|
||||
loading.value = true;
|
||||
await handleBackup(params, node.value)
|
||||
@@ -366,6 +385,7 @@ const onBackup = async () => {
|
||||
description.value = '';
|
||||
secret.value = '';
|
||||
args.value = [];
|
||||
stopBefore.value = false;
|
||||
isBackup.value = true;
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
@@ -132,7 +132,6 @@ const handleBack = () => {
|
||||
} else {
|
||||
localOpenPage.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
emit('beforeClose', done);
|
||||
@@ -142,7 +141,6 @@ const handleBack = () => {
|
||||
} else {
|
||||
localOpenPage.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ interface DialogProps {
|
||||
protocol: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
@@ -58,6 +59,9 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
if (params.query) {
|
||||
url += params.query.startsWith('?') ? params.query : `?${params.query}`;
|
||||
}
|
||||
if (params.hash) {
|
||||
url += params.hash.startsWith('#') ? params.hash : `#${params.hash}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
if (res.data.systemIP.indexOf(':') === -1) {
|
||||
|
||||
@@ -106,7 +106,8 @@ const toLxware = () => {
|
||||
};
|
||||
|
||||
const to1Panel = () => {
|
||||
window.open('https://1panel.cn', '_blank', 'noopener,noreferrer');
|
||||
let url = globalStore.isIntl ? 'https://1panel.pro' : 'https://1panel.cn';
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const toDoc = () => {
|
||||
@@ -117,7 +118,7 @@ const toForum = () => {
|
||||
let url = globalStore.isIntl
|
||||
? 'https://github.com/1Panel-dev/1Panel/discussions'
|
||||
: 'https://bbs.fit2cloud.com/c/1p/7';
|
||||
window.open(url, '_blank');
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const toGithub = () => {
|
||||
|
||||
@@ -2,21 +2,28 @@
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.release')" @close="handleClose" size="large">
|
||||
<div class="note" v-loading="loading">
|
||||
<el-form ref="formRef" :model="form" :rules="rules">
|
||||
<el-form-item :label="$t('setting.versionItem')" prop="version">
|
||||
<el-input class="p-w-200" disabled v-model="form.version">
|
||||
<template #append>
|
||||
<CopyButton class="w-16" :isIcon="false" :content="form.version" type="primary" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.backupCopies')" prop="backupCopies">
|
||||
<el-input class="p-w-200" type="number" v-model.number="form.backupCopies">
|
||||
<template #append>
|
||||
<el-button @click="onSave(formRef)" class="w-16">{{ $t('commons.button.save') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">{{ $t('setting.backupCopiesHelper') }}</span>
|
||||
</el-form-item>
|
||||
<div class="release-settings">
|
||||
<el-form-item :label="$t('setting.versionItem')" prop="version" class="compact-form-item">
|
||||
<div class="setting-control">
|
||||
<el-input disabled v-model="form.version" />
|
||||
<CopyButton
|
||||
class="setting-action-btn"
|
||||
:isIcon="false"
|
||||
:content="form.version"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.backupCopies')" prop="backupCopies" class="compact-form-item">
|
||||
<div class="setting-control">
|
||||
<el-input-number v-model="form.backupCopies" :min="0" controls-position="right" />
|
||||
<el-button @click="onSave(formRef)" class="setting-action-btn" type="primary">
|
||||
{{ $t('commons.button.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span class="input-help">{{ $t('setting.backupCopiesHelper') }}</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<el-collapse v-if="notes && notes.length !== 0" v-model="currentVersion" :accordion="true">
|
||||
<div v-for="(item, index) in notes" :key="index">
|
||||
@@ -183,4 +190,31 @@ defineExpose({
|
||||
margin-left: 5px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
.release-settings {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 14px 10px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.compact-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.compact-form-item :deep(.el-form-item__label) {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.setting-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
.setting-control :deep(.el-input),
|
||||
.setting-control :deep(.el-input-number) {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
.setting-action-btn {
|
||||
min-width: 64px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
{{ $t('database.formatHelper', [remark]) }}
|
||||
</li>
|
||||
<li v-if="isDb()">{{ $t('database.supportUpType') }}</li>
|
||||
<li v-if="!isDb()">{{ $t('website.websiteBackupWarn') }}</li>
|
||||
<li v-if="!isDb()">{{ $t('website.supportUpType', [type]) }}</li>
|
||||
<li v-if="type === 'website' || type === 'app'">
|
||||
{{ $t('website.websiteBackupWarn') }}
|
||||
</li>
|
||||
<li v-if="type === 'container'">{{ $t('container.importContainerBackupTip') }}</li>
|
||||
<li v-else-if="type === 'compose'">{{ $t('container.importComposeBackupTip') }}</li>
|
||||
<li v-else-if="!isDb()">{{ $t('website.supportUpType', [type]) }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
@@ -86,7 +90,7 @@
|
||||
|
||||
<DialogPro
|
||||
v-model="recoverDialog"
|
||||
:title="$t('commons.button.recover') + ' - ' + name"
|
||||
:title="name ? $t('commons.button.recover') + ' - ' + name : $t('commons.button.recover')"
|
||||
@close="handleRecoverClose"
|
||||
size="small"
|
||||
>
|
||||
@@ -125,7 +129,7 @@
|
||||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<FileList ref="fileRef" @choose="loadFile" />
|
||||
<TaskLog ref="taskLogRef" @close="search" />
|
||||
<TaskLog ref="taskLogRef" @close="handleTaskLogClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -141,11 +145,14 @@ import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { handleRecoverByUpload, uploadByRecover } from '@/api/modules/backup';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
interface DialogProps {
|
||||
type: string;
|
||||
name: string;
|
||||
detailName: string;
|
||||
remark: string;
|
||||
node?: string;
|
||||
}
|
||||
const loading = ref();
|
||||
const fileRef = ref();
|
||||
@@ -171,6 +178,7 @@ const secret = ref();
|
||||
const timeoutItem = ref(30);
|
||||
const timeoutUnit = ref('m');
|
||||
const taskLogRef = ref();
|
||||
const node = ref();
|
||||
|
||||
const recoverDialog = ref();
|
||||
|
||||
@@ -179,8 +187,9 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
name.value = params.name;
|
||||
detailName.value = params.detailName;
|
||||
remark.value = params.remark;
|
||||
node.value = params.node;
|
||||
|
||||
const pathRes = await loadBaseDir();
|
||||
const pathRes = await loadBaseDir(node.value);
|
||||
switch (type.value) {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
@@ -201,6 +210,23 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
case 'app':
|
||||
title.value = name.value;
|
||||
baseDir.value = `${pathRes.data}/uploads/app/${type.value}/${name.value}/`;
|
||||
break;
|
||||
case 'container':
|
||||
title.value = name.value || i18n.global.t('menu.container');
|
||||
if (name.value) {
|
||||
baseDir.value = `${pathRes.data}/uploads/container/${name.value}/`;
|
||||
} else {
|
||||
baseDir.value = `${pathRes.data}/uploads/container/import/`;
|
||||
}
|
||||
break;
|
||||
case 'compose':
|
||||
title.value = name.value || i18n.global.t('container.compose');
|
||||
if (name.value) {
|
||||
baseDir.value = `${pathRes.data}/uploads/compose/${name.value}/`;
|
||||
} else {
|
||||
baseDir.value = `${pathRes.data}/uploads/compose/import/`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
uploadOpen.value = true;
|
||||
search();
|
||||
@@ -236,6 +262,14 @@ const beforeUpload = (fileName: string) => {
|
||||
const allowedExtensions = ['.tar.gz'];
|
||||
const isValidFile = allowedExtensions.some((ext) => itemName.endsWith(ext));
|
||||
if (!isValidFile) {
|
||||
if (type.value === 'compose') {
|
||||
MsgError(i18n.global.t('container.importComposeBackupTip'));
|
||||
return false;
|
||||
}
|
||||
if (type.value === 'container') {
|
||||
MsgError(i18n.global.t('container.importContainerBackupTip'));
|
||||
return false;
|
||||
}
|
||||
MsgError(i18n.global.t('website.supportUpType'));
|
||||
return false;
|
||||
}
|
||||
@@ -255,7 +289,7 @@ const loadFile = async (path: string) => {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
uploadByRecover(path, baseDir.value)
|
||||
uploadByRecover(path, baseDir.value, node.value)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
@@ -267,7 +301,7 @@ const loadFile = async (path: string) => {
|
||||
};
|
||||
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
taskLogRef.value.openWithTaskID(taskID, true, node.value);
|
||||
};
|
||||
|
||||
const onHandleRecover = async () => {
|
||||
@@ -282,7 +316,7 @@ const onHandleRecover = async () => {
|
||||
timeout: timeoutItem.value === -1 ? -1 : transferTimeToSecond(timeoutItem.value + timeoutUnit.value),
|
||||
};
|
||||
loading.value = true;
|
||||
await handleRecoverByUpload(params)
|
||||
await handleRecoverByUpload(params, node.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
handleUploadClose();
|
||||
@@ -343,6 +377,10 @@ const handleUploadClose = () => {
|
||||
uploaderFiles.value = [];
|
||||
uploadRef.value!.clearFiles();
|
||||
uploadOpen.value = false;
|
||||
emit('close');
|
||||
};
|
||||
const handleTaskLogClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleRecoverClose = () => {
|
||||
|
||||
+143
-142
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,8 @@ const message = {
|
||||
show: 'Mostrar',
|
||||
hide: 'Ocultar',
|
||||
migrate: 'Migrar',
|
||||
disConn: 'Desconectar',
|
||||
visit: 'Visitar',
|
||||
},
|
||||
operate: {
|
||||
start: 'Iniciar',
|
||||
@@ -189,6 +191,8 @@ const message = {
|
||||
errImportFormat: 'Los datos de importación o el formato son incorrectos, verifica y vuelve a intentarlo',
|
||||
importHelper: 'Si hay conflictos, los datos importados sobrescribirán los existentes.',
|
||||
errImport: 'El contenido del archivo es anormal:',
|
||||
backupSuccess: 'Copia completada',
|
||||
restoreSuccess: 'Restauración completada',
|
||||
},
|
||||
login: {
|
||||
username: 'Usuario',
|
||||
@@ -294,7 +298,7 @@ const message = {
|
||||
commonError: 'La solicitud falló',
|
||||
},
|
||||
service: {
|
||||
serviceNotStarted: `El servicio {0} no está iniciado.`,
|
||||
serviceNotStarted: 'El servicio {0} no está iniciado.',
|
||||
},
|
||||
status: {
|
||||
running: 'En ejecución',
|
||||
@@ -408,6 +412,7 @@ const message = {
|
||||
app: 'Aplicación',
|
||||
msgCenter: 'Tareas',
|
||||
disk: 'Disco',
|
||||
filter: 'Filtro',
|
||||
},
|
||||
home: {
|
||||
recommend: 'Recomendado',
|
||||
@@ -475,6 +480,7 @@ const message = {
|
||||
memoPlaceholder: 'Haz clic en el botón de editar para habilitar la edición.',
|
||||
carouselSetting: 'Configuración del carrusel',
|
||||
tooltipSensitiveInfo: 'Mostrar/Ocultar información sensible',
|
||||
alias: 'Alias',
|
||||
},
|
||||
tabs: {
|
||||
more: 'Más',
|
||||
@@ -524,7 +530,6 @@ const message = {
|
||||
changePasswordHelper:
|
||||
'La base de datos ha sido asociada a una aplicación. Cambiar la contraseña también cambiará la contraseña de la base de datos en la aplicación. El cambio se aplica después de reiniciar la aplicación.',
|
||||
recoverTimeoutHelper: '-1 significa sin límite de tiempo de espera',
|
||||
|
||||
confChange: 'Configuración',
|
||||
confNotFound:
|
||||
'No se pudo encontrar el archivo de configuración. Por favor actualice la aplicación a la última versión en la tienda y vuelva a intentarlo.',
|
||||
@@ -561,7 +566,6 @@ const message = {
|
||||
hasCA: 'Posee certificado CA',
|
||||
skipVerify: 'Omitir la verificación de validez del certificado',
|
||||
initialDB: 'Base de Datos Inicial',
|
||||
|
||||
formatHelper:
|
||||
'El conjunto de caracteres actual de la base de datos es {0}, la inconsistencia de conjuntos puede causar errores al recuperar',
|
||||
dropHelper: 'Puede arrastrar y soltar el archivo aquí o',
|
||||
@@ -658,7 +662,6 @@ const message = {
|
||||
rdbHelper2: 'piezas de datos',
|
||||
rdbHelper3: 'Cumplir cualquiera de las condiciones activará la persistencia RDB.',
|
||||
rdbInfo: 'Asegúrese de que el valor de la regla esté entre 1 y 100000',
|
||||
|
||||
containerConn: 'Conexión del contenedor',
|
||||
copyConnURL: 'Copiar URL de conexión',
|
||||
connAddress: 'Dirección',
|
||||
@@ -684,33 +687,38 @@ const message = {
|
||||
copawType: 'CoPaw',
|
||||
appVersion: 'Versión de la app',
|
||||
webuiPort: 'Puerto WebUI',
|
||||
bridgePort: 'Puerto Bridge',
|
||||
allowedOrigins: 'Direcciones de acceso',
|
||||
allowedOriginsHelper:
|
||||
'Introduce una dirección de acceso HTTPS completa por línea, por ejemplo https://192.168.1.2:18789. Si no hay una dirección predeterminada configurada, rellénala manualmente.',
|
||||
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Introduce al menos una dirección de acceso',
|
||||
allowedOriginsInvalid: 'Usa el formato http(s)://host-o-ip:puerto',
|
||||
allowedOriginsHttpsOnly: 'Las direcciones de acceso deben comenzar con https://',
|
||||
provider: 'Proveedor de modelos',
|
||||
apiKey: 'Clave API',
|
||||
baseUrl: 'URL base',
|
||||
token: 'Token',
|
||||
manualModel: 'Entrada manual de modelo',
|
||||
verified: 'Verificado',
|
||||
verifySkipped: 'Omitido',
|
||||
verifySkipped: 'Sin verificacion',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Zona horaria',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
feishu: 'Feishu',
|
||||
pluginNotInstalled: 'El plugin no está instalado. Instálalo primero.',
|
||||
dmPolicy: 'DM Policy',
|
||||
groupPolicy: 'Group Policy',
|
||||
policyPairing: 'Pairing',
|
||||
policyOpen: 'Open',
|
||||
policyDisabled: 'Disabled',
|
||||
botName: 'Bot Name',
|
||||
botId: 'Bot ID',
|
||||
appId: 'App ID',
|
||||
appSecret: 'App Secret',
|
||||
saveAndRestartGateway: 'Save and restart gateway',
|
||||
@@ -722,9 +730,11 @@ const message = {
|
||||
pairingCodeRequired: 'Please enter pairing code',
|
||||
pairingApproveSuccess: 'Pairing approved successfully',
|
||||
customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible',
|
||||
feishuSaveSuccess: 'Guardado en Feishu',
|
||||
},
|
||||
model: {
|
||||
model: 'Modelo',
|
||||
localModel: 'Modelos locales',
|
||||
create: 'Agregar modelo',
|
||||
create_helper: 'Descargar "{0}"',
|
||||
ollama_doc: 'Puede visitar el sitio oficial de Ollama para buscar y encontrar más modelos.',
|
||||
@@ -733,6 +743,7 @@ const message = {
|
||||
'Al sincronizar modelos de Ollama, se detectaron los siguientes modelos que no existen. ¿Desea eliminarlos?',
|
||||
from_remote: 'Este modelo no fue descargado vía 1Panel, no hay registros de descarga relacionados.',
|
||||
no_logs: 'Los registros de descarga de este modelo han sido eliminados y no se pueden consultar.',
|
||||
vllmVersionHelper: 'Para servidores FusionXpark GB 10, seleccione la versión -cu130.',
|
||||
},
|
||||
proxy: {
|
||||
proxy: 'Mejoras de proxy de IA',
|
||||
@@ -770,7 +781,6 @@ const message = {
|
||||
memoryUsed: 'Memoria Utilizada',
|
||||
memoryTotal: 'Memoria Total',
|
||||
percent: 'Utilización',
|
||||
|
||||
base: 'Información Básica',
|
||||
driverVersion: 'Versión del Controlador',
|
||||
cudaVersion: 'Versión de CUDA',
|
||||
@@ -802,6 +812,7 @@ const message = {
|
||||
current: 'Monitoreo en Tiempo Real',
|
||||
history: 'Registros Históricos',
|
||||
notSupport: 'La versión actual o el controlador no admiten mostrar este parámetro.',
|
||||
processCount: 'Procesos',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Servidor MCP',
|
||||
@@ -842,6 +853,12 @@ const message = {
|
||||
},
|
||||
container: {
|
||||
createByCommand: 'Crear por comando',
|
||||
importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz',
|
||||
importComposeBackupTip: 'Importar archivo de copia de seguridad de compose, solo se admite .tar.gz',
|
||||
stopContainerBeforeBackup: 'Detener contenedor antes de la copia de seguridad',
|
||||
stopComposeBeforeBackup: 'Detener compose antes de la copia de seguridad',
|
||||
stopBeforeBackupHelper:
|
||||
'Al habilitarlo, se detendrá el contenedor o servicio compose antes del respaldo y se restaurará automáticamente al finalizar para garantizar la consistencia de datos.',
|
||||
commandInput: 'Introducir comando',
|
||||
commandRule: 'Por favor introduzca el comando correcto para crear el contenedor con docker run.',
|
||||
commandHelper: 'Este comando se ejecutará en el servidor para crear el contenedor. ¿Desea continuar?',
|
||||
@@ -918,7 +935,6 @@ const message = {
|
||||
'El motor de contenedores usa un valor base de 1024 para la proporción de CPU. Puede aumentarlo para darle al contenedor más tiempo de CPU.',
|
||||
inputIpv4: 'Ejemplo: 192.168.1.1',
|
||||
inputIpv6: 'Ejemplo: 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
|
||||
diskUsage: 'Uso del Disco',
|
||||
localVolume: 'Volumen de Almacenamiento Local',
|
||||
buildCache: 'Caché de Construcción',
|
||||
@@ -933,7 +949,6 @@ const message = {
|
||||
sizeRootFs: 'Tamaño Virtual',
|
||||
sizeRootFsHelper:
|
||||
'Tamaño total de todas las capas de imagen de las que depende el contenedor + capa del contenedor',
|
||||
|
||||
containerFromAppHelper:
|
||||
'Se detectó que este contenedor proviene de la tienda de aplicaciones. Las operaciones sobre la app pueden invalidar los cambios actuales.',
|
||||
containerFromAppHelper1:
|
||||
@@ -956,6 +971,7 @@ const message = {
|
||||
'Los contenedores que usan la misma imagen pueden actualizarse en lote después de seleccionarlos',
|
||||
targetImage: 'Imagen objetivo',
|
||||
imageLoadErr: 'No se detectó un nombre de imagen para el contenedor',
|
||||
imageUpdateTagEmpty: 'No se detectaron etiquetas de imagen actualizables',
|
||||
appHelper:
|
||||
'El contenedor proviene de la tienda de aplicaciones, y al actualizar podría hacer que el servicio no esté disponible.',
|
||||
resource: 'Recurso',
|
||||
@@ -963,6 +979,8 @@ const message = {
|
||||
forcePull: 'Siempre descargar imagen',
|
||||
forcePullHelper:
|
||||
'Esto ignorará las imágenes existentes en el servidor y descargará la más reciente desde el repositorio.',
|
||||
imageUpdateHelper:
|
||||
'Comprobar la misma etiqueta en el registro y, si hay cambios, descargar y actualizar la imagen local.',
|
||||
server: 'Servidor',
|
||||
serverExample: '80, 80-88, ip:80 o ip:80-88',
|
||||
containerExample: '80 o 80-88',
|
||||
@@ -1073,7 +1091,6 @@ const message = {
|
||||
'Si hay varios mirrors, deben estar en líneas separadas. Ejemplo:\nhttp://xxxxxx.m.daocloud.io\nhttps://xxxxxx.mirror.aliyuncs.com',
|
||||
registrieHelper:
|
||||
'Si existen varios repositorios privados, deben estar en líneas separadas. Ejemplo:\n172.16.10.111:8081\n172.16.10.112:8081',
|
||||
|
||||
compose: 'Compose | Composes',
|
||||
composeFile: 'Archivo de Orquestación',
|
||||
fromChangeHelper: 'Cambiar la fuente limpiará el contenido actualmente editado. ¿Desea continuar?',
|
||||
@@ -1100,9 +1117,10 @@ const message = {
|
||||
'Esto detendrá y eliminará todos los contenedores y redes bajo la composición {0}. ¿Desea continuar?',
|
||||
composeEnvHelper2:
|
||||
'Esta orquestación fue creada por la Tienda de Aplicaciones 1Panel. Modifique las variables de entorno en las aplicaciones instaladas.',
|
||||
|
||||
composeContentPlaceholder: 'Escriba o pegue el contenido de su archivo docker-compose aquí',
|
||||
setting: 'Configuración | Configuraciones',
|
||||
goSetting: 'Ir a editar',
|
||||
restartHelper: 'Los cambios de configuración requieren reiniciar el servicio Docker para surtir efecto',
|
||||
operatorStatusHelper: 'Esto "{0}" el servicio Docker. ¿Desea continuar?',
|
||||
dockerStatus: 'Servicio Docker',
|
||||
daemonJsonPathHelper:
|
||||
@@ -1153,6 +1171,7 @@ const message = {
|
||||
ifPause: '¿Pausar el contenedor durante la creación?',
|
||||
ifMakeImageWithContainer: '¿Crear nueva imagen a partir de este contenedor?',
|
||||
finishTime: 'Hora de la última detención',
|
||||
exportHelper: 'Exporta las imágenes seleccionadas en un solo archivo tar.',
|
||||
},
|
||||
cronjob: {
|
||||
importHelper:
|
||||
@@ -1208,7 +1227,8 @@ const message = {
|
||||
directory: 'Directorio de respaldo',
|
||||
sourceDir: 'Directorio de respaldo',
|
||||
snapshot: 'Instantánea del sistema',
|
||||
allOptionHelper: `El plan actual es respaldar todos los [{0}]. La descarga directa no está soportada por ahora. Puede consultar la lista de respaldos en el menú [{0}].`,
|
||||
allOptionHelper:
|
||||
'El plan actual es respaldar todos los [{0}]. La descarga directa no está soportada por ahora. Puede consultar la lista de respaldos en el menú [{0}].',
|
||||
exclusionRules: 'Reglas de exclusión',
|
||||
exclusionRulesHelper:
|
||||
'Seleccione o ingrese reglas de exclusión, presione Enter después de cada conjunto para continuar. Las reglas de exclusión se aplicarán a todas las operaciones de compresión en esta copia de seguridad',
|
||||
@@ -1270,7 +1290,6 @@ const message = {
|
||||
cutWebsiteLog: 'Rotación de logs del sitio',
|
||||
cutWebsiteLogHelper: 'Los archivos de log rotados se respaldarán en el directorio de respaldos de 1Panel.',
|
||||
syncIpGroup: 'Sincronizar grupos de IP de WAF',
|
||||
|
||||
requestExpirationTime: 'Tiempo de expiración de solicitud de subida (Horas)',
|
||||
unitHours: 'Unidad: Horas',
|
||||
alertTitle: 'Tarea programada - {0} 「{1}」 Alerta de fallo',
|
||||
@@ -1298,6 +1317,9 @@ const message = {
|
||||
syncHelper:
|
||||
'Se va a sincronizar la librería de scripts del sistema. Solo afecta a los scripts del sistema. ¿Desea continuar?',
|
||||
},
|
||||
logHelper1: '1. Registro del sistema de 1Panel',
|
||||
cleanLog: 'Limpieza automática de logs',
|
||||
cleanLogscope: 'Retención de logs',
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Filtro Global',
|
||||
@@ -1310,7 +1332,6 @@ const message = {
|
||||
defaultIOHelper: 'Opción de disco predeterminada mostrada en las interfaces de monitoreo y resumen',
|
||||
cleanMonitor: 'Limpiar Registros de Monitoreo',
|
||||
cleanHelper: 'Esta operación borrará todos los registros de monitoreo, incluidos los de GPU. ¿Continuar?',
|
||||
|
||||
avgLoad: 'Carga promedio',
|
||||
loadDetail: 'Detalle de carga',
|
||||
resourceUsage: 'Utilización',
|
||||
@@ -1440,6 +1461,7 @@ const message = {
|
||||
dnsCheck: 'Probar disponibilidad',
|
||||
dnsOK: '¡La configuración DNS es válida!',
|
||||
dnsTestFailed: 'La configuración DNS no es válida.',
|
||||
syncSiteHelper: '¿Sincronizar la hora del sistema con {0}?',
|
||||
},
|
||||
fail2ban: {
|
||||
sshPort: 'Puerto SSH',
|
||||
@@ -1784,6 +1806,7 @@ const message = {
|
||||
loginTime: 'Hora de inicio de sesión',
|
||||
loginIP: 'IP de inicio de sesión',
|
||||
stopSSHWarn: 'Indica si se debe desconectar esta conexión SSH',
|
||||
remote: 'SSH remoto',
|
||||
},
|
||||
setting: {
|
||||
panel: 'Panel',
|
||||
@@ -1819,6 +1842,10 @@ const message = {
|
||||
light: 'Claro',
|
||||
auto: 'Seguir sistema',
|
||||
language: 'Idioma',
|
||||
runtimeEnv: 'Entorno de ejecución',
|
||||
docSource: 'Fuente de documentación',
|
||||
withByRegion: 'Seguir región de operación (Predeterminado)',
|
||||
withByLang: 'Seguir idioma del sistema',
|
||||
region: 'Región de operación',
|
||||
cn: 'China continental',
|
||||
intl: 'Global',
|
||||
@@ -1827,6 +1854,10 @@ const message = {
|
||||
regionHelper2: 'Tienda de aplicaciones y biblioteca de scripts',
|
||||
regionHelper3: 'Manual de usuario y documentación relacionada',
|
||||
regionHelper4: 'Esto puede afectar futuras descargas y accesos. Proceda con precaución.',
|
||||
regionTip:
|
||||
'La región de operación afecta la fuente de actualización del sistema y la dirección de descarga de paquetes de instalación.',
|
||||
docSourceTip:
|
||||
'La fuente de documentación determina el idioma de los enlaces de documentación y registro de actualizaciones.',
|
||||
languageHelper:
|
||||
'Por defecto sigue el idioma del navegador. Este parámetro solo tiene efecto en el navegador actual',
|
||||
sessionTimeout: 'Tiempo de espera de sesión',
|
||||
@@ -1843,7 +1874,8 @@ const message = {
|
||||
proxyHelper2:
|
||||
'Actualización del sistema y recuperación de información de actualización (solo edición profesional)',
|
||||
proxyHelper3: 'Verificación y sincronización de licencias del sistema',
|
||||
proxyHelper4: 'La red de Docker se accederá a través de un servidor proxy (solo edición profesional)',
|
||||
proxyHelper4:
|
||||
'Las solicitudes de red, como la descarga de imágenes de Docker, se realizarán a través del servidor proxy (solo edición profesional)',
|
||||
proxyHelper5: 'Descarga y sincronización unificada para bibliotecas de scripts del sistema (Profesional)',
|
||||
proxyHelper6: 'Solicitud de certificado (Profesional)',
|
||||
proxyType: 'Tipo de proxy',
|
||||
@@ -2067,8 +2099,6 @@ const message = {
|
||||
bindDomain: 'Vincular dominio',
|
||||
unBindDomain: 'Desvincular dominio',
|
||||
panelSSL: 'SSL del panel',
|
||||
panelSSLHelper:
|
||||
'Tras la renovación automática del SSL del panel, deberás reiniciar manualmente el servicio 1Panel para que los cambios surtan efecto.',
|
||||
unBindDomainHelper:
|
||||
'La acción de desvincular un dominio puede provocar inseguridad en el sistema. ¿Deseas continuar?',
|
||||
bindDomainHelper: 'Tras vincular el dominio, solo ese dominio podrá acceder al servicio de 1Panel.',
|
||||
@@ -2220,10 +2250,9 @@ const message = {
|
||||
'La versión del nodo no coincide con la del nodo principal. Actualiza en la gestión de nodos antes de reintentar.',
|
||||
versionCompare:
|
||||
'Se detectó que el nodo {0} ya está en la última versión actualizable. Verifica la versión del nodo principal e inténtalo de nuevo.',
|
||||
|
||||
about: 'Acerca de',
|
||||
versionItem: 'Versión Actual',
|
||||
backupCopies: 'Número de Copias a Conservar',
|
||||
backupCopies: 'Copias de Respaldo',
|
||||
backupCopiesHelper:
|
||||
'Establezca el número de copias de respaldo de actualización para conservar para la reversión de versión. 0 significa conservar todas.',
|
||||
backupCopiesRule: 'Conserve al menos 3 registros de respaldo de actualización',
|
||||
@@ -2252,6 +2281,8 @@ const message = {
|
||||
compressPassword: 'Contraseña de compresión',
|
||||
backupRecoverMessage:
|
||||
'Introduce la contraseña de compresión o descompresión (déjalo en blanco para no establecerla)',
|
||||
componentSize: 'Tamaño del componente',
|
||||
recoverFailed: 'Error al restaurar la instantánea',
|
||||
},
|
||||
license: {
|
||||
community: 'OSS',
|
||||
@@ -2303,6 +2334,7 @@ const message = {
|
||||
app: 'Pro permite ver información de servicio, monitoreo, etc. vía app móvil.',
|
||||
cluster:
|
||||
'Actualizar a la Edición Profesional te permite gestionar clústeres maestro-esclavo de MySQL/Postgres/Redis.',
|
||||
offLine: 'Sin conexión',
|
||||
},
|
||||
clean: {
|
||||
scan: 'Iniciar escaneo',
|
||||
@@ -2330,7 +2362,6 @@ const message = {
|
||||
upgradeHelper: '(Mantenga el último respaldo para restaurar el sistema)',
|
||||
snapshot: 'Archivos temporales de instantáneas',
|
||||
rollback: 'Archivos de respaldo antes de recuperación',
|
||||
|
||||
backup: 'Copia de Seguridad del Sistema',
|
||||
backupHelper: 'Archivos de copia de seguridad no asociados con cuentas de copia de seguridad locales',
|
||||
unknownBackup: 'Copia de Seguridad del Sistema',
|
||||
@@ -2339,13 +2370,11 @@ const message = {
|
||||
unknownDatabase: 'Copia de Seguridad de Base de Datos no Asociada',
|
||||
unknownWebsite: 'Copia de Seguridad de Sitio Web no Asociada',
|
||||
unknownSnapshot: 'Copia de Seguridad de Instantánea no Asociada',
|
||||
|
||||
upload: 'Archivos temporales de carga',
|
||||
uploadHelper: 'Archivos temporales subidos en la lista de respaldos',
|
||||
download: 'Archivos temporales de descarga',
|
||||
downloadHelper: 'Archivos temporales descargados desde respaldos externos',
|
||||
directory: 'Directorio',
|
||||
|
||||
systemLog: 'Archivo de registro',
|
||||
systemLogHelper: 'Registros del sistema, registros de tareas, archivos de registro del sitio web',
|
||||
dockerLog: 'Archivos de registro de operaciones de contenedores',
|
||||
@@ -2353,12 +2382,10 @@ const message = {
|
||||
shell: 'Tareas programadas de script de shell',
|
||||
containerShell: 'Tareas programadas de script de shell ejecutadas dentro de contenedores',
|
||||
curl: 'Tareas programadas de CURL',
|
||||
|
||||
docker: 'Basura de contenedores',
|
||||
dockerHelper: 'Archivos como contenedores, imágenes, volúmenes, caché de construcción, etc.',
|
||||
volumes: 'Volúmenes',
|
||||
buildCache: 'Caché de build de contenedores',
|
||||
|
||||
appTmpDownload: 'Archivo de descarga temporal de la aplicación',
|
||||
unknownWebsiteLog: 'Archivo de respaldo de registro de sitio web no vinculado',
|
||||
},
|
||||
@@ -2446,6 +2473,12 @@ const message = {
|
||||
showLocal: 'Mostrar apps locales',
|
||||
reload: 'Recargar',
|
||||
upgradeWarn: 'Actualizar reemplazará docker-compose.yml. Si hay cambios, puede ver la comparación.',
|
||||
openclawHttpsUpgradeNoticeTitle:
|
||||
'Nota: Las siguientes instrucciones solo se aplican a los usuarios que actualizan OpenClaw desde versiones anteriores a 2026.3.13:',
|
||||
openclawHttpsUpgradeNoticeItem1:
|
||||
'Después de actualizar el agente desplegado, vaya a Configuración -> Ajustes -> Seguridad y agregue manualmente la dirección de acceso.',
|
||||
openclawHttpsUpgradeNoticeItem2:
|
||||
'La nueva versión exige HTTPS para acceder al agente. Si antes usaba un sitio con proxy inverso, cambie el destino del proxy a https://IP:Port.',
|
||||
newVersion: 'Nueva versión',
|
||||
oldVersion: 'Versión actual',
|
||||
composeDiff: 'Comparación de archivos',
|
||||
@@ -2583,7 +2616,8 @@ const message = {
|
||||
deleteAppHelper: 'Eliminar también aplicaciones y backups asociados',
|
||||
deleteBackupHelper: 'Eliminar también copias de seguridad del sitio',
|
||||
deleteDatabaseHelper: 'También eliminar la base de datos asociada al sitio web',
|
||||
deleteConfirmHelper: `Esta acción es irreversible. Escribe <span style="color:red"> "{0}" </span> para confirmar.`,
|
||||
deleteConfirmHelper:
|
||||
'Esta acción es irreversible. Escribe <span style="color:red"> "{0}" </span> para confirmar.',
|
||||
staticPath: 'El directorio principal correspondiente es ',
|
||||
limit: 'Esquema',
|
||||
blog: 'Foro/Blog',
|
||||
@@ -2797,7 +2831,6 @@ const message = {
|
||||
failTimeout: 'Tiempo de espera de fallo',
|
||||
failTimeoutHelper:
|
||||
'La duración de la ventana de tiempo para las comprobaciones de estado del servidor. Cuando el número acumulado de fallos alcanza el umbral dentro de este período, el servidor se eliminará temporalmente y se reintentará después de la misma duración. Por defecto 10 segundos',
|
||||
|
||||
staticChangePHPHelper: 'Actualmente es un sitio estático, puedes cambiarlo a PHP',
|
||||
proxyCache: 'Caché de proxy inverso',
|
||||
cacheLimit: 'Límite de espacio de caché',
|
||||
@@ -2822,7 +2855,6 @@ const message = {
|
||||
preflight: 'Respuesta rápida a solicitud de preflight',
|
||||
preflightHleper:
|
||||
'Cuando está habilitado, cuando el navegador envía una solicitud de preflight de origen cruzado (solicitud OPTIONS), el sistema devolverá automáticamente un código de estado 204 y establecerá los encabezados de respuesta de origen cruzado necesarios',
|
||||
|
||||
changeDatabase: 'Cambiar base de datos',
|
||||
changeDatabaseHelper1: 'La asociación se usa en backups/restauraciones.',
|
||||
changeDatabaseHelper2: 'Cambiar de DB invalida backups previos.',
|
||||
@@ -2853,7 +2885,6 @@ const message = {
|
||||
execUser: 'Usuario de ejecución',
|
||||
execDir: 'Directorio de ejecución',
|
||||
packagist: 'Mirror China completo',
|
||||
|
||||
batchOperate: 'Operación en Lote',
|
||||
batchOperateHelper: 'Lote {0} sitios web, ¿continuar operación?',
|
||||
stream: 'Proxy TCP/UDP',
|
||||
@@ -2862,8 +2893,8 @@ const message = {
|
||||
'Configura el número de puerto de escucha externo, los clientes accederán al servicio a través de este puerto, separado por comas, p. ej., 5222,5223',
|
||||
streamHelper: 'Reenvío de Puertos y Balanceo de Carga TCP/UDP',
|
||||
udp: 'Habilitar UDP',
|
||||
|
||||
syncHtmlHelper: 'Sincronizar con PHP y sitios web estáticos',
|
||||
wafValueHelper: 'Valor',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Soporte de etiquetas cortas',
|
||||
@@ -2892,6 +2923,7 @@ const message = {
|
||||
containerConfigHelper:
|
||||
'Las variables de entorno y demás se pueden modificar en Configuración - Configuración del Contenedor después de la creación',
|
||||
dateTimezoneHelper: 'Ejemplo: TZ=America/Mexico_City (ajusta según necesites)',
|
||||
extensionsHelper: 'Para varias extensiones, sepáralas con comas',
|
||||
},
|
||||
nginx: {
|
||||
serverNamesHashBucketSizeHelper: 'Tamaño de la tabla hash para nombres de servidor',
|
||||
@@ -3029,7 +3061,7 @@ const message = {
|
||||
unUsed: 'No usado',
|
||||
dockerRestart: 'Las operaciones del firewall requieren reiniciar el servicio de Docker',
|
||||
firewallHelper: 'Firewall del sistema {0}',
|
||||
firewallNotStart: `El firewall del sistema no está habilitado actualmente. Actívalo primero.`,
|
||||
firewallNotStart: 'El firewall del sistema no está habilitado actualmente. Actívalo primero.',
|
||||
restartFirewallHelper: 'Esta operación reiniciará el firewall actual. ¿Deseas continuar?',
|
||||
stopFirewallHelper: 'Esto hará que el servidor pierda protección de seguridad. ¿Deseas continuar?',
|
||||
startFirewallHelper:
|
||||
@@ -3038,7 +3070,7 @@ const message = {
|
||||
enableBanPing: 'Bloquear Ping',
|
||||
disableBanPing: 'Desbloquear Ping',
|
||||
noPingTitle: 'Deshabilitar ping',
|
||||
noPingHelper: `Esto deshabilitará el ping, y el servidor no responderá con eco ICMP. ¿Deseas continuar?`,
|
||||
noPingHelper: 'Esto deshabilitará el ping, y el servidor no responderá con eco ICMP. ¿Deseas continuar?',
|
||||
onPingHelper: 'Esto habilitará el ping, y los atacantes podrían descubrir tu servidor. ¿Deseas continuar?',
|
||||
changeStrategy: 'Cambiar la estrategia de {0}',
|
||||
changeStrategyIPHelper1:
|
||||
@@ -3055,7 +3087,6 @@ const message = {
|
||||
portHelper2: 'Rango de puertos, ej. 8080-8089',
|
||||
changeStrategyHelper:
|
||||
'Cambiar estrategia de {0} [{1}] a [{2}]. Después de configurarla, {0} tendrá acceso externo como {2}. ¿Deseas continuar?',
|
||||
|
||||
strategy: 'Estrategia',
|
||||
accept: 'Aceptar',
|
||||
drop: 'Rechazar',
|
||||
@@ -3082,7 +3113,6 @@ const message = {
|
||||
exportHelper: 'A punto de exportar {0} reglas de firewall. ¿Continuar?',
|
||||
importSuccess: 'Se importaron correctamente {0} reglas',
|
||||
importPartialSuccess: 'Importación completada: {0} correctas, {1} fallidas',
|
||||
|
||||
ipv4Limit: 'La operación actual solo admite direcciones IPv4',
|
||||
basicStatus: 'La cadena actual {0} no está vinculada, ¡vincule primero!',
|
||||
baseIptables: 'Servicio iptables',
|
||||
@@ -3115,17 +3145,22 @@ const message = {
|
||||
portHelper: '0 significa cualquier puerto',
|
||||
allPorts: 'Todos los Puertos',
|
||||
deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?',
|
||||
advancedControl: 'Control avanzado',
|
||||
advancedControlNotAvailable: 'Actualmente se usa el firewall {0}; las reglas avanzadas solo admiten iptables',
|
||||
destination: 'Destino',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
workDir: 'Directorio de trabajo',
|
||||
localHelper: 'Para problemas de instalación en entorno local y uso en entorno sin conexión, consulte ',
|
||||
versionHelper: 'Versión de PHP, ej. v8.0',
|
||||
buildHelper: `Si se seleccionan más extensiones, el uso de CPU será mayor durante el proceso de creación de la imagen. Evita seleccionar todas las extensiones.`,
|
||||
buildHelper:
|
||||
'Si se seleccionan más extensiones, el uso de CPU será mayor durante el proceso de creación de la imagen. Evita seleccionar todas las extensiones.',
|
||||
openrestyWarn: 'PHP necesita actualizarse a OpenResty versión 1.21.4.1 o superior para poder usarse',
|
||||
toupgrade: 'Actualizar',
|
||||
edit: 'Editar runtime',
|
||||
extendHelper: `Si las extensiones que necesitas no están en la lista, puedes introducir el nombre manualmente. Ejemplo: "sockets", luego selecciona la primera.`,
|
||||
extendHelper:
|
||||
'Si las extensiones que necesitas no están en la lista, puedes introducir el nombre manualmente. Ejemplo: "sockets", luego selecciona la primera.',
|
||||
rebuildHelper: 'Tras editar la extensión, debes reconstruir la aplicación PHP para que surta efecto',
|
||||
rebuild: 'Reconstruir aplicación PHP',
|
||||
source: 'Origen de extensiones PHP',
|
||||
@@ -3240,7 +3275,8 @@ const message = {
|
||||
list: 'Procesos en segundo plano',
|
||||
config: 'Configuración de Supervisor',
|
||||
primaryConfig: 'Ubicación del archivo de configuración principal',
|
||||
notSupportCtl: `No se detectó supervisorctl, ve a la página de librería de scripts para instalarlo manualmente`,
|
||||
notSupportCtl:
|
||||
'No se detectó supervisorctl, ve a la página de librería de scripts para instalarlo manualmente',
|
||||
user: 'Usuario',
|
||||
command: 'Comando',
|
||||
dir: 'Directorio',
|
||||
@@ -3249,7 +3285,7 @@ const message = {
|
||||
'Esto modificará el valor de "files" en la sección "[include]" del archivo de configuración principal. El directorio de otros archivos de configuración será: "{directorio de instalación de 1Panel}/1panel/tools/supervisord/supervisor.d/".',
|
||||
operatorHelper: 'La operación {1} se ejecutará en {0}, ¿quieres continuar?',
|
||||
uptime: 'Tiempo en ejecución',
|
||||
notStartWarn: `Supervisor no está iniciado. Inícialo primero.`,
|
||||
notStartWarn: 'Supervisor no está iniciado. Inícialo primero.',
|
||||
serviceName: 'Nombre del servicio',
|
||||
initHelper:
|
||||
'El servicio Supervisor está detectado pero no inicializado. Haz clic en el botón de inicialización en la barra de estado superior para configurarlo.',
|
||||
@@ -3570,6 +3606,7 @@ const message = {
|
||||
importRuleHelper: 'Una regla por línea',
|
||||
},
|
||||
monitor: {
|
||||
'360': '360 Search',
|
||||
name: 'Monitorización Web',
|
||||
pv: 'Page Views (PV)',
|
||||
uv: 'Unique Visitors (UV)',
|
||||
@@ -3637,7 +3674,6 @@ const message = {
|
||||
statusErr: 'Formato de código de estado inválido',
|
||||
shenma: 'Shenma',
|
||||
duckduckgo: 'DuckDuckGo',
|
||||
'360': '360 Search',
|
||||
excludeUri: 'Excluir URIs',
|
||||
top100Helper: 'Mostrar top 100',
|
||||
logSaveDay: 'Retención de logs (días)',
|
||||
@@ -3785,7 +3821,6 @@ const message = {
|
||||
alertTitle3: 'Configuración de Alertas',
|
||||
alertContent3:
|
||||
'Permite configurar números de teléfono, frecuencia y horarios de notificación para alertas más personalizadas.',
|
||||
|
||||
nodeDashTitle1: 'Gestión de Aplicaciones',
|
||||
nodeDashContent1:
|
||||
'Gestión unificada de aplicaciones multi-nodo, admite monitoreo de estado, inicio/parada rápida, conexión de terminal y copia de seguridad',
|
||||
@@ -3798,7 +3833,6 @@ const message = {
|
||||
nodeDashTitle4: 'Gestión de Tareas Programadas',
|
||||
nodeDashContent4:
|
||||
'Gestión unificada de tareas programadas multi-nodo, admite monitoreo de estado, inicio/parada rápida y ejecución manual por activación',
|
||||
|
||||
nodeTitle1: 'Adición de Nodo en un Clic',
|
||||
nodeContent1: 'Integra rápidamente múltiples nodos de servidor',
|
||||
nodeTitle2: 'Actualización en Lote',
|
||||
@@ -3815,11 +3849,9 @@ const message = {
|
||||
fileExchangeTitle3: 'Intercomunicación Multi-Nodo',
|
||||
fileExchangeContent3:
|
||||
'Permite transferir fácilmente archivos entre diferentes nodos y gestionar múltiples servidores de forma flexible.',
|
||||
|
||||
nodeAppTitle1: 'Gestión de Actualización de Aplicaciones',
|
||||
nodeAppContent1:
|
||||
'Monitoreo unificado de actualizaciones de aplicaciones multi-nodo, admite actualización con un clic',
|
||||
|
||||
appTitle1: 'Gestión Flexible del Panel',
|
||||
appContent1: 'Gestiona tu servidor 1Panel en cualquier momento y lugar.',
|
||||
appTitle2: 'Información Completa de Servicios',
|
||||
@@ -3843,7 +3875,7 @@ const message = {
|
||||
masterBackup: 'Respaldo del Nodo Principal',
|
||||
backupNode: 'Nodo de Respaldo',
|
||||
backupFrequency: 'Frecuencia de Respaldo (horas)',
|
||||
backupCopies: 'Copias de Retención de Respaldo',
|
||||
backupCopies: 'Copias de Respaldo',
|
||||
noBackupNode: 'Actualmente no hay nodo de respaldo configurado. Selecciona uno y vuelve a intentarlo.',
|
||||
masterBackupAlert:
|
||||
'No se ha configurado un respaldo del nodo principal. Para garantizar la seguridad de los datos, configura un nodo de respaldo lo antes posible y así facilitar el cambio manual en caso de fallo.',
|
||||
@@ -3936,9 +3968,13 @@ const message = {
|
||||
offline: 'Modo Offline',
|
||||
freeCount: 'Cuota gratuita [{0}]',
|
||||
offlineHelper: 'Usado cuando el nodo está en un entorno sin conexión',
|
||||
|
||||
appUpgrade: 'Actualización de la aplicación',
|
||||
appUpgradeHelper: 'Hay {0} aplicaciones que necesitan ser actualizadas',
|
||||
addPanel: 'Añadir panel',
|
||||
addPanelHelper: 'Tras añadir el panel, podrás acceder rápidamente en [Resumen - Paneles].',
|
||||
panel: 'Panel 1Panel',
|
||||
others: 'Otros paneles',
|
||||
operatePanelHelper: 'Los siguientes paneles ejecutarán {0}. ¿Continuar?',
|
||||
},
|
||||
customApp: {
|
||||
name: 'Repositorio de Apps Personalizadas',
|
||||
|
||||
+214
-124
File diff suppressed because it is too large
Load Diff
+160
-88
@@ -84,6 +84,10 @@ const message = {
|
||||
hide: '숨기기',
|
||||
visit: '방문',
|
||||
migrate: '마이그레이션',
|
||||
prev: '이전',
|
||||
next: '다음',
|
||||
setDefault: '기본값 복원',
|
||||
bind: '바인딩',
|
||||
},
|
||||
operate: {
|
||||
start: '시작',
|
||||
@@ -133,6 +137,8 @@ const message = {
|
||||
manageGroup: '그룹 관리',
|
||||
backToList: '목록으로 돌아가기',
|
||||
keepEdit: '계속 편집',
|
||||
default: '기본값',
|
||||
noRefresh: '자동 새로고침 안 함',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: '시스템 업그레이드 중입니다. 잠시만 기다려 주십시오...',
|
||||
@@ -142,14 +148,14 @@ const message = {
|
||||
},
|
||||
msg: {
|
||||
noneData: '데이터가 없습니다',
|
||||
delete: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
|
||||
clean: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
|
||||
delete: '이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
|
||||
clean: '이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
|
||||
closeDrawerHelper: '시스템에서 변경 사항을 저장하지 않을 수 있습니다. 계속하시겠습니까?',
|
||||
deleteSuccess: '삭제 완료',
|
||||
loginSuccess: '로그인 성공',
|
||||
operationSuccess: '작업 완료',
|
||||
copySuccess: '복사 완료',
|
||||
notSupportOperation: `이 작업은 지원되지 않습니다`,
|
||||
notSupportOperation: '이 작업은 지원되지 않습니다',
|
||||
requestTimeout: '요청이 시간 초과되었습니다. 나중에 다시 시도해 주십시오',
|
||||
infoTitle: '안내',
|
||||
notRecords: '현재 작업에 대한 실행 기록이 생성되지 않았습니다',
|
||||
@@ -160,13 +166,13 @@ const message = {
|
||||
operateConfirm: '작업을 확인하려면 수동으로 입력하십시오: ',
|
||||
inputOrSelect: '선택하거나 입력해 주십시오',
|
||||
copyFailed: '복사 실패',
|
||||
operatorHelper: `"{0}"에 대해 "{1}" 작업이 수행되며 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
|
||||
operatorHelper: '"{0}"에 대해 "{1}" 작업이 수행되며 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?',
|
||||
notFound: '죄송합니다. 요청하신 페이지를 찾을 수 없습니다.',
|
||||
unSupportType: `현재 파일 형식은 지원되지 않습니다.`,
|
||||
unSupportType: '현재 파일 형식은 지원되지 않습니다.',
|
||||
unSupportSize: '업로드된 파일이 {0}M을 초과했습니다. 확인해 주십시오!',
|
||||
fileExist: `현재 폴더에 이미 동일한 파일이 존재합니다. 중복 업로드는 지원되지 않습니다.`,
|
||||
fileExist: '현재 폴더에 이미 동일한 파일이 존재합니다. 중복 업로드는 지원되지 않습니다.',
|
||||
fileNameErr: '파일 이름은 1~256 자 사이의 영어, 중국어, 숫자, 또는 점(.-_)만 포함해야 합니다.',
|
||||
confirmNoNull: `{0} 값이 비어 있지 않은지 확인하십시오.`,
|
||||
confirmNoNull: '{0} 값이 비어 있지 않은지 확인하십시오.',
|
||||
errPort: '포트 정보가 올바르지 않습니다. 확인해 주십시오!',
|
||||
remove: '제거',
|
||||
backupHelper: '현재 작업은 {0}을(를) 백업합니다. 계속하시겠습니까?',
|
||||
@@ -180,6 +186,10 @@ const message = {
|
||||
importHelper:
|
||||
'충돌하거나 중복되는 데이터를 가져올 때 가져온 내용을 기준으로 원래 데이터베이스 데이터를 업데이트합니다.',
|
||||
errImport: '파일 내용이 비정상입니다:',
|
||||
backupSuccess: '백업 성공',
|
||||
restoreSuccess: '복원 성공',
|
||||
installSuccess: '설치 성공',
|
||||
uninstallSuccess: '제거 성공',
|
||||
},
|
||||
login: {
|
||||
username: '사용자 이름',
|
||||
@@ -278,7 +288,7 @@ const message = {
|
||||
commonError: '요청이 실패했습니다',
|
||||
},
|
||||
service: {
|
||||
serviceNotStarted: `{0} 서비스가 시작되지 않았습니다.`,
|
||||
serviceNotStarted: '{0} 서비스가 시작되지 않았습니다.',
|
||||
},
|
||||
status: {
|
||||
running: '실행 중',
|
||||
@@ -340,6 +350,7 @@ const message = {
|
||||
conflict: '충돌',
|
||||
duplicate: '중복',
|
||||
unexecuted: '실행되지 않음',
|
||||
unused: '미사용',
|
||||
},
|
||||
units: {
|
||||
second: '초 | 초 | 초',
|
||||
@@ -393,6 +404,7 @@ const message = {
|
||||
app: '애플리케이션',
|
||||
msgCenter: '작업',
|
||||
disk: '디스크',
|
||||
filter: '필터',
|
||||
},
|
||||
home: {
|
||||
recommend: '추천',
|
||||
@@ -425,12 +437,10 @@ const message = {
|
||||
runningTime: '가동 시간',
|
||||
mem: '시스템 메모리',
|
||||
swapMem: '스왑 파티션',
|
||||
|
||||
runSmoothly: '낮은 부하',
|
||||
runNormal: '보통 부하',
|
||||
runSlowly: '높은 부하',
|
||||
runJam: '심한 부하',
|
||||
|
||||
core: '물리적 코어',
|
||||
logicCore: '논리 코어',
|
||||
corePercent: '코어 사용률',
|
||||
@@ -456,7 +466,6 @@ const message = {
|
||||
available: '사용 가능',
|
||||
percent: '사용률',
|
||||
goInstall: 'Go 설치',
|
||||
|
||||
networkCard: '네트워크 카드',
|
||||
disk: '디스크',
|
||||
memo: '메모',
|
||||
@@ -518,7 +527,8 @@ const message = {
|
||||
portHelper: '이 포트는 컨테이너의 노출된 포트입니다. 수정을 별도로 저장하고 컨테이너를 재시작해야 합니다!',
|
||||
loadFromRemote: '동기화',
|
||||
userBind: '사용자 바인딩',
|
||||
pgBindHelper: `이 작업은 새 사용자를 생성하여 대상 데이터베이스에 바인딩하는 데 사용됩니다. 현재 데이터베이스에 이미 존재하는 사용자 선택은 지원되지 않습니다.`,
|
||||
pgBindHelper:
|
||||
'이 작업은 새 사용자를 생성하여 대상 데이터베이스에 바인딩하는 데 사용됩니다. 현재 데이터베이스에 이미 존재하는 사용자 선택은 지원되지 않습니다.',
|
||||
pgSuperUser: '슈퍼 사용자',
|
||||
loadFromRemoteHelper: '이 작업은 서버의 데이터베이스 정보를 1Panel로 동기화합니다. 계속 진행하시겠습니까?',
|
||||
passwordHelper: '확인 불가, 수정하려면 클릭',
|
||||
@@ -543,13 +553,11 @@ const message = {
|
||||
hasCA: 'CA 인증서 있음',
|
||||
skipVerify: '인증서 유효성 검사 무시',
|
||||
initialDB: '초기 데이터베이스',
|
||||
|
||||
formatHelper: '현재 데이터베이스 문자셋은 {0} 입니다. 문자셋 불일치로 인해 복구에 실패할 수 있습니다.',
|
||||
dropHelper: '여기에 업로드한 파일을 드래그 앤 드롭하거나',
|
||||
clickHelper: '클릭하여 업로드',
|
||||
supportUpType:
|
||||
'sql, sql.gz, tar.gz, .zip 파일 형식만 지원합니다. 가져오는 압축 파일에는 하나의 .sql 파일만 있거나 test.sql이 포함되어 있어야 합니다',
|
||||
|
||||
currentStatus: '현재 상태',
|
||||
baseParam: '기본 파라미터',
|
||||
performanceParam: '성능 파라미터',
|
||||
@@ -581,7 +589,6 @@ const message = {
|
||||
sortMergePassesHelper: '값이 너무 크면 "sort_buffer_size" 값을 증가시켜야 합니다.',
|
||||
tableLocksWaited: '테이블 잠금 대기',
|
||||
tableLocksWaitedHelper: '값이 너무 크면 데이터베이스 성능을 증가시키는 것을 고려해야 합니다.',
|
||||
|
||||
performanceTuning: '성능 튜닝',
|
||||
optimizationScheme: '최적화 방안',
|
||||
keyBufferSizeHelper: '인덱스용 버퍼 크기',
|
||||
@@ -599,14 +606,11 @@ const message = {
|
||||
tableOpenCacheHelper: '테이블 캐시',
|
||||
maxConnectionsHelper: '최대 연결 수',
|
||||
restart: '재시작',
|
||||
|
||||
slowLog: '느린 로그',
|
||||
noData: '아직 느린 로그가 없습니다.',
|
||||
|
||||
isOn: '켜짐',
|
||||
longQueryTime: '임계값(s)',
|
||||
thresholdRangeHelper: '올바른 임계값을 입력하십시오 (1 - 600).',
|
||||
|
||||
timeout: '타임아웃(s)',
|
||||
timeoutHelper: '유휴 연결의 타임아웃 기간. 0은 연결이 지속적으로 유지됨을 의미합니다.',
|
||||
maxclients: '최대 클라이언트',
|
||||
@@ -629,24 +633,21 @@ const message = {
|
||||
keyspaceMisses: '데이터베이스 키를 찾지 못한 횟수.',
|
||||
hit: '데이터베이스 키 발견 비율.',
|
||||
latestForkUsec: '마지막 fork() 작업에 소요된 마이크로초 수.',
|
||||
redisCliHelper: `"redis-cli" 서비스가 감지되지 않았습니다. 서비스를 먼저 활성화하십시오.`,
|
||||
redisCliHelper: '"redis-cli" 서비스가 감지되지 않았습니다. 서비스를 먼저 활성화하십시오.',
|
||||
redisQuickCmd: 'Redis 빠른 명령',
|
||||
recoverHelper: '이 작업은 데이터를 [{0}]으로 덮어씁니다. 계속하시겠습니까?',
|
||||
submitIt: '데이터 덮어쓰기',
|
||||
|
||||
baseConf: '기본 설정',
|
||||
allConf: '모든 설정',
|
||||
restartNow: '지금 재시작',
|
||||
restartNowHelper1:
|
||||
'구성 변경 사항이 적용되려면 시스템을 재시작해야 합니다. 데이터가 지속되어야 하는 경우 먼저 저장 작업을 수행하십시오.',
|
||||
restartNowHelper: '이 작업은 시스템이 재시작된 후에만 적용됩니다.',
|
||||
|
||||
persistence: '지속성',
|
||||
rdbHelper1: '초 단위, 삽입',
|
||||
rdbHelper2: '데이터 항목 수',
|
||||
rdbHelper3: '조건을 충족하면 RDB 지속성이 트리거됩니다.',
|
||||
rdbInfo: '규칙 목록의 값이 1에서 100000 사이여야 합니다.',
|
||||
|
||||
containerConn: '컨테이너 연결',
|
||||
copyConnURL: '연결 URL 복사',
|
||||
connAddress: '주소',
|
||||
@@ -671,33 +672,38 @@ const message = {
|
||||
copawType: 'CoPaw',
|
||||
appVersion: '앱 버전',
|
||||
webuiPort: 'WebUI 포트',
|
||||
bridgePort: 'Bridge 포트',
|
||||
allowedOrigins: '접속 주소',
|
||||
allowedOriginsHelper:
|
||||
'한 줄에 하나의 전체 HTTPS 접속 주소를 입력하세요. 예: https://192.168.1.2:18789. 기본 접속 주소가 설정되지 않은 경우 수동으로 입력하세요.',
|
||||
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
|
||||
allowedOriginsRequired: '접속 주소를 하나 이상 입력하세요',
|
||||
allowedOriginsInvalid: 'http(s)://host-or-ip:port 형식으로 입력하세요',
|
||||
allowedOriginsHttpsOnly: '접속 주소는 https:// 로 시작해야 합니다',
|
||||
provider: '모델 제공자',
|
||||
apiKey: 'API 키',
|
||||
baseUrl: '기본 URL',
|
||||
token: '토큰',
|
||||
manualModel: '수동 입력',
|
||||
verified: '검증됨',
|
||||
verifySkipped: '건너뜀',
|
||||
verifySkipped: '검증 안 함',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: '시간대',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
feishu: 'Feishu',
|
||||
pluginNotInstalled: '플러그인이 설치되지 않았습니다. 먼저 설치해 주세요.',
|
||||
dmPolicy: 'DM Policy',
|
||||
groupPolicy: 'Group Policy',
|
||||
policyPairing: 'Pairing',
|
||||
policyOpen: 'Open',
|
||||
policyDisabled: 'Disabled',
|
||||
botName: 'Bot Name',
|
||||
botId: 'Bot ID',
|
||||
appId: 'App ID',
|
||||
appSecret: 'App Secret',
|
||||
saveAndRestartGateway: 'Save and restart gateway',
|
||||
@@ -709,9 +715,11 @@ const message = {
|
||||
pairingCodeRequired: 'Please enter pairing code',
|
||||
pairingApproveSuccess: 'Pairing approved successfully',
|
||||
customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다',
|
||||
feishuSaveSuccess: 'Feishu에 저장됨',
|
||||
},
|
||||
model: {
|
||||
model: '모델',
|
||||
localModel: '로컬 모델',
|
||||
create: '모델 추가',
|
||||
create_helper: '가져오기 "{0}"',
|
||||
ollama_doc: 'Ollama 공식 웹사이트를 방문하여 더 많은 모델을 검색하고 찾을 수 있습니다.',
|
||||
@@ -719,6 +727,7 @@ const message = {
|
||||
ollama_sync: 'Ollama 모델 동기화 중 다음 모델이 존재하지 않음을 발견했습니다. 삭제하시겠습니까?',
|
||||
from_remote: '이 모델은 1Panel을 통해 다운로드되지 않았으며 관련 풀 로그가 없습니다.',
|
||||
no_logs: '이 모델의 풀 로그가 삭제되어 관련 로그를 볼 수 없습니다.',
|
||||
vllmVersionHelper: 'FusionXpark GB 10 서버는 -cu130 버전을 선택하세요.',
|
||||
},
|
||||
proxy: {
|
||||
proxy: 'AI 프록시 강화',
|
||||
@@ -755,7 +764,6 @@ const message = {
|
||||
memoryUsed: '사용된 메모리',
|
||||
memoryTotal: '전체 메모리',
|
||||
percent: '사용률',
|
||||
|
||||
base: '기본 정보',
|
||||
driverVersion: '드라이버 버전',
|
||||
cudaVersion: 'CUDA 버전',
|
||||
@@ -785,6 +793,7 @@ const message = {
|
||||
current: '실시간 모니터링',
|
||||
history: '기록',
|
||||
notSupport: '현재 버전 또는 드라이버는 이 매개변수 표시를 지원하지 않습니다.',
|
||||
processCount: '프로세스 수',
|
||||
},
|
||||
mcp: {
|
||||
server: 'MCP サーバー',
|
||||
@@ -822,6 +831,12 @@ const message = {
|
||||
},
|
||||
container: {
|
||||
createByCommand: '명령으로 생성',
|
||||
importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
|
||||
importComposeBackupTip: 'Compose 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
|
||||
stopContainerBeforeBackup: '백업 전에 컨테이너 중지',
|
||||
stopComposeBeforeBackup: '백업 전에 Compose 중지',
|
||||
stopBeforeBackupHelper:
|
||||
'활성화하면 백업 전에 컨테이너 또는 Compose 서비스를 중지하고, 완료 후 자동으로 복구하여 데이터 일관성을 보장합니다.',
|
||||
commandInput: '명령 입력',
|
||||
commandRule: '컨테이너 생성을 위한 올바른 docker run 명령을 입력하세요.',
|
||||
commandHelper: '이 명령은 서버에서 실행되어 컨테이너를 생성합니다. 계속하시겠습니까?',
|
||||
@@ -892,7 +907,6 @@ const message = {
|
||||
'컨테이너 엔진은 기본값으로 1024를 사용합니다. 이를 늘리면 컨테이너에 더 많은 CPU 시간을 할당할 수 있습니다.',
|
||||
inputIpv4: '예시: 192.168.1.1',
|
||||
inputIpv6: '예시: 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
|
||||
diskUsage: '디스크 사용량',
|
||||
localVolume: '로컬 스토리지 볼륨',
|
||||
buildCache: '빌드 캐시',
|
||||
@@ -906,7 +920,6 @@ const message = {
|
||||
sizeRwHelper: '컨테이너에 고유한 쓰기 가능 레이어 크기',
|
||||
sizeRootFs: '가상 크기',
|
||||
sizeRootFsHelper: '컨테이너가 의존하는 모든 이미지 레이어 + 컨테이너 레이어의 총 크기',
|
||||
|
||||
containerFromAppHelper:
|
||||
'이 컨테이너가 앱 스토어에서 왔음을 감지했습니다. 앱 작업으로 현재 편집이 무효화될 수 있습니다.',
|
||||
containerFromAppHelper1:
|
||||
@@ -928,10 +941,12 @@ const message = {
|
||||
sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 후 일괄 업그레이드 가능',
|
||||
targetImage: '대상 이미지',
|
||||
imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.',
|
||||
imageUpdateTagEmpty: '업데이트 가능한 이미지 태그를 찾지 못했습니다.',
|
||||
appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.',
|
||||
input: '수동 입력',
|
||||
forcePull: '이미지 강제 풀',
|
||||
forcePullHelper: '이 작업은 서버에 있는 기존 이미지를 무시하고 레지스트리에서 최신 이미지를 강제로 가져옵니다.',
|
||||
imageUpdateHelper: '레지스트리의 동일 태그를 확인하고, 변경이 있으면 가져와 로컬 이미지를 업데이트합니다.',
|
||||
server: '호스트',
|
||||
serverExample: '80, 80-88, ip:80 또는 ip:80-88',
|
||||
containerExample: '80 또는 80-88',
|
||||
@@ -1001,9 +1016,9 @@ const message = {
|
||||
tagHelper: '한 줄에 하나씩. 예시:\nkey1=value1\nkey2=value2',
|
||||
imageNameHelper: '이미지 이름과 태그, 예시: nginx:latest',
|
||||
cleanBuildCache: '빌드 캐시 정리',
|
||||
delBuildCacheHelper: `이 작업은 빌드 중 생성된 모든 캐시된 아티팩트를 삭제하며 되돌릴 수 없습니다. 계속 하시겠습니까?`,
|
||||
delBuildCacheHelper:
|
||||
'이 작업은 빌드 중 생성된 모든 캐시된 아티팩트를 삭제하며 되돌릴 수 없습니다. 계속 하시겠습니까?',
|
||||
urlWarning: 'URL 접두어에 http:// 또는 https://를 포함할 필요는 없습니다. 수정해 주세요.',
|
||||
|
||||
network: '네트워크 | 네트워크들',
|
||||
networkHelper: '이로 인해 일부 애플리케이션과 실행 환경이 제대로 작동하지 않을 수 있습니다. 계속 하시겠습니까?',
|
||||
networkName: '이름',
|
||||
@@ -1015,7 +1030,6 @@ const message = {
|
||||
scope: 'IP 범위',
|
||||
gateway: '게이트웨이',
|
||||
auxAddress: '제외 IP',
|
||||
|
||||
volume: '볼륨 | 볼륨들',
|
||||
volumeDir: '볼륨 디렉터리',
|
||||
nfsEnable: 'NFS 스토리지 사용',
|
||||
@@ -1023,7 +1037,6 @@ const message = {
|
||||
mountpoint: '마운트 지점',
|
||||
mountpointNFSHelper: '예: /nfs, /nfs-share',
|
||||
options: '옵션',
|
||||
|
||||
repo: '레지스트리',
|
||||
httpRepoHelper: 'HTTP 타입 저장소 작업 시 Docker 서비스 재시작이 필요합니다.',
|
||||
httpRepo: 'HTTP 프로토콜을 선택하면 Docker 서비스를 재시작하여 불안정한 레지스트리에 추가해야 합니다.',
|
||||
@@ -1038,7 +1051,6 @@ const message = {
|
||||
'거울이 여러 개 있을 경우 각 줄에 하나씩 표시해야 합니다. 예시:\nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
|
||||
registrieHelper:
|
||||
'개인 레지스트리가 여러 개 있을 경우 각 줄에 하나씩 표시해야 합니다. 예시:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: '컴포즈 | 컴포즈들',
|
||||
composeFile: '컴포즈 파일',
|
||||
fromChangeHelper: '소스를 변경하면 현재 편집한 내용이 삭제됩니다. 계속 하시겠습니까?',
|
||||
@@ -1063,8 +1075,9 @@ const message = {
|
||||
'이 작업은 {0} 컴포즈 아래의 모든 컨테이너와 네트워크를 중지하고 제거합니다. 계속 하시겠습니까?',
|
||||
composeEnvHelper2:
|
||||
'이 오케스트레이션은 1Panel 앱 스토어에서 생성되었습니다. 설치된 애플리케이션에서 환경 변수를 수정하세요.',
|
||||
|
||||
composeContentPlaceholder: '여기에 docker-compose 파일 내용을 작성하거나 붙여넣으세요',
|
||||
setting: '설정 | 설정들',
|
||||
restartHelper: '구성 변경 사항을 적용하려면 Docker 서비스를 재시작해야 합니다',
|
||||
operatorStatusHelper: '이 작업은 Docker 서비스를 "{0}" 합니다. 계속 하시겠습니까?',
|
||||
dockerStatus: 'Docker 서비스',
|
||||
daemonJsonPathHelper: '구성 경로가 docker.service 에 지정된 경로와 동일한지 확인하십시오.',
|
||||
@@ -1091,7 +1104,7 @@ const message = {
|
||||
iptablesHelper1: 'Docker 에 대한 iptables 규칙을 자동으로 구성합니다.',
|
||||
iptablesHelper2: 'iptables 를 비활성화하면 컨테이너가 외부 네트워크와 통신할 수 없습니다.',
|
||||
daemonJsonPath: '구성 경로',
|
||||
serviceUnavailable: `현재 Docker 서비스가 시작되지 않았습니다.`,
|
||||
serviceUnavailable: '현재 Docker 서비스가 시작되지 않았습니다.',
|
||||
startIn: '시작하려면',
|
||||
sockPath: '유닉스 도메인 소켓',
|
||||
sockPathHelper: 'Docker 데몬과 클라이언트 간의 통신 채널입니다.',
|
||||
@@ -1101,12 +1114,10 @@ const message = {
|
||||
related: '관련',
|
||||
includeAppstore: '앱 스토어에서 컨테이너 표시',
|
||||
excludeAppstore: '앱스토어 컨테이너 숨기기',
|
||||
|
||||
cleanDockerDiskZone: 'Docker 에서 사용하는 디스크 공간 정리',
|
||||
cleanImagesHelper: '(사용되지 않는 모든 이미지를 정리합니다.)',
|
||||
cleanContainersHelper: '(정지된 모든 컨테이너를 정리합니다.)',
|
||||
cleanVolumesHelper: '(사용되지 않는 모든 로컬 볼륨을 정리합니다.)',
|
||||
|
||||
makeImage: '이미지 생성',
|
||||
newImageName: '새 이미지 이름',
|
||||
commitMessage: '커밋 메시지',
|
||||
@@ -1114,6 +1125,11 @@ const message = {
|
||||
ifPause: '생성 중 컨테이너 일시 정지',
|
||||
ifMakeImageWithContainer: '이 컨테이너에서 새 이미지를 생성하시겠습니까?',
|
||||
finishTime: '마지막 중지 시간',
|
||||
workingDir: '작업 디렉토리',
|
||||
resource: '리소스',
|
||||
macAddr: 'MAC 주소',
|
||||
exportHelper: '선택한 이미지를 하나의 tar 파일로 내보냅니다.',
|
||||
goSetting: '편집으로 이동',
|
||||
},
|
||||
cronjob: {
|
||||
importHelper:
|
||||
@@ -1163,7 +1179,8 @@ const message = {
|
||||
directory: '백업 디렉토리',
|
||||
sourceDir: '백업 디렉토리',
|
||||
snapshot: '시스템 스냅샷',
|
||||
allOptionHelper: `현재 작업 계획은 모든 [{0}]을 백업하는 것입니다. 현재 직접 다운로드는 지원되지 않습니다. [{0}] 메뉴에서 백업 목록을 확인하실 수 있습니다.`,
|
||||
allOptionHelper:
|
||||
'현재 작업 계획은 모든 [{0}]을 백업하는 것입니다. 현재 직접 다운로드는 지원되지 않습니다. [{0}] 메뉴에서 백업 목록을 확인하실 수 있습니다.',
|
||||
exclusionRules: '배제 규칙',
|
||||
exclusionRulesHelper:
|
||||
'제외 규칙을 선택하거나 입력하고, 각 세트 입력 후 Enter 키를 눌러 계속합니다. 제외 규칙은 이 백업의 모든 압축 작업에 적용됩니다',
|
||||
@@ -1222,7 +1239,6 @@ const message = {
|
||||
cutWebsiteLog: '웹사이트 로그 회전',
|
||||
cutWebsiteLogHelper: '회전된 로그 파일은 1Panel 의 백업 디렉토리로 백업됩니다.',
|
||||
syncIpGroup: 'WAF IP 그룹 동기화',
|
||||
|
||||
requestExpirationTime: '업로드 요청 만료 시간(시간)',
|
||||
unitHours: '단위: 시간',
|
||||
alertTitle: '예정된 작업 - {0} 「{1}」 작업 실패 경고',
|
||||
@@ -1247,6 +1263,13 @@ const message = {
|
||||
syncHelper:
|
||||
'시스템 스크립트 라이브러리를 동기화합니다. 이 작업은 시스템 스크립트에만 적용됩니다. 계속하시겠습니까?',
|
||||
},
|
||||
nextTime: '다음 5회 실행',
|
||||
logHelper1: '1. 1Panel 시스템 로그',
|
||||
backupContent: '백업 내용',
|
||||
cronSpecRule2: '실행 주기 형식이 올바르지 않습니다. 확인 후 다시 시도하세요.',
|
||||
executor: '실행자',
|
||||
cleanLog: '로그 자동 정리',
|
||||
cleanLogscope: '로그 보관 기간',
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: '전역 필터',
|
||||
@@ -1258,7 +1281,6 @@ const message = {
|
||||
defaultIOHelper: '모니터링 및 개요 인터페이스에 표시되는 기본 디스크 옵션',
|
||||
cleanMonitor: '모니터링 기록 지우기',
|
||||
cleanHelper: '이 작업은 GPU를 포함한 모든 모니터링 기록을 지웁니다. 계속하시겠습니까?',
|
||||
|
||||
avgLoad: '평균 부하',
|
||||
loadDetail: '부하 세부사항',
|
||||
resourceUsage: '자원 사용률',
|
||||
@@ -1369,20 +1391,25 @@ const message = {
|
||||
timeZone: '시간대',
|
||||
localTime: '서버 시간',
|
||||
timeZoneChangeHelper: '시스템 시간대를 변경하려면 서비스를 재시작해야 합니다. 계속하시겠습니까?',
|
||||
timeZoneHelper: `"timedatectl" 명령이 설치되지 않은 경우 시간대를 변경할 수 없습니다. 시스템은 시간대 변경에 이 명령을 사용합니다.`,
|
||||
timeZoneHelper:
|
||||
'"timedatectl" 명령이 설치되지 않은 경우 시간대를 변경할 수 없습니다. 시스템은 시간대 변경에 이 명령을 사용합니다.',
|
||||
timeZoneCN: '베이징',
|
||||
timeZoneAM: '로스앤젤레스',
|
||||
timeZoneNY: '뉴욕',
|
||||
ntpALi: '알리바바',
|
||||
ntpGoogle: '구글',
|
||||
syncSite: 'NTP 서버',
|
||||
hostnameHelper: `호스트 이름 수정은 "hostnamectl" 명령에 따라 달라집니다. 명령이 설치되지 않은 경우 수정이 실패할 수 있습니다.`,
|
||||
userHelper: `사용자 이름은 "whoami" 명령을 사용하여 검색됩니다. 명령이 설치되지 않은 경우 검색이 실패할 수 있습니다.`,
|
||||
passwordHelper: `비밀번호 수정은 "chpasswd" 명령에 따라 달라집니다. 명령이 설치되지 않은 경우 수정이 실패할 수 있습니다.`,
|
||||
hostnameHelper:
|
||||
'호스트 이름 수정은 "hostnamectl" 명령에 따라 달라집니다. 명령이 설치되지 않은 경우 수정이 실패할 수 있습니다.',
|
||||
userHelper:
|
||||
'사용자 이름은 "whoami" 명령을 사용하여 검색됩니다. 명령이 설치되지 않은 경우 검색이 실패할 수 있습니다.',
|
||||
passwordHelper:
|
||||
'비밀번호 수정은 "chpasswd" 명령에 따라 달라집니다. 명령이 설치되지 않은 경우 수정이 실패할 수 있습니다.',
|
||||
hostHelper: '제공된 내용에 빈 값이 포함되어 있습니다. 확인 후 수정하여 다시 시도해주세요!',
|
||||
dnsCheck: '가용성 테스트',
|
||||
dnsOK: 'DNS 구성 정보가 유효합니다!',
|
||||
dnsTestFailed: 'DNS 구성 정보가 유효하지 않습니다.',
|
||||
syncSiteHelper: '{0}을(를) 기준으로 시스템 시간을 동기화하시겠습니까?',
|
||||
},
|
||||
fail2ban: {
|
||||
sshPort: 'SSH 포트 청취',
|
||||
@@ -1498,6 +1525,9 @@ const message = {
|
||||
websiteLog: '웹사이트 로그',
|
||||
runLog: '실행 로그',
|
||||
errLog: '에러 로그',
|
||||
task: '작업 로그',
|
||||
taskName: '작업 이름',
|
||||
taskRunning: '실행 중',
|
||||
},
|
||||
file: {
|
||||
fileDirNum: '총 {0}개 디렉터리, {1}개 파일,',
|
||||
@@ -1584,7 +1614,7 @@ const message = {
|
||||
currentSelect: '현재 선택: ',
|
||||
unsupportedType: '지원되지 않는 파일 유형',
|
||||
deleteHelper: '다음 파일을 삭제하시겠습니까? 기본적으로 삭제 후 휴지통으로 이동합니다.',
|
||||
fileHelper: `참고:\n1. 검색 결과는 정렬할 수 없습니다.\n2. 폴더는 크기로 정렬할 수 없습니다.`,
|
||||
fileHelper: '참고:\n1. 검색 결과는 정렬할 수 없습니다.\n2. 폴더는 크기로 정렬할 수 없습니다.',
|
||||
forceDeleteHelper: '파일을 영구적으로 삭제합니다(휴지통으로 이동하지 않고 바로 삭제).',
|
||||
recycleBin: '휴지통',
|
||||
sourcePath: '원래 경로',
|
||||
@@ -1611,7 +1641,7 @@ const message = {
|
||||
'안전하지 않은 서버 연결을 허용하면 데이터 유출 또는 변조가 발생할 수 있습니다. 이 옵션은 다운로드 소스를 신뢰할 때만 사용하세요.',
|
||||
uploadOverLimit: '파일 수가 1000 개를 초과했습니다! 압축하여 업로드하세요.',
|
||||
clashDidNotSupport: '파일 이름에 .1panel_clash 를 포함할 수 없습니다.',
|
||||
clashDeleteAlert: `"휴지통" 폴더는 삭제할 수 없습니다.`,
|
||||
clashDeleteAlert: '"휴지통" 폴더는 삭제할 수 없습니다.',
|
||||
clashOpenAlert: '휴지통 디렉터리를 열려면 "휴지통" 버튼을 클릭하세요.',
|
||||
right: '앞으로',
|
||||
back: '뒤로',
|
||||
@@ -1629,7 +1659,7 @@ const message = {
|
||||
previewTruncated: '파일이 너무 커서 마지막 부분만 표시됩니다',
|
||||
previewEmpty: '파일이 비어 있거나 텍스트 파일이 아닙니다',
|
||||
previewLargeFile: '미리보기',
|
||||
panelInstallDir: `1Panel 설치 디렉터리는 삭제할 수 없습니다.`,
|
||||
panelInstallDir: '1Panel 설치 디렉터리는 삭제할 수 없습니다.',
|
||||
wgetTask: '다운로드 작업',
|
||||
existFileTitle: '동일한 이름의 파일 경고',
|
||||
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
|
||||
@@ -1721,6 +1751,7 @@ const message = {
|
||||
loginTime: '로그인 시간',
|
||||
loginIP: '로그인 IP',
|
||||
stopSSHWarn: '이 SSH 연결을 끊으시겠습니까?',
|
||||
remote: '원격 SSH',
|
||||
},
|
||||
setting: {
|
||||
panel: '패널',
|
||||
@@ -1755,6 +1786,10 @@ const message = {
|
||||
light: '라이트',
|
||||
auto: '시스템 따라가기',
|
||||
language: '언어',
|
||||
runtimeEnv: '실행 환경',
|
||||
docSource: '문서 출처',
|
||||
withByRegion: '운영 지역 따름(기본값)',
|
||||
withByLang: '시스템 언어 따름',
|
||||
region: '운영 지역',
|
||||
cn: '중국 본토',
|
||||
intl: '글로벌',
|
||||
@@ -1763,6 +1798,8 @@ const message = {
|
||||
regionHelper2: '앱 스토어 및 스크립트 라이브러리',
|
||||
regionHelper3: '사용자 매뉴얼 및 관련 문서',
|
||||
regionHelper4: '향후 다운로드 및 접근에 영향을 줄 수 있습니다. 주의하십시오.',
|
||||
regionTip: '운영 지역은 시스템 업데이트 소스와 설치 패키지 다운로드 주소에 영향을 미칩니다.',
|
||||
docSourceTip: '문서 출처는 문서와 업데이트 로그의 이동 언어를 결정합니다.',
|
||||
languageHelper: '기본적으로 브라우저 언어를 따릅니다. 이 설정은 현재 브라우저에서만 적용됩니다.',
|
||||
sessionTimeout: '세션 타임아웃',
|
||||
sessionTimeoutError: '최소 세션 타임아웃은 300초입니다.',
|
||||
@@ -1774,7 +1811,7 @@ const message = {
|
||||
proxyHelper: '프록시 서버를 설정한 후 다음 시나리오에서 적용됩니다:',
|
||||
proxyHelper1: '설치 패키지 다운로드 및 앱 스토어 동기화 (전문 버전에서만 제공)',
|
||||
proxyHelper2: '시스템 업데이트 및 업데이트 정보 가져오기 (전문 버전에서만 제공)',
|
||||
proxyHelper4: 'Docker 네트워크가 프록시 서버를 통해 액세스됩니다 (전문 버전에서만 제공)',
|
||||
proxyHelper4: 'Docker 이미지 pull 등 네트워크 요청은 프록시 서버를 통해 이루어집니다 (전문 버전에서만 제공)',
|
||||
proxyHelper3: '시스템 라이선스 인증 및 동기화',
|
||||
proxyHelper5: '시스템 유형 스크립트 라이브러리의 통합 다운로드 및 동기화 (프로페셔널 에디션 기능)',
|
||||
proxyHelper6: '인증서 신청 (프로 버전 기능)',
|
||||
@@ -1908,7 +1945,6 @@ const message = {
|
||||
accountKey: '계정 키',
|
||||
address: '주소',
|
||||
path: '경로',
|
||||
|
||||
safe: '보안',
|
||||
passkey: '패스키',
|
||||
passkeyManage: '관리',
|
||||
@@ -2011,7 +2047,6 @@ const message = {
|
||||
error416: '범위 불만족',
|
||||
error444: '연결 닫힘',
|
||||
error500: '서버 오류',
|
||||
|
||||
https: '패널에 HTTPS를 설정하면 액세스 보안이 향상됩니다',
|
||||
strictHelper: '비 HTTPS 트래픽은 패널에 연결할 수 없습니다',
|
||||
muxHelper:
|
||||
@@ -2026,7 +2061,6 @@ const message = {
|
||||
primaryKey: '개인 키',
|
||||
certificate: '인증서',
|
||||
backupJump: '현재 백업 목록에 없는 파일입니다. 파일 디렉토리에서 다운로드하여 백업에 가져오기를 시도하세요.',
|
||||
|
||||
snapshot: '스냅샷 | 스냅샷들',
|
||||
noAppData: '선택할 수 있는 시스템 앱이 없습니다',
|
||||
noBackupData: '선택할 수 있는 백업 데이터가 없습니다',
|
||||
@@ -2094,7 +2128,6 @@ const message = {
|
||||
rollback: '롤백',
|
||||
rollbackHelper:
|
||||
'이 복구를 롤백하면 해당 복구의 모든 파일이 대체되며 Docker 및 1Panel 서비스를 재시작해야 할 수 있습니다. 계속하시겠습니까?',
|
||||
|
||||
upgradeHelper: '업그레이드에는 1Panel 서비스를 재시작해야 합니다. 계속하시겠습니까?',
|
||||
rollbackLocalHelper:
|
||||
'마스터 노드는 직접 롤백을 지원하지 않습니다. 수동으로 [1pctl restore] 명령어를 실행하여 롤백하세요!',
|
||||
@@ -2105,10 +2138,9 @@ const message = {
|
||||
versionNotSame: '노드 버전이 메인 노드와 일치하지 않습니다. 노드 관리에서 업그레이드한 후 다시 시도해 주세요.',
|
||||
versionCompare:
|
||||
'노드 {0}이(가) 이미 업그레이드 가능한 최신 버전입니다. 마스터 노드 버전을 확인 후 다시 시도하세요!',
|
||||
|
||||
about: '정보',
|
||||
versionItem: '현재 버전',
|
||||
backupCopies: '보관할 백업 복사본 수',
|
||||
backupCopies: '백업 수',
|
||||
backupCopiesHelper:
|
||||
'버전 롤백을 위해 보관할 업그레이드 백업 복사본 수를 설정합니다. 0은 모두 보관을 의미합니다.',
|
||||
backupCopiesRule: '최소 3개의 업그레이드 백업 기록을 보관하세요',
|
||||
@@ -2123,7 +2155,6 @@ const message = {
|
||||
forum: '토론',
|
||||
doc2: '문서',
|
||||
currentVersion: '버전',
|
||||
|
||||
license: '라이선스',
|
||||
bindNode: '노드 바인딩',
|
||||
menuSetting: '메뉴 설정',
|
||||
@@ -2136,6 +2167,31 @@ const message = {
|
||||
recoverMessage: '페이지가 새로고침되어 메뉴 목록이 초기 상태로 복원됩니다. 계속하시겠습니까?',
|
||||
compressPassword: '압축 비밀번호',
|
||||
backupRecoverMessage: '압축 또는 압축 해제 비밀번호를 입력하세요 (설정하지 않으려면 비워 두세요)',
|
||||
componentSize: '컴포넌트 크기',
|
||||
scope: '범위',
|
||||
public: '공개',
|
||||
publicHelper: '공개 타입 백업 계정은 각 하위 노드로 동기화되어 함께 사용할 수 있습니다',
|
||||
private: '비공개',
|
||||
privateHelper: '비공개 타입 백업 계정은 현재 노드에서만 생성되며 현재 노드에서만 사용됩니다',
|
||||
UPYUN: 'UPYUN',
|
||||
ALIYUN: 'Aliyun Drive',
|
||||
ALIYUNHelper:
|
||||
'Aliyun Drive의 비클라이언트 다운로드 최대 용량은 100MB입니다. 초과 시 클라이언트로 다운로드하세요.',
|
||||
ALIYUNRecover:
|
||||
'Aliyun Drive의 비클라이언트 다운로드 최대 용량은 100MB입니다. 초과 시 클라이언트로 로컬 다운로드 후 스냅샷을 동기화해 복원하세요.',
|
||||
GoogleDrive: 'Google Drive',
|
||||
analysis: '분석',
|
||||
analysisHelper:
|
||||
'전체 토큰 내용을 붙여넣으면 필요한 항목을 자동으로 파싱합니다. 자세한 내용은 공식 문서를 참고하세요.',
|
||||
serviceName: '서비스 이름',
|
||||
operator: '운영자',
|
||||
clickToRefresh: '클릭하여 새로고침',
|
||||
recoverFailed: '스냅샷 복원 실패',
|
||||
upgradeRecord: '업그레이드 기록',
|
||||
upgrading: '업그레이드 중, 잠시만 기다려 주세요...',
|
||||
versionHelper: '명명 규칙: [메이저].[기능].[버그 수정], 예시:',
|
||||
upgradeCheck: '업데이트 확인',
|
||||
panelSSLHelper: '패널 SSL 자동 갱신 후 1Panel 서비스를 수동 재시작해야 적용됩니다.',
|
||||
},
|
||||
license: {
|
||||
offLine: '오프라인 버전',
|
||||
@@ -2189,6 +2245,9 @@ const message = {
|
||||
app: '프로페셔널 버전으로 업그레이드하면 모바일 APP을 통해 서비스 정보, 이상 모니터링 등을 확인할 수 있습니다.',
|
||||
cluster:
|
||||
'프로페셔널 에디션으로 업그레이드하면 MySQL/Postgres/Redis 마스터-슬레이브 클러스터를 관리할 수 있습니다.',
|
||||
exceptionalHelper:
|
||||
'라이선스 동기화 검증이 비정상입니다. 수동으로 동기화 버튼을 눌러 프로 버전 기능을 확인하세요. 상세: ',
|
||||
tamperHelper: '작업 실패, 파일/폴더에 변조 방지가 활성화되어 있습니다. 확인 후 다시 시도하세요!',
|
||||
},
|
||||
clean: {
|
||||
scan: '스캔 시작',
|
||||
@@ -2207,7 +2266,6 @@ const message = {
|
||||
suggest: '(권장)',
|
||||
totalScan: '정리해야 할 총 불필요 파일: ',
|
||||
selectScan: '선택된 불필요 파일 총합: ',
|
||||
|
||||
system: '시스템 불필요 파일',
|
||||
systemHelper: '스냅샷, 업그레이드 및 기타 프로세스 중에 생성된 임시 파일',
|
||||
panelOriginal: '시스템 스냅샷 복원 전 백업 파일',
|
||||
@@ -2215,7 +2273,6 @@ const message = {
|
||||
agentPackages: '이전 버전 자식 노드 업그레이드/설치 패키지',
|
||||
upgradeHelper: '(시스템 롤백을 위해 최신 업그레이드 백업을 유지하는 것이 좋습니다)',
|
||||
rollback: '복구 전 백업 파일',
|
||||
|
||||
backup: '시스템 백업',
|
||||
backupHelper: '로컬 백업 계정에 연결되지 않은 백업 파일',
|
||||
unknownBackup: '시스템 백업',
|
||||
@@ -2224,13 +2281,11 @@ const message = {
|
||||
unknownDatabase: '연결되지 않은 데이터베이스 백업',
|
||||
unknownWebsite: '연결되지 않은 웹사이트 백업',
|
||||
unknownSnapshot: '연결되지 않은 스냅샷 백업',
|
||||
|
||||
upload: '임시 업로드 파일',
|
||||
uploadHelper: '시스템 백업 리스트에서 업로드된 임시 파일',
|
||||
download: '임시 다운로드 파일',
|
||||
downloadHelper: '시스템에서 제3자 백업 계정으로 다운로드된 임시 파일',
|
||||
directory: '디렉터리',
|
||||
|
||||
systemLog: '로그 파일',
|
||||
systemLogHelper: '시스템 로그, 작업 로그, 웹사이트 로그 파일',
|
||||
dockerLog: '컨테이너 작업 로그 파일',
|
||||
@@ -2238,14 +2293,13 @@ const message = {
|
||||
shell: '셸 스크립트 예약 작업',
|
||||
containerShell: '컨테이너 내에서 실행하는 셸 스크립트 예약 작업',
|
||||
curl: 'CURL 예약 작업',
|
||||
|
||||
docker: '컨테이너 쓰레기',
|
||||
dockerHelper: '컨테이너, 이미지, 볼륨, 빌드 캐시 등의 파일',
|
||||
volumes: '볼륨',
|
||||
buildCache: '컨테이너 빌드 캐시',
|
||||
|
||||
appTmpDownload: '앱 임시 다운로드 파일',
|
||||
unknownWebsiteLog: '연결되지 않은 웹사이트 로그 백업 파일',
|
||||
snapshot: '시스템 스냅샷 임시 파일',
|
||||
},
|
||||
app: {
|
||||
app: '애플리케이션 | 애플리케이션들',
|
||||
@@ -2334,6 +2388,12 @@ const message = {
|
||||
reload: '새로고침',
|
||||
upgradeWarn:
|
||||
'애플리케이션 업그레이드는 docker-compose.yml 파일을 교체합니다. 변경 사항이 있으면 파일 비교를 클릭하여 확인할 수 있습니다.',
|
||||
openclawHttpsUpgradeNoticeTitle:
|
||||
'주의: 다음 안내는 2026.3.13 이전 버전에서 OpenClaw 를 업그레이드하는 사용자에게만 적용됩니다:',
|
||||
openclawHttpsUpgradeNoticeItem1:
|
||||
'배포된 에이전트 업그레이드가 완료되면 구성 -> 설정 -> 보안 페이지로 이동해 접근 주소를 수동으로 추가해야 합니다.',
|
||||
openclawHttpsUpgradeNoticeItem2:
|
||||
'새 버전은 에이전트 접근에 HTTPS 를 강제합니다. 이전에 리버스 프록시 사이트로 접근했다면 프록시 대상 주소를 https://IP:Port 로 변경하세요.',
|
||||
newVersion: '새 버전',
|
||||
oldVersion: '현재 버전',
|
||||
composeDiff: '파일 비교',
|
||||
@@ -2692,7 +2752,6 @@ const message = {
|
||||
failTimeout: '장애 시간 초과',
|
||||
failTimeoutHelper:
|
||||
'서버 상태 점검 시간 창 길이. 이 기간 동안 누적 실패 횟수가 임계값에 도달하면 서버가 일시적으로 제거되고 동일한 시간 후에 재시도됩니다. 기본값 10초',
|
||||
|
||||
staticChangePHPHelper: '현재 정적 웹사이트이며 PHP 웹사이트로 전환할 수 있습니다.',
|
||||
proxyCache: '리버스 프록시 캐시',
|
||||
cacheLimit: '캐시 공간 제한',
|
||||
@@ -2720,7 +2779,6 @@ const message = {
|
||||
preflight: '프리플라이트 요청 빠른 응답',
|
||||
preflightHleper:
|
||||
'활성화하면 브라우저가 교차 출처 프리플라이트 요청(OPTIONS 요청)을 보낼 때 시스템이 자동으로 204 상태 코드를 반환하고 필요한 교차 출처 응답 헤더를 설정합니다',
|
||||
|
||||
changeDatabase: '데이터베이스 전환',
|
||||
changeDatabaseHelper1: '데이터베이스 연관은 웹사이트 백업 및 복원에 사용됩니다.',
|
||||
changeDatabaseHelper2: '다른 데이터베이스로 전환하면 이전 백업을 복원할 수 없게 될 수 있습니다.',
|
||||
@@ -2754,7 +2812,6 @@ const message = {
|
||||
execUser: '실행 사용자',
|
||||
execDir: '실행 디렉토리',
|
||||
packagist: '중국 전체 미러',
|
||||
|
||||
batchOperate: '일괄 작업',
|
||||
batchOperateHelper: '웹사이트를 일괄 {0}, 계속 작업하시겠습니까?',
|
||||
stream: 'TCP/UDP 프록시',
|
||||
@@ -2763,8 +2820,12 @@ const message = {
|
||||
'외부 수신 포트 번호를 설정합니다. 클라이언트는 이 포트를 통해 서비스에 액세스합니다. 쉼표로 구분하세요. 예: 5222,5223',
|
||||
streamHelper: 'TCP/UDP 포트 포워딩 및 로드 밸런싱',
|
||||
udp: 'UDP 활성화',
|
||||
|
||||
syncHtmlHelper: 'PHP 및 정적 웹사이트에 동기화',
|
||||
wafValueHelper: '값',
|
||||
parentWbeiste: '상위 웹사이트',
|
||||
shareCahe: '캐시 수 메모리 크기',
|
||||
shareCaheHelper: '메모리 1M당 약 8000개의 캐시 객체를 저장할 수 있습니다',
|
||||
cacheExpireJHelper: '만료 후 미적중 시 캐시가 삭제됩니다',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '짧은 태그 지원',
|
||||
@@ -2791,6 +2852,7 @@ const message = {
|
||||
containerConfig: '컨테이너 구성',
|
||||
containerConfigHelper: '환경 변수 및 기타 정보는 생성 후 구성 - 컨테이너 구성에서 수정할 수 있습니다',
|
||||
dateTimezoneHelper: '예: TZ=Asia/Shanghai(필요에 따라 추가하세요)',
|
||||
extensionsHelper: '여러 확장은 쉼표로 구분하세요',
|
||||
},
|
||||
nginx: {
|
||||
serverNamesHashBucketSizeHelper: '서버 이름의 해시 테이블 크기',
|
||||
@@ -2953,7 +3015,6 @@ const message = {
|
||||
portHelper2: '포트 범위, 예: 8080-8089',
|
||||
changeStrategyHelper:
|
||||
'[{1}] {0} 전략을 [{2}]로 변경합니다. 설정 후 {0}은(는) {2}로 외부 접근을 허용합니다. 계속하시겠습니까?',
|
||||
|
||||
strategy: '전략',
|
||||
accept: '허용',
|
||||
drop: '차단',
|
||||
@@ -2980,7 +3041,6 @@ const message = {
|
||||
exportHelper: '{0}개의 방화벽 규칙을 내보내려고 합니다. 계속하시겠습니까?',
|
||||
importSuccess: '{0}개의 규칙을 성공적으로 가져왔습니다',
|
||||
importPartialSuccess: '가져오기 완료: 성공 {0}건, 실패 {1}건',
|
||||
|
||||
ipv4Limit: '현재 작업은 IPv4 주소만 지원합니다',
|
||||
basicStatus: '현재 체인 {0}이(가) 바인딩되지 않았습니다. 먼저 바인딩하세요!',
|
||||
baseIptables: 'iptables 서비스',
|
||||
@@ -3011,6 +3071,10 @@ const message = {
|
||||
portHelper: '0은 모든 포트를 의미합니다',
|
||||
allPorts: '모든 포트',
|
||||
deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?',
|
||||
advancedControl: '고급 제어',
|
||||
advancedControlNotAvailable: '현재 {0} 방화벽 사용 중이며, 고급 규칙은 iptables만 지원합니다',
|
||||
quickJump: '빠른 이동',
|
||||
destination: '대상',
|
||||
},
|
||||
runtime: {
|
||||
runtime: '실행 환경',
|
||||
@@ -3068,6 +3132,26 @@ const message = {
|
||||
concurrency: '동시성 체계',
|
||||
loadStatus: '부하 상태',
|
||||
extraHosts: '호스트 매핑',
|
||||
extension: '확장',
|
||||
installExtension: '확장 {0} 설치를 확인하시겠습니까',
|
||||
loadedExtension: '로드된 확장',
|
||||
popularExtension: '인기 확장',
|
||||
uninstallExtension: '확장 {0}을(를) 제거하시겠습니까',
|
||||
phpConfigHelper: '설정 변경 후 운영 환경 재시작이 필요합니다. 계속하시겠습니까',
|
||||
operateMode: '운영 모드',
|
||||
dynamic: '동적',
|
||||
static: '고정',
|
||||
ondemand: '온디맨드',
|
||||
dynamicHelper: '프로세스 수를 동적으로 조정합니다. 트래픽 변동이 크거나 메모리가 적은 사이트에 적합합니다',
|
||||
staticHelper: '프로세스 수를 고정합니다. 고동시성·안정 트래픽 사이트에 적합하지만 자원 소모가 큽니다',
|
||||
ondemandHelper: '필요 시 프로세스를 시작/종료합니다. 자원 효율은 좋지만 초기 응답이 느릴 수 있습니다',
|
||||
max_children: '생성 가능한 최대 프로세스 수',
|
||||
start_servers: '시작 시 생성되는 프로세스 수',
|
||||
min_spare_servers: '최소 유휴 프로세스 수',
|
||||
max_spare_servers: '최대 유휴 프로세스 수',
|
||||
envKey: '이름',
|
||||
envValue: '값',
|
||||
environment: '환경 변수',
|
||||
},
|
||||
process: {
|
||||
pid: '프로세스 ID',
|
||||
@@ -3114,7 +3198,7 @@ const message = {
|
||||
list: '데몬 프로세스 | 데몬 프로세스들',
|
||||
config: 'Supervisor 설정',
|
||||
primaryConfig: '주 설정 파일 위치',
|
||||
notSupportCtl: `supervisorctl 이 감지되지 않았습니다. 스크립트 라이브러리 페이지에서 수동으로 설치하세요.`,
|
||||
notSupportCtl: 'supervisorctl 이 감지되지 않았습니다. 스크립트 라이브러리 페이지에서 수동으로 설치하세요.',
|
||||
user: '사용자',
|
||||
command: '명령어',
|
||||
dir: '디렉토리',
|
||||
@@ -3123,7 +3207,7 @@ const message = {
|
||||
'이 작업은 주 설정 파일의 "[include]" 섹션에 있는 "files" 값을 수정합니다. 다른 설정 파일의 디렉토리는 "{1Panel 설치 디렉토리}/1panel/tools/supervisord/supervisor.d/"입니다.',
|
||||
operatorHelper: '{1} 작업을 {0}에서 수행합니다. 계속하시겠습니까?',
|
||||
uptime: '운영 시간',
|
||||
notStartWarn: `Supervisor 가 시작되지 않았습니다. 먼저 시작하세요.`,
|
||||
notStartWarn: 'Supervisor 가 시작되지 않았습니다. 먼저 시작하세요.',
|
||||
serviceName: '서비스 이름',
|
||||
initHelper:
|
||||
'Supervisor 서비스가 감지되었지만 초기화되지 않았습니다. 상단 상태 표시줄의 초기화 버튼을 클릭하여 구성하세요.',
|
||||
@@ -3441,8 +3525,10 @@ const message = {
|
||||
fileToLarge: '파일이 1MB를 초과하여 업로드할 수 없습니다',
|
||||
uploadOverLimit: '업로드된 파일 수가 제한을 초과했습니다, 최대 1개',
|
||||
importRuleHelper: '한 줄에 하나의 규칙',
|
||||
name: 'WAF 이름',
|
||||
},
|
||||
monitor: {
|
||||
'360': '360 검색',
|
||||
name: '웹사이트 모니터링',
|
||||
pv: '페이지 조회수',
|
||||
uv: '고유 방문자 수',
|
||||
@@ -3512,7 +3598,6 @@ const message = {
|
||||
statusErr: '잘못된 상태 코드 형식',
|
||||
shenma: '션마 검색',
|
||||
duckduckgo: '덕덕고',
|
||||
'360': '360 검색',
|
||||
excludeUri: 'URI 제외',
|
||||
top100Helper: '상위 100 개의 데이터를 표시합니다',
|
||||
logSaveDay: '로그 보관 기간 (일)',
|
||||
@@ -3617,7 +3702,6 @@ const message = {
|
||||
wafContent3: '요청이 차단된 후 표시할 사용자 지정 페이지를 생성합니다.',
|
||||
wafTitle4: '사용자 지정 규칙 (ACL)',
|
||||
wafContent4: '사용자 지정 규칙에 따라 요청을 차단합니다.',
|
||||
|
||||
tamperTitle1: '파일 무결성 모니터링',
|
||||
tamperContent1: '핵심 파일, 스크립트 파일, 구성 파일을 포함한 웹사이트 파일의 무결성을 모니터링합니다.',
|
||||
tamperTitle2: '실시간 스캔 및 감지',
|
||||
@@ -3628,14 +3712,12 @@ const message = {
|
||||
tamperTitle4: '로그 기록 및 분석',
|
||||
tamperContent4:
|
||||
'파일 접근 및 작업 로그를 기록하여 관리자가 감사 및 분석을 수행할 수 있도록 하고, 잠재적 보안 위협을 식별합니다.',
|
||||
|
||||
settingTitle1: '사용자 정의 환영 메시지',
|
||||
settingContent1: '1Panel 로그인 페이지에 사용자 정의 환영 메시지를 설정합니다.',
|
||||
settingTitle2: '사용자 정의 로고',
|
||||
settingContent2: '브랜드명이나 텍스트가 포함된 로고 이미지를 업로드할 수 있습니다.',
|
||||
settingTitle3: '사용자 정의 웹사이트 아이콘',
|
||||
settingContent3: '브라우저 기본 아이콘을 대체할 사용자 정의 아이콘을 업로드하여 사용자 경험을 개선합니다.',
|
||||
|
||||
monitorTitle1: '방문자 추세',
|
||||
monitorContent1: '웹사이트 방문자 추세를 통계적으로 표시합니다.',
|
||||
monitorTitle2: '방문자 지도',
|
||||
@@ -3644,7 +3726,6 @@ const message = {
|
||||
monitorContent3: '웹사이트 요청 정보(스파이더, 접속 장치, 요청 상태 등)에 대한 통계를 제공합니다.',
|
||||
monitorTitle4: '실시간 모니터링',
|
||||
monitorContent4: '웹사이트 요청 정보(요청 수, 트래픽 등)를 실시간으로 모니터링합니다.',
|
||||
|
||||
alertTitle1: 'SMS 알림',
|
||||
alertContent1:
|
||||
'서버 리소스 사용량 이상, 웹사이트 및 인증서 만료, 새로운 버전 업데이트, 비밀번호 만료 등의 문제가 발생하면 SMS 알림을 통해 사용자가 신속히 처리할 수 있도록 합니다.',
|
||||
@@ -3654,7 +3735,6 @@ const message = {
|
||||
alertTitle3: '알림 설정',
|
||||
alertContent3:
|
||||
'사용자에게 전화번호, 일일 푸시 빈도, 일일 푸시 시간 등을 사용자 정의할 수 있는 설정을 제공하여 보다 합리적인 푸시 알림을 설정할 수 있도록 합니다.',
|
||||
|
||||
nodeDashTitle1: '애플리케이션 관리',
|
||||
nodeDashContent1:
|
||||
'다중 노드 애플리케이션의 통합 관리, 상태 모니터링, 빠른 시작/중지, 터미널 연결 및 백업 지원',
|
||||
@@ -3664,7 +3744,6 @@ const message = {
|
||||
nodeDashContent3: '다중 노드 데이터베이스의 통합 관리, 주요 상태 한눈에 확인, 원클릭 백업 지원',
|
||||
nodeDashTitle4: '예약 작업 관리',
|
||||
nodeDashContent4: '다중 노드 예약 작업의 통합 관리, 상태 모니터링, 빠른 시작/중지 및 수동 트리거 실행 지원',
|
||||
|
||||
nodeTitle1: '원클릭 노드 추가',
|
||||
nodeContent1: '여러 서버 노드를 빠르게 통합',
|
||||
nodeTitle2: '일괄 업그레이드',
|
||||
@@ -3673,17 +3752,14 @@ const message = {
|
||||
nodeContent3: '각 노드의 운영 상태를 실시간으로 파악',
|
||||
nodeTitle4: '빠른 원격 연결',
|
||||
nodeContent4: '원클릭으로 노드 원격 터미널에 직접 연결',
|
||||
|
||||
fileExchangeTitle1: '키 인증 전송',
|
||||
fileExchangeContent1: 'SSH 키를 통해 인증하여 전송 보안을 보장합니다.',
|
||||
fileExchangeTitle2: '효율적인 파일 동기화',
|
||||
fileExchangeContent2: '변경된 내용만 동기화하여 전송 속도와 안정성을 크게 향상시킵니다.',
|
||||
fileExchangeTitle3: '다중 노드 상호 통신 지원',
|
||||
fileExchangeContent3: '다른 노드 간에 프로젝트 파일을 쉽게 전송하고, 여러 서버를 유연하게 관리합니다.',
|
||||
|
||||
nodeAppTitle1: '애플리케이션 업그레이드 관리',
|
||||
nodeAppContent1: '다중 노드 애플리케이션 업데이트 통합 모니터링, 원클릭 업그레이드 지원',
|
||||
|
||||
appTitle1: '유연한 패널 관리',
|
||||
appContent1: '언제 어디서나 1Panel 서버를 쉽게 관리하세요.',
|
||||
appTitle2: '종합적인 서비스 정보',
|
||||
@@ -3692,7 +3768,6 @@ const message = {
|
||||
appTitle3: '실시간 이상 모니터링',
|
||||
appContent3:
|
||||
'모바일 앱에서 서버 상태, WAF 보안 모니터링, 웹사이트 방문 통계 및 프로세스 건강 상태를 실시간으로 확인하세요.',
|
||||
|
||||
clusterTitle1: '마스터-슬레이브 배포',
|
||||
clusterContent1:
|
||||
'다른 노드에서 MySQL/Postgres/Redis 마스터-슬레이브 인스턴스를 생성하는 것을 지원하며, 자동으로 마스터-슬레이브 연결 및 초기화를 완료합니다',
|
||||
@@ -3708,7 +3783,7 @@ const message = {
|
||||
masterBackup: '마스터 노드 백업',
|
||||
backupNode: '백업 노드',
|
||||
backupFrequency: '백업 주기(시간)',
|
||||
backupCopies: '백업 기록 보관 수',
|
||||
backupCopies: '백업 수',
|
||||
noBackupNode: '현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오!',
|
||||
masterBackupAlert:
|
||||
'현재 마스터 노드 백업이 구성되지 않았습니다. 데이터 보안을 위해 장애 시 새로운 마스터 노드로 수동 전환이 가능하도록 가능한 빨리 백업 노드를 설정하십시오.',
|
||||
@@ -3759,7 +3834,6 @@ const message = {
|
||||
nodeSyncHelper: '노드 정보 동기화는 다음 정보를 동기화합니다:',
|
||||
nodeSyncHelper1: '1. 공용 백업 계정 정보',
|
||||
nodeSyncHelper2: '2. 주 노드와 하위 노드 간의 연결 정보',
|
||||
|
||||
nodeCheck: '가용성 확인',
|
||||
checkSSH: '노드 SSH 연결 확인',
|
||||
checkUserPermission: '노드 사용자 권한 확인',
|
||||
@@ -3781,7 +3855,6 @@ const message = {
|
||||
errLicense: '이 노드에 바인딩된 라이선스를 사용할 수 없습니다. 확인 후 다시 시도하십시오!',
|
||||
errNodePort:
|
||||
'노드 포트 [ {0} ]에 접근할 수 없는 것으로 감지되었습니다. 방화벽 또는 보안 그룹에서 해당 포트가 허용되었는지 확인하십시오.',
|
||||
|
||||
reinstallHelper: '노드 {0}를 재설치합니다. 계속하시겠습니까?',
|
||||
unhealthyCheck: '비정상 체크',
|
||||
fixOperation: '수정 작업',
|
||||
@@ -3805,7 +3878,6 @@ const message = {
|
||||
offline: '오프라인 모드',
|
||||
freeCount: '무료 할당량 [{0}]',
|
||||
offlineHelper: '노드가 오프라인 환경일 때 사용',
|
||||
|
||||
appUpgrade: '앱 업그레이드',
|
||||
appUpgradeHelper: '업그레이드가 필요한 앱이 {0}개 있습니다',
|
||||
},
|
||||
|
||||
+156
-88
@@ -84,6 +84,10 @@ const message = {
|
||||
hide: 'Sembunyikan',
|
||||
visit: 'Lawati',
|
||||
migrate: 'Migrasi',
|
||||
prev: 'Sebelumnya',
|
||||
next: 'Seterusnya',
|
||||
setDefault: 'Pulihkan lalai',
|
||||
bind: 'Ikat',
|
||||
},
|
||||
operate: {
|
||||
start: 'Mula',
|
||||
@@ -133,6 +137,8 @@ const message = {
|
||||
manageGroup: 'Urus Kumpulan',
|
||||
backToList: 'Kembali ke Senarai',
|
||||
keepEdit: 'Teruskan Mengedit',
|
||||
default: 'Lalai',
|
||||
noRefresh: 'Tiada segar semula auto',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Peningkatan sistem, sila tunggu...',
|
||||
@@ -183,6 +189,10 @@ const message = {
|
||||
importHelper:
|
||||
'Apabila mengimport data yang bercanggah atau pendua, kandungan yang diimport akan digunakan sebagai piawai untuk mengemas kini data pangkalan data asal.',
|
||||
errImport: 'Kandungan fail adalah tidak normal:',
|
||||
backupSuccess: 'Sandaran berjaya',
|
||||
restoreSuccess: 'Pemulihan berjaya',
|
||||
installSuccess: 'Pemasangan berjaya',
|
||||
uninstallSuccess: 'Nyahpasang berjaya',
|
||||
},
|
||||
login: {
|
||||
username: 'Pengguna',
|
||||
@@ -225,9 +235,11 @@ const message = {
|
||||
'Ruangan ini mesti bermula dengan aksara bukan khas dan mesti terdiri daripada aksara rumi, Cina, nombor, ".", "-", dan "_" dengan panjang 1-128 aksara.',
|
||||
userName:
|
||||
'Menyokong bermula dengan bukan aksara khas, Bahasa Inggeris, Bahasa Cina, nombor, dan _, panjang 3-30',
|
||||
simpleName: `Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 3-30 aksara.`,
|
||||
simplePassword: `Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 1-30 aksara.`,
|
||||
dbName: `Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 1-64 aksara.`,
|
||||
simpleName:
|
||||
'Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 3-30 aksara.',
|
||||
simplePassword:
|
||||
'Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 1-30 aksara.',
|
||||
dbName: 'Ruangan ini tidak boleh bermula dengan aksara garis bawah ("_") dan mesti terdiri daripada aksara rumi, nombor, dan "_" dengan panjang 1-64 aksara.',
|
||||
imageName: 'Menyokong bermula dengan bukan aksara khas, Bahasa Inggeris, nombor, :@/.-_, panjang 1-256',
|
||||
composeName:
|
||||
'Menyokong aksara bukan khas pada permulaan, huruf kecil, nombor, "-", dan "_", dengan panjang 1-256 aksara.',
|
||||
@@ -238,7 +250,8 @@ const message = {
|
||||
complexityPassword:
|
||||
'Ruangan ini mesti terdiri daripada aksara rumi, nombor dengan panjang 8-30 aksara dan mengandungi sekurang-kurangnya dua aksara khas.',
|
||||
commonPassword: 'Panjang ruangan ini mesti melebihi 6 aksara.',
|
||||
linuxName: `Panjang ruangan ini mesti antara 1 hingga 128 aksara. Ruangan ini tidak boleh mengandungi aksara khas berikut: "{0}".`,
|
||||
linuxName:
|
||||
'Panjang ruangan ini mesti antara 1 hingga 128 aksara. Ruangan ini tidak boleh mengandungi aksara khas berikut: "{0}".',
|
||||
email: 'Ruangan ini mesti mengandungi alamat emel yang sah.',
|
||||
number: 'Ruangan ini mesti mengandungi nombor.',
|
||||
integer: 'Ruangan ini mesti mengandungi integer positif.',
|
||||
@@ -284,7 +297,7 @@ const message = {
|
||||
commonError: 'Permintaan gagal',
|
||||
},
|
||||
service: {
|
||||
serviceNotStarted: `Perkhidmatan {0} belum dimulakan.`,
|
||||
serviceNotStarted: 'Perkhidmatan {0} belum dimulakan.',
|
||||
},
|
||||
status: {
|
||||
running: 'Sedang Berjalan',
|
||||
@@ -346,6 +359,7 @@ const message = {
|
||||
conflict: 'Konflik',
|
||||
duplicate: 'Pendua',
|
||||
unexecuted: 'Tidak Dilaksanakan',
|
||||
unused: 'Tidak digunakan',
|
||||
},
|
||||
units: {
|
||||
second: 'saat | saat | saat',
|
||||
@@ -399,6 +413,7 @@ const message = {
|
||||
app: 'Aplikasi',
|
||||
msgCenter: 'Tugas',
|
||||
disk: 'Disk',
|
||||
filter: 'Penapis',
|
||||
},
|
||||
home: {
|
||||
recommend: 'Disyorkan',
|
||||
@@ -432,12 +447,10 @@ const message = {
|
||||
runningTime: 'Uptime',
|
||||
mem: 'Memori Sistem',
|
||||
swapMem: 'Swap partition',
|
||||
|
||||
runSmoothly: 'Beban rendah',
|
||||
runNormal: 'Beban sederhana',
|
||||
runSlowly: 'Beban tinggi',
|
||||
runJam: 'Beban berat',
|
||||
|
||||
core: 'Teras Fizikal',
|
||||
corePercent: 'Penggunaan Teras',
|
||||
cpuFrequency: 'Frekuensi CPU',
|
||||
@@ -463,7 +476,6 @@ const message = {
|
||||
available: 'Tersedia',
|
||||
percent: 'Penggunaan',
|
||||
goInstall: 'Pergi pasang',
|
||||
|
||||
networkCard: 'Kad rangkaian',
|
||||
disk: 'Disk',
|
||||
memo: 'Catatan',
|
||||
@@ -519,14 +531,11 @@ const message = {
|
||||
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.',
|
||||
recoverTimeoutHelper: '-1 bermaksud tiada had masa tamat',
|
||||
|
||||
confChange: 'Konfigurasi',
|
||||
confNotFound:
|
||||
'Fail konfigurasi tidak dapat dijumpai. Sila tingkatkan aplikasi ke versi terkini di gedung aplikasi dan cuba lagi!',
|
||||
|
||||
portHelper:
|
||||
'Port ini adalah port yang didedahkan oleh kontena. Anda perlu menyimpan pengubahsuaian secara berasingan dan memulakan semula kontena!',
|
||||
|
||||
loadFromRemote: 'Selaras',
|
||||
userBind: 'Kaitkan pengguna',
|
||||
pgBindHelper:
|
||||
@@ -557,14 +566,12 @@ const message = {
|
||||
hasCA: 'Mempunyai sijil CA',
|
||||
skipVerify: 'Abaikan pemeriksaan kesahihan sijil',
|
||||
initialDB: 'Pangkalan Data Awal',
|
||||
|
||||
formatHelper:
|
||||
'Set aksara pangkalan data semasa adalah {0}, ketidakkonsistenan set aksara mungkin menyebabkan kegagalan pemulihan.',
|
||||
dropHelper: 'Anda boleh seret dan lepaskan fail yang ingin dimuat naik di sini atau',
|
||||
clickHelper: 'klik untuk memuat naik',
|
||||
supportUpType:
|
||||
'Hanya menyokong format fail sql, sql.gz, tar.gz, .zip. Fail termampat yang diimport mesti mengandungi hanya satu fail .sql atau termasuk test.sql',
|
||||
|
||||
currentStatus: 'Keadaan semasa',
|
||||
baseParam: 'Parameter asas',
|
||||
performanceParam: 'Parameter prestasi',
|
||||
@@ -597,7 +604,6 @@ const message = {
|
||||
tableLocksWaited: 'Bilangan kunci jadual',
|
||||
tableLocksWaitedHelper:
|
||||
'Jika nilai terlalu besar, pertimbangkan untuk meningkatkan prestasi pangkalan data anda.',
|
||||
|
||||
performanceTuning: 'Penalaan prestasi',
|
||||
optimizationScheme: 'Skema pengoptimuman',
|
||||
keyBufferSizeHelper: 'Saiz penimbal untuk indeks',
|
||||
@@ -615,14 +621,11 @@ const message = {
|
||||
tableOpenCacheHelper: 'Cache jadual',
|
||||
maxConnectionsHelper: 'Sambungan maksimum',
|
||||
restart: 'Mulakan semula',
|
||||
|
||||
slowLog: 'Log lambat',
|
||||
noData: 'Tiada log lambat lagi.',
|
||||
|
||||
isOn: 'Hidup',
|
||||
longQueryTime: 'Ambang (saat)',
|
||||
thresholdRangeHelper: 'Sila masukkan ambang yang betul (1 - 600).',
|
||||
|
||||
timeout: 'Tamat masa(saat)',
|
||||
timeoutHelper: 'Tempoh tamat masa sambungan tidak aktif. 0 menunjukkan sambungan sentiasa aktif.',
|
||||
maxclients: 'Klien maksimum',
|
||||
@@ -649,20 +652,17 @@ const message = {
|
||||
redisQuickCmd: 'Arahan pantas Redis',
|
||||
recoverHelper: 'Ini akan menimpa data dengan [{0}]. Adakah anda mahu meneruskan?',
|
||||
submitIt: 'Tindih data',
|
||||
|
||||
baseConf: 'Asas',
|
||||
allConf: 'Semua',
|
||||
restartNow: 'Mula semula sekarang',
|
||||
restartNowHelper1:
|
||||
'Anda perlu memulakan semula sistem selepas perubahan konfigurasi berkuat kuasa. Jika data anda perlu dipelihara, lakukan operasi simpan terlebih dahulu.',
|
||||
restartNowHelper: 'Perubahan ini hanya akan berkuat kuasa selepas sistem dimulakan semula.',
|
||||
|
||||
persistence: 'Ketekalan',
|
||||
rdbHelper1: 'saat, masukkan',
|
||||
rdbHelper2: 'data',
|
||||
rdbHelper3: 'Memenuhi mana-mana syarat akan mencetuskan ketekalan RDB.',
|
||||
rdbInfo: 'Pastikan nilai dalam senarai peraturan berada dalam julat 1 hingga 100000',
|
||||
|
||||
containerConn: 'Sambungan kontena',
|
||||
copyConnURL: 'Salin URL sambungan',
|
||||
connAddress: 'Alamat',
|
||||
@@ -687,33 +687,38 @@ const message = {
|
||||
copawType: 'CoPaw',
|
||||
appVersion: 'Versi aplikasi',
|
||||
webuiPort: 'Port WebUI',
|
||||
bridgePort: 'Port Bridge',
|
||||
allowedOrigins: 'Alamat akses',
|
||||
allowedOriginsHelper:
|
||||
'Masukkan satu alamat akses HTTPS penuh bagi setiap baris, contohnya https://192.168.1.2:18789. Isikan secara manual jika alamat akses lalai belum dikonfigurasi.',
|
||||
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Masukkan sekurang-kurangnya satu alamat akses',
|
||||
allowedOriginsInvalid: 'Gunakan format http(s)://hos-atau-ip:port',
|
||||
allowedOriginsHttpsOnly: 'Alamat akses mesti bermula dengan https://',
|
||||
provider: 'Penyedia model',
|
||||
apiKey: 'Kunci API',
|
||||
baseUrl: 'URL asas',
|
||||
token: 'Token',
|
||||
manualModel: 'Input manual',
|
||||
verified: 'Disahkan',
|
||||
verifySkipped: 'Dilangkau',
|
||||
verifySkipped: 'Tanpa pengesahan',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Zon Waktu',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
feishu: 'Feishu',
|
||||
pluginNotInstalled: 'Plugin belum dipasang. Sila pasang dahulu.',
|
||||
dmPolicy: 'DM Policy',
|
||||
groupPolicy: 'Group Policy',
|
||||
policyPairing: 'Pairing',
|
||||
policyOpen: 'Open',
|
||||
policyDisabled: 'Disabled',
|
||||
botName: 'Bot Name',
|
||||
botId: 'Bot ID',
|
||||
appId: 'App ID',
|
||||
appSecret: 'App Secret',
|
||||
saveAndRestartGateway: 'Save and restart gateway',
|
||||
@@ -725,9 +730,11 @@ const message = {
|
||||
pairingCodeRequired: 'Please enter pairing code',
|
||||
pairingApproveSuccess: 'Pairing approved successfully',
|
||||
customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan',
|
||||
feishuSaveSuccess: 'Disimpan ke Feishu',
|
||||
},
|
||||
model: {
|
||||
model: 'Model',
|
||||
localModel: 'Model Tempatan',
|
||||
create: 'Tambah Model',
|
||||
create_helper: 'Tarik "{0}"',
|
||||
ollama_doc: 'Anda boleh melawat laman web rasmi Ollama untuk mencari dan menemui lebih banyak model.',
|
||||
@@ -736,6 +743,7 @@ const message = {
|
||||
'Sincronizando o modelo Ollama, encontrou que os seguintes modelos não existem, deseja excluí-los?',
|
||||
from_remote: 'Este modelo não foi baixado via 1Panel, sem logs de pull relacionados.',
|
||||
no_logs: 'Os logs de pull deste modelo foram excluídos e não podem ser visualizados.',
|
||||
vllmVersionHelper: 'Untuk pelayan FusionXpark GB 10, sila pilih versi -cu130.',
|
||||
},
|
||||
proxy: {
|
||||
proxy: 'Peningkatan Proksi AI',
|
||||
@@ -773,7 +781,6 @@ const message = {
|
||||
memoryTotal: 'Jumlah Memori',
|
||||
percent: 'Penggunaan',
|
||||
base: 'Maklumat Asas',
|
||||
|
||||
driverVersion: 'Versi Pemacu',
|
||||
cudaVersion: 'Versi CUDA',
|
||||
processMemoryUsage: 'Penggunaan Memori',
|
||||
@@ -804,6 +811,7 @@ const message = {
|
||||
current: 'Pemantauan Masa Nyata',
|
||||
history: 'Rekod Sejarah',
|
||||
notSupport: 'Versi atau pemacu semasa tidak menyokong paparan parameter ini.',
|
||||
processCount: 'Bilangan proses',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Pelayan MCP',
|
||||
@@ -842,6 +850,12 @@ const message = {
|
||||
},
|
||||
container: {
|
||||
createByCommand: 'Cipta melalui perintah',
|
||||
importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong',
|
||||
importComposeBackupTip: 'Import fail sandaran compose, hanya format .tar.gz disokong',
|
||||
stopContainerBeforeBackup: 'Hentikan kontena sebelum sandaran',
|
||||
stopComposeBeforeBackup: 'Hentikan compose sebelum sandaran',
|
||||
stopBeforeBackupHelper:
|
||||
'Apabila diaktifkan, kontena atau perkhidmatan compose akan dihentikan sebelum sandaran dan dipulihkan secara automatik selepas selesai untuk memastikan ketekalan data.',
|
||||
commandInput: 'Input perintah',
|
||||
commandRule: 'Sila masukkan arahan docker run yang sah untuk mencipta kontena.',
|
||||
commandHelper: 'Arahan ini akan dijalankan pada pelayan untuk mencipta kontena. Teruskan?',
|
||||
@@ -914,7 +928,6 @@ const message = {
|
||||
'Enjin kontena menggunakan nilai asas 1024 untuk bahagian CPU. Anda boleh meningkatkannya untuk memberikan lebih masa CPU kepada kontena.',
|
||||
inputIpv4: 'Contoh: 192.168.1.1',
|
||||
inputIpv6: 'Contoh: 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
|
||||
diskUsage: 'Penggunaan Cakera',
|
||||
localVolume: 'Isipadu Storan Tempatan',
|
||||
buildCache: 'Cache Binaan',
|
||||
@@ -928,7 +941,6 @@ const message = {
|
||||
sizeRwHelper: 'Saiz lapisan boleh tulis yang unik untuk bekas',
|
||||
sizeRootFs: 'Saiz Maya',
|
||||
sizeRootFsHelper: 'Jumlah saiz semua lapisan imej yang disandarkan oleh bekas + lapisan bekas',
|
||||
|
||||
containerFromAppHelper:
|
||||
'Dikesan bahawa kontena ini berasal dari gedung aplikasi. Operasi aplikasi boleh menyebabkan suntingan semasa menjadi tidak sah.',
|
||||
containerFromAppHelper1:
|
||||
@@ -942,7 +954,6 @@ const message = {
|
||||
privileged: 'Privileged',
|
||||
privilegedHelper:
|
||||
'Benarkan kontena menjalankan operasi teristimewa tertentu pada hos, yang boleh meningkatkan risiko kontena. Gunakan dengan berhati-hati!',
|
||||
|
||||
upgradeHelper: 'Nama Repository/Nama Imej: Versi Imej',
|
||||
upgradeWarning2:
|
||||
'Operasi peningkatan memerlukan pembinaan semula kontena, sebarang data yang tidak disimpan akan hilang. Adakah anda mahu meneruskan?',
|
||||
@@ -951,11 +962,14 @@ const message = {
|
||||
sameImageHelper: 'Kontena yang menggunakan imej sama boleh dinaik taraf secara berkumpulan setelah dipilih',
|
||||
targetImage: 'Imej sasaran',
|
||||
imageLoadErr: 'Tiada nama imej dikesan untuk kontena',
|
||||
imageUpdateTagEmpty: 'Tiada tag imej yang boleh dikemas kini dikesan',
|
||||
appHelper:
|
||||
'Kontena berasal dari gedung aplikasi, dan peningkatan boleh menyebabkan perkhidmatan tidak tersedia.',
|
||||
input: 'Input manual',
|
||||
forcePull: 'Tarik imej sentiasa ',
|
||||
forcePullHelper: 'Ini akan mengabaikan imej sedia ada di pelayan dan menarik imej terkini dari pendaftaran.',
|
||||
imageUpdateHelper:
|
||||
'Semak tag yang sama di registry, jika ada kemas kini barulah tarik dan kemas kini imej tempatan.',
|
||||
server: 'Hos',
|
||||
serverExample: '80, 80-88, ip:80 atau ip:80-88',
|
||||
containerExample: '80 atau 80-88',
|
||||
@@ -995,10 +1009,8 @@ const message = {
|
||||
unlessStopped: 'melainkan dihentikan',
|
||||
onFailure: 'gagal (lima kali secara lalai)',
|
||||
no: 'tidak pernah',
|
||||
|
||||
refreshTime: 'Selang penyegaran',
|
||||
cache: 'Cache',
|
||||
|
||||
image: 'Imej | Imej-imej',
|
||||
imagePull: 'Tarik',
|
||||
imagePullHelper:
|
||||
@@ -1031,7 +1043,6 @@ const message = {
|
||||
delBuildCacheHelper:
|
||||
'Ini akan memadam semua artefak cache yang dijana semasa binaan dan tidak boleh diundur. Adakah anda mahu meneruskan?',
|
||||
urlWarning: 'Awalan URL tidak perlu termasuk http:// atau https://. Sila ubah.',
|
||||
|
||||
network: 'Rangkaian | Rangkaian-rangkaian',
|
||||
networkHelper:
|
||||
'Ini boleh menyebabkan beberapa aplikasi dan persekitaran runtime tidak berfungsi dengan betul. Adakah anda mahu meneruskan?',
|
||||
@@ -1044,7 +1055,6 @@ const message = {
|
||||
scope: 'Skop IP',
|
||||
gateway: 'Gerbang',
|
||||
auxAddress: 'Kecualikan IP',
|
||||
|
||||
volume: 'Volum | Volum-volum',
|
||||
volumeDir: 'Direktori volum',
|
||||
nfsEnable: 'Aktifkan storan NFS',
|
||||
@@ -1052,7 +1062,6 @@ const message = {
|
||||
mountpoint: 'Titik pemasangan',
|
||||
mountpointNFSHelper: 'contoh: /nfs, /nfs-share',
|
||||
options: 'Pilihan',
|
||||
|
||||
repo: 'Pendaftaran',
|
||||
httpRepoHelper: 'Mengoperasikan repositori jenis HTTP memerlukan mulakan semula perkhidmatan Docker.',
|
||||
httpRepo:
|
||||
@@ -1068,7 +1077,6 @@ const message = {
|
||||
'Jika terdapat banyak cermin, baris baru mesti dipaparkan, contohnya:\nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
|
||||
registrieHelper:
|
||||
'Jika terdapat banyak repositori persendirian, baris baru mesti dipaparkan, contohnya:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: 'Compose | Compose-compose',
|
||||
composeFile: 'Fail Susunan',
|
||||
fromChangeHelper: 'Menukar sumber akan membersihkan kandungan yang sedang diedit. Adakah anda mahu meneruskan?',
|
||||
@@ -1094,8 +1102,9 @@ const message = {
|
||||
'Ini akan menghentikan dan menghapuskan semua kontena dan rangkaian di bawah komposisi {0}. Adakah anda mahu meneruskan?',
|
||||
composeEnvHelper2:
|
||||
'Penyelarasan ini dibuat oleh Kedai Apl 1Panel. Sila ubah pembolehubah persekitaran dalam aplikasi yang dipasang.',
|
||||
|
||||
composeContentPlaceholder: 'Tulis atau tampal kandungan fail docker-compose anda di sini',
|
||||
setting: 'Tetapan | Tetapan',
|
||||
restartHelper: 'Perubahan konfigurasi memerlukan perkhidmatan Docker dimulakan semula untuk berkuat kuasa',
|
||||
operatorStatusHelper: 'Ini akan "{0}" perkhidmatan Docker. Adakah anda mahu meneruskan?',
|
||||
dockerStatus: 'Perkhidmatan Docker',
|
||||
daemonJsonPathHelper: 'Pastikan laluan konfigurasi sama seperti yang dinyatakan dalam docker.service.',
|
||||
@@ -1135,12 +1144,10 @@ const message = {
|
||||
related: 'Berkaitan',
|
||||
includeAppstore: 'Tunjukkan kontena dari gedung aplikasi',
|
||||
excludeAppstore: 'Sembunyikan Kontena Kedai Aplikasi',
|
||||
|
||||
cleanDockerDiskZone: 'Bersihkan ruang cakera yang digunakan oleh Docker',
|
||||
cleanImagesHelper: '( Bersihkan semua imej yang tidak digunakan oleh mana-mana kontena )',
|
||||
cleanContainersHelper: '( Bersihkan semua kontena yang dihentikan )',
|
||||
cleanVolumesHelper: '( Bersihkan semua volum tempatan yang tidak digunakan )',
|
||||
|
||||
makeImage: 'Cipta imej',
|
||||
newImageName: 'Nama imej baru',
|
||||
commitMessage: 'Mesej komit',
|
||||
@@ -1148,6 +1155,12 @@ const message = {
|
||||
ifPause: 'Jeda Kontena Semasa Penciptaan',
|
||||
ifMakeImageWithContainer: 'Cipta Imej Baru daripada Kontena Ini?',
|
||||
finishTime: 'Masa berhenti terakhir',
|
||||
workingDir: 'Direktori kerja',
|
||||
resource: 'Sumber',
|
||||
macAddr: 'Alamat MAC',
|
||||
volumeHelper: 'Pastikan kandungan volum storan adalah betul',
|
||||
exportHelper: 'Eksport imej terpilih ke satu fail tar.',
|
||||
goSetting: 'Pergi ke edit',
|
||||
},
|
||||
cronjob: {
|
||||
importHelper:
|
||||
@@ -1260,7 +1273,6 @@ const message = {
|
||||
cutWebsiteLog: 'Putaran log laman web',
|
||||
cutWebsiteLogHelper: 'Fail log yang diputar akan disandarkan ke direktori sandaran 1Panel.',
|
||||
syncIpGroup: 'Segerakkan kumpulan IP WAF',
|
||||
|
||||
requestExpirationTime: 'Waktu luput permintaan muat naik (Jam)',
|
||||
unitHours: 'Unit: Jam',
|
||||
alertTitle: 'Tugas Terancang - {0} 「{1}」 Amaran Kegagalan Tugas',
|
||||
@@ -1287,6 +1299,13 @@ const message = {
|
||||
'Perkhidmatan {0} tidak dikesan. Sila pasang dengan cepat menggunakan pustaka skrip terlebih dahulu!',
|
||||
syncHelper: 'Akan menyelaraskan pustaka skrip sistem. Operasi ini hanya melibatkan skrip sistem. Teruskan?',
|
||||
},
|
||||
nextTime: '5 pelaksanaan seterusnya',
|
||||
logHelper1: '1. Log sistem 1Panel',
|
||||
backupContent: 'Kandungan sandaran',
|
||||
cronSpecRule2: 'Format tempoh pelaksanaan tidak betul, semak dan cuba lagi!',
|
||||
executor: 'Pelaksana',
|
||||
cleanLog: 'Bersih log automatik',
|
||||
cleanLogscope: 'Tempoh simpan log',
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Penapis Global',
|
||||
@@ -1299,7 +1318,6 @@ const message = {
|
||||
defaultIOHelper: 'Pilihan cakera lalai yang dipaparkan dalam antara muka pemantauan dan gambaran keseluruhan',
|
||||
cleanMonitor: 'Kosongkan Rekod Pemantauan',
|
||||
cleanHelper: 'Operasi ini akan membersihkan semua rekod pemantauan termasuk GPU. Teruskan?',
|
||||
|
||||
avgLoad: 'Purata beban',
|
||||
loadDetail: 'Butiran beban',
|
||||
resourceUsage: 'Penggunaan sumber',
|
||||
@@ -1437,6 +1455,7 @@ const message = {
|
||||
dnsCheck: 'Uji Ketersediaan',
|
||||
dnsOK: 'Maklumat konfigurasi DNS tersedia!',
|
||||
dnsTestFailed: 'Maklumat konfigurasi DNS tidak tersedia.',
|
||||
syncSiteHelper: 'Gunakan {0} untuk penyegerakan masa sistem. Teruskan?',
|
||||
},
|
||||
fail2ban: {
|
||||
sshPort: 'Dengar pada port SSH',
|
||||
@@ -1559,6 +1578,9 @@ const message = {
|
||||
websiteLog: 'Log Laman Web',
|
||||
runLog: 'Log Jalankan',
|
||||
errLog: 'Log Ralat',
|
||||
task: 'Log tugas',
|
||||
taskName: 'Nama tugas',
|
||||
taskRunning: 'Sedang berjalan',
|
||||
},
|
||||
file: {
|
||||
fileDirNum: '{0} direktori, {1} fail,',
|
||||
@@ -1788,6 +1810,7 @@ const message = {
|
||||
loginTime: 'Waktu log masuk',
|
||||
loginIP: 'IP log masuk',
|
||||
stopSSHWarn: 'Adakah anda mahu memutuskan sambungan SSH ini',
|
||||
remote: 'SSH jauh',
|
||||
},
|
||||
setting: {
|
||||
panel: 'Panel',
|
||||
@@ -1823,6 +1846,10 @@ const message = {
|
||||
light: 'Terang',
|
||||
auto: 'Ikut Sistem',
|
||||
language: 'Bahasa',
|
||||
runtimeEnv: 'Persekitaran operasi',
|
||||
docSource: 'Sumber dokumentasi',
|
||||
withByRegion: 'Ikut wilayah operasi (Lalai)',
|
||||
withByLang: 'Ikut bahasa sistem',
|
||||
region: 'Wilayah operasi',
|
||||
cn: 'Tanah Besar China',
|
||||
intl: 'Global',
|
||||
@@ -1832,6 +1859,8 @@ const message = {
|
||||
regionHelper3: 'Manual pengguna dan dokumen berkaitan',
|
||||
regionHelper4:
|
||||
'Tindakan ini mungkin menjejaskan muat turun dan akses seterusnya. Sila teruskan dengan berhati-hati.',
|
||||
regionTip: 'Wilayah operasi mempengaruhi sumber kemas kini sistem serta alamat muat turun pakej pemasangan.',
|
||||
docSourceTip: 'Sumber dokumentasi menentukan bahasa pautan untuk dokumentasi dan log kemas kini.',
|
||||
languageHelper:
|
||||
'Secara lalai, ia mengikuti bahasa penyemak imbas. Parameter ini hanya berkuat kuasa pada penyemak imbas semasa',
|
||||
sessionTimeout: 'Tempoh tamat sesi',
|
||||
@@ -1844,7 +1873,8 @@ const message = {
|
||||
proxyHelper: 'Ia akan berkuat kuasa dalam senario berikut selepas anda menyediakan pelayan proksi:',
|
||||
proxyHelper1: 'Muat turun pakej pemasangan dan penyelarasan dari kedai aplikasi (Edisi Profesional sahaja)',
|
||||
proxyHelper2: 'Kemas kini sistem dan pengambilan maklumat kemas kini (Edisi Profesional sahaja)',
|
||||
proxyHelper4: 'Rangkaian Docker akan diakses melalui pelayan proksi (Edisi Profesional sahaja)',
|
||||
proxyHelper4:
|
||||
'Permintaan rangkaian seperti tarikan imej Docker akan melalui pelayan proksi (Edisi Profesional sahaja)',
|
||||
proxyHelper3: 'Pengesahan dan penyelarasan lesen sistem',
|
||||
proxyHelper5: 'Muat turun dan penyegerakan bersepadu untuk pustaka skrip jenis sistem (Ciri Edisi Profesional)',
|
||||
proxyHelper6: 'Mohon sijil (Fungsi versi Pro)',
|
||||
@@ -1988,7 +2018,6 @@ const message = {
|
||||
accountKey: 'Kunci akaun',
|
||||
address: 'Alamat',
|
||||
path: 'Laluan',
|
||||
|
||||
safe: 'Keselamatan',
|
||||
passkey: 'Passkey',
|
||||
passkeyManage: 'Urus',
|
||||
@@ -2101,7 +2130,6 @@ const message = {
|
||||
error416: 'Julat Tidak Memuaskan',
|
||||
error444: 'Sambungan ditutup',
|
||||
error500: 'Ralat Pelayan',
|
||||
|
||||
https: 'Mengatur HTTPS untuk panel meningkatkan keselamatan akses',
|
||||
strictHelper: 'Trafik bukan HTTPS tidak boleh menyambung ke panel',
|
||||
muxHelper:
|
||||
@@ -2118,7 +2146,6 @@ const message = {
|
||||
certificate: 'Sijil',
|
||||
backupJump:
|
||||
'Fail sandaran tidak berada dalam senarai sandaran semasa, sila cuba muat turun dari direktori fail dan import untuk sandaran.',
|
||||
|
||||
snapshot: 'Snapshot | Snapshots',
|
||||
noAppData: 'Tiada aplikasi sistem yang boleh dipilih',
|
||||
noBackupData: 'Tiada data sandaran yang boleh dipilih',
|
||||
@@ -2188,7 +2215,6 @@ const message = {
|
||||
rollback: 'Pulangkan semula',
|
||||
rollbackHelper:
|
||||
'Pulangkan semula pemulihan ini akan menggantikan semua fail dari pemulihan ini, dan mungkin memerlukan mulakan semula perkhidmatan Docker dan 1Panel. Adakah anda mahu meneruskan?',
|
||||
|
||||
upgradeHelper: 'Kemas kini memerlukan mulakan semula perkhidmatan 1Panel. Adakah anda mahu meneruskan?',
|
||||
rollbackLocalHelper:
|
||||
'Nod utama tidak menyokong rollback secara langsung. Sila laksanakan arahan [1pctl restore] secara manual untuk rollback!',
|
||||
@@ -2200,10 +2226,9 @@ const message = {
|
||||
'Versi nod tidak sepadan dengan nod utama. Sila naik taraf di Pengurusan Nod sebelum mencuba semula.',
|
||||
versionCompare:
|
||||
'Nod {0} telah berada pada versi terkini yang boleh dinaik taraf. Sila periksa versi nod utama dan cuba lagi!',
|
||||
|
||||
about: 'Mengenai',
|
||||
versionItem: 'Versi Semasa',
|
||||
backupCopies: 'Bilangan Salinan untuk Disimpan',
|
||||
backupCopies: 'Salinan Sandaran',
|
||||
backupCopiesHelper:
|
||||
'Tetapkan bilangan salinan sandaran naik taraf untuk disimpan untuk pemulihan versi. 0 bermakna simpan semua.',
|
||||
backupCopiesRule: 'Sila simpan sekurang-kurangnya 3 rekod sandaran naik taraf',
|
||||
@@ -2218,7 +2243,6 @@ const message = {
|
||||
forum: 'Perbincangan',
|
||||
doc2: 'Dokumen',
|
||||
currentVersion: 'Versi',
|
||||
|
||||
license: 'Lesen',
|
||||
bindNode: 'Ikatan Nod',
|
||||
menuSetting: 'Tetapan Menu',
|
||||
@@ -2232,6 +2256,32 @@ const message = {
|
||||
compressPassword: 'Kata laluan mampatan',
|
||||
backupRecoverMessage:
|
||||
'Sila masukkan kata laluan mampatan atau nyahmampatan (biarkan kosong jika tidak menetapkan)',
|
||||
componentSize: 'Saiz komponen',
|
||||
scope: 'Skop',
|
||||
public: 'Awam',
|
||||
publicHelper: 'Akaun sandaran jenis awam akan disegerakkan ke setiap subnod dan boleh digunakan bersama',
|
||||
private: 'Peribadi',
|
||||
privateHelper: 'Akaun sandaran jenis peribadi hanya dicipta pada nod semasa dan hanya untuk nod semasa',
|
||||
UPYUN: 'UPYUN',
|
||||
ALIYUN: 'Aliyun Drive',
|
||||
ALIYUNHelper: 'Had semasa muat turun tanpa klien Aliyun Drive ialah 100 MB. Melebihi had perlu guna klien.',
|
||||
ALIYUNRecover:
|
||||
'Had semasa muat turun tanpa klien Aliyun Drive ialah 100 MB. Jika melebihi, muat turun ke tempatan melalui klien dahulu, kemudian segerakkan syot kilat untuk pemulihan.',
|
||||
GoogleDrive: 'Google Drive',
|
||||
analysis: 'Analisis',
|
||||
analysisHelper:
|
||||
'Tampal keseluruhan kandungan token untuk parse automatik bahagian diperlukan. Rujuk dokumentasi rasmi untuk butiran.',
|
||||
serviceName: 'Nama servis',
|
||||
operator: 'Operator',
|
||||
clickToRefresh: 'Klik untuk segar semula',
|
||||
recoverFailed: 'Pemulihan syot kilat gagal',
|
||||
importHelper: 'Direktori syot kilat: ',
|
||||
upgradeRecord: 'Rekod naik taraf',
|
||||
upgrading: 'Sedang naik taraf, sila tunggu...',
|
||||
versionHelper: 'Peraturan nama: [versi utama].[versi fungsi].[versi pembaikan], seperti contoh berikut:',
|
||||
upgradeCheck: 'Semak kemas kini',
|
||||
panelSSLHelper:
|
||||
'Selepas pembaharuan automatik SSL panel, anda perlu mulakan semula servis 1Panel secara manual.',
|
||||
},
|
||||
license: {
|
||||
offLine: 'Versi Luar Talian',
|
||||
@@ -2286,6 +2336,9 @@ const message = {
|
||||
app: 'Tingkatkan ke versi profesional untuk melihat maklumat perkhidmatan, pemantauan abnormal, dll melalui aplikasi mudah alih.',
|
||||
cluster:
|
||||
'Versi Profesional menaik taraf membolehkan anda menguruskan kelompok induk-hamba MySQL/Postgres/Redis.',
|
||||
exceptionalHelper:
|
||||
'Pengesahan penyegerakan lesen tidak normal. Klik butang sync secara manual untuk pastikan fungsi Pro berjalan baik. butiran: ',
|
||||
tamperHelper: 'Operasi gagal, fail atau folder mempunyai perlindungan gangguan. Sila semak dan cuba lagi!',
|
||||
},
|
||||
clean: {
|
||||
scan: 'Mulakan imbasan',
|
||||
@@ -2304,7 +2357,6 @@ const message = {
|
||||
suggest: '(Disyorkan)',
|
||||
totalScan: 'Jumlah fail sampah untuk dibersihkan: ',
|
||||
selectScan: 'Jumlah fail sampah yang dipilih: ',
|
||||
|
||||
system: 'Fail Sampah Sistem',
|
||||
systemHelper: 'Fail sementara dijana semasa snapshot, naik taraf, dan proses lain',
|
||||
panelOriginal: 'Fail sandaran sebelum pemulihan snapshot sistem',
|
||||
@@ -2312,7 +2364,6 @@ const message = {
|
||||
agentPackages: 'Pakej naik taraf/pemasangan nod anak versi sejarah',
|
||||
upgradeHelper: '(Disarankan untuk mengekalkan sandaran peningkatan terbaru untuk pemulihan sistem)',
|
||||
rollback: 'Fail sandaran sebelum pemulihan',
|
||||
|
||||
backup: 'Sandaran Sistem',
|
||||
backupHelper: 'Fail sandaran tidak dikaitkan dengan akaun sandaran tempatan',
|
||||
unknownBackup: 'Sandaran Sistem',
|
||||
@@ -2321,13 +2372,11 @@ const message = {
|
||||
unknownDatabase: 'Sandaran Pangkalan Data Tidak Dikaitkan',
|
||||
unknownWebsite: 'Sandaran Laman Web Tidak Dikaitkan',
|
||||
unknownSnapshot: 'Sandaran Snapshot Tidak Dikaitkan',
|
||||
|
||||
upload: 'Fail Muat Naik Sementara',
|
||||
uploadHelper: 'Fail sementara dimuat naik dari senarai sandaran sistem',
|
||||
download: 'Fail Muat Turun Sementara',
|
||||
downloadHelper: 'Fail sementara dimuat turun dari akaun sandaran pihak ketiga oleh sistem',
|
||||
directory: 'Direktori',
|
||||
|
||||
systemLog: 'Fail log',
|
||||
systemLogHelper: 'Log sistem, log tugas, fail log laman web',
|
||||
dockerLog: 'Fail log operasi bekas',
|
||||
@@ -2335,14 +2384,13 @@ const message = {
|
||||
shell: 'Tugas berjadual skrip Shell',
|
||||
containerShell: 'Tugas berjadual skrip Shell dilaksanakan di dalam bekas',
|
||||
curl: 'Tugas berjadual CURL',
|
||||
|
||||
docker: 'Sampah bekas',
|
||||
dockerHelper: 'Fail seperti bekas, imej, isipadu, cache binaan, dsb.',
|
||||
volumes: 'Isipadu',
|
||||
buildCache: 'Cache Pembinaan Kontena',
|
||||
|
||||
appTmpDownload: 'Fail muat turun sementara aplikasi',
|
||||
unknownWebsiteLog: 'Fail sandaran log laman web tidak dikaitkan',
|
||||
snapshot: 'Fail sementara syot kilat sistem',
|
||||
},
|
||||
app: {
|
||||
app: 'Aplikasi | Aplikasi',
|
||||
@@ -2428,6 +2476,12 @@ const message = {
|
||||
showLocal: 'Papar aplikasi tempatan',
|
||||
reload: 'Muat Semula',
|
||||
upgradeWarn: 'Meningkatkan aplikasi akan menggantikan fail docker-compose.yml.',
|
||||
openclawHttpsUpgradeNoticeTitle:
|
||||
'Nota: Arahan berikut hanya terpakai kepada pengguna yang menaik taraf OpenClaw daripada versi lebih awal daripada 2026.3.13:',
|
||||
openclawHttpsUpgradeNoticeItem1:
|
||||
'Selepas ejen yang telah dipasang selesai dinaik taraf, pergi ke Konfigurasi -> Tetapan -> Keselamatan dan tambah alamat akses secara manual.',
|
||||
openclawHttpsUpgradeNoticeItem2:
|
||||
'Versi baharu kini mewajibkan HTTPS untuk mengakses ejen. Jika anda sebelum ini menggunakan laman reverse proxy, tukar alamat sasaran proksi kepada https://IP:Port.',
|
||||
newVersion: 'Versi baru',
|
||||
oldVersion: 'Versi semasa',
|
||||
composeDiff: 'Perbandingan fail',
|
||||
@@ -2569,7 +2623,8 @@ const message = {
|
||||
deleteAppHelper: 'Padam aplikasi yang berkaitan dan sandaran aplikasi pada masa yang sama',
|
||||
deleteBackupHelper: 'Juga padamkan sandaran laman web.',
|
||||
deleteDatabaseHelper: 'Juga hapuskan pangkalan data yang berkaitan dengan laman web',
|
||||
deleteConfirmHelper: `Operasi pemadaman tidak boleh dibatalkan. Masukkan <span style="color:red"> "{0}" </span> untuk mengesahkan pemadaman.`,
|
||||
deleteConfirmHelper:
|
||||
'Operasi pemadaman tidak boleh dibatalkan. Masukkan <span style="color:red"> "{0}" </span> untuk mengesahkan pemadaman.',
|
||||
staticPath: 'Direktori utama yang sepadan ialah ',
|
||||
limit: 'Skim',
|
||||
blog: 'Forum/Blog',
|
||||
@@ -2795,7 +2850,6 @@ const message = {
|
||||
failTimeout: 'Masa tamat kegagalan',
|
||||
failTimeoutHelper:
|
||||
'Panjang tetingkap masa untuk pemeriksaan kesihatan pelayan. Apabila bilangan kegagalan terkumpul mencapai ambang dalam tempoh ini, pelayan akan dikeluarkan buat sementara waktu dan dicuba semula selepas tempoh yang sama. Lalai 10 saat',
|
||||
|
||||
staticChangePHPHelper: 'Kini laman web statik, boleh ditukar ke laman web PHP.',
|
||||
proxyCache: 'Cache Proksi Terbalik',
|
||||
cacheLimit: 'Had Ruang Cache',
|
||||
@@ -2823,7 +2877,6 @@ const message = {
|
||||
preflight: 'Tindak balas pantas permintaan preflight',
|
||||
preflightHleper:
|
||||
'Apabila didayakan, apabila pelayar menghantar permintaan preflight asal silang (permintaan OPTIONS), sistem akan secara automatik mengembalikan kod status 204 dan menetapkan pengepala respons asal silang yang diperlukan',
|
||||
|
||||
changeDatabase: 'Tukar Pangkalan Data',
|
||||
changeDatabaseHelper1: 'Perkaitan pangkalan data digunakan untuk sandaran dan pemulihan laman web.',
|
||||
changeDatabaseHelper2:
|
||||
@@ -2858,7 +2911,6 @@ const message = {
|
||||
execUser: 'Pengguna Melaksanakan',
|
||||
execDir: 'Direktori Pelaksanaan',
|
||||
packagist: 'Cermin Penuh China',
|
||||
|
||||
batchOperate: 'Operasi Pukal',
|
||||
batchOperateHelper: 'Pukal {0} laman web, teruskan operasi?',
|
||||
stream: 'Proksi TCP/UDP',
|
||||
@@ -2867,8 +2919,12 @@ const message = {
|
||||
'Tetapkan nombor port pendengaran luaran, pelanggan akan mengakses perkhidmatan melalui port ini, dipisahkan dengan koma, cth., 5222,5223',
|
||||
streamHelper: 'Penerusan Port dan Pengimbangan Beban TCP/UDP',
|
||||
udp: 'Dayakan UDP',
|
||||
|
||||
syncHtmlHelper: 'Segerakkan ke PHP dan laman web statik',
|
||||
wafValueHelper: 'Nilai',
|
||||
parentWbeiste: 'Laman induk',
|
||||
shareCahe: 'Saiz memori kiraan cache',
|
||||
shareCaheHelper: 'Kira-kira 8000 objek cache boleh disimpan bagi setiap 1M memori',
|
||||
cacheExpireJHelper: 'Cache akan dipadam jika tiada hit selepas tamat tempoh',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Sokongan tag pendek',
|
||||
@@ -2898,6 +2954,7 @@ const message = {
|
||||
containerConfigHelper:
|
||||
'Pembolehubah persekitaran dan maklumat lain boleh diubah suai dalam Konfigurasi - Konfigurasi Bekas selepas penciptaan',
|
||||
dateTimezoneHelper: 'Contoh: TZ=Asia/Shanghai (Sila tambahkan jika perlu)',
|
||||
extensionsHelper: 'Untuk berbilang sambungan, asingkan dengan koma',
|
||||
},
|
||||
nginx: {
|
||||
serverNamesHashBucketSizeHelper: 'Saiz jadual hash nama pelayan',
|
||||
@@ -2992,9 +3049,9 @@ const message = {
|
||||
'Dalam mod resolusi manual, anda perlu klik butang mohon selepas penciptaan untuk mendapatkan nilai resolusi DNS',
|
||||
httpHelper:
|
||||
'Menggunakan mod HTTP memerlukan pemasangan OpenResty dan tidak menyokong permohonan sijil domain wildcard.',
|
||||
buypassHelper: `Buypass tidak boleh diakses di tanah besar China`,
|
||||
buypassHelper: 'Buypass tidak boleh diakses di tanah besar China',
|
||||
googleHelper: 'Cara mendapatkan EAB HmacKey dan EAB kid',
|
||||
googleCloudHelper: `Google Cloud API tidak boleh diakses di kebanyakan kawasan tanah besar China`,
|
||||
googleCloudHelper: 'Google Cloud API tidak boleh diakses di kebanyakan kawasan tanah besar China',
|
||||
skipDNSCheck: 'Langkau semakan DNS',
|
||||
skipDNSCheckHelper: 'Semak di sini hanya jika anda menghadapi isu tamat masa semasa permintaan pengesahan.',
|
||||
cfHelper: 'Jangan gunakan Global API Key',
|
||||
@@ -3032,7 +3089,6 @@ const message = {
|
||||
argsCheck: 'Pengesahan parameter GET',
|
||||
postCheck: 'Pengesahan parameter POST',
|
||||
cookieBlockList: 'Senarai blok Cookie',
|
||||
|
||||
dockerHelper:
|
||||
'Firewall semasa tidak boleh melumpuhkan pemetaan port bekas. Aplikasi yang dipasang boleh pergi ke halaman [Dipasang] untuk mengedit parameter aplikasi dan mengkonfigurasi peraturan pelepasan port.',
|
||||
iptablesHelper:
|
||||
@@ -3042,7 +3098,7 @@ const message = {
|
||||
unUsed: 'Tidak Digunakan',
|
||||
dockerRestart: 'Operasi firewall memerlukan memulakan semula perkhidmatan Docker',
|
||||
firewallHelper: '{0} firewall sistem',
|
||||
firewallNotStart: `Firewall sistem belum diaktifkan. Aktifkannya dahulu.`,
|
||||
firewallNotStart: 'Firewall sistem belum diaktifkan. Aktifkannya dahulu.',
|
||||
restartFirewallHelper: 'Operasi ini akan memulakan semula firewall semasa. Adakah anda mahu meneruskan?',
|
||||
stopFirewallHelper:
|
||||
'Ini akan menyebabkan pelayan kehilangan perlindungan keselamatan. Adakah anda mahu meneruskan?',
|
||||
@@ -3052,7 +3108,8 @@ const message = {
|
||||
enableBanPing: 'Sekat Ping',
|
||||
disableBanPing: 'Nyahsekat Ping',
|
||||
noPingTitle: 'Lumpuhkan ping',
|
||||
noPingHelper: `Ini akan melumpuhkan ping, dan pelayan tidak akan memberikan tindak balas ICMP. Adakah anda mahu meneruskan?`,
|
||||
noPingHelper:
|
||||
'Ini akan melumpuhkan ping, dan pelayan tidak akan memberikan tindak balas ICMP. Adakah anda mahu meneruskan?',
|
||||
onPingHelper:
|
||||
'Ini akan mengaktifkan ping, dan penggodam mungkin menemui pelayan anda. Adakah anda mahu meneruskan?',
|
||||
changeStrategy: 'Tukar strategi {0}',
|
||||
@@ -3070,7 +3127,6 @@ const message = {
|
||||
portHelper2: 'Port rentang, contohnya 8080-8089',
|
||||
changeStrategyHelper:
|
||||
'Tukar strategi {0} [{1}] kepada [{2}]. Selepas tetapan, {0} akan mengakses {2} secara luaran. Adakah anda mahu meneruskan?',
|
||||
|
||||
strategy: 'Strategi',
|
||||
accept: 'Terima',
|
||||
drop: 'Lumpuhkan',
|
||||
@@ -3097,7 +3153,6 @@ const message = {
|
||||
exportHelper: 'Akan mengeksport {0} peraturan firewall. Teruskan?',
|
||||
importSuccess: '{0} peraturan berjaya diimport',
|
||||
importPartialSuccess: 'Import selesai: {0} berjaya, {1} gagal',
|
||||
|
||||
ipv4Limit: 'Operasi semasa hanya menyokong alamat IPv4',
|
||||
basicStatus: 'Rantaian semasa {0} tidak terikat, sila ikat dahulu!',
|
||||
baseIptables: 'Perkhidmatan iptables',
|
||||
@@ -3128,6 +3183,9 @@ const message = {
|
||||
portHelper: '0 bermaksud mana-mana port',
|
||||
allPorts: 'Semua Port',
|
||||
deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?',
|
||||
advancedControl: 'Kawalan lanjutan',
|
||||
advancedControlNotAvailable: 'Kini menggunakan firewall {0}, peraturan lanjutan hanya menyokong iptables',
|
||||
destination: 'Destinasi',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Runtime',
|
||||
@@ -3187,6 +3245,27 @@ const message = {
|
||||
concurrency: 'Skim Serentak',
|
||||
loadStatus: 'Status Beban',
|
||||
extraHosts: 'Pemetaan hos',
|
||||
extension: 'Sambungan',
|
||||
installExtension: 'Adakah anda pasti untuk memasang sambungan {0}',
|
||||
loadedExtension: 'Sambungan dimuatkan',
|
||||
popularExtension: 'Sambungan popular',
|
||||
uninstallExtension: 'Adakah anda pasti untuk nyahpasang sambungan {0}',
|
||||
phpConfigHelper: 'Perubahan konfigurasi memerlukan persekitaran operasi dimulakan semula, teruskan',
|
||||
operateMode: 'mod operasi',
|
||||
dynamic: 'dinamik',
|
||||
static: 'statik',
|
||||
ondemand: 'atas permintaan',
|
||||
dynamicHelper: 'Laraskan bilangan proses secara dinamik, sesuai untuk trafik berubah atau memori rendah',
|
||||
staticHelper: 'Bilangan proses tetap, sesuai untuk trafik stabil dan serentak tinggi, penggunaan sumber tinggi',
|
||||
ondemandHelper:
|
||||
'Proses dimulakan dan ditutup mengikut permintaan, penggunaan sumber optimum tetapi respons awal mungkin perlahan',
|
||||
max_children: 'bilangan maksimum proses yang dibenarkan',
|
||||
start_servers: 'bilangan proses semasa permulaan',
|
||||
min_spare_servers: 'bilangan minimum proses melahu',
|
||||
max_spare_servers: 'bilangan maksimum proses melahu',
|
||||
envKey: 'Nama',
|
||||
envValue: 'Nilai',
|
||||
environment: 'Pembolehubah persekitaran',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
@@ -3281,6 +3360,13 @@ const message = {
|
||||
diskType: 'Jenis Cakera',
|
||||
serial: 'Nombor Siri',
|
||||
noFail: 'Kegagalan pemasangan tidak menjejaskan permulaan sistem',
|
||||
management: 'Pengurusan cakera',
|
||||
partition: 'Sekatan',
|
||||
unmount: 'Nyahlekap',
|
||||
unmountHelper: 'Selepas nyahlekap, data mungkin tidak boleh diakses sementara. Teruskan?',
|
||||
mount: 'Lekap',
|
||||
partitionAlert: 'Operasi ini memformat seluruh cakera. Pastikan data telah dibackup!',
|
||||
mountPoint: 'Titik lekap',
|
||||
},
|
||||
xpack: {
|
||||
expiresTrialAlert:
|
||||
@@ -3494,7 +3580,6 @@ const message = {
|
||||
globalHelper:
|
||||
'Mod Global: Digerakkan apabila jumlah permintaan ke mana-mana URL dalam unit masa melebihi had',
|
||||
uriModeHelper: 'Mod URL: Digerakkan apabila jumlah permintaan ke satu URL dalam unit masa melebihi had',
|
||||
|
||||
ip: 'Senarai Hitam IP',
|
||||
globalSettingHelper:
|
||||
'Tetapan dengan tag [Laman Web] perlu diaktifkan dalam [Tetapan Laman Web], dan tetapan global hanya tetapan lalai untuk laman web yang baru dicipta',
|
||||
@@ -3566,6 +3651,7 @@ const message = {
|
||||
importRuleHelper: 'Satu peraturan per baris',
|
||||
},
|
||||
monitor: {
|
||||
'360': 'Pencarian 360',
|
||||
name: 'Pemantauan Laman Web',
|
||||
pv: 'Paparan Halaman',
|
||||
uv: 'Pelawat Unik',
|
||||
@@ -3636,7 +3722,6 @@ const message = {
|
||||
statusErr: 'Format kod status tidak sah',
|
||||
shenma: 'Shenma Search',
|
||||
duckduckgo: 'DuckDuckGo',
|
||||
'360': 'Pencarian 360',
|
||||
excludeUri: 'Kecualikan URI',
|
||||
top100Helper: 'Tunjukkan data 100 teratas',
|
||||
logSaveDay: 'Tempoh Penahanan Log (hari)',
|
||||
@@ -3748,7 +3833,6 @@ const message = {
|
||||
wafContent3: 'Cipta halaman tersuai untuk dipaparkan selepas permintaan disekat',
|
||||
wafTitle4: 'Peraturan Tersuai (ACL)',
|
||||
wafContent4: 'Sekat permintaan mengikut peraturan tersuai',
|
||||
|
||||
tamperTitle1: 'Pemantauan Integriti Fail',
|
||||
tamperContent1: 'Pantau integriti fail laman web, termasuk fail teras, skrip, dan fail konfigurasi.',
|
||||
tamperTitle2: 'Pengimbasan dan Pengesanan Masa Nyata',
|
||||
@@ -3760,7 +3844,6 @@ const message = {
|
||||
tamperTitle4: 'Log dan Analisis',
|
||||
tamperContent4:
|
||||
'Rekod log akses dan operasi fail untuk audit dan analisis selanjutnya oleh pentadbir serta mengenal pasti potensi ancaman keselamatan.',
|
||||
|
||||
settingTitle1: 'Mesej Selamat Datang Tersuai',
|
||||
settingContent1: 'Tetapkan mesej selamat datang tersuai pada halaman log masuk 1Panel.',
|
||||
settingTitle2: 'Logo Tersuai',
|
||||
@@ -3768,7 +3851,6 @@ const message = {
|
||||
settingTitle3: 'Ikon Laman Web Tersuai',
|
||||
settingContent3:
|
||||
'Benarkan memuat naik ikon tersuai untuk menggantikan ikon pelayar lalai, meningkatkan pengalaman pengguna.',
|
||||
|
||||
monitorTitle1: 'Trend Pelawat',
|
||||
monitorContent1: 'Statistik dan memaparkan trend pelawat laman web',
|
||||
monitorTitle2: 'Peta Pelawat',
|
||||
@@ -3779,7 +3861,6 @@ const message = {
|
||||
monitorTitle4: 'Pemantauan Masa Nyata',
|
||||
monitorContent4:
|
||||
'Pemantauan masa nyata maklumat permintaan laman web, termasuk bilangan permintaan, trafik, dan sebagainya.',
|
||||
|
||||
alertTitle1: 'Amaran SMS',
|
||||
alertContent1:
|
||||
'Apabila berlaku penggunaan sumber pelayan yang tidak normal, tamat tempoh laman web dan sijil, kemas kini versi baru, tamat tempoh kata laluan, dan sebagainya, pengguna akan diberitahu melalui amaran SMS untuk memastikan pemprosesan tepat pada masanya.',
|
||||
@@ -3789,7 +3870,6 @@ const message = {
|
||||
alertTitle3: 'Tetapan Amaran',
|
||||
alertContent3:
|
||||
'Memberikan pengguna konfigurasi nombor telefon tersuai, kekerapan push harian, dan masa push harian, memudahkan pengguna untuk menetapkan amaran push yang lebih munasabah.',
|
||||
|
||||
nodeDashTitle1: 'Pengurusan Aplikasi',
|
||||
nodeDashContent1:
|
||||
'Pengurusan bersatu aplikasi berbilang nod, menyokong pemantauan status, mula/henti pantas, sambungan terminal dan sandaran',
|
||||
@@ -3802,7 +3882,6 @@ const message = {
|
||||
nodeDashTitle4: 'Pengurusan Tugas Berjadual',
|
||||
nodeDashContent4:
|
||||
'Pengurusan bersatu tugas berjadual berbilang nod, menyokong pemantauan status, mula/henti pantas dan pelaksanaan pencetus manual',
|
||||
|
||||
nodeTitle1: 'Satu Klik Tambah Node',
|
||||
nodeContent1: 'Mengintegrasikan pelbagai nod pelayan dengan cepat',
|
||||
nodeTitle2: 'Kelompok Naik Taraf',
|
||||
@@ -3811,7 +3890,6 @@ const message = {
|
||||
nodeContent3: 'Memantau status operasi setiap nod secara real-time',
|
||||
nodeTitle4: 'Sambungan Jauh Pantas',
|
||||
nodeContent4: 'Sambung terus ke terminal jauh nod dengan satu klik',
|
||||
|
||||
fileExchangeTitle1: 'Pengangkutan Pengesahan Kunci',
|
||||
fileExchangeContent1: 'Mengesahkan melalui kunci SSH untuk memastikan keselamatan pengangkutan.',
|
||||
fileExchangeTitle2: 'Segerakan Fail Cekap',
|
||||
@@ -3820,10 +3898,8 @@ const message = {
|
||||
fileExchangeTitle3: 'Sokongan Pertukaran Inter-Node',
|
||||
fileExchangeContent3:
|
||||
'Mudah memindahkan fail projek antara nod yang berbeza, mengurus pelbagai pelayan dengan fleksibiliti.',
|
||||
|
||||
nodeAppTitle1: 'Pengurusan Naik Taraf Aplikasi',
|
||||
nodeAppContent1: 'Pemantauan bersatu kemas kini aplikasi berbilang nod, menyokong naik taraf satu klik',
|
||||
|
||||
appTitle1: 'Pengurusan Panel yang Fleksibel',
|
||||
appContent1: 'Uruskan pelayan 1Panel anda dengan mudah pada bila-bila masa dan di mana sahaja.',
|
||||
appTitle2: 'Maklumat Perkhidmatan Komprehensif',
|
||||
@@ -3832,7 +3908,6 @@ const message = {
|
||||
appTitle3: 'Pemantauan Anomali Masa Nyata',
|
||||
appContent3:
|
||||
'Lihat status pelayan, pemantauan keselamatan WAF, statistik pelawat laman web dan status kesihatan proses di aplikasi mudah alih secara masa nyata.',
|
||||
|
||||
clusterTitle1: 'Penyebaran Utama-Hamba',
|
||||
clusterContent1:
|
||||
'Menyokong penciptaan contoh utama-hamba MySQL/Postgres/Redis pada nod yang berbeza, secara automatik melengkapkan perhubungan utama-hamba dan permulaan',
|
||||
@@ -3848,7 +3923,7 @@ const message = {
|
||||
masterBackup: 'Sandaran Nod Master',
|
||||
backupNode: 'Nod Sandaran',
|
||||
backupFrequency: 'Kekerapan Sandaran (jam)',
|
||||
backupCopies: 'Bilangan salinan sandaran yang disimpan',
|
||||
backupCopies: 'Salinan Sandaran',
|
||||
noBackupNode: 'Nod sandaran kosong. Sila pilih nod sandaran untuk disimpan dan cuba lagi!',
|
||||
masterBackupAlert:
|
||||
'Sandaran nod master belum dikonfigurasikan. Untuk memastikan keselamatan data, sila sediakan nod sandaran secepat mungkin untuk memudahkan pertukaran manual ke nod master baru sekiranya berlaku kegagalan.',
|
||||
@@ -3900,7 +3975,6 @@ const message = {
|
||||
nodeSyncHelper: 'Penyelarasan maklumat nod akan menyelaraskan maklumat berikut:',
|
||||
nodeSyncHelper1: '1. Maklumat akaun sandaran awam',
|
||||
nodeSyncHelper2: '2. Maklumat sambungan antara nod utama dan nod sub',
|
||||
|
||||
nodeCheck: 'Semakan ketersediaan',
|
||||
checkSSH: 'Periksa sambungan SSH nod',
|
||||
checkUserPermission: 'Semak kebenaran pengguna nod',
|
||||
@@ -3921,7 +3995,6 @@ const message = {
|
||||
errLicense: 'Lesen yang terikat pada nod ini tidak tersedia. Sila semak dan cuba lagi!',
|
||||
errNodePort:
|
||||
'Port nod [ {0} ] dikesan tidak boleh diakses. Sila semak sama ada firewall atau kumpulan keselamatan telah membenarkan port ini.',
|
||||
|
||||
reinstallHelper: 'Pasang semula nod {0}, adakah anda ingin meneruskan?',
|
||||
unhealthyCheck: 'Pemeriksaan Tidak Normal',
|
||||
fixOperation: 'Operasi Pembetulan',
|
||||
@@ -3947,7 +4020,6 @@ const message = {
|
||||
offline: 'Mod luar talian',
|
||||
freeCount: 'Had percuma [{0}]',
|
||||
offlineHelper: 'Digunakan apabila nod berada dalam persekitaran luar talian',
|
||||
|
||||
appUpgrade: 'Naik Taraf Apl',
|
||||
appUpgradeHelper: 'Terdapat {0} apl yang perlu dinaik taraf',
|
||||
},
|
||||
@@ -4044,7 +4116,6 @@ const message = {
|
||||
specifiedTime: 'Masa Tertentu',
|
||||
deleteTitle: 'Padam Amaran',
|
||||
deleteMsg: 'Adakah anda pasti ingin memadam tugas amaran?',
|
||||
|
||||
allSslTitle: 'Semua Amaran Tamat Tempoh Sijil SSL Laman Web',
|
||||
sslTitle: 'Amaran Tamat Tempoh Sijil SSL untuk Laman Web {0}',
|
||||
allSiteEndTimeTitle: 'Semua Amaran Tamat Tempoh Laman Web',
|
||||
@@ -4056,7 +4127,6 @@ const message = {
|
||||
loadTitle: 'Amaran Beban Tinggi',
|
||||
diskTitle: 'Amaran Penggunaan Cakera Tinggi untuk Direktori {0}',
|
||||
allDiskTitle: 'Amaran Penggunaan Cakera Tinggi',
|
||||
|
||||
timeRule: 'Masa baki kurang daripada {0} hari (jika tidak diurus, akan dihantar semula keesokan hari)',
|
||||
panelUpdateRule:
|
||||
'Hantar satu amaran apabila versi panel baharu dikesan (jika tidak diurus, akan dihantar semula keesokan hari)',
|
||||
@@ -4065,12 +4135,10 @@ const message = {
|
||||
diskRule:
|
||||
'Penggunaan cakera untuk direktori {0} melebihi {1}{2}, mencetuskan amaran, dihantar {3} kali sehari',
|
||||
allDiskRule: 'Penggunaan cakera melebihi {0}{1}, mencetuskan amaran, dihantar {2} kali sehari',
|
||||
|
||||
cpuName: ' CPU ',
|
||||
memoryName: 'Memori',
|
||||
loadName: 'Beban',
|
||||
diskName: 'Cakera',
|
||||
|
||||
syncAlertInfo: 'Tekan Manual',
|
||||
syncAlertInfoMsg: 'Adakah anda ingin menekan tugas amaran secara manual?',
|
||||
pushError: 'Tekanan Gagal',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+142
-78
@@ -84,6 +84,10 @@ const message = {
|
||||
hide: 'Скрыть',
|
||||
visit: 'Посетить',
|
||||
migrate: 'Мигрировать',
|
||||
prev: 'Назад',
|
||||
next: 'Далее',
|
||||
setDefault: 'Сбросить по умолчанию',
|
||||
bind: 'Привязать',
|
||||
},
|
||||
operate: {
|
||||
start: 'Запустить',
|
||||
@@ -133,6 +137,8 @@ const message = {
|
||||
manageGroup: 'Управление группами',
|
||||
backToList: 'Вернуться к списку',
|
||||
keepEdit: 'Продолжить редактирование',
|
||||
default: 'По умолчанию',
|
||||
noRefresh: 'Без автообновления',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Обновление системы, пожалуйста, подождите...',
|
||||
@@ -181,6 +187,10 @@ const message = {
|
||||
importHelper:
|
||||
'При импорте конфликтующих или дублирующихся данных импортированное содержимое будет использоваться в качестве стандарта для обновления исходных данных базы данных.',
|
||||
errImport: 'Содержимое файла ненормально:',
|
||||
backupSuccess: 'Резервное копирование выполнено',
|
||||
restoreSuccess: 'Восстановление выполнено',
|
||||
installSuccess: 'Установка выполнена',
|
||||
uninstallSuccess: 'Удаление выполнено',
|
||||
},
|
||||
login: {
|
||||
username: 'пользователя',
|
||||
@@ -341,6 +351,7 @@ const message = {
|
||||
conflict: 'Конфликт',
|
||||
duplicate: 'Дубликат',
|
||||
unexecuted: 'Не Выполнено',
|
||||
unused: 'Не используется',
|
||||
},
|
||||
units: {
|
||||
second: ' секунда | секунда | секунд',
|
||||
@@ -394,6 +405,7 @@ const message = {
|
||||
app: 'Приложение',
|
||||
msgCenter: 'Задачи',
|
||||
disk: 'Диск',
|
||||
filter: 'Фильтр',
|
||||
},
|
||||
home: {
|
||||
recommend: 'Рекомендуемое',
|
||||
@@ -426,12 +438,10 @@ const message = {
|
||||
runningTime: 'Время работы',
|
||||
mem: 'Системная Память',
|
||||
swapMem: 'Раздел подкачки',
|
||||
|
||||
runSmoothly: 'Низкая нагрузка',
|
||||
runNormal: 'Средняя нагрузка',
|
||||
runSlowly: 'Высокая нагрузка',
|
||||
runJam: 'Тяжелая нагрузка',
|
||||
|
||||
core: 'Физических ядер',
|
||||
logicCore: 'Логических ядер',
|
||||
corePercent: 'Использование Ядра',
|
||||
@@ -457,7 +467,6 @@ const message = {
|
||||
available: 'Доступно',
|
||||
percent: 'Утилизация',
|
||||
goInstall: 'Установить',
|
||||
|
||||
networkCard: 'Интерфейс',
|
||||
disk: 'Диск',
|
||||
memo: 'Заметка',
|
||||
@@ -510,14 +519,11 @@ const message = {
|
||||
changePasswordHelper:
|
||||
'База данных была связана с приложением. Изменение пароля изменит пароль базы данных приложения одновременно. Изменение вступит в силу после перезапуска приложения.',
|
||||
recoverTimeoutHelper: '-1 означает отсутствие ограничения по времени ожидания',
|
||||
|
||||
confChange: 'Конфигурация',
|
||||
confNotFound:
|
||||
'Файл конфигурации не найден. Пожалуйста, обновите приложение до последней версии в магазине приложений и попробуйте снова!',
|
||||
|
||||
portHelper:
|
||||
'Этот порт является открытым портом контейнера. Вам нужно сохранить изменение отдельно и перезапустить контейнер!',
|
||||
|
||||
loadFromRemote: 'Синхронизировать',
|
||||
userBind: 'Привязать пользователя',
|
||||
pgBindHelper:
|
||||
@@ -548,7 +554,6 @@ const message = {
|
||||
hasCA: 'Есть сертификат CA',
|
||||
skipVerify: 'Игнорировать проверку действительности сертификата',
|
||||
initialDB: 'Исходная База Данных',
|
||||
|
||||
formatHelper:
|
||||
'Текущая кодировка базы данных - {0}, несоответствие кодировок может привести к ошибке восстановления',
|
||||
selectFile: 'Выбрать файл',
|
||||
@@ -556,7 +561,6 @@ const message = {
|
||||
clickHelper: 'нажмите для загрузки',
|
||||
supportUpType:
|
||||
'Поддерживаются только форматы файлов sql, sql.gz, tar.gz, .zip. Импортируемый сжатый файл должен содержать только один файл .sql или включать test.sql',
|
||||
|
||||
currentStatus: 'Текущее состояние',
|
||||
baseParam: 'Базовые параметры',
|
||||
performanceParam: 'Параметры производительности',
|
||||
@@ -589,7 +593,6 @@ const message = {
|
||||
tableLocksWaited: 'Количество блокировок таблиц',
|
||||
tableLocksWaitedHelper:
|
||||
'Если значение слишком большое, рассмотрите возможность увеличения производительности базы данных.',
|
||||
|
||||
performanceTuning: 'Настройка производительности',
|
||||
optimizationScheme: 'Схема оптимизации',
|
||||
keyBufferSizeHelper: 'Размер буфера для индексов',
|
||||
@@ -607,14 +610,11 @@ const message = {
|
||||
tableOpenCacheHelper: 'Кэш таблиц',
|
||||
maxConnectionsHelper: 'Максимум подключений',
|
||||
restart: 'Перезапустить',
|
||||
|
||||
slowLog: 'Медленные запросы',
|
||||
noData: 'Пока нет медленных запросов.',
|
||||
|
||||
isOn: 'Включено',
|
||||
longQueryTime: 'порог (сек)',
|
||||
thresholdRangeHelper: 'Пожалуйста, введите корректный порог (1 - 600).',
|
||||
|
||||
timeout: 'Таймаут(сек)',
|
||||
timeoutHelper: 'Период таймаута неактивного подключения. 0 означает, что подключение постоянно активно.',
|
||||
maxclients: 'Макс. клиентов',
|
||||
@@ -641,20 +641,17 @@ const message = {
|
||||
redisQuickCmd: 'Быстрые команды Redis',
|
||||
recoverHelper: 'Это перезапишет данные с [{0}]. Хотите продолжить?',
|
||||
submitIt: 'Перезаписать данные',
|
||||
|
||||
baseConf: 'Базовая',
|
||||
allConf: 'Все',
|
||||
restartNow: 'Перезапустить сейчас',
|
||||
restartNowHelper1:
|
||||
'Необходимо перезапустить систему после вступления изменений конфигурации в силу. Если ваши данные требуют сохранения, сначала выполните операцию сохранения.',
|
||||
restartNowHelper: 'Это вступит в силу только после перезапуска системы.',
|
||||
|
||||
persistence: 'Сохранение',
|
||||
rdbHelper1: 'секунд(ы), вставить',
|
||||
rdbHelper2: 'элементов данных',
|
||||
rdbHelper3: 'Выполнение любого из условий запустит сохранение RDB.',
|
||||
rdbInfo: 'Убедитесь, что значение в списке правил находится в диапазоне от 1 до 100000',
|
||||
|
||||
containerConn: 'Подключение контейнера',
|
||||
copyConnURL: 'Копировать URL подключения',
|
||||
connAddress: 'Адрес',
|
||||
@@ -665,6 +662,9 @@ const message = {
|
||||
remoteConnHelper3:
|
||||
'Адрес доступа по умолчанию - это IP хоста. Для изменения перейдите к пункту конфигурации "Адрес доступа по умолчанию" на странице настроек панели.',
|
||||
localIP: 'Локальный IP',
|
||||
localhostHelper:
|
||||
'Если для БД в контейнере указать "localhost", внешний доступ будет недоступен. Выбирайте внимательно!',
|
||||
changeConnHelper: 'Это изменит базу данных {0}. Продолжить?',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
@@ -679,33 +679,38 @@ const message = {
|
||||
copawType: 'CoPaw',
|
||||
appVersion: 'Версия приложения',
|
||||
webuiPort: 'Порт WebUI',
|
||||
bridgePort: 'Порт Bridge',
|
||||
allowedOrigins: 'Адреса доступа',
|
||||
allowedOriginsHelper:
|
||||
'Указывайте по одному полному HTTPS-адресу доступа в строке, например https://192.168.1.2:18789. Если адрес по умолчанию не настроен, введите его вручную.',
|
||||
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Укажите хотя бы один адрес доступа',
|
||||
allowedOriginsInvalid: 'Используйте формат http(s)://host-or-ip:port',
|
||||
allowedOriginsHttpsOnly: 'Адреса доступа должны начинаться с https://',
|
||||
provider: 'Поставщик моделей',
|
||||
apiKey: 'API ключ',
|
||||
baseUrl: 'Базовый URL',
|
||||
token: 'Токен',
|
||||
manualModel: 'Ручной ввод',
|
||||
verified: 'Проверено',
|
||||
verifySkipped: 'Пропущено',
|
||||
verifySkipped: 'Без проверки',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Часовой пояс',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
feishu: 'Feishu',
|
||||
pluginNotInstalled: 'Плагин не установлен. Сначала установите его.',
|
||||
dmPolicy: 'DM Policy',
|
||||
groupPolicy: 'Group Policy',
|
||||
policyPairing: 'Pairing',
|
||||
policyOpen: 'Open',
|
||||
policyDisabled: 'Disabled',
|
||||
botName: 'Bot Name',
|
||||
botId: 'Bot ID',
|
||||
appId: 'App ID',
|
||||
appSecret: 'App Secret',
|
||||
saveAndRestartGateway: 'Save and restart gateway',
|
||||
@@ -717,9 +722,11 @@ const message = {
|
||||
pairingCodeRequired: 'Please enter pairing code',
|
||||
pairingApproveSuccess: 'Pairing approved successfully',
|
||||
customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется',
|
||||
feishuSaveSuccess: 'Сохранено в Feishu',
|
||||
},
|
||||
model: {
|
||||
model: 'Модель',
|
||||
localModel: 'Локальные модели',
|
||||
create: 'Добавить модель',
|
||||
create_helper: 'Загрузить "{0}"',
|
||||
ollama_doc: 'Вы можете посетить официальный сайт Ollama, чтобы искать и находить больше моделей.',
|
||||
@@ -728,6 +735,7 @@ const message = {
|
||||
'Синхронизация модели Ollama обнаружила, что следующие модели не существуют, хотите удалить их?',
|
||||
from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.',
|
||||
no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.',
|
||||
vllmVersionHelper: 'Для серверов FusionXpark GB 10 выберите версию -cu130.',
|
||||
},
|
||||
proxy: {
|
||||
proxy: 'Усиление AI-прокси',
|
||||
@@ -764,7 +772,6 @@ const message = {
|
||||
memoryUsed: 'Использованная Память',
|
||||
memoryTotal: 'Общая Память',
|
||||
percent: 'Использование',
|
||||
|
||||
base: 'Основная Информация',
|
||||
driverVersion: 'Версия Драйвера',
|
||||
cudaVersion: 'Версия CUDA',
|
||||
@@ -796,6 +803,7 @@ const message = {
|
||||
current: 'Мониторинг в Реальном Времени',
|
||||
history: 'Исторические Записи',
|
||||
notSupport: 'Текущая версия или драйвер не поддерживает отображение этого параметра.',
|
||||
processCount: 'Кол-во процессов',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Сервер MCP',
|
||||
@@ -834,6 +842,12 @@ const message = {
|
||||
},
|
||||
container: {
|
||||
createByCommand: 'Создать с помощью команды',
|
||||
importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz',
|
||||
importComposeBackupTip: 'Импорт файла резервной копии compose, поддерживается только .tar.gz',
|
||||
stopContainerBeforeBackup: 'Остановить контейнер перед резервным копированием',
|
||||
stopComposeBeforeBackup: 'Остановить compose перед резервным копированием',
|
||||
stopBeforeBackupHelper:
|
||||
'При включении контейнер или сервис compose будет остановлен перед резервным копированием и автоматически восстановлен после завершения для обеспечения целостности данных.',
|
||||
commandInput: 'Ввод команды',
|
||||
commandRule: 'Введите корректную команду docker run для создания контейнера.',
|
||||
commandHelper: 'Эта команда будет выполнена на сервере для создания контейнера. Продолжить?',
|
||||
@@ -908,7 +922,6 @@ const message = {
|
||||
'Движок контейнера использует базовое значение 1024 для долей CPU. Вы можете увеличить его, чтобы дать контейнеру больше времени CPU.',
|
||||
inputIpv4: 'Пример: 192.168.1.1',
|
||||
inputIpv6: 'Пример: 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
|
||||
diskUsage: 'Использование Диска',
|
||||
localVolume: 'Локальный Том Хранилища',
|
||||
buildCache: 'Кэш Сборки',
|
||||
@@ -921,7 +934,6 @@ const message = {
|
||||
sizeRwHelper: 'Размер записываемого слоя, уникального для контейнера',
|
||||
sizeRootFs: 'Виртуальный Размер',
|
||||
sizeRootFsHelper: 'Общий размер всех слоев образа, от которых зависит контейнер + слой контейнера',
|
||||
|
||||
containerFromAppHelper:
|
||||
'Обнаружено, что этот контейнер происходит из магазина приложений. Операции с приложением могут привести к недействительности текущих изменений.',
|
||||
containerFromAppHelper1:
|
||||
@@ -935,7 +947,6 @@ const message = {
|
||||
privileged: 'Привилегированный',
|
||||
privilegedHelper:
|
||||
'Разрешить контейнеру выполнять определенные привилегированные операции на хосте, что может повысить риски контейнера. Используйте с осторожностью!',
|
||||
|
||||
upgradeHelper: 'Имя репозитория/Имя образа: Версия образа',
|
||||
upgradeWarning2:
|
||||
'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?',
|
||||
@@ -944,12 +955,14 @@ const message = {
|
||||
sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора',
|
||||
targetImage: 'Целевой образ',
|
||||
imageLoadErr: 'Не обнаружено имя образа для контейнера',
|
||||
imageUpdateTagEmpty: 'Не обнаружены теги образов, доступные для обновления',
|
||||
appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.',
|
||||
|
||||
input: 'Ручной ввод',
|
||||
forcePull: 'Всегда загружать образ',
|
||||
forcePullHelper:
|
||||
'Это будет игнорировать существующие образы на сервере и загружать последний образ из реестра.',
|
||||
imageUpdateHelper:
|
||||
'Проверить одноимённый тег в реестре и, если есть обновление, загрузить и обновить локальный образ.',
|
||||
server: 'Хост',
|
||||
serverExample: '80, 80-88, ip:80 или ip:80-88',
|
||||
containerExample: '80 или 80-88',
|
||||
@@ -991,10 +1004,8 @@ const message = {
|
||||
unlessStopped: 'если не остановлен',
|
||||
onFailure: 'при сбое (по умолчанию пять раз)',
|
||||
no: 'никогда',
|
||||
|
||||
refreshTime: 'Интервал обновления',
|
||||
cache: 'Кэш',
|
||||
|
||||
image: 'Образ | Образы',
|
||||
imagePull: 'Загрузить',
|
||||
imagePullHelper:
|
||||
@@ -1027,7 +1038,6 @@ const message = {
|
||||
delBuildCacheHelper:
|
||||
'Это удалит все кэшированные артефакты, созданные во время сборки, и не может быть отменено. Хотите продолжить?',
|
||||
urlWarning: 'Префикс URL не должен включать http:// или https://. Пожалуйста, измените.',
|
||||
|
||||
network: 'Сеть | Сети',
|
||||
networkHelper:
|
||||
'Это может привести к неправильной работе некоторых приложений и сред выполнения. Хотите продолжить?',
|
||||
@@ -1040,7 +1050,6 @@ const message = {
|
||||
scope: 'Диапазон IP',
|
||||
gateway: 'Шлюз',
|
||||
auxAddress: 'Исключить IP',
|
||||
|
||||
volume: 'Том | Тома',
|
||||
volumeDir: 'Директория тома',
|
||||
nfsEnable: 'Включить NFS хранилище',
|
||||
@@ -1048,7 +1057,6 @@ const message = {
|
||||
mountpoint: 'Точка монтирования',
|
||||
mountpointNFSHelper: 'например, /nfs, /nfs-share',
|
||||
options: 'Опции',
|
||||
|
||||
repo: 'Реестры',
|
||||
httpRepoHelper: 'Работа с репозиторием HTTP-типа требует перезапуска службы Docker.',
|
||||
httpRepo: 'Выбор HTTP протокола требует перезапуска службы Docker для добавления в небезопасные реестры.',
|
||||
@@ -1062,7 +1070,6 @@ const message = {
|
||||
'Если есть несколько зеркал, они должны быть разделены новой строкой, например:\nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
|
||||
registrieHelper:
|
||||
'Если существует несколько частных репозиториев, они должны быть разделены новой строкой, например:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: 'Compose | Composes',
|
||||
composeFile: 'Файл Оркестрации',
|
||||
fromChangeHelper: 'Переключение источника очистит текущее отредактированное содержимое. Хотите продолжить?',
|
||||
@@ -1087,8 +1094,9 @@ const message = {
|
||||
composeDownHelper: 'Это остановит и удалит все контейнеры и сети под compose {0}. Хотите продолжить?',
|
||||
composeEnvHelper2:
|
||||
'Эта оркестрация создана Магазином приложений 1Panel. Измените переменные среды в установленных приложениях.',
|
||||
|
||||
composeContentPlaceholder: 'Введите или вставьте здесь содержимое вашего файла docker-compose',
|
||||
setting: 'Настройка | Настройки',
|
||||
restartHelper: 'Изменения конфигурации вступят в силу после перезапуска службы Docker',
|
||||
operatorStatusHelper: 'Это выполнит "{0}" службы Docker. Хотите продолжить?',
|
||||
dockerStatus: 'Служба Docker',
|
||||
daemonJsonPathHelper: 'Убедитесь, что путь конфигурации совпадает с указанным в docker.service.',
|
||||
@@ -1128,12 +1136,10 @@ const message = {
|
||||
related: 'Связанные',
|
||||
includeAppstore: 'Показывать контейнеры из магазина приложений',
|
||||
excludeAppstore: 'Скрыть контейнер магазина приложений',
|
||||
|
||||
cleanDockerDiskZone: 'Очистить дисковое пространство, используемое Docker',
|
||||
cleanImagesHelper: '(Очистить все образы, не используемые контейнерами)',
|
||||
cleanContainersHelper: '(Очистить все остановленные контейнеры)',
|
||||
cleanVolumesHelper: '(Очистить все неиспользуемые локальные тома)',
|
||||
|
||||
makeImage: 'Создать образ',
|
||||
newImageName: 'Новое имя образа',
|
||||
commitMessage: 'Сообщение коммита',
|
||||
@@ -1141,6 +1147,11 @@ const message = {
|
||||
ifPause: 'Приостановить контейнер во время создания',
|
||||
ifMakeImageWithContainer: 'Создать новый образ из этого контейнера?',
|
||||
finishTime: 'Время последней остановки',
|
||||
workingDir: 'Рабочий каталог',
|
||||
resource: 'Ресурс',
|
||||
macAddr: 'MAC-адрес',
|
||||
exportHelper: 'Экспортировать выбранные образы в один tar-файл.',
|
||||
goSetting: 'Перейти к редактированию',
|
||||
},
|
||||
cronjob: {
|
||||
importHelper:
|
||||
@@ -1253,7 +1264,6 @@ const message = {
|
||||
cutWebsiteLog: 'Ротация логов сайта',
|
||||
cutWebsiteLogHelper: 'Ротированные файлы логов будут сохранены в директории резервных копий 1Panel.',
|
||||
syncIpGroup: 'Синхронизировать группы IP WAF',
|
||||
|
||||
requestExpirationTime: 'Время истечения запроса на загрузку (часы)',
|
||||
unitHours: 'Единица: часы',
|
||||
alertTitle: 'Плановая задача - {0} «{1}» Оповещение о сбое задачи',
|
||||
@@ -1282,6 +1292,13 @@ const message = {
|
||||
syncHelper:
|
||||
'Выполнить синхронизацию библиотеки системных скриптов? Это действие затрагивает только системные скрипты. Продолжить?',
|
||||
},
|
||||
nextTime: 'Следующие 5 запусков',
|
||||
logHelper1: '1. Системный лог 1Panel',
|
||||
backupContent: 'Содержимое бэкапа',
|
||||
cronSpecRule2: 'Неверный формат периода выполнения, проверьте и попробуйте снова!',
|
||||
executor: 'Исполнитель',
|
||||
cleanLog: 'Автоочистка логов',
|
||||
cleanLogscope: 'Хранение логов',
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Глобальный Фильтр',
|
||||
@@ -1293,7 +1310,6 @@ const message = {
|
||||
defaultIOHelper: 'Опция диска по умолчанию, отображаемая в интерфейсах мониторинга и обзора',
|
||||
cleanMonitor: 'Очистить Записи Мониторинга',
|
||||
cleanHelper: 'Эта операция очистит все записи мониторинга, включая GPU. Продолжить?',
|
||||
|
||||
avgLoad: 'Средняя нагрузка',
|
||||
loadDetail: 'Детали нагрузки',
|
||||
resourceUsage: 'Использование',
|
||||
@@ -1431,6 +1447,7 @@ const message = {
|
||||
dnsCheck: 'Проверить доступность',
|
||||
dnsOK: 'Информация о конфигурации DNS доступна!',
|
||||
dnsTestFailed: 'Информация о конфигурации DNS недоступна.',
|
||||
syncSiteHelper: 'Синхронизировать системное время с {0}?',
|
||||
},
|
||||
fail2ban: {
|
||||
sshPort: 'Прослушивание SSH порта',
|
||||
@@ -1550,6 +1567,9 @@ const message = {
|
||||
websiteLog: 'Логи веб-сайта',
|
||||
runLog: 'Логи выполнения',
|
||||
errLog: 'Логи ошибок',
|
||||
task: 'Лог задач',
|
||||
taskName: 'Имя задачи',
|
||||
taskRunning: 'Выполняется',
|
||||
},
|
||||
file: {
|
||||
fileDirNum: '{0} каталогов, {1} файлов,',
|
||||
@@ -1776,6 +1796,7 @@ const message = {
|
||||
loginTime: 'Время входа',
|
||||
loginIP: 'IP входа',
|
||||
stopSSHWarn: 'Отключить это SSH-соединение',
|
||||
remote: 'Удаленный SSH',
|
||||
},
|
||||
setting: {
|
||||
panel: 'Панель',
|
||||
@@ -1811,6 +1832,10 @@ const message = {
|
||||
light: 'Светлая',
|
||||
auto: 'Как в системе',
|
||||
language: 'Язык',
|
||||
runtimeEnv: 'Среда выполнения',
|
||||
docSource: 'Источник документации',
|
||||
withByRegion: 'Следовать региону работы (по умолчанию)',
|
||||
withByLang: 'Следовать языку системы',
|
||||
region: 'Регион работы',
|
||||
cn: 'Материковый Китай',
|
||||
intl: 'Глобальный',
|
||||
@@ -1819,6 +1844,8 @@ const message = {
|
||||
regionHelper2: 'Магазин приложений и библиотека скриптов',
|
||||
regionHelper3: 'Руководство пользователя и сопутствующая документация',
|
||||
regionHelper4: 'Это может повлиять на последующие загрузки и доступ. Действуйте осторожно.',
|
||||
regionTip: 'Регион работы влияет на источник обновления системы и адрес загрузки установочных пакетов.',
|
||||
docSourceTip: 'Источник документации определяет язык перехода для документации и журнала обновлений.',
|
||||
languageHelper: 'По умолчанию следует языку браузера. Этот параметр действует только в текущем браузере',
|
||||
sessionTimeout: 'Время сессии',
|
||||
sessionTimeoutError: 'Минимальное время сессии 300 секунд',
|
||||
@@ -1831,7 +1858,8 @@ const message = {
|
||||
proxyHelper1:
|
||||
'Загрузка установочного пакета и синхронизация из магазина приложений (только профессиональная версия)',
|
||||
proxyHelper2: 'Обновление системы и получение информации об обновлениях (только профессиональная версия)',
|
||||
proxyHelper4: 'Сеть Docker будет доступна через прокси-сервер (только профессиональная версия)',
|
||||
proxyHelper4:
|
||||
'Сетевые запросы, такие как загрузка образов Docker, будут выполняться через прокси-сервер (только профессиональная версия)',
|
||||
proxyHelper3: 'Проверка и синхронизация системной лицензии',
|
||||
proxyHelper5: 'Единая загрузка и синхронизация системных скрипт-библиотек (функция Профессиональной версии)',
|
||||
proxyHelper6: 'Подать заявку на сертификат (функция профессиональной версии)',
|
||||
@@ -1975,7 +2003,6 @@ const message = {
|
||||
accountKey: 'Ключ аккаунта',
|
||||
address: 'Адрес',
|
||||
path: 'Путь',
|
||||
|
||||
safe: 'Безопасность',
|
||||
passkey: 'Passkey',
|
||||
passkeyManage: 'Управление',
|
||||
@@ -2086,7 +2113,6 @@ const message = {
|
||||
error416: 'Диапазон не удовлетворяется',
|
||||
error444: 'Соединение закрыто',
|
||||
error500: 'Ошибка сервера',
|
||||
|
||||
https: 'Настройка HTTPS для панели повышает безопасность доступа',
|
||||
strictHelper: 'Трафик без HTTPS не может подключиться к панели',
|
||||
muxHelper:
|
||||
@@ -2103,7 +2129,6 @@ const message = {
|
||||
certificate: 'Сертификат',
|
||||
backupJump:
|
||||
'Файлы резервной копии отсутствуют в текущем списке резервных копий, попробуйте скачать из директории файлов и импортировать для резервного копирования.',
|
||||
|
||||
snapshot: 'Снапшот | Снапшоты',
|
||||
noAppData: 'Нет доступных системных приложений для выбора',
|
||||
noBackupData: 'Нет доступных данных для резервного копирования',
|
||||
@@ -2186,10 +2211,9 @@ const message = {
|
||||
'Версия узла не совпадает с основной. Пожалуйста, обновите в Управлении узлами перед повторной попыткой.',
|
||||
versionCompare:
|
||||
'Обнаружено, что узел {0} уже имеет последнюю обновляемую версию. Пожалуйста, проверьте версию основного узла и повторите попытку!',
|
||||
|
||||
about: 'О программе',
|
||||
versionItem: 'Текущая Версия',
|
||||
backupCopies: 'Количество Копий для Сохранения',
|
||||
backupCopies: 'Копии Бэкапа',
|
||||
backupCopiesHelper:
|
||||
'Установите количество копий резервных копий обновления для сохранения для отката версии. 0 означает сохранить все.',
|
||||
backupCopiesRule: 'Пожалуйста, сохраните как минимум 3 записи резервных копий обновления',
|
||||
@@ -2204,7 +2228,6 @@ const message = {
|
||||
forum: 'Обсуждения',
|
||||
doc2: 'Документация',
|
||||
currentVersion: 'Версия',
|
||||
|
||||
license: 'Лицензия',
|
||||
bindNode: 'Привязать Узел',
|
||||
menuSetting: 'Настройки меню',
|
||||
@@ -2219,6 +2242,34 @@ const message = {
|
||||
compressPassword: 'Пароль сжатия',
|
||||
backupRecoverMessage:
|
||||
'Пожалуйста, введите пароль для сжатия или распаковки (оставьте пустым, чтобы не устанавливать)',
|
||||
componentSize: 'Размер компонента',
|
||||
scope: 'Область',
|
||||
public: 'Публичный',
|
||||
publicHelper:
|
||||
'Публичные аккаунты резервного копирования синхронизируются на все подузлы и могут использоваться совместно',
|
||||
private: 'Приватный',
|
||||
privateHelper:
|
||||
'Приватные аккаунты резервного копирования создаются только на текущем узле и используются только им',
|
||||
UPYUN: 'UPYUN',
|
||||
ALIYUN: 'Aliyun Drive',
|
||||
ALIYUNHelper:
|
||||
'Текущий лимит скачивания Aliyun Drive без клиента — 100MB. Больше можно скачать только через клиент.',
|
||||
ALIYUNRecover:
|
||||
'Текущий лимит скачивания Aliyun Drive без клиента — 100MB. При превышении скачайте через клиент локально, затем синхронизируйте снимок для восстановления.',
|
||||
GoogleDrive: 'Google Drive',
|
||||
analysis: 'Анализ',
|
||||
analysisHelper:
|
||||
'Вставьте весь токен, нужные поля будут разобраны автоматически. Подробности смотрите в официальной документации.',
|
||||
serviceName: 'Имя сервиса',
|
||||
operator: 'Оператор',
|
||||
clickToRefresh: 'Нажмите для обновления',
|
||||
recoverFailed: 'Не удалось восстановить снимок',
|
||||
upgradeRecord: 'История обновлений',
|
||||
upgrading: 'Идет обновление, пожалуйста, подождите...',
|
||||
versionHelper: 'Правило именования: [мажор].[функции].[исправления], пример ниже:',
|
||||
upgradeCheck: 'Проверить обновления',
|
||||
panelSSLHelper:
|
||||
'После автообновления SSL панели нужно вручную перезапустить сервис 1Panel, чтобы изменения вступили в силу.',
|
||||
},
|
||||
license: {
|
||||
offLine: 'Офлайн версия',
|
||||
@@ -2275,6 +2326,9 @@ const message = {
|
||||
app: 'Обновите до профессиональной версии, чтобы просматривать информацию о сервисах, мониторинг аномалий и т.д. через мобильное приложение.',
|
||||
cluster:
|
||||
'Обновление до профессиональной версии позволяет управлять кластерами мастер-слейв MySQL/Postgres/Redis.',
|
||||
exceptionalHelper:
|
||||
'Проверка синхронизации лицензии выполнена с ошибкой. Нажмите синхронизацию вручную для корректной работы Pro. Детали: ',
|
||||
tamperHelper: 'Операция не выполнена: для файла или папки включена защита от изменений. Проверьте и повторите!',
|
||||
},
|
||||
clean: {
|
||||
scan: 'Начать сканирование',
|
||||
@@ -2293,7 +2347,6 @@ const message = {
|
||||
suggest: '(Рекомендуется)',
|
||||
totalScan: 'Всего мусорных файлов для очистки: ',
|
||||
selectScan: 'Всего выбранных мусорных файлов: ',
|
||||
|
||||
system: 'Системные мусорные файлы',
|
||||
systemHelper: 'Временные файлы, созданные во время снимков, обновлений и других процессов',
|
||||
panelOriginal: 'Файлы резервных копий перед восстановлением снимка системы',
|
||||
@@ -2301,7 +2354,6 @@ const message = {
|
||||
agentPackages: 'Пакеты обновления/установки дочерних узлов исторических версий',
|
||||
upgradeHelper: '(Рекомендуется сохранять последнюю резервную копию обновления для отката системы)',
|
||||
rollback: 'Файлы резервных копий перед восстановлением',
|
||||
|
||||
backup: 'Резервная Копия Системы',
|
||||
backupHelper: 'Файлы резервных копий, не связанные с локальными учетными записями резервного копирования',
|
||||
unknownBackup: 'Резервная Копия Системы',
|
||||
@@ -2310,13 +2362,11 @@ const message = {
|
||||
unknownDatabase: 'Несвязанная Резервная Копия Базы Данных',
|
||||
unknownWebsite: 'Несвязанная Резервная Копия Веб-сайта',
|
||||
unknownSnapshot: 'Несвязанная Резервная Копия Снимка',
|
||||
|
||||
upload: 'Временные файлы загрузки',
|
||||
uploadHelper: 'Временные файлы, загруженные из списка системных резервных копий',
|
||||
download: 'Временные файлы скачивания',
|
||||
downloadHelper: 'Временные файлы, скачанные из сторонних аккаунтов резервного копирования системой',
|
||||
directory: 'Директория',
|
||||
|
||||
systemLog: 'Файл журнала',
|
||||
systemLogHelper: 'Системные журналы, журналы задач, файлы журналов веб-сайта',
|
||||
dockerLog: 'Файлы журналов операций с контейнерами',
|
||||
@@ -2324,14 +2374,13 @@ const message = {
|
||||
shell: 'Запланированные задачи скриптов оболочки',
|
||||
containerShell: 'Запланированные задачи скриптов оболочки, выполняемые внутри контейнеров',
|
||||
curl: 'Запланированные задачи CURL',
|
||||
|
||||
docker: 'Мусор контейнеров',
|
||||
dockerHelper: 'Файлы, такие как контейнеры, образы, тома, кэш сборки и т.д.',
|
||||
volumes: 'Тома',
|
||||
buildCache: 'Кэш сборки контейнеров',
|
||||
|
||||
appTmpDownload: 'Временный файл загрузки приложения',
|
||||
unknownWebsiteLog: 'Arquivo de backup de log de site não vinculado',
|
||||
snapshot: 'Временные файлы системного снимка',
|
||||
},
|
||||
app: {
|
||||
app: 'Приложение | Приложения',
|
||||
@@ -2422,6 +2471,12 @@ const message = {
|
||||
reload: 'Перезагрузить',
|
||||
upgradeWarn:
|
||||
'Обновление приложения заменит файл docker-compose.yml. Если есть изменения, вы можете нажать для просмотра сравнения файлов',
|
||||
openclawHttpsUpgradeNoticeTitle:
|
||||
'Примечание: Следующие инструкции применяются только к пользователям, обновляющим OpenClaw с версий ниже 2026.3.13:',
|
||||
openclawHttpsUpgradeNoticeItem1:
|
||||
'После завершения обновления развернутого агента перейдите в Конфигурация -> Настройки -> Безопасность и вручную добавьте адрес доступа.',
|
||||
openclawHttpsUpgradeNoticeItem2:
|
||||
'Новая версия теперь требует HTTPS для доступа к агенту. Если раньше использовался сайт с обратным прокси, измените адрес назначения прокси на https://IP:Port.',
|
||||
newVersion: 'Новая версия',
|
||||
oldVersion: 'Текущая версия',
|
||||
composeDiff: 'Сравнение файлов',
|
||||
@@ -2794,7 +2849,6 @@ const message = {
|
||||
failTimeout: 'Таймаут отказа',
|
||||
failTimeoutHelper:
|
||||
'Длина временного окна для проверки работоспособности сервера. Когда кумулятивное количество отказов достигает порога в течение этого периода, сервер будет временно удален и повторно проверен через тот же промежуток времени. По умолчанию 10 секунд',
|
||||
|
||||
staticChangePHPHelper: 'В настоящее время статический сайт, можно переключить на PHP сайт.',
|
||||
proxyCache: 'Кэш Обратного Прокси',
|
||||
cacheLimit: 'Ограничение Пространства Кэша',
|
||||
@@ -2823,7 +2877,6 @@ const message = {
|
||||
preflight: 'Быстрый ответ на предварительный запрос',
|
||||
preflightHleper:
|
||||
'При включении, когда браузер отправляет межсайтовый предварительный запрос (запрос OPTIONS), система автоматически вернет статус 204 и установит необходимые межсайтовые заголовки ответа',
|
||||
|
||||
changeDatabase: 'Сменить Базу Данных',
|
||||
changeDatabaseHelper1: 'Связь базы данных используется для резервного копирования и восстановления сайта.',
|
||||
changeDatabaseHelper2:
|
||||
@@ -2858,7 +2911,6 @@ const message = {
|
||||
execUser: 'Пользователь выполнения',
|
||||
execDir: 'Каталог выполнения',
|
||||
packagist: 'Полное зеркало Китая',
|
||||
|
||||
batchOperate: 'Пакетная операция',
|
||||
batchOperateHelper: 'Пакетное {0} веб-сайтов, продолжить операцию?',
|
||||
stream: 'Прокси TCP/UDP',
|
||||
@@ -2867,8 +2919,13 @@ const message = {
|
||||
'Установите номер внешнего порта прослушивания, клиенты будут получать доступ к службе через этот порт, разделяйте запятыми, например: 5222,5223',
|
||||
streamHelper: 'Перенаправление портов и балансировка нагрузки TCP/UDP',
|
||||
udp: 'Включить UDP',
|
||||
|
||||
syncHtmlHelper: 'Синхронизировать с PHP и статическими сайтами',
|
||||
wafValueHelper: 'Значение',
|
||||
createDb: 'Создать базу данных',
|
||||
parentWbeiste: 'Родительский сайт',
|
||||
shareCahe: 'Размер памяти под кэш',
|
||||
shareCaheHelper: 'На 1M памяти можно хранить около 8000 кэш-объектов',
|
||||
cacheExpireJHelper: 'Кэш будет удален, если после истечения срока по нему не было обращений',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Поддержка коротких тегов',
|
||||
@@ -2897,6 +2954,7 @@ const message = {
|
||||
containerConfigHelper:
|
||||
'Переменные окружения и другие данные можно изменить в разделе Конфигурация - Конфигурация контейнера после создания',
|
||||
dateTimezoneHelper: 'Пример: TZ=Asia/Shanghai (Пожалуйста, добавьте по мере необходимости)',
|
||||
extensionsHelper: 'Для нескольких расширений используйте запятую',
|
||||
},
|
||||
nginx: {
|
||||
serverNamesHashBucketSizeHelper: 'Размер хэш-таблицы для имен серверов',
|
||||
@@ -3031,7 +3089,6 @@ const message = {
|
||||
argsCheck: 'Проверка GET параметров',
|
||||
postCheck: 'Проверка POST параметров',
|
||||
cookieBlockList: 'Черный список Cookie',
|
||||
|
||||
dockerHelper:
|
||||
'Текущий брандмауэр не может отключить сопоставление портов контейнера. Установленные приложения могут перейти на страницу [Установленные], чтобы редактировать параметры приложения и настраивать правила открытия портов.',
|
||||
iptablesHelper:
|
||||
@@ -3067,7 +3124,6 @@ const message = {
|
||||
portHelper2: 'Диапазон портов, например 8080-8089',
|
||||
changeStrategyHelper:
|
||||
'Изменить стратегию {0} [{1}] на [{2}]. После установки {0} будет иметь внешний доступ {2}. Хотите продолжить?',
|
||||
|
||||
strategy: 'Стратегия',
|
||||
accept: 'Принять',
|
||||
drop: 'Отбросить',
|
||||
@@ -3095,7 +3151,6 @@ const message = {
|
||||
exportHelper: 'Собираюсь экспортировать {0} правил брандмауэра. Продолжить?',
|
||||
importSuccess: 'Успешно импортировано {0} правил',
|
||||
importPartialSuccess: 'Импорт завершён: {0} успешно, {1} с ошибкой',
|
||||
|
||||
ipv4Limit: 'Текущая операция поддерживает только адреса IPv4',
|
||||
basicStatus: 'Текущая цепочка {0} не привязана, сначала привяжите!',
|
||||
baseIptables: 'Сервис iptables',
|
||||
@@ -3127,6 +3182,10 @@ const message = {
|
||||
portHelper: '0 означает любой порт',
|
||||
allPorts: 'Все Порта',
|
||||
deleteRuleConfirm: 'Удалит {0} правил. Продолжить?',
|
||||
advancedControl: 'Расширенное управление',
|
||||
advancedControlNotAvailable:
|
||||
'Сейчас используется firewall {0}, расширенные правила поддерживаются только для iptables',
|
||||
destination: 'Назначение',
|
||||
},
|
||||
runtime: {
|
||||
runtime: 'Среда выполнения',
|
||||
@@ -3184,6 +3243,29 @@ const message = {
|
||||
concurrency: 'Схема параллелизма',
|
||||
loadStatus: 'Состояние нагрузки',
|
||||
extraHosts: 'Сопоставление хостов',
|
||||
extension: 'Расширение',
|
||||
installExtension: 'Подтвердите установку расширения {0}',
|
||||
loadedExtension: 'Загруженные расширения',
|
||||
popularExtension: 'Популярные расширения',
|
||||
uninstallExtension: 'Вы уверены, что хотите удалить расширение {0}',
|
||||
phpConfigHelper: 'Для изменения конфигурации требуется перезапуск окружения. Продолжить',
|
||||
operateMode: 'Режим работы',
|
||||
dynamic: 'динамический',
|
||||
static: 'статический',
|
||||
ondemand: 'по требованию',
|
||||
dynamicHelper:
|
||||
'Динамически регулирует число процессов. Подходит для сайтов с плавающей нагрузкой или малой памятью',
|
||||
staticHelper:
|
||||
'Фиксированное число процессов, подходит для стабильного высокого трафика, но потребляет больше ресурсов',
|
||||
ondemandHelper:
|
||||
'Процессы запускаются/завершаются по требованию, ресурсы используются лучше, но первый отклик может быть медленнее',
|
||||
max_children: 'максимальное число создаваемых процессов',
|
||||
start_servers: 'число процессов при запуске',
|
||||
min_spare_servers: 'минимальное число свободных процессов',
|
||||
max_spare_servers: 'максимальное число свободных процессов',
|
||||
envKey: 'Имя',
|
||||
envValue: 'Значение',
|
||||
environment: 'Переменная окружения',
|
||||
},
|
||||
process: {
|
||||
pid: 'ID процесса',
|
||||
@@ -3498,7 +3580,6 @@ const message = {
|
||||
'Глобальный режим: активируется, когда общее количество запросов к любому URL за определенный промежуток времени превышает порог',
|
||||
uriModeHelper:
|
||||
'Режим URL: активируется, когда количество запросов к одному URL за определенный промежуток времени превышает порог',
|
||||
|
||||
ip: 'Черный список IP',
|
||||
globalSettingHelper:
|
||||
'Настройки с тегом [Website] должны быть включены в [Настройки сайта], а глобальные настройки являются только настройками по умолчанию для новых сайтов',
|
||||
@@ -3570,6 +3651,7 @@ const message = {
|
||||
importRuleHelper: 'Одно правило на строку',
|
||||
},
|
||||
monitor: {
|
||||
'360': '360 Search',
|
||||
name: 'Мониторинг веб-сайта',
|
||||
pv: 'Просмотры страниц',
|
||||
uv: 'Уникальные посетители',
|
||||
@@ -3639,7 +3721,6 @@ const message = {
|
||||
statusErr: 'Неверный формат статус-кода',
|
||||
shenma: 'Shenma Search',
|
||||
duckduckgo: 'DuckDuckGo',
|
||||
'360': '360 Search',
|
||||
excludeUri: 'Исключить URI',
|
||||
top100Helper: 'Показать топ 100 данных',
|
||||
logSaveDay: 'Период хранения логов (дни)',
|
||||
@@ -3750,7 +3831,6 @@ const message = {
|
||||
wafContent3: 'Создайте пользовательскую страницу для отображения после перехвата запроса',
|
||||
wafTitle4: 'Пользовательские Правила (ACL)',
|
||||
wafContent4: 'Перехватывайте запросы в соответствии с пользовательскими правилами',
|
||||
|
||||
tamperTitle1: 'Мониторинг Целостности Файлов',
|
||||
tamperContent1:
|
||||
'Мониторинг целостности файлов веб-сайта, включая основные файлы, скрипты и файлы конфигурации.',
|
||||
@@ -3763,7 +3843,6 @@ const message = {
|
||||
tamperTitle4: 'Ведение Логов и Анализ',
|
||||
tamperContent4:
|
||||
'Записывайте логи доступа и операций с файлами для последующего аудита и анализа администраторами, а также для выявления потенциальных угроз безопасности.',
|
||||
|
||||
settingTitle1: 'Пользовательское Приветственное Сообщение',
|
||||
settingContent1: 'Установите пользовательское приветственное сообщение на странице входа в 1Panel.',
|
||||
settingTitle2: 'Пользовательский Логотип',
|
||||
@@ -3771,7 +3850,6 @@ const message = {
|
||||
settingTitle3: 'Пользовательская Иконка Сайта',
|
||||
settingContent3:
|
||||
'Разрешите загрузку пользовательских иконок для замены стандартной иконки браузера, улучшая пользовательский опыт.',
|
||||
|
||||
monitorTitle1: 'Тренд Посетителей',
|
||||
monitorContent1: 'Статистика и отображение тенденций посещаемости веб-сайта',
|
||||
monitorTitle2: 'Карта Посетителей',
|
||||
@@ -3782,7 +3860,6 @@ const message = {
|
||||
monitorTitle4: 'Мониторинг в Реальном Времени',
|
||||
monitorContent4:
|
||||
'Мониторинг запросов веб-сайта в реальном времени, включая количество запросов, трафик и т. д.',
|
||||
|
||||
alertTitle1: 'SMS Уведомления',
|
||||
alertContent1:
|
||||
'При аномальном использовании ресурсов сервера, истечении срока действия сайта и сертификата, появлении новой версии или истечении срока действия пароля пользователи будут уведомлены через SMS, чтобы обеспечить своевременную обработку.',
|
||||
@@ -3792,7 +3869,6 @@ const message = {
|
||||
alertTitle3: 'Настройки Уведомлений',
|
||||
alertContent3:
|
||||
'Предоставляет пользователям возможность настройки номеров телефонов, частоты и времени отправки уведомлений в день, что позволяет настраивать более удобные уведомления.',
|
||||
|
||||
nodeDashTitle1: 'Управление Приложениями',
|
||||
nodeDashContent1:
|
||||
'Единое управление приложениями на нескольких узлах, поддерживает мониторинг состояния, быстрое запуск/остановку, подключение терминала и резервное копирование',
|
||||
@@ -3805,7 +3881,6 @@ const message = {
|
||||
nodeDashTitle4: 'Управление Запланированными Задачами',
|
||||
nodeDashContent4:
|
||||
'Единое управление запланированными задачами на нескольких узлах, поддерживает мониторинг состояния, быстрый запуск/остановку и выполнение по ручному триггеру',
|
||||
|
||||
nodeTitle1: 'Добавление узла одним кликом',
|
||||
nodeContent1: 'Быстро интегрируйте несколько серверных узлов',
|
||||
nodeTitle2: 'Пакетное обновление',
|
||||
@@ -3814,7 +3889,6 @@ const message = {
|
||||
nodeContent3: 'Реальное наблюдение за рабочим статусом каждого узла',
|
||||
nodeTitle4: 'Быстрое удаленное подключение',
|
||||
nodeContent4: 'Одним кликом подключитесь к удаленным терминалам узлов',
|
||||
|
||||
fileExchangeTitle1: 'Передача с аутентификацией по ключу',
|
||||
fileExchangeContent1: 'Аутентифицируйтесь через SSH-ключи, чтобы обеспечить безопасность передачи.',
|
||||
fileExchangeTitle2: 'Эффективная синхронизация файлов',
|
||||
@@ -3823,11 +3897,9 @@ const message = {
|
||||
fileExchangeTitle3: 'Поддержка обмена между несколькими узлами',
|
||||
fileExchangeContent3:
|
||||
'Легко передавайте проектные файлы между разными узлами, гибко управляйте несколькими серверами.',
|
||||
|
||||
nodeAppTitle1: 'Управление Обновлениями Приложений',
|
||||
nodeAppContent1:
|
||||
'Единый мониторинг обновлений приложений на нескольких узлах, поддерживает обновление в один клик',
|
||||
|
||||
appTitle1: 'Гибкое управление панелью',
|
||||
appContent1: 'Легко управляйте сервером 1Panel в любое время и в любом месте.',
|
||||
appTitle2: 'Полная информация о сервисе',
|
||||
@@ -3836,7 +3908,6 @@ const message = {
|
||||
appTitle3: 'Мониторинг аномалий в реальном времени',
|
||||
appContent3:
|
||||
'Просматривайте статус сервера, мониторинг безопасности WAF, статистику посещений сайта и состояние процессов в реальном времени через мобильное приложение.',
|
||||
|
||||
clusterTitle1: 'Развертывание мастер-слейв',
|
||||
clusterContent1:
|
||||
'Поддерживает создание мастер-слейв экземпляров MySQL/Postgres/Redis на разных узлах, автоматически завершая связь мастер-слейв и инициализацию',
|
||||
@@ -3852,7 +3923,7 @@ const message = {
|
||||
masterBackup: 'Резервная копия главного узла',
|
||||
backupNode: 'Резервный узел',
|
||||
backupFrequency: 'Частота резервного копирования (часы)',
|
||||
backupCopies: 'Количество сохраняемых резервных копий',
|
||||
backupCopies: 'Копии Бэкапа',
|
||||
noBackupNode:
|
||||
'Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку!',
|
||||
masterBackupAlert:
|
||||
@@ -3907,7 +3978,6 @@ const message = {
|
||||
nodeSyncHelper: 'Синхронизация информации о узле будет синхронизировать следующую информацию:',
|
||||
nodeSyncHelper1: '1. Информация о публичной резервной учетной записи',
|
||||
nodeSyncHelper2: '2. Информация о соединении между основным узлом и подузлами',
|
||||
|
||||
nodeCheck: 'Проверка доступности',
|
||||
checkSSH: 'Проверить SSH-подключение узла',
|
||||
checkUserPermission: 'Проверка прав пользователя узла',
|
||||
@@ -3929,7 +3999,6 @@ const message = {
|
||||
errLicense: 'Лицензия, привязанная к этому узлу, недоступна. Пожалуйста, проверьте и повторите попытку!',
|
||||
errNodePort:
|
||||
'Обнаружено, что порт узла [ {0} ] недоступен. Проверьте, разрешен ли этот порт в брандмауэре или группе безопасности.',
|
||||
|
||||
reinstallHelper: 'Переустановить узел {0}, вы хотите продолжить?',
|
||||
unhealthyCheck: 'Проверка на неисправности',
|
||||
fixOperation: 'Решение проблемы',
|
||||
@@ -3954,7 +4023,6 @@ const message = {
|
||||
offline: 'Автономный режим',
|
||||
freeCount: 'Бесплатная квота [{0}]',
|
||||
offlineHelper: 'Используется, когда узел находится в автономной среде',
|
||||
|
||||
appUpgrade: 'Обновление приложения',
|
||||
appUpgradeHelper: 'Есть {0} приложений, которые необходимо обновить',
|
||||
},
|
||||
@@ -4050,7 +4118,6 @@ const message = {
|
||||
specifiedTime: 'Указанное Время',
|
||||
deleteTitle: 'Удалить Уведомление',
|
||||
deleteMsg: 'Вы уверены, что хотите удалить задачу уведомления?',
|
||||
|
||||
allSslTitle: 'Уведомления об истечении срока действия всех SSL сертификатов сайтов',
|
||||
sslTitle: 'Уведомление об истечении срока действия SSL сертификата для сайта {0}',
|
||||
allSiteEndTimeTitle: 'Уведомления об истечении срока действия всех сайтов',
|
||||
@@ -4062,7 +4129,6 @@ const message = {
|
||||
loadTitle: 'Уведомление о высокой нагрузке',
|
||||
diskTitle: 'Уведомление о высокой загрузке диска для точки монтирования {0}',
|
||||
allDiskTitle: 'Уведомление о высокой загрузке диска',
|
||||
|
||||
timeRule:
|
||||
'Оставшееся время менее {0} дней (если не обработано, будет отправлено повторно на следующий день)',
|
||||
panelUpdateRule:
|
||||
@@ -4070,12 +4136,10 @@ const message = {
|
||||
avgRule: 'Среднее использование {1} превышает {2}% в течение {0} минут, отправляется {3} раз в день',
|
||||
diskRule: 'Использование диска для точки монтирования {0} превышает {1}{2}, отправляется {3} раз в день',
|
||||
allDiskRule: 'Использование диска превышает {0}{1}, отправляется {2} раз в день',
|
||||
|
||||
cpuName: 'Процессор',
|
||||
memoryName: 'Память',
|
||||
loadName: 'Нагрузка',
|
||||
diskName: 'Диск',
|
||||
|
||||
syncAlertInfo: 'Ручная отправка',
|
||||
syncAlertInfoMsg: 'Вы хотите вручную отправить задачу уведомления?',
|
||||
pushError: 'Не удалось отправить',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user