Compare commits

..
48 Commits
Author SHA1 Message Date
CityFun 142c9b9a71 fix: Fixed issue with build backend failed (#12199) 2026-03-17 14:50:45 +08:00
ssongliu f3c7603f6e fix: respect region-based release doc source (#12198) 2026-03-17 14:33:11 +08:00
ssongliuandssongliu db26da1700 chore: format english proxy helper text (#12197)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-17 14:06:20 +08:00
ssongliuandssongliu d5ff59f4e7 fix: remove unused compose backup status param (#12196)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-17 13:56:34 +08:00
王贺 85b88db99e docs: refine Docker proxy desc (#12195) 2026-03-17 11:15:27 +08:00
ssongliuandssongliu f7053934b5 fix: improve compose backup and container restore handling (#12192)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 22:39:34 +08:00
wangdage12andfanbook-wangdage bf1339bb40 fix: show loading state when confirming deletion of multiple folders and reduce unnecessary basedir requests
Co-authored-by: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com>
2026-03-16 19:07:19 +08:00
CityFun 43c5e62c23 feat: change allowedOrigin param for openclaw upgrade (#12188) 2026-03-16 10:53:50 +00:00
ssongliuandssongliu 28634ce631 fix(middleware): preserve numeric format in operation logs (#12173) (#12185)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 17:33:07 +08:00
ssongliuandssongliu 72263dfdda fix(container): allow backup for stopped containers (#12184)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 16:39:16 +08:00
ssongliuandssongliu 479d36e25f fix(log): localize upgrade and rebuild operation labels (#12183)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 16:25:34 +08:00
ssongliu 2f3ad3537e feat: add docs source config and runtime env linkage (#12182) 2026-03-16 08:13:51 +00:00
CityFun 2dfe5096f0 feat: Optimize reverse proxy handling for applications with HTTPS ports. (#12181) 2026-03-16 08:07:51 +00:00
CityFun 4375722aef feat: add checkOnStart config for openclaw (#12178) 2026-03-16 06:45:49 +00:00
CityFun bde4d06842 fix: Fixed issue with build source code failed (#12177)
Refs https://github.com/1Panel-dev/1Panel/issues/12176
2026-03-16 05:51:50 +00:00
CityFun 17a486b2c4 feat: Modify the OpenClaw upgrade logic. (#12175) 2026-03-16 04:11:50 +00:00
CityFun 8499c09571 feat: Add OpenClaw security adaptation (#12168) 2026-03-13 18:41:44 +08:00
CityFun 7cb70f785a feat: change openclaw port jump logic (#12164) 2026-03-12 14:27:34 +00:00
CityFun f3781320de feat: add icon for agent (#12163) 2026-03-12 18:52:04 +08:00
CityFun 869ee75020 feat: Add allowedOrigins config for openclaw (#12162) 2026-03-12 09:37:33 +00:00
ssongliu 1d5a26373d chore: optimize lint and cleanup warnings (#12161) 2026-03-12 09:35:33 +00:00
ssongliu 176fd0f8ad feat: support file operations inside containers (#12160)
Refs #523
2026-03-12 09:33:33 +00:00
ssongliu 341a55ae63 feat: add Agent quick jump with priority show strategy (#12159)
Refs #12158
2026-03-12 09:31:33 +00:00
CityFun fb87cf545f feat: Modify the Gemini account verification logic (#12155)
Refs https://github.com/1Panel-dev/1Panel/issues/12149
2026-03-12 09:07:33 +00:00
ssongliu 6db2a9352e feat: refactor image update flow and align i18n messages (#12154)
Refs #10786
2026-03-12 09:05:34 +00:00
ssongliu 21826bcc1a feat: complete backup and recovery for containers and compose (#12153)
Refs #9656
2026-03-12 09:03:34 +00:00
ssongliu eedbc2e95c feat: load registry auth from docker cli config when pulling image (#12152)
Refs #10460
2026-03-12 09:01:33 +00:00
CityFun 46763df7bb fix: Fixed issue with ai menu change failed (#12147) 2026-03-11 10:55:27 +00:00
CityFun 175162753e feat: openclaw channel support wecom (#12146) 2026-03-11 10:53:27 +00:00
CityFun d114933fe9 feat: agent account add openRouter (#12143) 2026-03-11 04:00:55 +00:00
蘭 432412e596 ref: Adjust the column widths of the file system (#12141) 2026-03-11 10:38:15 +08:00
ssongliu 1b577e3d41 fix(core): resolve operation data by agent for non-local nodes (#12136) 2026-03-10 17:23:06 +08:00
CityFun eda9641d35 feat: change some prompt (#12135) 2026-03-10 16:11:31 +08:00
CityFun 6808e4892c fix: Fixed ai menu router error (#12134) 2026-03-10 14:55:00 +08:00
CityFun 0d71cd4db2 feat: change custom model name logic (#12132) 2026-03-10 03:25:58 +00:00
CityFun b6b3304a71 style: change some prompt (#12131) 2026-03-10 11:09:20 +08:00
CityFun 59a0a573ed feat: 增加 vLLM 管理 (#12129) 2026-03-09 20:01:39 +08:00
CityFun e82d686940 feat: add vLLm management (#12128) 2026-03-09 19:55:20 +08:00
ssongliu b48bb88aa1 fix(postgresql): check active sessions before dropping db (#12127) 2026-03-09 16:04:58 +08:00
ssongliu 5ce9ec5b64 feat(container): refine compose and setting i18n (#12125)
Refs #12109
2026-03-09 08:03:52 +00:00
ssongliu 740f3549ad fix: optimize operation log resolve flow and i18n replacements (#12123) 2026-03-09 16:02:12 +08:00
zhengkunwang223 aa8a9e8b1e feat: add QQbot channel for ai Agent 2026-03-09 15:11:04 +08:00
ssongliu 14218e3495 fix(compose): load directory when compose path changes (#12122)
Refs #12106
2026-03-09 06:57:50 +00:00
蘭 c526eb2962 fix: Resolve the issue of not receiving emails when the sender's name is in Chinese (#12121)
#12006
2026-03-09 06:55:50 +00:00
CityFun 1ff8292be4 feat: add ai agent create limit (#12124) 2026-03-09 13:54:06 +08:00
A_Words d4394616af feat: support toggling file list columns (#12099)
* feat: support toggling file list columns

* feat: use fu table column select for file list

* chore: remove unused file column i18n keys

* feat: simplify file column visibility logic and remove unused configurations
2026-03-09 09:51:53 +08:00
王贺 462ca6b373 feat: rename AI model menu to local model (#12108) 2026-03-09 09:51:24 +08:00
ssongliu 657c3ad37a chore: localize newly added i18n keys (#12104) 2026-03-06 14:05:40 +00:00
136 changed files with 13402 additions and 2235 deletions
+3 -1
View File
@@ -73,4 +73,6 @@ agent/.golangci.yml
openspec
CLAUDE.md
AGENTS.md
opencode.json
opencode.json
superpowers
.worktrees/
+136 -33
View File
@@ -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
+30
View File
@@ -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)
}
+159
View File
@@ -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
View File
@@ -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"`
}
+3 -2
View File
@@ -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"`
+28
View 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"`
+1
View File
@@ -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
View File
@@ -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"},
},
},
}
+339
View File
@@ -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")
}
+146
View File
@@ -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
}
+2 -1
View File
@@ -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"},
}
}
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -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,
}
+16 -31
View File
@@ -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
+9 -6
View File
@@ -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
}
+5
View File
@@ -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 {
+707
View File
@@ -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
}
+914
View File
@@ -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
}
+448
View File
@@ -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 {
+11 -3
View File
@@ -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
})
}
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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
}
+1 -2
View File
@@ -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
}
+2 -10
View File
@@ -114,7 +114,7 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
err = errors.New("invalid proxy config, no location found")
return
}
location.UpdateDirective("proxy_pass", []string{req.ProxyPass})
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
@@ -143,15 +143,7 @@ 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 {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"})
+25 -1
View File
@@ -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)
+2 -1
View File
@@ -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 {
+6
View File
@@ -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"
)
+31
View File
@@ -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 }}'
+31
View File
@@ -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.'
+31
View File
@@ -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: 'イメージはすでに存在します!イメージ名を変更してください。'
+31
View File
@@ -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: '이미지가 이미 존재합니다 이미지 이름을 수정하세요.'
+31
View File
@@ -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.'
+31
View File
@@ -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.'
+31
View File
@@ -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: 'Изображение уже существует Пожалуйста, измените имя изображения.'
+31
View File
@@ -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.'
+31
View File
@@ -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: '映像已存在!請修改映像名稱。'
+31
View File
@@ -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 }} 文件不存在!请检查源文件完整性!"
+1
View File
@@ -43,6 +43,7 @@ func InitAgentDB() {
migrations.InitLocalSSHShow,
migrations.InitRecordStatus,
migrations.AddShowNameForQuickJump,
migrations.AddAgentQuickJump,
migrations.AddTimeoutForClam,
migrations.UpdateCronjobSpec,
migrations.UpdateWebsiteSSLAddColumn,
+61
View File
@@ -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 {
+1
View File
@@ -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)
}
+208
View File
@@ -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)
}
+8 -3
View File
@@ -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)
+6
View File
@@ -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)
+3 -1
View File
@@ -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 -1
View File
@@ -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
}
+99 -2
View File
@@ -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)
}
+1 -2
View File
@@ -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, ",")
+17 -4
View File
@@ -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)
}
+1 -1
View File
@@ -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
+32
View File
@@ -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)
+25
View File
@@ -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 {
-1
View File
@@ -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
+2
View File
@@ -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,
}
-5
View File
@@ -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"`
+4
View File
@@ -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
}
+1
View File
@@ -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"`
+12 -3
View File
@@ -286,10 +286,19 @@ type noteDetailHelper struct {
}
func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
docSource, _ := settingRepo.GetValueByKey("DocSource")
lang, _ := settingRepo.GetValueByKey("Language")
var notes []dto.ReleasesNotes
url := "https://docs.1panel.pro/v2/search/search_index.json"
if global.CONF.Base.Edition != "intl" {
url = "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 {
+543
View File
@@ -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": {
+543
View File
@@ -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": {
+4 -1
View File
@@ -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))
}
}
+30
View File
@@ -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"
+1
View File
@@ -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": {},
+1 -1
View File
@@ -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},
}},
+2
View File
@@ -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)
+32
View File
@@ -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
View File
@@ -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
}
+62 -13
View File
@@ -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;
}
}
+1
View File
@@ -93,6 +93,7 @@ export namespace Backup {
detailName: string;
secret: string;
taskID: string;
stopBefore?: boolean;
}
export interface Recover {
downloadAccountID: number;
+20
View File
@@ -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;
+1
View File
@@ -21,6 +21,7 @@ export namespace Setting {
theme: string;
menuTabs: string;
language: string;
docSource: string;
defaultIO: string;
defaultNetwork: string;
lastCleanTime: string;
+28 -8
View File
@@ -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) => {
+6 -4
View File
@@ -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}` : '';
+24
View File
@@ -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`);
};
+3 -2
View File
@@ -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

+21 -1
View File
@@ -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) {
+46 -8
View File
@@ -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 = () => {
File diff suppressed because it is too large Load Diff
+83 -45
View File
@@ -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',
@@ -2218,7 +2250,6 @@ 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: 'Copias de Respaldo',
@@ -2250,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',
@@ -2301,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',
@@ -2328,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',
@@ -2337,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',
@@ -2351,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',
},
@@ -2444,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',
@@ -2581,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',
@@ -2795,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é',
@@ -2820,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.',
@@ -2851,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',
@@ -2860,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',
@@ -2890,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',
@@ -3027,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:
@@ -3036,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:
@@ -3053,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',
@@ -3080,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',
@@ -3113,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',
@@ -3238,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',
@@ -3247,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.',
@@ -3568,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)',
@@ -3635,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)',
@@ -3783,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',
@@ -3796,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',
@@ -3813,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',
@@ -3934,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',
File diff suppressed because it is too large Load Diff
+158 -86
View File
@@ -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,7 +2138,6 @@ const message = {
versionNotSame: '노드 버전이 메인 노드와 일치하지 않습니다. 노드 관리에서 업그레이드한 후 다시 시도해 주세요.',
versionCompare:
'노드 {0}이(가) 이미 업그레이드 가능한 최신 버전입니다. 마스터 노드 버전을 확인 후 다시 시도하세요!',
about: '정보',
versionItem: '현재 버전',
backupCopies: '백업 수',
@@ -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 마스터-슬레이브 인스턴스를 생성하는 것을 지원하며, 자동으로 마스터-슬레이브 연결 및 초기화를 완료합니다',
@@ -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}개 있습니다',
},
+154 -86
View File
@@ -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,7 +2226,6 @@ 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: 'Salinan Sandaran',
@@ -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',
@@ -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
+140 -76
View File
@@ -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,7 +2211,6 @@ const message = {
'Версия узла не совпадает с основной. Пожалуйста, обновите в Управлении узлами перед повторной попыткой.',
versionCompare:
'Обнаружено, что узел {0} уже имеет последнюю обновляемую версию. Пожалуйста, проверьте версию основного узла и повторите попытку!',
about: 'О программе',
versionItem: 'Текущая Версия',
backupCopies: 'Копии Бэкапа',
@@ -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 на разных узлах, автоматически завершая связь мастер-слейв и инициализацию',
@@ -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: 'Не удалось отправить',
+70 -70
View File
@@ -87,6 +87,7 @@ const message = {
hide: 'Gizle',
visit: 'Visit',
migrate: 'Taşı',
disConn: 'Bağlantıyı kes',
},
operate: {
start: 'Başlat',
@@ -192,6 +193,8 @@ const message = {
importHelper:
'Çakışan veya yinelenen verileri içe aktarırken, içe aktarılan içerik orijinal veritabanı verilerini güncellemek için standart olarak kullanılacaktır.',
errImport: 'Dosya içeriği anormal:',
backupSuccess: 'Yedekleme başarılı',
restoreSuccess: 'Geri yükleme başarılı',
},
login: {
username: 'Kullanıcı adı',
@@ -405,6 +408,8 @@ const message = {
tamper: 'Kurcalama Koruması',
app: 'Uygulama',
msgCenter: 'Görevler',
filter: 'Filtre',
disk: 'Disk',
},
home: {
recommend: 'Önerilen',
@@ -437,12 +442,10 @@ const message = {
runningTime: 'Şu tarihten beri açık',
mem: 'Sistem Belleği',
swapMem: 'Swap Bölümü',
runSmoothly: 'Düşük yük',
runNormal: 'Orta yük',
runSlowly: 'Yüksek yük',
runJam: 'Ağır yük',
core: 'Fiziksel çekirdek',
logicCore: 'Mantıksal çekirdek',
corePercent: 'Çekirdek Kullanımı',
@@ -468,7 +471,6 @@ const message = {
available: 'Kullanılabilir',
percent: 'Kullanım',
goInstall: 'Yüklemeye git',
networkCard: 'Ağ kartı',
disk: 'Disk',
memo: 'Not',
@@ -524,14 +526,11 @@ const message = {
changePasswordHelper:
'Veritabanı bir uygulamayla ilişkilendirildi. Şifre değiştirmek aynı zamanda uygulamanın veritabanı şifresini de değiştirecek. Değişiklik uygulama yeniden başlatıldıktan sonra etkili olur.',
recoverTimeoutHelper: '-1, zaman aşımı sınırı olmadığı anlamına gelir',
confChange: 'Yapılandırma',
confNotFound:
'Yapılandırma dosyası bulunamadı. Lütfen uygulama mağazasından uygulamayı en son sürüme yükseltin ve tekrar deneyin!',
portHelper:
'Bu port konteynerin açığa çıkan portudur. Değişikliği ayrı olarak kaydetmeniz ve konteyneri yeniden başlatmanız gerekir!',
loadFromRemote: 'Sunucudan senkronize et',
userBind: 'Kullanıcı bağla',
pgBindHelper:
@@ -563,14 +562,12 @@ const message = {
hasCA: 'CA sertifikası var',
skipVerify: 'Sertifika geçerlilik kontrolünü yoksay',
initialDB: 'Başlangıç Veritabanı',
formatHelper:
'Mevcut veritabanı karakter seti {0}, karakter seti tutarsızlığı kurtarma işleminin başarısız olmasına neden olabilir',
dropHelper: 'Yüklenen dosyayı buraya sürükleyip bırakabilir veya',
clickHelper: 'yüklemek için tıklayın',
supportUpType:
'Yalnızca sql, sql.gz, tar.gz, .zip dosya formatlarını destekler. İçe aktarılan sıkıştırılmış dosya yalnızca bir .sql dosyası içermeli veya test.sql içermelidir',
currentStatus: 'Mevcut durum',
baseParam: 'Temel parametre',
performanceParam: 'Performans parametresi',
@@ -602,7 +599,6 @@ const message = {
sortMergePassesHelper: 'Değer çok büyükse, "sort_buffer_size" değerini artırın.',
tableLocksWaited: 'Kilitli tablo sayısı',
tableLocksWaitedHelper: 'Değer çok büyükse, veritabanı performansınızı artırmayı düşünün.',
performanceTuning: 'Performans ayarlama',
optimizationScheme: 'Optimizasyon şeması',
keyBufferSizeHelper: 'İndeks için tampon boyutu',
@@ -620,14 +616,11 @@ const message = {
tableOpenCacheHelper: 'Tablo önbelleği',
maxConnectionsHelper: 'Maksimum bağlantılar',
restart: 'Yeniden başlat',
slowLog: 'Yavaş loglar',
noData: 'Henüz yavaş log yok.',
isOn: 'Açık',
longQueryTime: 'eşik(saniye)',
thresholdRangeHelper: 'Lütfen doğru eşik değerini girin (1 - 600).',
timeout: 'Zaman aşımı(saniye)',
timeoutHelper: 'Boştaki bağlantı zaman aşımı süresi. 0, bağlantının sürekli açık olduğunu gösterir.',
maxclients: 'Maksimum istemci',
@@ -654,20 +647,17 @@ const message = {
redisQuickCmd: 'Redis hızlı komutları',
recoverHelper: 'Bu işlem verileri [{0}] ile üzerine yazacak. Devam etmek istiyor musunuz?',
submitIt: 'Verilerin üzerine yaz',
baseConf: 'Temel',
allConf: 'Tümü',
restartNow: 'Şimdi yeniden başlat',
restartNowHelper1:
'Yapılandırma değişikliklerinin etkili olması için sistemi yeniden başlatmanız gerekir. Verilerinizin kalıcı olması gerekiyorsa, önce kaydetme işlemini gerçekleştirin.',
restartNowHelper: 'Bu yalnızca sistem yeniden başlatıldıktan sonra etkili olacaktır.',
persistence: 'Kalıcılık',
rdbHelper1: 'saniye, ekleme',
rdbHelper2: 'veri parçası',
rdbHelper3: 'Koşullardan herhangi birinin karşılanması RDB kalıcılığını tetikleyecektir.',
rdbInfo: 'Kural listesindeki değerin 1 ile 100000 arasında olduğundan emin olun',
containerConn: 'Konteyner bağlantısı',
copyConnURL: "Bağlantı URL'sini kopyala",
connAddress: 'Adres',
@@ -693,33 +683,38 @@ const message = {
copawType: 'CoPaw',
appVersion: 'Uygulama sürümü',
webuiPort: 'WebUI portu',
bridgePort: 'Bridge portu',
allowedOrigins: 'Erişim adresleri',
allowedOriginsHelper:
'Her satıra bir tam HTTPS erişim adresi girin. Örnek: https://192.168.1.2:18789. Varsayılan erişim adresi yapılandırılmamışsa elle girin.',
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
allowedOriginsRequired: 'En az bir erişim adresi girin',
allowedOriginsInvalid: 'http(s)://host-veya-ip:port biçimini kullanın',
allowedOriginsHttpsOnly: 'Erişim adresleri https:// ile başlamalıdır',
provider: 'Model sağlayıcı',
apiKey: 'API anahtarı',
baseUrl: 'Temel URL',
token: 'Token',
manualModel: 'Manuel giriş',
verified: 'Doğrulandı',
verifySkipped: 'Atlandı',
verifySkipped: 'Doğrulama yok',
configTitle: 'Configuration',
settingsTab: 'Settings',
browserTab: 'Browser',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Saat Dilimi',
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: 'Eklenti yüklü değil. Lütfen önce yükleyin.',
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',
@@ -731,9 +726,11 @@ const message = {
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz',
feishuSaveSuccess: "Feishu'ya kaydedildi",
},
model: {
model: 'Model',
localModel: 'Yerel Modeller',
create: 'Model Ekle',
create_helper: '"{0}" çek',
ollama_doc: 'Daha fazla model aramak ve bulmak için Ollama resmi web sitesini ziyaret edebilirsiniz.',
@@ -742,6 +739,7 @@ const message = {
'Ollama modelini senkronize ederken aşağıdaki modellerin mevcut olmadığı tespit edildi, bunları silmek istiyor musunuz?',
from_remote: 'Bu model 1Panel aracılığıyla indirilmedi, ilgili çekme logları yok.',
no_logs: 'Bu modelin çekme logları silindi ve görüntülenemiyor.',
vllmVersionHelper: 'FusionXpark GB 10 sunucuları için lütfen -cu130 sürümünü seçin.',
},
proxy: {
proxy: 'AI Proxy Geliştirmesi',
@@ -778,7 +776,6 @@ const message = {
memoryUsed: 'Kullanılan Bellek',
memoryTotal: 'Toplam Bellek',
percent: 'Kullanım',
base: 'Temel Bilgiler',
driverVersion: 'Sürücü Sürümü',
cudaVersion: 'CUDA Sürümü',
@@ -810,6 +807,7 @@ const message = {
current: 'Gerçek Zamanlı İzleme',
history: 'Geçmiş Kayıtlar',
notSupport: 'Mevcut sürüm veya sürücü bu parametrenin görüntülenmesini desteklemiyor.',
processCount: 'Süreç sayısı',
},
mcp: {
server: 'MCP Sunucusu',
@@ -849,6 +847,12 @@ const message = {
},
container: {
createByCommand: 'Komutla oluştur',
importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
importComposeBackupTip: 'Compose yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
stopContainerBeforeBackup: 'Yedekleme öncesi konteyneri durdur',
stopComposeBeforeBackup: 'Yedekleme öncesi compose’u durdur',
stopBeforeBackupHelper:
'Etkinleştirildiğinde, yedekleme öncesinde konteyner veya compose servisi durdurulur ve veri tutarlılığını sağlamak için işlem tamamlandıktan sonra otomatik olarak geri açılır.',
commandInput: 'Komut girişi',
commandRule: 'Lütfen doğru docker run konteyner oluşturma komutunu girin!',
commandHelper: 'Bu komut konteyneri oluşturmak için sunucuda çalıştırılacak. Devam etmek istiyor musunuz?',
@@ -923,7 +927,6 @@ const message = {
'Konteyner motoru CPU paylaşımları için 1024 temel değerini kullanır. Konteynere daha fazla CPU zamanı vermek için bunu artırabilirsiniz.',
inputIpv4: 'Örnek: 192.168.1.1',
inputIpv6: 'Örnek: 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
diskUsage: 'Disk Kullanımı',
localVolume: 'Yerel Depolama Birimi',
buildCache: 'Derleme Önbelleği',
@@ -937,7 +940,6 @@ const message = {
sizeRwHelper: 'Konteynere özel yazılabilir katman boyutu',
sizeRootFs: 'Sanal Boyut',
sizeRootFsHelper: 'Konteynerin bağımlı olduğu tüm görüntü katmanları + konteyner katmanının toplam boyutu',
containerFromAppHelper:
'Bu konteynerin uygulama mağazasından geldiği tespit edildi. Uygulama işlemleri mevcut düzenlemelerin geçersiz hale gelmesine neden olabilir.',
containerFromAppHelper1:
@@ -951,7 +953,6 @@ const message = {
privileged: 'Ayrıcalıklı',
privilegedHelper:
'Konteynerin ana bilgisayarda belirli ayrıcalıklı işlemler gerçekleştirmesine izin verir, bu da konteyner risklerini artırabilir. Dikkatli kullanın!',
upgradeHelper: 'Depo Adı/İmaj Adı: İmaj Sürümü',
upgradeWarning2:
'Yükseltme işlemi konteynerin yeniden oluşturulmasını gerektirir, kalıcı olmayan tüm veriler kaybedilecektir. Devam etmek istiyor musunuz?',
@@ -960,12 +961,13 @@ const message = {
sameImageHelper: 'Aynı imajı kullanan konteynerlar seçilerek toplu şekilde güncellenebilir',
targetImage: 'Hedef imaj',
imageLoadErr: 'Konteyner için imaj adı algılanmadı',
imageUpdateTagEmpty: 'Güncellenebilir imaj etiketi algılanmadı',
appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.',
resource: 'Kaynak',
input: 'Manuel giriş',
forcePull: 'Her zaman imajı çek ',
forcePullHelper: 'Bu, sunucudaki mevcut imajları yok sayacak ve kayıt defterinden en son imajı çekecektir.',
imageUpdateHelper: 'Kayıt deposundaki aynı etiketi kontrol et, güncelleme varsa çekip yerel imajı güncelle.',
server: 'Ana bilgisayar',
serverExample: '80, 80-88, ip:80 veya ip:80-88',
containerExample: '80 veya 80-88',
@@ -1007,10 +1009,8 @@ const message = {
unlessStopped: 'unless-stopped',
onFailure: 'on-failure (varsayılan olarak beş kere)',
no: 'never',
refreshTime: 'Yenileme aralığı',
cache: 'Önbellek',
image: 'İmaj | İmajlar',
imagePull: 'Çek',
imagePullHelper:
@@ -1043,7 +1043,6 @@ const message = {
delBuildCacheHelper:
'Bu, oluşturma sırasında üretilen tüm önbelleğe alınmış yapıları silecek ve geri alınamaz. Devam etmek istiyor musunuz?',
urlWarning: 'URL öneki http:// veya https:// içermemelidir. Lütfen değiştirin.',
network: 'Ağ | Ağlar',
networkHelper:
'Bu, bazı uygulamaların ve çalışma zamanı ortamlarının düzgün çalışmamasına neden olabilir. Devam etmek istiyor musunuz?',
@@ -1056,7 +1055,6 @@ const message = {
scope: 'IP kapsamı',
gateway: 'Ağ geçidi',
auxAddress: 'IP hariç tut',
volume: 'Birim | Birimler',
volumeDir: 'Birim dizini',
nfsEnable: 'NFS depolamayı etkinleştir',
@@ -1064,7 +1062,6 @@ const message = {
mountpoint: 'Bağlama noktası',
mountpointNFSHelper: 'örn. /nfs, /nfs-share',
options: 'Seçenekler',
repo: 'Depo | Depolar',
httpRepoHelper: 'HTTP tipinde bir depo işlemi Docker servisinin yeniden başlatılmasını gerektirir.',
httpRepo:
@@ -1080,7 +1077,6 @@ const message = {
'Birden fazla ayna varsa, yeni satırlar gösterilmelidir, örneğin:\nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
registrieHelper:
'Birden fazla özel depo varsa, yeni satırlar gösterilmelidir, örneğin:\n172.16.10.111:8081 \n172.16.10.112:8081',
compose: 'Compose | Composelar',
composeFile: 'Düzenleme Dosyası',
fromChangeHelper:
@@ -1107,9 +1103,11 @@ const message = {
'Bu, {0} compose altındaki tüm konteynerleri ve ağları durduracak ve kaldıracaktır. Devam etmek istiyor musunuz?',
composeEnvHelper2:
'Bu düzenleme 1Panel Uygulama Mağazası tarafından oluşturuldu. Lütfen ortam değişkenlerini yüklü uygulamalarda değiştirin.',
composeContentPlaceholder: 'Docker-compose dosyanızın içeriğini buraya yazın veya yapıştırın',
setting: 'Ayar | Ayarlar',
goSetting: 'Düzenlemeye git',
restartHelper:
'Yapılandırma değişikliklerinin geçerli olması için Docker servisinin yeniden başlatılması gerekir',
operatorStatusHelper: 'Bu, Docker servisini "{0}" yapacaktır. Devam etmek istiyor musunuz?',
dockerStatus: 'Docker Servisi',
daemonJsonPathHelper: 'Yapılandırma yolunun docker.servicede belirtilen ile aynı olduğundan emin olun.',
@@ -1149,12 +1147,10 @@ const message = {
related: 'İlgili',
includeAppstore: 'Uygulama mağazasından konteynerleri göster',
excludeAppstore: 'Uygulama Mağazası Konteynerini Gizle',
cleanDockerDiskZone: 'Docker tarafından kullanılan disk alanını temizle',
cleanImagesHelper: '( Herhangi bir konteyner tarafından kullanılmayan tüm imajları temizle )',
cleanContainersHelper: '( Durmuş olan tüm konteynerleri temizle )',
cleanVolumesHelper: '( Kullanılmayan tüm yerel birimleri temizle )',
makeImage: 'İmaj oluştur',
newImageName: 'Yeni imaj adı',
commitMessage: 'Commit mesajı',
@@ -1162,6 +1158,7 @@ const message = {
ifPause: 'Oluşturma Sırasında Konteyneri Duraklat',
ifMakeImageWithContainer: 'Bu Konteynerden Yeni İmaj Oluşturulsun mu?',
finishTime: 'Son durdurma zamanı',
exportHelper: 'Seçilen imajları tek bir tar dosyasına aktarır.',
},
cronjob: {
importHelper:
@@ -1276,7 +1273,6 @@ const message = {
cutWebsiteLog: 'Website log döndürme',
cutWebsiteLogHelper: 'Döndürülen log dosyaları 1Panel yedek dizinine yedeklenecektir.',
syncIpGroup: 'WAF IP gruplarını senkronize et',
requestExpirationTime: 'Yükleme isteği son kullanma süresi(Saat)',
unitHours: 'Birim: Saat',
alertTitle: 'Planlanmış Görev - {0} 「{1}」 Görev Başarısızlık Uyarısı',
@@ -1303,6 +1299,11 @@ const message = {
syncHelper:
'Sistem script kütüphanesini senkronize etmek üzere. Bu işlem sadece sistem scriptlerini etkiler. Devam edilsin mi?',
},
logHelper1: '1. 1Panel sistem günlüğü',
ignoreErr: 'Hataları yok say',
ignoreErrHelper: 'Tüm görevlerin tamamlanması için yedekleme hatalarını yok say',
cleanLog: 'Günlükleri otomatik temizle',
cleanLogscope: 'Günlük saklama',
},
monitor: {
globalFilter: 'Genel Filtre',
@@ -1314,7 +1315,6 @@ const message = {
defaultIOHelper: 'İzleme ve genel bakış arayüzlerinde görüntülenen varsayılan disk seçeneği',
cleanMonitor: 'İzleme Kayıtlarını Temizle',
cleanHelper: 'Bu işlem GPU dahil tüm izleme kayıtlarını temizleyecektir. Devam etmek istiyor musunuz?',
avgLoad: 'Ortalama yük',
loadDetail: 'Yük detayı',
resourceUsage: 'Kullanım',
@@ -1451,6 +1451,7 @@ const message = {
dnsCheck: 'Kullanılabilirliği Test Et',
dnsOK: 'DNS yapılandırma bilgileri mevcut!',
dnsTestFailed: 'DNS yapılandırma bilgileri mevcut değil.',
syncSiteHelper: 'Sistem saati {0} ile eşitlensin mi?',
},
fail2ban: {
sshPort: 'SSH portunu dinle',
@@ -1487,6 +1488,10 @@ const message = {
'Seçilen FTP hesabını devre dışı bırakmak erişim izinlerini iptal edecektir. Devam etmek istiyor musunuz?',
syncHelper:
'Sunucu ve veritabanı arasında FTP hesap verilerini senkronize et. Devam etmek istiyor musunuz?',
dirSystem:
'Bu dizin sistem tarafından ayrılmıştır. Değişiklik sistemin çökmesine neden olabilir, kontrol edip tekrar deneyin!',
dirHelper: "FTP'yi etkinleştirmek dizin izinlerini değiştirir, lütfen dikkatle seçin",
dirMsg: 'FTP etkinleştirildiğinde {0} dizininin tamamının izinleri değiştirilecektir. Devam edilsin mi?',
},
clam: {
clam: 'Virüs taraması',
@@ -1798,6 +1803,7 @@ const message = {
loginTime: 'Oturum açma zamanı',
loginIP: 'Oturum açma IP’si',
stopSSHWarn: 'Bu SSH bağlantısını kesmek ister misiniz?',
remote: 'Uzak SSH',
},
setting: {
panel: 'Panel',
@@ -1833,6 +1839,10 @@ const message = {
light: 'Açık',
auto: 'Sistemi takip et',
language: 'Dil',
runtimeEnv: 'Çalışma ortamı',
docSource: 'Doküman kaynağı',
withByRegion: 'Çalışma bölgesini takip et (Varsayılan)',
withByLang: 'Sistem dilini takip et',
region: 'Çalışma bölgesi',
cn: 'Çin anakarası',
intl: 'Küresel',
@@ -1841,6 +1851,9 @@ const message = {
regionHelper2: 'Uygulama mağazası ve betik kütüphanesi',
regionHelper3: 'Kullanım kılavuzu ve ilgili dokümanlar',
regionHelper4: 'Bu işlem sonraki indirme ve erişimleri etkileyebilir. Lütfen dikkatli olun.',
regionTip:
'Çalışma bölgesi, sistem güncelleme kaynağını ve kurulum paketi indirme adresini belirgin şekilde etkiler.',
docSourceTip: 'Doküman kaynağı, dokümantasyon ve güncelleme günlüğü bağlantılarının dilini belirler.',
languageHelper:
'Varsayılan olarak tarayıcı dilini takip eder. Bu parametre yalnızca geçerli tarayıcıda etkilidir',
sessionTimeout: 'Oturum zaman aşımı',
@@ -1854,7 +1867,8 @@ const message = {
proxyHelper1: 'Uygulama mağazasından kurulum paketi indirme ve senkronizasyon (Yalnızca Profesyonel sürüm)',
proxyHelper2: 'Sistem güncelleme ve güncelleme bilgisi alma (Yalnızca Profesyonel sürüm)',
proxyHelper3: 'Sistem lisansı doğrulama ve senkronizasyon',
proxyHelper4: 'Docker ağı vekil sunucu üzerinden erişilecek (Yalnızca Profesyonel sürüm)',
proxyHelper4:
'Docker imajı çekme gibi ağ istekleri vekil sunucu üzerinden yapılacaktır (Yalnızca Profesyonel sürüm)',
proxyHelper5: 'Sistem tipi komut kütüphaneleri için birleşik indirme ve senkronizasyon (Profesyonel)',
proxyHelper6: 'Sertifika başvurusu (Profesyonel)',
proxyType: 'Vekil türü',
@@ -2223,7 +2237,6 @@ const message = {
versionNotSame: 'Düğüm sürümü ana düğümle uyuşmuyor. Lütfen Düğüm Yönetiminde yükseltin ve tekrar deneyin.',
versionCompare:
'{0} düğümünün zaten en son yükseltilebilir sürümde olduğu tespit edildi. Lütfen birincil düğüm sürümünü kontrol edin ve tekrar deneyin!',
about: 'Hakkında',
versionItem: 'Mevcut Sürüm',
backupCopies: 'Yedek Kopya',
@@ -2254,6 +2267,9 @@ const message = {
'Sayfa yenilenecek ve menü listesi başlangıç durumuna geri yüklenecektir. Devam etmek istiyor musunuz?',
compressPassword: 'Sıkıştırma parolası',
backupRecoverMessage: 'Lütfen sıkıştırma veya sıkıştırma açma parolasını girin (ayarlamamak için boş bırakın)',
componentSize: 'Bileşen boyutu',
dockerConf: 'Docker yapılandırmasını koru',
recoverFailed: 'Anlık görüntü geri yükleme başarısız',
},
license: {
offLine: 'Çevrimdışı Sürüm',
@@ -2328,7 +2344,6 @@ const message = {
suggest: '(Önerilen)',
totalScan: 'Temizlenecek toplam gereksiz dosya: ',
selectScan: 'Seçilen toplam gereksiz dosya: ',
system: 'Sistem Gereksiz Dosyaları',
systemHelper: 'Anlık görüntü, yükseltme ve diğer işlemler sırasında oluşturulan geçici dosyalar',
panelOriginal: 'Sistem anlık görüntüsü geri yükleme öncesi yedekleme dosyaları',
@@ -2337,7 +2352,6 @@ const message = {
upgradeHelper: '(Sistem geri alımı için en son yükseltme yedeğini tutmanız önerilir)',
snapshot: 'Sistem anlık görüntü geçici dosyaları',
rollback: 'Kurtarma öncesi yedek dosyaları',
backup: 'Sistem Yedekleme',
backupHelper: 'Yerel yedekleme hesaplarıyla ilişkili olmayan yedekleme dosyaları',
unknownBackup: 'Sistem Yedekleme',
@@ -2346,13 +2360,11 @@ const message = {
unknownDatabase: 'İlişkili Olmayan Veritabanı Yedeklemesi',
unknownWebsite: 'İlişkili Olmayan Web Sitesi Yedeklemesi',
unknownSnapshot: 'İlişkili Olmayan Anlık Görüntü Yedeklemesi',
upload: 'Geçici Yükleme Dosyaları',
uploadHelper: 'Sistem yedek listesinden yüklenen geçici dosyalar',
download: 'Geçici İndirme Dosyaları',
downloadHelper: 'Sistem tarafından üçüncü taraf yedek hesaplarından indirilen geçici dosyalar',
directory: 'Dizin',
systemLog: 'Günlük dosyası',
systemLogHelper: 'Sistem günlükleri, görev günlükleri, web sitesi günlük dosyaları',
dockerLog: 'Konteyner işlem günlüğü dosyaları',
@@ -2360,14 +2372,13 @@ const message = {
shell: 'Shell script zamanlanmış görevler',
containerShell: 'Konteynerler içinde yürütülen shell script zamanlanmış görevler',
curl: 'CURL zamanlanmış görevler',
docker: 'Konteyner çöpü',
dockerHelper: 'Konteynerler, görüntüler, hacimler, derleme önbelleği vb. dosyalar',
volumes: 'Birimler',
buildCache: 'Konteyner Oluşturma Önbelleği',
appTmpDownload: 'Uygulama geçici indirme dosyası',
unknownWebsiteLog: 'Bağlantısız web sitesi günlük yedekleme dosyası',
statusSuggest: 'Disk ekleme/silme/değiştirme sonrası son durumun doğru olması için taramayı tekrar yapın',
},
app: {
app: 'Uygulama | Uygulamalar',
@@ -2461,6 +2472,12 @@ const message = {
reload: 'Yeniden yükle',
upgradeWarn:
'Uygulamanın yükseltilmesi docker-compose.yml dosyasını değiştirecektir. Herhangi bir değişiklik varsa, dosya karşılaştırmasını görüntülemek için tıklayabilirsiniz',
openclawHttpsUpgradeNoticeTitle:
"Not: Aşağıdaki talimatlar yalnızca OpenClaw'ı 2026.3.13 öncesi sürümlerden yükselten kullanıcılar için geçerlidir:",
openclawHttpsUpgradeNoticeItem1:
'Dağıtılmış ajan yükseltmesi tamamlandıktan sonra Yapılandırma -> Ayarlar -> Güvenlik sayfasına gidip erişim adresini manuel olarak ekleyin.',
openclawHttpsUpgradeNoticeItem2:
'Yeni sürüm artık ajana erişim için HTTPS zorunlu kılıyor. Daha önce ters proxy sitesi kullanıyorsanız proxy hedef adresini https://IP:Port olarak değiştirin.',
newVersion: 'Yeni sürüm',
oldVersion: 'Mevcut sürüm',
composeDiff: 'Dosya karşılaştırması',
@@ -2830,7 +2847,6 @@ const message = {
failTimeout: 'Hata zaman aşımı',
failTimeoutHelper:
'Sunucu sağlık kontrolleri için zaman penceresi uzunluğu. Bu süre içinde biriken hata sayısı eşiğe ulaştığında, sunucu geçici olarak kaldırılacak ve aynı süre sonunda yeniden denenir. Varsayılan 10 saniye',
staticChangePHPHelper: 'Şu anda statik bir web sitesi, PHP web sitesine geçiş yapabilirsiniz',
proxyCache: 'Ters Vekil Önbelleği',
cacheLimit: 'Önbellek Alanı Sınırı',
@@ -2858,7 +2874,6 @@ const message = {
preflight: 'Ön istek hızlı yanıtı',
preflightHleper:
'Etkinleştirildiğinde, tarayıcı çapraz kaynak ön isteği (OPTIONS isteği) gönderdiğinde, sistem otomatik olarak 204 durum kodunu döndürecek ve gerekli çapraz kaynak yanıt başlıklarını ayarlayacaktır',
changeDatabase: 'Veritabanını Değiştir',
changeDatabaseHelper1:
'Veritabanı ilişkilendirmesi, web sitesinin yedeklenmesi ve geri yüklenmesi için kullanılır.',
@@ -2894,7 +2909,6 @@ const message = {
execUser: 'Çalıştıran Kullanıcı',
execDir: 'Çalıştırma Dizini',
packagist: 'Çin Tam Aynası',
batchOperate: 'Toplu İşlem',
batchOperateHelper: 'Toplu {0} web siteleri, işlemi devam ettir?',
stream: 'TCP/UDP Proxy',
@@ -2903,8 +2917,8 @@ const message = {
'Harici dinleme bağlantı noktası numarasını ayarlayın, istemciler bu bağlantı noktası üzerinden hizmete erişecektir, virgülle ayırın, örn.: 5222,5223',
streamHelper: 'TCP/UDP Port Yönlendirme ve Yük Dengeleme',
udp: "UDP'yi etkinleştir",
syncHtmlHelper: 'PHP ve statik web siteleriyle senkronize et',
wafValueHelper: 'Değer',
},
php: {
short_open_tag: 'Kısa etiket desteği',
@@ -2934,6 +2948,7 @@ const message = {
containerConfigHelper:
'Oluşturulduktan sonra Yapılandırma - Konteyner Yapılandırması’nda ortam değişkenleri ve diğer bilgiler değiştirilebilir',
dateTimezoneHelper: 'Örnek: TZ=Asia/Shanghai (Gerektiğinde ekleyin)',
extensionsHelper: 'Birden fazla uzantı için virgül ile ayırın',
},
nginx: {
serverNamesHashBucketSizeHelper: 'Sunucu adının karma tablosu boyutu',
@@ -3069,7 +3084,6 @@ const message = {
argsCheck: 'GET parametre kontrolü',
postCheck: 'POST parametre doğrulaması',
cookieBlockList: 'Çerez engelleme listesi',
dockerHelper:
'Mevcut güvenlik duvarı konteyner port eşlemesini devre dışı bırakamaz. Yüklü uygulamalar, uygulama parametrelerini düzenlemek ve port serbest bırakma kurallarını yapılandırmak için [Yüklü] sayfasına gidebilir.',
iptablesHelper:
@@ -3132,7 +3146,6 @@ const message = {
exportHelper: '{0} güvenlik duvarı kuralını dışa aktarmak üzere. Devam etmek istiyor musunuz?',
importSuccess: '{0} kural başarıyla içe aktarıldı',
importPartialSuccess: 'İçe aktarma tamamlandı: {0} başarılı, {1} başarısız',
ipv4Limit: 'Mevcut işlem yalnızca IPv4 adreslerini destekler',
basicStatus: 'Mevcut zincir {0} bağlı değil, lütfen önce bağlayın!',
baseIptables: 'iptables Servisi',
@@ -3164,6 +3177,10 @@ const message = {
portHelper: '0 herhangi bir port anlamına gelir',
allPorts: 'Tüm Portlar',
deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?',
advancedControl: 'Gelişmiş kontrol',
advancedControlNotAvailable:
'Şu anda {0} güvenlik duvarı kullanılıyor, gelişmiş kurallar yalnızca iptables destekler',
destination: 'Hedef',
},
runtime: {
runtime: 'Çalışma Zamanı',
@@ -3627,6 +3644,7 @@ const message = {
importRuleHelper: 'Her satıra bir kural',
},
monitor: {
'360': '360 Arama',
name: 'Web Sitesi İzleme',
pv: 'Sayfa Görüntülemeleri',
uv: 'Benzersiz Ziyaretçiler',
@@ -3696,7 +3714,6 @@ const message = {
statusErr: 'Geçersiz durum kodu formatı',
shenma: 'Shenma Arama',
duckduckgo: 'DuckDuckGo',
'360': '360 Arama',
excludeUri: 'URI’leri Hariç Tut',
top100Helper: 'İlk 100 veriyi göster',
logSaveDay: 'Günlük Saklama Süresi (gün)',
@@ -3807,7 +3824,6 @@ const message = {
wafContent3: 'Bir istek engellendiğinde gösterilecek özel bir sayfa oluşturun',
wafTitle4: 'Özel Kurallar (ACL)',
wafContent4: 'İstekleri özel kurallara göre engelleyin',
tamperTitle1: 'Dosya Bütünlüğü İzleme',
tamperContent1:
'Web sitesi dosyalarının bütünlüğünü izler, çekirdek dosyalar, betik dosyaları ve yapılandırma dosyaları dahil.',
@@ -3820,7 +3836,6 @@ const message = {
tamperTitle4: 'Günlük Kaydı ve Analiz',
tamperContent4:
'Yöneticilerin sonraki denetim ve analiz için dosya erişimi ve işlem günlüklerini kaydeder, potansiyel güvenlik tehditlerini belirler.',
settingTitle1: 'Özel Hoş Geldiniz Mesajı',
settingContent1: '1Panel giriş sayfasında özel bir hoş geldiniz mesajı ayarlayın.',
settingTitle2: 'Özel Logo',
@@ -3828,7 +3843,6 @@ const message = {
settingTitle3: 'Özel Web Sitesi Simgesi',
settingContent3:
'Varsayılan tarayıcı simgesini değiştirmek için özel simgelerin yüklenmesine izin verir, kullanıcı deneyimini iyileştirir.',
monitorTitle1: 'Ziyaretçi Trendi',
monitorContent1: 'Web sitesi ziyaretçi trendlerini istatistiksel olarak toplar ve gösterir',
monitorTitle2: 'Ziyaretçi Haritası',
@@ -3839,7 +3853,6 @@ const message = {
'Web sitesi istek bilgileri hakkında istatistikler, örümcekler, erişim cihazları, istek durumu vb. içerir.',
monitorTitle4: 'Gerçek Zamanlı İzleme',
monitorContent4: 'Web sitesi istek bilgilerinin gerçek zamanlı izlenmesi, istek sayısı, trafik vb. içerir.',
alertTitle1: 'SMS Uyarıları',
alertContent1:
'Anormal sunucu kaynak kullanımı, web sitesi ve sertifika sona ermesi, yeni sürüm güncellemesi, şifre sona ermesi vb. durumlarda kullanıcılar SMS alarmı ile bilgilendirilir, zamanında işlem yapılmasını sağlar.',
@@ -3849,7 +3862,6 @@ const message = {
alertTitle3: 'Uyarı Ayarları',
alertContent3:
'Kullanıcılara özel telefon numaraları, günlük gönderim sıklığı ve günlük gönderim zamanı yapılandırmaları sağlar, kullanıcıların daha makul gönderim uyarıları ayarlamasını kolaylaştırır.',
nodeDashTitle1: 'Uygulama Yönetimi',
nodeDashContent1:
'Çoklu düğüm uygulamalarının birleşik yönetimi, durum izleme, hızlı başlatma/durdurma, terminal bağlantısı ve yedekleme desteği',
@@ -3862,7 +3874,6 @@ const message = {
nodeDashTitle4: 'Zamanlanmış Görev Yönetimi',
nodeDashContent4:
'Çoklu düğüm zamanlanmış görevlerinin birleşik yönetimi, durum izleme, hızlı başlatma/durdurma ve manuel tetikleme çalıştırma desteği',
nodeTitle1: 'Tek Tıkla Düğüm Ekleme',
nodeContent1: 'Birden fazla sunucu düğümünü hızlıca entegre eder',
nodeTitle2: 'Toplu Yükseltme',
@@ -3871,7 +3882,6 @@ const message = {
nodeContent3: 'Her düğümün çalışma durumunu gerçek zamanlı olarak izler',
nodeTitle4: 'Hızlı Uzak Bağlantı',
nodeContent4: 'Düğümlere tek tıkla doğrudan uzak terminal bağlantısı',
fileExchangeTitle1: 'Anahtar Kimlik Doğrulama İletimi',
fileExchangeContent1: 'İletim güvenliğini sağlamak için SSH anahtarları ile kimlik doğrulaması yapar.',
fileExchangeTitle2: 'Verimli Dosya Senkronizasyonu',
@@ -3880,11 +3890,9 @@ const message = {
fileExchangeTitle3: 'Çoklu Düğüm İletişimini Destekler',
fileExchangeContent3:
'Farklı düğümler arasında proje dosyalarını kolayca aktarır, birden fazla sunucuyu esnek bir şekilde yönetir.',
nodeAppTitle1: 'Uygulama Yükseltme Yönetimi',
nodeAppContent1:
'Çoklu düğüm uygulama güncellemelerinin birleşik izlenmesi, tek tıklamayla yükseltme desteği',
appTitle1: 'Esnek Panel Yönetimi',
appContent1: '1Panel sunucunuzu her zaman, her yerde kolayca yönetin.',
appTitle2: 'Kapsamlı Hizmet Bilgisi',
@@ -3893,7 +3901,6 @@ const message = {
appTitle3: 'Gerçek Zamanlı Anormallik İzleme',
appContent3:
'Mobil uygulama üzerinden gerçek zamanlı sunucu durumu, WAF güvenlik izleme, web sitesi trafik istatistikleri ve işlem sağlığı durumunu görüntüleyin.',
clusterTitle1: 'Ana-Çalışan Dağıtımı',
clusterContent1:
'Farklı düğümlerde MySQL/Postgres/Redis ana-çalışan örnekleri oluşturmayı destekler, ana-çalışan ilişkisi ve başlatmayı otomatik olarak tamamlar',
@@ -3962,7 +3969,6 @@ const message = {
nodeSyncHelper: 'Düğüm bilgisi senkronizasyonu aşağıdaki bilgileri senkronize eder:',
nodeSyncHelper1: '1. Genel yedekleme hesabı bilgileri',
nodeSyncHelper2: '2. Ana düğüm ile alt düğümler arasındaki bağlantı bilgileri',
nodeCheck: 'Kullanılabilirlik kontrolü',
checkSSH: 'Düğüm SSH bağlantısını kontrol et',
checkUserPermission: 'Düğüm kullanıcı izinlerini kontrol et',
@@ -3983,7 +3989,6 @@ const message = {
errLicense: 'Bu düğüme bağlı lisans kullanılamıyor. Lütfen kontrol edin ve tekrar deneyin!',
errNodePort:
'Düğüm portu [ {0} ] erişilemez olarak algılandı. Lütfen güvenlik duvarı veya güvenlik grubunun bu portu izin verdiğinden emin olun.',
reinstallHelper: '{0} düğümünü yeniden kurmak istiyor musunuz?',
unhealthyCheck: 'Anormal Kontrol',
fixOperation: 'Düzeltme İşlemi',
@@ -4008,7 +4013,6 @@ const message = {
offline: 'Çevrimdışı mod',
freeCount: 'Ücretsiz kota [{0}]',
offlineHelper: 'Düğüm çevrimdışı bir ortamda olduğunda kullanılır',
appUpgrade: 'Uygulama Yükseltme',
appUpgradeHelper: 'Yükseltilmesi gereken {0} uygulama var',
},
@@ -4105,7 +4109,6 @@ const message = {
specifiedTime: 'Belirtilen Süre',
deleteTitle: 'Uyarıyı Sil',
deleteMsg: 'Uyarı görevini silmek istediğinizden emin misiniz?',
allSslTitle: 'Tüm Web Sitesi SSL Sertifikası Sona Erme Uyarıları',
sslTitle: 'Web Sitesi {0} için SSL Sertifikası Sona Erme Uyarısı',
allSiteEndTimeTitle: 'Tüm Web Sitesi Sona Erme Uyarıları',
@@ -4117,7 +4120,6 @@ const message = {
loadTitle: 'Yüksek Yük Uyarısı',
diskTitle: 'Bağlama Dizini {0} için Yüksek Disk Kullanımı Uyarısı',
allDiskTitle: 'Yüksek Disk Kullanımı Uyarısı',
timeRule: 'Kalan süre {0} günden az olduğunda (işlenmezse ertesi gün tekrar gönderilir)',
panelUpdateRule:
'Yeni bir panel sürümü algılandığında bir kez uyarı gönder (işlenmezse ertesi gün tekrar gönderilir)',
@@ -4126,12 +4128,10 @@ const message = {
diskRule:
'{0} bağlama dizini için disk kullanımı {1}{2}’yi aşarsa, uyarı tetiklenir, günde {3} kez gönderilir',
allDiskRule: 'Disk kullanımı {0}{1}’yi aşarsa, uyarı tetiklenir, günde {2} kez gönderilir',
cpuName: ' CPU ',
memoryName: 'Bellek',
loadName: 'Yük',
diskName: 'Disk',
syncAlertInfo: 'Manuel Gönderim',
syncAlertInfoMsg: 'Uyarı görevini manuel olarak göndermek istiyor musunuz?',
pushError: 'Gönderim Başarısız',
+57 -78
View File
@@ -389,6 +389,7 @@ const message = {
app: '應用',
msgCenter: '任務中心',
disk: '磁碟管理',
filter: '篩選器',
},
home: {
recommend: '推薦',
@@ -421,12 +422,10 @@ const message = {
runningTime: '執行時間',
mem: '系統記憶體',
swapMem: 'Swap 分區',
runSmoothly: '執行流暢',
runNormal: '執行正常',
runSlowly: '執行緩慢',
runJam: '執行堵塞',
core: '物理核心',
logicCore: '邏輯核心',
corePercent: '核心使用率',
@@ -452,7 +451,6 @@ const message = {
available: '可用',
percent: '使用率',
goInstall: '去安裝',
networkCard: '網卡',
disk: '磁碟',
memo: '備忘錄',
@@ -505,12 +503,9 @@ const message = {
changeConnHelper: '此操作將修改目前資料庫 {0},是否繼續?',
changePasswordHelper: '目前資料庫已經關聯應用,修改密碼將同步修改應用中資料庫密碼,修改後重新啟動生效。',
recoverTimeoutHelper: '-1 表示不限制逾時時間',
portHelper: '該埠為容器對外暴露埠,修改需要單獨儲存並且重新啟動容器',
confChange: '設定修改',
confNotFound: '未能找到該應用設定檔,請在應用商店升級該應用至最新版本後重試',
loadFromRemote: '從伺服器同步',
userBind: '綁定使用者',
pgBindHelper: '此操作用於建立新使用者並將其綁定到目標資料庫,暫不支援選擇已存在於資料庫中的使用者。',
@@ -537,13 +532,11 @@ const message = {
caCert: 'CA 憑證',
skipVerify: '忽略校驗憑證可用性偵測',
initialDB: '初始資料庫',
formatHelper: '目前資料庫字元集為 {0},字元集不一致可能導致復原失敗',
dropHelper: '將上傳檔案拖曳到此處,或',
clickHelper: '點選上傳',
supportUpType:
'僅支援 sql、sql.gz、tar.gz、.zip 檔案格式,匯入的壓縮檔案必須保證只有一個 .sql 檔案或包含 test.sql',
currentStatus: '當前狀態',
baseParam: '基礎參數',
performanceParam: '效能參數',
@@ -575,7 +568,6 @@ const message = {
sortMergePassesHelper: '若值過大,增加sort_buffer_size',
tableLocksWaited: '鎖表次數',
tableLocksWaitedHelper: '若值過大,請考慮增加您的資料庫效能',
performanceTuning: '效能調整',
optimizationScheme: '最佳化方案',
keyBufferSizeHelper: '用於索引的緩衝區大小',
@@ -593,14 +585,11 @@ const message = {
tableOpenCacheHelper: '表快取',
maxConnectionsHelper: '最大連接數',
restart: '重新啟動資料庫',
slowLog: '慢日誌',
noData: '暫無慢日誌...',
isOn: '開啟',
longQueryTime: '閾值(秒)',
thresholdRangeHelper: '請輸入正確的閾值(1 - 600)',
timeout: '逾時時間(秒)',
timeoutHelper: '空閒連接逾時時間,0表示不斷開',
maxclients: '最大連接數',
@@ -624,22 +613,18 @@ const message = {
latestForkUsec: '最近一次 fork() 操作耗費的微秒數',
redisCliHelper: '未偵測到 redis-cli 服務,請先啟用服務',
redisQuickCmd: 'Redis 快速指令',
recoverHelper: '即將使用 [{0}] 對資料進行覆蓋,是否繼續?',
submitIt: '覆蓋資料',
baseConf: '基本設定',
allConf: '全部設定',
restartNow: '立即重新啟動',
restartNowHelper1: '修改設定後需要重新啟動生效,若您的資料需要持久化請先執行 save 操作。',
restartNowHelper: '修改設定後需要重新啟動生效。',
persistence: '持久化',
rdbHelper1: '秒內,插入',
rdbHelper2: '條資料',
rdbHelper3: '符合任意一個條件將會觸發RDB持久化',
rdbInfo: '請確認規則列表中值在 1-100000 之間',
containerConn: '容器連接',
copyConnURL: '複製連接 URL',
connAddress: '地址',
@@ -662,33 +647,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)://網域或IP:埠',
allowedOriginsHttpsOnly: '訪問地址必須以 https:// 開頭',
provider: '模型供應商',
apiKey: 'API Key',
baseUrl: 'Base URL',
token: 'Token',
manualModel: '手動輸入模型',
verified: '驗證狀態',
verifySkipped: '已跳過',
verifySkipped: '不驗證',
configTitle: '設定',
settingsTab: '設定',
browserTab: '瀏覽器',
securityTab: '安全',
otherTab: '其他',
timeZone: '時區',
browserEnabled: '瀏覽器開關',
headless: '無頭模式',
noSandbox: '禁用沙箱',
defaultProfile: '預設設定檔',
executablePath: '瀏覽器可執行路徑',
switchModelSuccess: '模型切換成功',
channelsTab: '聊天渠道',
channelsTab: '頻道',
wecom: '企業微信',
feishu: '飛書',
pluginNotInstalled: '插件未安裝,請先安裝插件',
dmPolicy: '私聊策略',
groupPolicy: '群組策略',
policyPairing: '配對',
policyOpen: '開放',
policyDisabled: '禁用',
botName: '機器人名稱',
botId: 'Bot ID',
appId: '應用 App ID',
appSecret: '應用 App Secret',
saveAndRestartGateway: '保存並重新啟動網關',
@@ -700,9 +690,11 @@ const message = {
pairingCodeRequired: '請輸入配對碼',
pairingApproveSuccess: '配對成功',
customProviderHelper: '自訂模型供應商不驗證帳號是否可用',
feishuSaveSuccess: '儲存成功',
},
model: {
model: '模型',
localModel: '本地模型',
create: '新增模型',
create_helper: '拉取 "{0}"',
ollama_doc: '您可以瀏覽 Ollama 官方網站,搜尋並尋找更多模型。',
@@ -710,6 +702,7 @@ const message = {
ollama_sync: '同步 Ollama 模型發現下列模型不存在,是否刪除?',
from_remote: '該模型並非透過 1Panel 下載,無相關拉取日誌。',
no_logs: '該模型的拉取日誌已被刪除,無法檢視相關日誌。',
vllmVersionHelper: 'FusionXpark GB 10 伺服器請選擇 -cu130 版本',
},
proxy: {
proxy: 'AI 代理增強',
@@ -745,7 +738,6 @@ const message = {
memoryUsed: '顯存使用',
memoryTotal: '顯存總計',
percent: '使用率',
base: '基礎資訊',
driverVersion: '驅動程式版本',
cudaVersion: 'CUDA 版本',
@@ -774,6 +766,7 @@ const message = {
current: '即時監控',
history: '歷史記錄',
notSupport: '當前版本或驅動不支援顯示該參數。',
processCount: '行程數',
},
mcp: {
server: 'MCP Server',
@@ -810,6 +803,11 @@ const message = {
},
container: {
createByCommand: '指令建立',
importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式',
importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式',
stopContainerBeforeBackup: '備份前停止容器',
stopComposeBeforeBackup: '備份前停止編排',
stopBeforeBackupHelper: '啟用後備份前將停止容器或編排服務,完成後自動恢復,以確保資料一致性',
commandInput: '指令輸入',
commandRule: '請輸入正確的 docker run 容器建立指令',
commandHelper: '將在伺服器上執行該條指令以建立容器,是否繼續?',
@@ -879,7 +877,6 @@ const message = {
cpuShareHelper: '容器預設份額為 1024 個 CPU,增大可使目前容器獲得更多的 CPU 時間',
inputIpv4: '請輸入 IPv4 位址',
inputIpv6: '請輸入 IPv6 位址',
diskUsage: '磁碟佔用',
localVolume: '本機儲存卷',
buildCache: '建置快取',
@@ -891,7 +888,6 @@ const message = {
sizeRwHelper: '容器獨有的可寫層大小',
sizeRootFs: '虛擬大小',
sizeRootFsHelper: '容器依賴的所有映像層 + 容器層的總大小',
containerFromAppHelper: '偵測到該容器來源於應用商店,應用操作可能會導致目前編輯失效',
containerFromAppHelper1: '在已安裝應用程式列表點選 [參數] 按鈕,進入編輯頁面即可修改容器名稱。',
command: '指令',
@@ -902,7 +898,6 @@ const message = {
emptyUser: '為空時,將使用容器預設的使用者登入',
privileged: '特權模式',
privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟',
upgradeHelper: '倉庫名稱/映像名稱:映像版本',
upgradeWarning2: '升級操作需要重建容器,任何未持久化的資料將會遺失,是否繼續?',
oldImage: '目前映像',
@@ -910,12 +905,13 @@ const message = {
sameImageHelper: '同映像容器可勾選後批次升級',
targetImage: '目標映像',
imageLoadErr: '未偵測到容器的映像名稱',
imageUpdateTagEmpty: '未偵測到可更新的映像標籤',
appHelper: '該容器來源於應用商店,升級可能導致該服務不可用',
resource: '資源',
input: '手動輸入',
forcePull: '強制拉取映像',
forcePullHelper: '忽略伺服器已存在的映像,重新拉取一次',
imageUpdateHelper: '將檢查映像倉庫中的同名標籤,若有更新則拉取並更新本地映像。',
server: '伺服器',
serverExample: '80, 80-88, ip:80 或 ip:80-88',
containerExample: '80 或 80-88',
@@ -957,10 +953,8 @@ const message = {
unlessStopped: '未手動停止則重新啟動',
onFailure: '失敗後重新啟動(預設重新啟動 5 次)',
no: '不重新啟動',
refreshTime: '重新整理間隔',
cache: '快取',
image: '映像',
imagePull: '拉取映像',
imagePullHelper: '支援選擇拉取多個映像,輸入一組映像後Enter繼續',
@@ -992,7 +986,6 @@ const message = {
cleanBuildCache: '清理建置快取',
delBuildCacheHelper: '清理建置快取將刪除所有建置所產生的快取,此操作無法回復。是否繼續?',
urlWarning: '路徑前綴不需要新增 http:// 或 https://,請修改',
network: '網路',
networkHelper: '刪除 1panel-network 容器網路將影響部分應用和執行環境的正常使用,是否繼續?',
networkName: '網路名',
@@ -1004,7 +997,6 @@ const message = {
scope: 'IP 範圍',
gateway: '閘道器',
auxAddress: '排除 IP',
volume: '磁碟區',
volumeDir: '磁碟區目錄',
nfsEnable: '啟用 NFS 儲存',
@@ -1012,7 +1004,6 @@ const message = {
mountpoint: '掛載點',
mountpointNFSHelper: '例:/nfs, /nfs-share',
options: '可選參數',
repo: '倉庫',
httpRepoHelper: '操作 HTTP 類型倉庫需要重新啟動 Docker 服務。',
downloadUrl: '下載網址',
@@ -1022,7 +1013,6 @@ const message = {
mirrorHelper:
'當存在多個加速器時,需要換行顯示,例: \nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
registrieHelper: '當存在多個私有倉庫時,需要換行顯示,例:\n172.16.10.111:8081 \n172.16.10.112:8081',
compose: '編排',
composeFile: '編排檔案',
fromChangeHelper: '切換來源將清空當前已編輯內容,是否繼續?',
@@ -1044,9 +1034,10 @@ const message = {
composeOperatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?',
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網路,是否繼續?',
composeEnvHelper2: '該編排為 1Panel 應用商店建立,請在已安裝應用中修改環境變數。',
composeContentPlaceholder: '在此寫入或貼上 docker-compose 檔案內容',
setting: '設定',
goSetting: '去修改',
restartHelper: '修改設定後需要重新啟動 Docker 服務才能生效',
operatorStatusHelper: '此操作將{0}Docker 服務,是否繼續?',
dockerStatus: 'Docker 服務',
daemonJsonPathHelper: '請保證設定路徑與 docker.service 中指定的設定路徑保持一致。',
@@ -1081,12 +1072,10 @@ const message = {
related: '相關資源',
includeAppstore: '顯示應用商店容器',
excludeAppstore: '隱藏應用商店容器',
cleanDockerDiskZone: '清理 Docker 使用的磁碟空間',
cleanImagesHelper: '( 清理所有未被任何容器使用的映像 )',
cleanContainersHelper: '( 清理所有處於停止狀態的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本機磁碟區 )',
makeImage: '製作映像',
newImageName: '新映像名稱',
commitMessage: '提交資訊',
@@ -1094,6 +1083,7 @@ const message = {
ifPause: '製作過程中是否暫停容器',
ifMakeImageWithContainer: '是否根據此容器製作新映像?',
finishTime: '上一次停止時間',
exportHelper: '即將匯出 {0} 條編排範本,是否繼續?',
},
cronjob: {
importHelper:
@@ -1198,7 +1188,6 @@ const message = {
cutWebsiteLog: '切割網站日誌',
cutWebsiteLogHelper: '切割的日誌檔案會備份到 1Panel 的 backup 目錄下',
syncIpGroup: '同步 WAF IP 組',
requestExpirationTime: '上傳請求過期時間(小時)',
unitHours: '單位:小時',
alertTitle: '計劃任務-{0}「{1}」任務失敗告警',
@@ -1221,6 +1210,9 @@ const message = {
noSuchApp: '未偵測到 {0} 服務,請前往腳本庫頁面手動安裝',
syncHelper: '即將同步系統腳本庫,該操作僅針對系統腳本,是否繼續?',
},
withImageHelper: '備份應用商店映像,但會增加快照檔案體積。',
cleanLog: '清理日誌',
cleanLogscope: '清理類型',
},
monitor: {
globalFilter: '全域過濾',
@@ -1232,7 +1224,6 @@ const message = {
defaultIOHelper: '預設監控和概覽介面顯示的磁碟選項',
cleanMonitor: '清空監控記錄',
cleanHelper: '該操作將清空包括 GPU 在內的所有監控記錄,是否繼續?',
avgLoad: '平均負載',
loadDetail: '負載詳情',
resourceUsage: '資源使用率',
@@ -1719,6 +1710,10 @@ const message = {
light: '亮色',
auto: '跟隨系統',
language: '系統語言',
runtimeEnv: '運行環境',
docSource: '文件來源',
withByRegion: '跟隨運行區域(預設)',
withByLang: '跟隨系統語言',
region: '運行區域',
cn: '中國大陸',
intl: '全球',
@@ -1727,6 +1722,8 @@ const message = {
regionHelper2: '應用商店與腳本庫',
regionHelper3: '使用手冊及相關文件',
regionHelper4: '此操作可能影響後續資源下載與存取,請謹慎操作。',
regionTip: '運行區域影響系統更新源以及安裝包下載地址。',
docSourceTip: '文件來源決定文件與更新日誌的跳轉語言。',
languageHelper: '預設跟隨瀏覽器語言,設定後只對目前瀏覽器生效,更換瀏覽器後需要重新設定',
sessionTimeout: '逾時時間',
sessionTimeoutError: '最小逾時時間為 300 秒',
@@ -1738,7 +1735,7 @@ const message = {
proxyHelper1: '應用商店的安裝包下載和同步(專業版功能)',
proxyHelper2: '系統版本升級及取得更新說明(專業版功能)',
proxyHelper3: '系統許可證的驗證和同步',
proxyHelper4: 'Docker 的網路存取將透過代理伺服器進行(專業版功能)',
proxyHelper4: 'Docker 拉取映像等網路請求將透過代理伺服器存取(專業版功能)',
proxyHelper5: '系統類型腳本庫的統一下載與同步(專業版功能)',
proxyHelper6: '申請憑證(專業版功能)',
proxyType: '代理類型',
@@ -1785,7 +1782,6 @@ const message = {
diskClean: '快取清理',
developerMode: '預覽體驗計劃',
developerModeHelper: '取得 1Panel 的預覽版本,以分享有關新功能和更新的回饋',
thirdParty: '第三方帳號',
scope: '使用範圍',
public: '公有',
@@ -1872,7 +1868,6 @@ const message = {
address: '地址',
path: '路徑',
backupJump: '未在目前備份列表中的備份檔案,請嘗試從檔案目錄中下載後匯入備份。',
snapshot: '快照',
noAppData: '暫無可選擇系統應用',
noBackupData: '暫無可選擇備份資料',
@@ -1939,7 +1934,6 @@ const message = {
rollback: '回滾',
rollbackHelper:
'即將回滾本次復原,回滾將取代所有本次復原的檔案,過程中可能需要重新啟動 Docker 以及 1Panel 服務,是否繼續?',
upgradeRecord: '升級記錄',
upgrading: '正在升級中,請稍候',
upgradeHelper: '升級操作需要重新啟動 1Panel 服務,是否繼續?',
@@ -1952,7 +1946,6 @@ const message = {
source: '下載源',
versionNotSame: '節點版本與主節點不一致,請在節點管理中升級後重試。',
versionCompare: '偵測到節點 {0} 版本已是目前可升級最新版本,請檢查主節點版本後重試',
safe: '安全',
passkey: '通行金鑰',
passkeyManage: '管理',
@@ -2045,7 +2038,6 @@ const message = {
error416: '無效請求',
error444: '連線已關閉',
error500: '內部伺服器錯誤',
https: '為面板設置 HTTPS 可提升存取安全性',
strictHelper: '非 HTTPS 流量無法連接面板',
muxHelper: '面板將同時監聽 HTTP 和 HTTPS 協議,並將 HTTP 重定向到 HTTPS,此模式下可能會降低面板安全性',
@@ -2058,7 +2050,6 @@ const message = {
rootCrtDownload: '根憑證下載',
primaryKey: '金鑰',
certificate: '憑證',
about: '關於',
versionItem: '當前版本',
backupCopies: '備份份數',
@@ -2074,7 +2065,6 @@ const message = {
forum: '論壇求助',
doc2: '使用手冊',
currentVersion: '當前執行版本:',
license: '許可證',
bindNode: '綁定節點',
menuSetting: '選單設定',
@@ -2131,6 +2121,7 @@ const message = {
fileExchange: '升級專業版可以在多台伺服器之間快速傳輸檔案。',
app: '升級專業版可透過手機APP,檢視服務資訊、異常監控等。',
cluster: '升級專業版可以管理 MySQL/Postgres/Redis 主從叢集。',
vllm: '升級專業版可將 Vllm 服務統一納入 1Panel 管理,集中完成部署、設定調整、狀態維運與任務追蹤,降低本地模型服務維護成本。',
},
clean: {
scan: '開始掃描',
@@ -2149,7 +2140,6 @@ const message = {
suggest: '( 建議清理 )',
totalScan: '待清理垃圾檔案共計: ',
selectScan: '已選中垃圾檔案共計: ',
system: '系統垃圾',
systemHelper: '快照、升級等過程中產生的臨時檔案',
panelOriginal: '系統快照恢復前備份檔案',
@@ -2158,7 +2148,6 @@ const message = {
upgradeHelper: '( 建議保留最新的升級備份用於系統回滾 )',
snapshot: '系統快照暫存檔',
rollback: '復原前備份目錄',
backup: '系統備份',
backupHelper: '本地備份帳號中未關聯的備份檔案',
unknownBackup: '系統備份',
@@ -2167,13 +2156,11 @@ const message = {
unknownDatabase: '未關聯資料庫備份',
unknownWebsite: '未關聯網站備份',
unknownSnapshot: '未關聯快照備份',
upload: '臨時上傳檔案',
uploadHelper: '系統上傳備份列表中上傳的暫存檔',
download: '臨時下載檔案',
downloadHelper: '系統從第三方備份帳號下載的暫存檔',
directory: '資料夾',
systemLog: '日誌檔案',
systemLogHelper: '系統日誌、任務日誌、網站日誌檔案',
dockerLog: '容器操作日誌檔案',
@@ -2181,12 +2168,10 @@ const message = {
shell: 'Shell 腳本計劃任務',
containerShell: '容器內執行 Shell 腳本計劃任務',
curl: 'CURL 計劃任務',
docker: '容器垃圾',
dockerHelper: '容器、映像、儲存卷、建置快取等檔案',
volumes: '磁碟區',
buildCache: '容器建置快取',
appTmpDownload: '應用程式暫存下載檔案',
unknownWebsiteLog: '未關聯網站日誌備份檔案',
},
@@ -2267,6 +2252,10 @@ const message = {
showLocal: '顯示本機應用程式',
reload: '重載',
upgradeWarn: '升級應用程式會取代 docker-compose.yml 檔案,如有更改,可以點選檢視檔案對比',
openclawHttpsUpgradeNoticeTitle: '注意:以下說明僅適用於 OpenClaw 從 2026.3.13 之前版本升級的使用者:',
openclawHttpsUpgradeNoticeItem1: '已部署的智慧體升級完成後,需要進入 配置 → 設定 → 安全 頁面手動新增存取地址。',
openclawHttpsUpgradeNoticeItem2:
'新版本已強制使用 HTTPS 存取智慧體。如果之前透過反向代理網站存取,請將代理目標地址修改為 https://IP:Port。',
newVersion: '新版本',
oldVersion: '當前版本',
composeDiff: '檔案對比',
@@ -2606,7 +2595,6 @@ const message = {
failTimeout: '故障逾時',
failTimeoutHelper:
'伺服器健康檢查的時間視窗長度。在該時間段內累計失敗次數達到閾值時,伺服器將被暫時移除,並在經過相同時長後重新嘗試。預設 10 秒',
staticChangePHPHelper: '目前為靜態網站,可切換為 PHP 網站',
proxyCache: '反向代理快取',
cacheLimit: '快取空間限制',
@@ -2633,7 +2621,6 @@ const message = {
preflight: '預檢請求快速回應',
preflightHleper:
'開啟後,當瀏覽器發送跨域預檢請求(OPTIONS 請求)時,系統會自動返回 204 狀態碼並設定必要的跨域回應標頭',
changeDatabase: '切換資料庫',
changeDatabaseHelper1: '資料庫關聯用於備份復原網站。',
changeDatabaseHelper2: '切換其他資料庫會導致以前的備份無法復原。',
@@ -2664,7 +2651,6 @@ const message = {
execUser: '執行使用者',
execDir: '執行目錄',
packagist: '中國全量映像',
batchOperate: '批次操作',
batchOperateHelper: '批次{0}網站,是否繼續?',
stream: 'TCP/UDP 代理',
@@ -2672,7 +2658,6 @@ const message = {
streamPortsHelper: '設定對外監聽的埠號,客戶端將透過此埠存取服務,按逗號分割,例:5222,5223',
streamHelper: 'TCP/UDP 埠轉發與負載均衡',
udp: '啟用 UDP',
syncHtmlHelper: '同步到 PHP 和靜態網站',
},
php: {
@@ -2700,6 +2685,7 @@ const message = {
containerConfig: '容器設定',
containerConfigHelper: '環境變數等資訊可以在建立完成之後在設定-容器設定中修改',
dateTimezoneHelper: '範例:TZ=Asia/Shanghai(請根據需要自行新增)',
extensionsHelper: '多個擴展請用 , 分隔',
},
nginx: {
serverNamesHashBucketSizeHelper: '伺服器名字的hash表大小',
@@ -2825,7 +2811,6 @@ const message = {
argsCheck: 'GET 參數校驗',
postCheck: 'POST 參數校驗',
cookieBlockList: 'Cookie 黑名單',
dockerHelper: '目前防火牆無法停用容器埠映射,已安裝應用可前往【已安裝】頁面編輯應用參數,設定埠放行規則。',
iptablesHelper: '偵測到系統正在使用 {0} 防火牆,如需切換至 iptables,請先手動解除安裝',
quickJump: '快速跳轉',
@@ -2878,7 +2863,6 @@ const message = {
exportHelper: '即將匯出 {0} 條防火牆規則,是否繼續?',
importSuccess: '成功匯入 {0} 條規則',
importPartialSuccess: '匯入完成:成功 {0} 條,失敗 {1} 條',
ipv4Limit: '目前操作暫僅支援 IPv4 地址',
basicStatus: '目前未綁定鏈 {0} ,請先綁定',
baseIptables: 'iptables 服務',
@@ -2906,6 +2890,9 @@ const message = {
portHelper: '0 表示任意埠',
allPorts: '所有埠',
deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?',
advancedControl: '進階控制',
advancedControlNotAvailable: '目前使用 {0} 防火牆,進階規則僅支援 iptables',
destination: '目的地',
},
runtime: {
runtime: '執行環境',
@@ -3328,6 +3315,7 @@ const message = {
importRuleHelper: '一行一個規則',
},
monitor: {
'360': '360 搜尋',
name: '網站監控',
pv: '瀏覽量',
uv: '訪客數',
@@ -3396,7 +3384,6 @@ const message = {
statusErr: '狀態碼格式錯誤',
shenma: '神馬搜尋',
duckduckgo: 'DuckDuckGo',
'360': '360 搜尋',
exceptUri: '排除 Uri',
top100Helper: '顯示 Top 100 的資料',
logSaveDay: '日誌儲存天數',
@@ -3418,6 +3405,7 @@ const message = {
trend: '趨勢統計',
reqCount: '請求數',
uriHelper: '可以使用 /test/* 或 /*/index.php 來排除 Uri',
excludeUri: '排除 Uri',
},
tamper: {
tamper: '網站防篡改',
@@ -3496,7 +3484,6 @@ const message = {
wafContent3: '自訂請求被攔截之後的顯示頁面',
wafTitle4: '自訂規則(ACL)',
wafContent4: '根據自訂的規則攔截請求',
tamperTitle1: '檔案完整性監控',
tamperContent1: '監控網站檔案的完整性,包括核心檔案、腳本檔案和設定檔案等。',
tamperTitle2: '即時掃描和偵測',
@@ -3506,14 +3493,12 @@ const message = {
'透過合理的權限設定和瀏覽控制策略,網站防篡改功能可以限制對網站檔案的瀏覽權限,減少潛在的攻擊面。',
tamperTitle4: '日誌紀錄與分析',
tamperContent4: '紀錄檔案瀏覽和操作日誌,以便管理員進行後續的審計和分析,以及發現潛在的安全威脅。',
settingTitle1: '自訂歡迎語',
settingContent1: '在 1Panel 登入頁上設定自訂的歡迎語。',
settingTitle2: '自訂 Logo',
settingContent2: '允許上傳包含品牌名稱或其他文字的 Logo 圖像。',
settingTitle3: '自訂網站圖示',
settingContent3: '允許上傳自訂的圖示,以替代預設的瀏覽器圖示,提升使用者體驗。',
monitorTitle1: '訪客趨勢',
monitorContent1: '統計並顯示網站的訪客趨勢',
monitorTitle2: '訪客地圖',
@@ -3522,7 +3507,6 @@ const message = {
monitorContent3: '統計網站的請求資訊,包括蜘蛛,瀏覽裝置,請求狀態等',
monitorTitle4: '即時監控',
monitorContent4: '即時監控網站的請求訊息,包括請求數,流量等',
alertTitle1: '簡訊告警',
alertContent1:
'當伺服器資源使用異常、網站及憑證過期、新版本更新、密碼過期等情況發生時,透過簡訊告警通知使用者,確保及時處理。',
@@ -3531,7 +3515,6 @@ const message = {
alertTitle3: '告警設定',
alertContent3:
'為使用者提供自訂手機號碼、每日推送次數、每日推送時間的設定,方便使用者的設定來更加合理的進行推送告警。',
nodeDashTitle1: '應用管理',
nodeDashContent1: '統一管理多節點應用,支援狀態監控、快速起停、終端連線及備份',
nodeDashTitle2: '網站管理',
@@ -3540,7 +3523,6 @@ const message = {
nodeDashContent3: '統一管理多節點資料庫,關鍵狀態一目了然,支援一鍵備份',
nodeDashTitle4: '計劃任務管理',
nodeDashContent4: '統一管理多節點計劃任務,支援狀態監控、快速啟停與手動觸發執行',
nodeTitle1: '一鍵新增節點',
nodeContent1: '快速接入多台伺服器節點',
nodeTitle2: '批次升級',
@@ -3549,30 +3531,34 @@ const message = {
nodeContent3: '即時掌握各節點執行狀態',
nodeTitle4: '快速遠端連接',
nodeContent4: '一鍵直連節點遠端終端',
fileExchangeTitle1: '金鑰認證傳輸',
fileExchangeContent1: '通過 SSH 金鑰進行身份驗證,確保傳輸的安全性。',
fileExchangeTitle2: '高效檔案同步',
fileExchangeContent2: '僅同步變化內容,大幅提高傳輸速度與穩定性。',
fileExchangeTitle3: '支援多節點互傳',
fileExchangeContent3: '可在不同節點間便捷傳送項目檔案,靈活管理多台伺服器。',
nodeAppTitle1: '應用升級管理',
nodeAppContent1: '統一監控多節點應用更新情況,支援一鍵升級',
appTitle1: '靈活管理面板',
appContent1: '隨時隨地輕鬆管理你的 1Panel 伺服器。',
appTitle2: '全面服務資訊',
appContent2: '在行動端進行應用、網站、Docker、資料庫等基礎管理,支援快速建立應用與網站。',
appTitle3: '即時異常監控',
appContent3: '行動端即時檢視伺服器狀態、WAF 安全監控、網站存取統計與行程健康狀況。',
clusterTitle1: '主從部署',
clusterContent1: '支援在不同節點建立 MySQL/Postgres/Redis 主從實例,自動完成主從關聯與初始化',
clusterTitle2: '主從管理',
clusterContent2: '統一頁面集中管理多個主從節點,檢視其角色、執行狀態等資訊',
clusterTitle3: '複製狀態',
clusterContent3: '展示主從複製狀態與延遲資訊,輔助排查同步異常問題',
vllmTitle1: '統一納管',
vllmContent1: '將分散的 Vllm 服務統一接入 1Panel,集中檢視實例、版本、狀態與執行目錄。',
vllmTitle2: '快速部署',
vllmContent2: '透過視覺化表單完成版本選擇、模型目錄掛載與啟動參數設定,減少手動編排成本。',
vllmTitle3: '維運便捷',
vllmContent3: '在同一頁面完成建立、編輯、啟停、重啟、刪除與任務追蹤,提升本地模型服務維護效率。',
vllmTitle4: '設定可控',
vllmContent4: '結合進階設定統一管理連接埠、容器、啟動命令與 Compose 設定,便於標準化交付與後續調整。',
},
node: {
master: '主節點',
@@ -3623,7 +3609,6 @@ const message = {
nodeSyncHelper: '節點資訊同步將同步以下資訊:',
nodeSyncHelper1: '1. 公用的備份帳號資訊',
nodeSyncHelper2: '2. 主節點與子節點的連接資訊',
nodeCheck: '可用性檢查',
checkSSH: '檢查節點 SSH 連接',
checkUserPermission: '檢查節點使用者權限',
@@ -3639,7 +3624,6 @@ const message = {
oldDataExist: '檢查到該節點存在 1Panel V2 歷史資料,將使用以下資訊覆蓋目前設定',
errLicense: '檢查到該節點綁定的許可證不可用,請檢查後重試',
errNodePort: '檢查到節點埠 [ {0} ] 無法存取,請檢查防火牆或安全組是否已放行該埠。',
reinstallHelper: '重新安裝節點 {0},是否繼續?',
unhealthyCheck: '異常檢查',
fixOperation: '修復方案',
@@ -3663,7 +3647,6 @@ const message = {
offline: '離線模式',
freeCount: '免費額度 [{0}]',
offlineHelper: '當節點處於離線環境時使用',
appUpgrade: '應用程式升級',
appUpgradeHelper: '有 {0} 個應用程式需要升級',
},
@@ -3753,7 +3736,6 @@ const message = {
specifiedTime: '指定時間',
deleteTitle: '刪除告警',
deleteMsg: '是否確認刪除告警任務?',
allSslTitle: '所有網站憑證到期告警',
sslTitle: '網站「{0}」憑證到期告警',
allSiteEndTimeTitle: '所有網站到期告警',
@@ -3765,18 +3747,15 @@ const message = {
loadTitle: '負載佔用過高告警',
diskTitle: '掛載目錄「{0}」的磁碟佔用過高告警',
allDiskTitle: '磁碟佔用過高告警',
timeRule: '剩餘時間小於{0}天(如未處理,次日會重新發送)',
panelUpdateRule: '偵測到面板有新版本時發送一次(如未處理,次日會重新發送)',
avgRule: '{0}分鐘內平均{1}佔用超過{2}%觸發,每天發送{3}次',
diskRule: '掛載目錄「{0}」的磁碟佔用超過{1}{2}觸發,每天發送{3}次',
allDiskRule: '磁碟佔用超過{0}{1}觸發,每天發送{2}次',
cpuName: ' CPU ',
memoryName: '記憶體',
loadName: '負載',
diskName: '磁碟',
syncAlertInfo: '同步告警資訊',
syncAlertInfoMsg: '是否同步告警資訊內容狀態?',
pushError: '推送失敗',
+45 -14
View File
@@ -646,33 +646,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)://域名或IP:端口',
allowedOriginsHttpsOnly: '访问地址必须以 https:// 开头',
provider: '模型供应商',
apiKey: 'API Key',
baseUrl: 'Base URL',
token: 'Token',
manualModel: '手动输入模型',
verified: '验证状态',
verifySkipped: '已跳过',
verifySkipped: '不验证',
configTitle: '配置',
settingsTab: '设置',
browserTab: '浏览器',
securityTab: '安全',
otherTab: '其他',
timeZone: '时区',
browserEnabled: '浏览器开关',
headless: '无头模式',
noSandbox: '禁用沙箱',
defaultProfile: '默认配置文件',
executablePath: '浏览器可执行路径',
switchModelSuccess: '模型切换成功',
channelsTab: '聊天渠道',
channelsTab: '频道',
wecom: '企业微信',
feishu: '飞书',
pluginNotInstalled: '插件未安装,请先安装插件',
dmPolicy: '私聊策略',
groupPolicy: '群组策略',
policyPairing: '配对码',
policyOpen: '开放',
policyDisabled: '禁用',
botName: '机器人名称',
botId: 'Bot ID',
appId: '应用 App ID',
appSecret: '应用 App Secret',
pairingCode: '配对码',
@@ -687,6 +692,7 @@ const message = {
},
model: {
model: '模型',
localModel: '本地模型',
create: '添加模型',
create_helper: '拉取 "{0}"',
ollama_doc: '您可以访问 Ollama 官网,搜索并查找更多模型。',
@@ -694,6 +700,7 @@ const message = {
ollama_sync: '同步 Ollama 模型发现下列模型不存在,是否删除?',
from_remote: '该模型并非通过 1Panel 下载,无相关拉取日志。',
no_logs: '该模型的拉取日志已被删除,无法查看相关日志。',
vllmVersionHelper: 'FusionXpark GB 10 服务器请选择 -cu130 版本',
},
proxy: {
proxy: 'AI 代理增强',
@@ -795,6 +802,11 @@ const message = {
},
container: {
createByCommand: '命令创建',
importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式',
importComposeBackupTip: '导入编排备份文件,仅支持 .tar.gz 格式',
stopContainerBeforeBackup: '备份前停止容器',
stopComposeBeforeBackup: '备份前停止编排',
stopBeforeBackupHelper: '启用后备份前将停止容器或编排服务,完成后自动恢复,以确保数据一致性',
commandInput: '命令输入',
commandRule: '请输入正确的 docker run 容器创建命令!',
commandHelper: '将在服务器上执行该条命令以创建容器,是否继续?',
@@ -895,12 +907,14 @@ const message = {
sameImageHelper: '同镜像容器可勾选后批量升级',
targetImage: '目标镜像',
imageLoadErr: '未检测到容器的镜像名称',
imageUpdateTagEmpty: '未检测到可更新的镜像标签',
appHelper: '该容器来源于应用商店,升级可能导致该服务不可用',
resource: '资源',
input: '手动输入',
forcePull: '强制拉取镜像',
forcePullHelper: '忽略服务器已存在的镜像,重新拉取一次',
imageUpdateHelper: '将检查镜像仓库中的同名标签,若有更新则拉取并更新本地镜像。',
server: '服务器',
serverExample: '80, 80-88, ip:80 或 ip:80-88',
containerExample: '80 或 80-88',
@@ -1030,9 +1044,11 @@ const message = {
composeOperatorHelper: '将对 {0} 进行 {1} 操作,是否继续?',
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络,是否继续?',
composeEnvHelper2: '该编排为 1Panel 应用商店创建,请在已安装应用中修改环境变量。',
composeContentPlaceholder: '在此写入或粘贴 docker-compose 文件内容',
setting: '配置',
goSetting: '去修改',
restartHelper: '修改配置后需要重启 Docker 服务生效',
operatorStatusHelper: '此操作将{0}Docker 服务,是否继续?',
dockerStatus: 'Docker 服务',
daemonJsonPathHelper: '请保证配置路径与 docker.service 中指定的配置路径保持一致。',
@@ -1695,14 +1711,16 @@ const message = {
light: '亮色',
auto: '跟随系统',
language: '系统语言',
docSource: '文档来源',
withByRegion: '跟随运行区域(默认)',
withByLang: '跟随系统语言',
runtimeEnv: '运行环境',
region: '运行区域',
cn: '中国大陆',
intl: '全球',
regionHelper: '更改运行区域后,将调整以下资源来源:',
regionHelper1: '系统安装包',
regionHelper2: '应用商店与脚本库',
regionHelper3: '使用手册及相关文档',
regionHelper4: '该操作可能影响后续资源下载与访问,请谨慎操作。',
regionHelper: '该操作可能影响后续资源下载与访问,是否继续?',
regionTip: '运行区域影响系统更新源以及安装包下载地址。',
docSourceTip: '文档来源决定文档与更新日志的跳转语言。',
languageHelper: '默认跟随浏览器语言,设置后只对当前浏览器生效,更换浏览器后需要重新设置',
sessionTimeout: '超时时间',
sessionTimeoutError: '最小超时时间为 300 秒',
@@ -1714,7 +1732,7 @@ const message = {
proxyHelper1: '应用商店的安装包下载和同步(专业版功能)',
proxyHelper2: '系统版本升级及获取更新说明(专业版功能)',
proxyHelper3: '系统许可证的验证和同步',
proxyHelper4: 'Docker 的网络访问将通过代理服务器进行(专业版功能)',
proxyHelper4: 'Docker 拉取镜像等网络请求将通过代理服务器访问(专业版功能)',
proxyHelper5: '系统类型脚本库的统一下载与同步(专业版功能)',
proxyHelper6: '申请证书(专业版功能)',
proxyType: '代理类型',
@@ -2100,6 +2118,7 @@ const message = {
fileExchange: '升级专业版可以在多台服务器之间快速传输文件。',
app: '升级专业版可通过手机 APP,查看服务信息、异常监控等。',
cluster: '升级专业版可以管理 MySQL/Postgres/Redis 主从集群。',
vllm: '升级专业版可将 Vllm 服务统一纳入 1Panel 管理,集中完成部署、配置调整、状态运维与任务追踪,降低本地模型服务维护成本。',
},
clean: {
scan: '开始扫描',
@@ -2233,6 +2252,10 @@ const message = {
showLocal: '本地应用',
reload: '重载',
upgradeWarn: '升级应用会替换 docker-compose.yml 文件,如有更改,可以点击查看文件对比',
openclawHttpsUpgradeNoticeTitle: '注意:以下说明仅适用于 OpenClaw 从 2026.3.13 之前版本升级的用户:',
openclawHttpsUpgradeNoticeItem1: '已部署的智能体升级完成后,需要进入 配置 → 设置 → 安全 页面手动添加访问地址。',
openclawHttpsUpgradeNoticeItem2:
'新版本已强制使用 HTTPS 访问智能体。如果之前通过反向代理网站访问,请将代理目标地址修改为 https://IP:Port。',
newVersion: '新版本',
oldVersion: '当前版本',
composeDiff: '文件对比',
@@ -3527,6 +3550,14 @@ const message = {
clusterContent2: '统一页面集中管理多个主从节点,查看其角色、运行状态等信息',
clusterTitle3: '复制状态',
clusterContent3: '展示主从复制状态与延迟信息,辅助排查同步异常问题',
vllmTitle1: '统一纳管',
vllmContent1: '将分散的 Vllm 服务统一接入 1Panel,集中查看实例、版本、状态与运行目录。',
vllmTitle2: '快速部署',
vllmContent2: '通过可视化表单完成版本选择、模型目录挂载与启动参数配置,减少手工编排成本。',
vllmTitle3: '运维便捷',
vllmContent3: '在同一页面完成创建、编辑、启停、重启、删除与任务追踪,提升本地模型服务维护效率。',
vllmTitle4: '配置可控',
vllmContent4: '结合高级设置统一管理端口、容器、启动命令与 Compose 配置,便于标准化交付与后续调整。',
},
node: {
master: '主节点',
+22 -1
View File
@@ -1,5 +1,14 @@
import { Layout } from '@/routers/constant';
const vllmModules = import.meta.glob('@/xpack/views/vllm/index.vue');
const loadVllmView = () => {
const loader = vllmModules['/src/xpack/views/vllm/index.vue'];
if (loader) {
return loader();
}
return import('@/views/ai/model/vllm/index.vue');
};
const databaseRouter = {
sort: 4,
path: '/ai',
@@ -37,7 +46,8 @@ const databaseRouter = {
component: () => import('@/views/ai/model/ollama/index.vue'),
meta: {
icon: 'p-moxing-menu',
title: 'aiTools.model.model',
title: 'aiTools.model.localModel',
activeMenu: '/ai/model/ollama',
requiresAuth: true,
},
},
@@ -62,6 +72,17 @@ const databaseRouter = {
requiresAuth: true,
},
},
{
path: '/ai/model/vllm',
hidden: true,
name: 'Vllm',
component: loadVllmView,
meta: {
title: 'vLLM',
activeMenu: '/ai/model/ollama',
requiresAuth: true,
},
},
{
path: '/ai/gpu/current',
name: 'GPU',
+1
View File
@@ -53,6 +53,7 @@ export interface GlobalState {
isProductPro: boolean;
isIntl: boolean;
docWithRegion: boolean;
productProExpires: number;
isMasterProductPro: boolean;
isOffLine: boolean;
+12 -1
View File
@@ -4,6 +4,9 @@ import { GlobalState, ThemeConfigProp } from '../interface';
import { DeviceType } from '@/enums/app';
import i18n, { setActiveLocale } from '@/lang';
const CN_DOCS_URL = 'https://1panel.cn/docs/v2';
const INTL_DOCS_URL = 'https://docs.1panel.pro/v2';
const GlobalStore = defineStore({
id: 'GlobalState',
state: (): GlobalState => ({
@@ -47,6 +50,7 @@ const GlobalStore = defineStore({
isProductPro: false,
isIntl: false,
docWithRegion: true,
productProExpires: 0,
isMasterProductPro: false,
isOffLine: false,
@@ -60,7 +64,14 @@ const GlobalStore = defineStore({
state.themeConfig.theme === 'dark' ||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro/v2' : 'https://1panel.cn/docs/v2'),
docsUrl: (state) => {
if (state.docWithRegion) {
return state.isIntl ? INTL_DOCS_URL : CN_DOCS_URL;
}
const lang = state.language.toLowerCase();
const isChinese = lang === 'zh';
return isChinese ? CN_DOCS_URL : INTL_DOCS_URL;
},
isMaster: (state) => state.currentNode === 'local',
},
actions: {
+75
View File
@@ -1,8 +1,83 @@
import i18n from '@/lang';
interface AllowedOriginOptions {
httpsOnly?: boolean;
}
export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
if (provider === 'custom' || displayName === 'Custom') {
return i18n.global.t('container.custom');
}
if (provider === 'vllm' || displayName === 'vLLM') {
return 'vLLM';
}
return displayName || provider;
};
export const buildDefaultAllowedOrigin = (systemIP: string, port?: number | string): string => {
const target = String(systemIP || '').trim();
if (!target || !port) {
return '';
}
const host = target.includes(':') && !target.startsWith('[') && !target.endsWith(']') ? `[${target}]` : target;
return `https://${host}:${port}`;
};
export const normalizeAllowedOrigin = (value: string, options: AllowedOriginOptions = {}): string => {
const target = String(value || '').trim();
if (!target) {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
let parsed: URL;
try {
parsed = new URL(target);
} catch (error) {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
if (options.httpsOnly && parsed.protocol !== 'https:') {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsHttpsOnly'));
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
if (parsed.pathname && parsed.pathname !== '/') {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
if (!parsed.host || !parsed.port) {
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
}
return `https://${parsed.host}`;
};
export const parseAllowedOriginsInput = (value: string, options: AllowedOriginOptions = {}): string[] => {
const result: string[] = [];
const seen = new Set<string>();
for (const line of String(value || '').split(/\r?\n/)) {
const target = line.trim();
if (!target) {
continue;
}
const normalized = normalizeAllowedOrigin(target, options);
if (seen.has(normalized)) {
continue;
}
seen.add(normalized);
result.push(normalized);
}
return result;
};
export const validateAllowedOriginsInput = (value: string, options: AllowedOriginOptions = {}): string => {
try {
const parsed = parseAllowedOriginsInput(value, options);
if (parsed.length === 0) {
return i18n.global.t('aiTools.agents.allowedOriginsRequired');
}
return '';
} catch (error: any) {
return error?.message || i18n.global.t('aiTools.agents.allowedOriginsInvalid');
}
};
+15
View File
@@ -1,5 +1,8 @@
import { jumpToPath } from './util';
import router from '@/routers';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
export const jumpToInstall = (type: string, key: string) => {
switch (type) {
@@ -26,6 +29,18 @@ export const jumpToInstall = (type: string, key: string) => {
},
});
return true;
case 'vllm':
if (globalStore.isProductPro) {
router.push({
path: '/ai/model/vllm',
query: {
uncached: 'true',
open: 'create',
},
});
return true;
}
return false;
case 'mysql-cluster':
jumpToPath(router, '/xpack/cluster/mysql');
return true;
@@ -21,10 +21,19 @@
</el-form-item>
<el-form-item
v-if="form.agentType === 'openclaw'"
:label="$t('aiTools.agents.bridgePort')"
prop="bridgePort"
:label="$t('aiTools.agents.allowedOrigins')"
prop="allowedOrigins"
>
<el-input-number v-model="form.bridgePort" :min="1" :max="65535" />
<el-input
v-model="form.allowedOrigins"
type="textarea"
:rows="3"
:placeholder="$t('aiTools.agents.allowedOriginsPlaceholder')"
@input="handleAllowedOriginsInput"
/>
<span class="input-help">
{{ $t('aiTools.agents.allowedOriginsHelper') }}
</span>
</el-form-item>
</el-card>
<el-card class="form-card" v-if="form.agentType === 'openclaw'">
@@ -103,8 +112,14 @@ import { checkNumberRange, Rules } from '@/global/form-rules';
import { createAgent, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import { getAppByKey, getAppDetail } from '@/api/modules/app';
import { getAgentSettingByKey } from '@/api/modules/setting';
import { getRandomStr, newUUID } from '@/utils/util';
import { getAgentProviderDisplayName } from '@/utils/agent';
import {
buildDefaultAllowedOrigin,
getAgentProviderDisplayName,
parseAllowedOriginsInput,
validateAllowedOriginsInput,
} from '@/utils/agent';
import { App } from '@/api/interface/app';
import AdvancedSetting from '@/components/advanced-setting/index.vue';
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
@@ -122,6 +137,9 @@ const providerAccountCount = ref<Record<string, number>>({});
const manualModel = ref(false);
const appInfo = ref<App.AppDTO>();
const accountAddRef = ref();
const systemIP = ref('');
const lastAutoAllowedOrigins = ref('');
const allowedOriginsAutoFilled = ref(true);
const { isIntl } = useGlobalStore();
const form = reactive({
@@ -129,7 +147,7 @@ const form = reactive({
agentType: 'openclaw' as 'openclaw' | 'copaw',
appVersion: '',
webUIPort: 18789,
bridgePort: 18790,
allowedOrigins: '',
provider: 'deepseek',
accountId: undefined as unknown as number,
model: '',
@@ -152,37 +170,28 @@ const form = reactive({
dockerCompose: '',
});
const validateOpenclawOnly = (field: 'provider' | 'accountId' | 'model' | 'bridgePort') => {
return (_rule: any, value: any, callback: (error?: Error) => void) => {
if (form.agentType !== 'openclaw') {
callback();
return;
}
if (field === 'bridgePort') {
if (!value || Number(value) <= 0) {
callback(new Error('bridge port is required'));
return;
}
callback();
return;
}
if (value === undefined || value === null || value === '') {
callback(new Error(`${field} is required`));
return;
}
callback();
};
};
const rules = reactive({
name: [Rules.requiredInput],
agentType: [Rules.requiredSelect],
appVersion: [Rules.requiredSelect],
webUIPort: [Rules.requiredInput],
bridgePort: [{ validator: validateOpenclawOnly('bridgePort'), trigger: 'blur' }],
provider: [{ validator: validateOpenclawOnly('provider'), trigger: 'change' }],
accountId: [{ validator: validateOpenclawOnly('accountId'), trigger: 'change' }],
model: [{ validator: validateOpenclawOnly('model'), trigger: 'change' }],
allowedOrigins: [
Rules.requiredInput,
{
validator: (_rule: any, value: any, callback: (error?: Error) => void) => {
const message = validateAllowedOriginsInput(String(value || ''));
if (message) {
callback(new Error(message));
return;
}
callback();
},
trigger: 'blur',
},
],
provider: [Rules.requiredSelect],
accountId: [Rules.requiredSelect],
model: [Rules.requiredInput],
containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect],
cpuQuota: [checkNumberRange(0, 99999)],
@@ -192,6 +201,28 @@ const rules = reactive({
const filteredModels = computed(() => providerModels.value[form.provider] || []);
const syncAllowedOriginsWithDefault = (force = false) => {
if (form.agentType !== 'openclaw') {
return;
}
const defaultOrigin = buildDefaultAllowedOrigin(systemIP.value, form.webUIPort);
if (!force && !allowedOriginsAutoFilled.value && form.allowedOrigins !== lastAutoAllowedOrigins.value) {
return;
}
form.allowedOrigins = defaultOrigin;
lastAutoAllowedOrigins.value = defaultOrigin;
allowedOriginsAutoFilled.value = true;
};
const loadSystemIP = async () => {
try {
const res = await getAgentSettingByKey('SystemIP');
systemIP.value = String(res.data || '').trim();
} catch (error) {
systemIP.value = '';
}
};
const loadVersions = async (appKey: 'openclaw' | 'copaw') => {
const res = await getAppByKey(appKey);
appInfo.value = res.data;
@@ -300,12 +331,17 @@ const handleAgentTypeChange = async () => {
form.baseURL = '';
form.apiType = 'openai-completions';
if (form.agentType === 'openclaw') {
form.bridgePort = form.bridgePort || 18790;
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
syncAllowedOriginsWithDefault(true);
await loadVersions('openclaw');
await loadProviders();
await loadAccounts();
return;
}
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('copaw');
};
@@ -326,7 +362,7 @@ const handleAccountChange = () => {
form.apiType = selected.apiType || 'openai-completions';
form.maxTokens = selected.maxTokens || 8192;
form.contextWindow = selected.contextWindow || 128000;
if (selected.provider === 'custom' && selected.model && !manualModel.value) {
if ((selected.provider === 'custom' || selected.provider === 'vllm') && selected.model && !manualModel.value) {
form.model = selected.model;
}
}
@@ -345,7 +381,7 @@ const setDefaultModel = () => {
form.model = models[0].id;
return;
}
if (form.provider === 'custom') {
if (form.provider === 'custom' || form.provider === 'vllm') {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (selected?.model && !form.model) {
form.model = selected.model;
@@ -353,6 +389,10 @@ const setDefaultModel = () => {
}
};
const handleAllowedOriginsInput = () => {
allowedOriginsAutoFilled.value = form.allowedOrigins === lastAutoAllowedOrigins.value;
};
const submit = async () => {
if (!formRef.value) {
return;
@@ -367,7 +407,7 @@ const submit = async () => {
name: form.name,
appVersion: form.appVersion,
webUIPort: form.webUIPort,
bridgePort: form.agentType === 'openclaw' ? form.bridgePort : undefined,
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
agentType: form.agentType,
provider: form.agentType === 'openclaw' ? form.provider : undefined,
model: form.agentType === 'openclaw' ? form.model : undefined,
@@ -408,7 +448,10 @@ const submit = async () => {
const handleClose = () => {
formRef.value?.resetFields();
form.token = '';
form.allowedOrigins = '';
form.dockerCompose = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
};
const openDrawer = async (agentType?: 'openclaw' | 'copaw') => {
@@ -419,12 +462,18 @@ const openDrawer = async (agentType?: 'openclaw' | 'copaw') => {
form.agentType = targetType;
form.token = getRandomStr(32).toLowerCase();
if (form.agentType === 'copaw') {
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('copaw');
providerOptions.value = [];
providerModels.value = {};
accountOptions.value = [];
return;
}
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
syncAllowedOriginsWithDefault(true);
await loadVersions('openclaw');
await loadProviders();
await loadAccounts();
@@ -464,6 +513,13 @@ watch(
},
);
watch(
() => form.webUIPort,
() => {
syncAllowedOriginsWithDefault();
},
);
defineExpose({
open: openDrawer,
});

Some files were not shown because too many files have changed in this diff Show More