Compare commits

...
39 Commits
Author SHA1 Message Date
CityFun 7159aca226 fix: Fix the issue where deleting the host mapping in the runtime environment causes an error." (#12779) 2026-05-20 14:58:35 +08:00
ssongliu 2c5728e27e fix: add license free node count message (#12778) 2026-05-20 14:30:17 +08:00
ssongliu defb40702c fix snapshot recover tar extraction (#12777) 2026-05-20 14:12:54 +08:00
ssongliu b476df3b61 chore: update pro edition copy (#12776) 2026-05-20 13:55:04 +08:00
蘭 4bbd9b6f06 fix: Fix the issue of deleting project directories caused by failed creation of the running environment (#12756) 2026-05-18 18:08:13 +08:00
蘭 325b9c4d88 fix: Fix directory issue where file management loading does not exist (#12744) 2026-05-15 17:57:47 +08:00
蘭 76d965b3b7 feat: enhance file management UI and improve selection handling (#12740) 2026-05-15 11:35:15 +08:00
KOMATA 4b1b27abd8 feat: enhance upgrade process with space check and file handling (#12708)
* feat: enhance upgrade process with space check and file handling improvements

* fix: update minimum upgrade free space requirement to 500MB and simplify space check logic
2026-05-15 11:29:35 +08:00
CityFun f26b4f290d feat: add some translate (#12738) 2026-05-15 11:25:50 +08:00
蘭 5d2221203a fix: optimize directory size calculation with concurrency control (#12737) 2026-05-15 11:25:35 +08:00
蘭 0aff0d529e fix: improve error handling and refactor file history content retrieval (#12736) 2026-05-15 11:25:16 +08:00
CityFun 20f09b3a0c feat: add model downloader (#12694) 2026-05-09 17:17:34 +08:00
蘭 c392b72470 fix: Fix file decompression issue (#12684)
Refs #12675
2026-05-08 06:42:21 +00:00
王贺 50a70ef1e5 fix: skip large binary file history snapshots (#12683)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12681

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-05-08 06:40:22 +00:00
ssongliu 75266ef659 fix: pause home carousel while editing memo (#12680)
Refs #12679
2026-05-08 02:18:20 +00:00
王贺 b3c593b852 fix openwrt procd init script selection (#12670) 2026-05-07 18:28:30 +08:00
ssongliu 73530bb8b9 fix: route terminal websocket by operate node (#12676) 2026-05-07 18:28:17 +08:00
王贺 1b6e70964b fix: preserve upload file permissions (#12672) 2026-05-07 18:26:39 +08:00
王贺 61d42b05e9 fix: optimize dashboard monitor chart (#12673) 2026-05-07 18:25:27 +08:00
CityFun 79703e3c71 fix: resolve missing arrow in compose file comparison during app upgrade (#12663) 2026-05-06 07:38:15 +00:00
ssongliu aee4e4e282 fix: adjust sidebar collapse popover (#12662) 2026-05-06 07:36:15 +00:00
CityFun 744c7559f3 feat: add package-lock.json (#12661) 2026-05-06 14:47:56 +08:00
ssongliu d94c3ec9cc fix: adjust frontend panel sizing (#12660) 2026-05-06 06:22:15 +00:00
ssongliu f2625eab16 fix: refactor local agent proxy handling (#12659) 2026-05-06 06:20:14 +00:00
CityFun c6d498a9be style: remove table column sorting (#12658) 2026-05-06 03:26:14 +00:00
Sanjay Santhanam b06bc0a455 fix(cmd): use exec.LookPath in Which() so detection works without external 'which' (#12651)
cmd.Which() previously shelled out to `which <name>` to determine whether
a binary was on PATH. On distributions that do not ship a `which` package
by default — Arch Linux is the canonical example, but the same applies to
several minimal container images — `which` itself is missing, so every
call to Which() returned false and 1Panel reported core dependencies
(notably Docker) as 'not installed' even when they were running normally.

Switch the primary path to Go's exec.LookPath, which uses the process
PATH directly and has no external dependency. The original shell-out is
preserved as a fallback so any environment where the agent's PATH
differs from the user's interactive shell PATH (the original reason the
shell-out existed) keeps working unchanged.

Both copies are updated (agent/utils/cmd/cmd.go and core/utils/cmd/cmd.go)
and a small unit test is added to each package to guard the regression.

Fixes #12605

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
2026-05-06 10:47:02 +08:00
Sanjay Santhanam 68411bddd9 fix(ssl): support IPv6 hosts in panel SSL self-signed certificate flow (#12652)
Enabling Panel SSL with the self-sign provider rejected IPv6 hosts with
"domain format invalid". Two coupled bugs caused this:

1. The frontend extracted the host from window.location.href with
   href.split('//')[1].split(':')[0]. For an IPv6 URL like
   https://[::1]:1234 that yields '[' \u2014 not a valid host \u2014 because
   the second split splits on the first colon inside the bracketed
   address. Use window.location.hostname, which natively returns the
   bracket-stripped IPv6 host.

2. The backend ObtainSSL flow used net.ParseIP(domain) directly. Even if
   the frontend sent the bracketed form ('[::1]'), net.ParseIP rejects
   brackets, so the value flowed into IsValidDomain() and failed the
   regex.

Add common.ParseIPLoose() that accepts both bare and bracketed IPv6 in
addition to bare IPv4. Use it at both call sites in ObtainSSL (renew
path and create path). A unit test guards the regression.

Files:
  - agent/utils/common/common.go               (new ParseIPLoose helper)
  - agent/utils/common/parse_ip_test.go        (12 cases, all green)
  - agent/app/service/website_ca.go            (call sites switched)
  - frontend/src/views/setting/safe/ssl/index.vue
                                                (host extraction fix)

Fixes #12646

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
2026-05-06 10:36:23 +08:00
Ran a2ac2e0b22 fix(agent/file): correct return type of NewIFileService (#12648)
`NewIFileService` returns the bare struct type `FileService`, but its
body returns a pointer (`&FileService{}`), and the function name suggests
it should return the interface (`IFileService`). The current signature
breaks `go build ./...` on the `agent` module:

    app/service/file.go:95:9: cannot use &FileService{} (value of type
        *FileService) as FileService value in return statement
    app/service/website_proxy.go:276:36: cannot call pointer method
        GetFileList on FileService

Both errors resolve when the constructor returns the interface, since
`*FileService` implements every method on `IFileService` (verified with
`go vet ./...`).

After this change, `go build ./...` and `go vet ./...` are clean on the
`agent` module.
2026-05-06 10:33:04 +08:00
ssongliu a5a5d9f1ac fix: adjust container log download timeout (#12632) 2026-04-29 09:41:53 +00:00
ssongliu 82876eac8d fix: close task log dialog state (#12631)
Refs #12595
2026-04-29 02:43:42 +00:00
ssongliu 19c9c0f4e5 fix: adjust agent terminal height (#12630)
Refs #12611
2026-04-29 02:41:42 +00:00
ssongliu 70f044dc50 fix: adjust home memo empty state (#12629)
Refs #12609
2026-04-29 02:39:41 +00:00
Waynexxxx 1c8418026c fix: clean up docker process in DownloadContainerLogs on timeout (#12607) 2026-04-28 15:47:03 +08:00
CityFun 17f8471c14 fix: Fix the issue where *.domain.com fails to redirect. (#12625) 2026-04-28 07:41:40 +00:00
CityFun 4deb2ea484 feat: add deepseek-v4-pro and deepseek-v4-flash model (#12622)
https://github.com/1Panel-dev/1Panel/issues/12614
2026-04-28 07:39:40 +00:00
蘭 d05317dbf5 feat: enhance VS Code connection instructions and add SSH setup script details (#12613)
#9695
2026-04-28 07:37:42 +00:00
蘭 1db6ccd72b fix: Fix the right-click and filtering function of Monaco editor (#12612)
#12593
2026-04-28 07:35:41 +00:00
ssongliu bc00760404 fix: fix home monitor chart loading (#12591) 2026-04-28 07:33:40 +00:00
蘭 e57dc1240b feat: add stop functionality for decompress tasks and improve UI for task management (#12582) 2026-04-28 07:31:40 +00:00
88 changed files with 14763 additions and 969 deletions
+33 -5
View File
@@ -292,6 +292,26 @@ func (b *BaseApi) DeCompressFile(c *gin.Context) {
helper.Success(c)
}
// @Tags File
// @Summary Stop decompress task
// @Accept json
// @Param request body request.FileDeCompressStopReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/decompress/stop [post]
func (b *BaseApi) StopDeCompressFile(c *gin.Context) {
var req request.FileDeCompressStopReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fileService.StopDeCompress(req.TaskID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags File
// @Summary Load file content
// @Accept json
@@ -391,7 +411,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
helper.BadRequest(c, errors.New("error paths in request"))
return
}
dir := path.Dir(paths[0])
dir := path.Clean(paths[0])
_, err = os.Stat(dir)
if err != nil && os.IsNotExist(err) {
@@ -432,8 +452,14 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
}
_ = os.Chown(dstDir, uid, gid)
}
dstDirMode := mode
dstFileMode := mode.Perm()
if dstDirInfo, err := os.Stat(dstDir); err == nil {
dstDirMode = dstDirInfo.Mode()
dstFileMode = dstDirInfo.Mode().Perm()
}
tmpFilename := dstFilename + ".tmp"
if err := c.SaveUploadedFile(file, tmpFilename); err != nil {
if err := c.SaveUploadedFile(file, tmpFilename, dstDirMode); err != nil {
_ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
failures[file.Filename] = e
@@ -456,7 +482,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
if statErr == nil {
_ = os.Chmod(dstFilename, dstInfo.Mode())
} else {
_ = os.Chmod(dstFilename, mode)
_ = os.Chmod(dstFilename, dstFileMode)
}
if uid != -1 && gid != -1 {
_ = os.Chown(dstFilename, uid, gid)
@@ -767,12 +793,13 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
return err
}
}
if dstDirInfo, err := os.Stat(dstDir); err == nil {
mode = dstDirInfo.Mode().Perm()
}
dstFileName := filepath.Join(dstDir, fileName)
dstInfo, statErr := os.Stat(dstFileName)
if statErr == nil {
mode = dstInfo.Mode()
} else {
mode = 0644
}
if overwrite {
_ = os.Remove(dstFileName)
@@ -794,6 +821,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
}
_ = os.Remove(chunkPath)
}
_ = os.Chmod(dstFileName, mode)
return nil
}
+5
View File
@@ -90,6 +90,11 @@ type FileDeCompress struct {
Type string `json:"type" validate:"required"`
Path string `json:"path" validate:"required"`
Secret string `json:"secret"`
TaskID string `json:"taskID"`
}
type FileDeCompressStopReq struct {
TaskID string `json:"taskID" validate:"required"`
}
type FileEdit struct {
+2 -2
View File
@@ -83,8 +83,8 @@ var catalog = map[string]Meta{
Input: []string{"text"},
},
Models: []Model{
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", MaxTokens: 65536, ContextWindow: 131072, Reasoning: true},
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
},
},
"bailian-coding-plan": {
+1 -1
View File
@@ -896,7 +896,7 @@ func getAppFromRepo(downloadPath string) error {
return err
}
if err := fileOp.Decompress(packagePath, global.Dir.ResourceDir, files.SdkZip, ""); err != nil {
if err := fileOp.Decompress(context.Background(), packagePath, global.Dir.ResourceDir, files.SdkZip, ""); err != nil {
return err
}
defer func() {
+1 -1
View File
@@ -1034,7 +1034,7 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
}
return
}
if err = fileOp.Decompress(filePath, appResourceDir, files.SdkTarGz, ""); err != nil {
if err = fileOp.Decompress(context.Background(), filePath, appResourceDir, files.SdkTarGz, ""); err != nil {
if logger == nil {
global.LOG.Errorf("decompress app[%s] error %v", app.Name, err)
} else {
+2 -1
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"fmt"
"os"
"path"
@@ -263,7 +264,7 @@ func loadSqlFile(file string) (string, error) {
_ = os.RemoveAll(dstDir)
return "", err
}
if err := archiver.Extract(file, dstDir, ""); err != nil {
if err := archiver.Extract(context.Background(), file, dstDir, ""); err != nil {
_ = os.RemoveAll(dstDir)
return "", err
}
+9 -5
View File
@@ -1076,14 +1076,18 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
}
stdout, err := dockerCmd.StdoutPipe()
if err != nil {
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
return err
}
dockerCmd.Stderr = dockerCmd.Stdout
if err := dockerCmd.Start(); err != nil {
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
return err
}
defer func() {
if dockerCmd.Process != nil {
_ = dockerCmd.Process.Kill()
_ = dockerCmd.Wait()
}
}()
tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil {
@@ -1095,7 +1099,7 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
global.LOG.Errorf("os.Remove() failed: %v", err)
}
}()
errCh := make(chan error)
errCh := make(chan error, 1)
go func() {
scanner := bufio.NewScanner(stdout)
var ansiRegex = re.GetRegex(re.AnsiEscapePattern)
@@ -1118,8 +1122,8 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(3 * time.Second):
global.LOG.Errorf("Timeout reached")
case <-time.After(40 * time.Second):
global.LOG.Errorf("Download container logs timeout reached")
}
info, _ := tempFile.Stat()
+225 -18
View File
@@ -49,6 +49,8 @@ import (
type FileService struct {
}
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
type IFileService interface {
GetFileList(op request.FileOption) (response.FileInfo, error)
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
@@ -59,6 +61,7 @@ type IFileService interface {
Compress(c request.FileCompress) error
StopCompress(taskID string) error
DeCompress(c request.FileDeCompress) error
StopDeCompress(taskID string) error
GetContent(op request.FileContentReq) (response.FileInfo, error)
GetPreviewContent(op request.FileContentReq) (response.FileInfo, error)
SaveContent(edit request.FileEdit) error
@@ -472,6 +475,16 @@ func preflightCompressTool(compressType files.CompressType) error {
}
}
func preflightDecompressTool(decompressType files.CompressType) error {
switch decompressType {
case files.Rar, files.X7z:
_, err := files.NewExtractShellArchiver(decompressType)
return err
default:
return nil
}
}
func (f *FileService) StopCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel()
@@ -480,12 +493,180 @@ func (f *FileService) StopCompress(taskID string) error {
return buserr.New("TaskNotFound")
}
func (f *FileService) StopDeCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel()
return nil
}
return buserr.New("TaskNotFound")
}
func (f *FileService) DeCompress(c request.FileDeCompress) error {
fo := files.NewFileOp()
if c.Type == "tar" && len(c.Secret) != 0 {
c.Type = "tar.gz"
}
return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret)
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
return err
}
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil {
return err
}
go func() {
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
t.LogStart(c.Path)
dstExisted := fo.Stat(c.Dst)
parentDir := filepath.Dir(c.Dst)
if !fo.Stat(parentDir) {
if err := fo.CreateDir(parentDir, constant.DirPerm); err != nil {
return err
}
}
tempDst, err := os.MkdirTemp(parentDir, ".decompress-*")
if err != nil {
return err
}
success := false
defer func() {
_ = os.RemoveAll(tempDst)
if !success && !dstExisted {
_ = os.RemoveAll(c.Dst)
}
}()
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
return err
}
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
return err
}
if err := copyDecompressTree(t.TaskCtx, tempDst, c.Dst); err != nil {
return err
}
success = true
return nil
}, nil)
_ = taskItem.Execute()
}()
return nil
}
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
return err
}
}
return nil
}
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
if err := ctx.Err(); err != nil {
return err
}
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
target, err := os.Readlink(srcPath)
if err != nil {
return err
}
if err := os.Symlink(target, dstPath); err != nil {
return err
}
return applyDecompressOwnership(srcPath, dstPath)
}
if info.IsDir() {
dstInfo, err := os.Lstat(dstPath)
keepExistingDir := err == nil && dstInfo.IsDir() && dstInfo.Mode()&os.ModeSymlink == 0
if err != nil && !os.IsNotExist(err) {
return err
}
if !keepExistingDir {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(dstPath, info.Mode().Perm()); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
return err
}
}
if keepExistingDir {
return nil
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return err
}
defer func() {
if cerr := dstFile.Close(); cerr != nil && retErr == nil {
retErr = cerr
}
}()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
func applyDecompressOwnership(srcPath, dstPath string) error {
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
stat, ok := info.Sys().(*unix.Stat_t)
if !ok {
return nil
}
if info.Mode()&os.ModeSymlink != 0 {
return os.Lchown(dstPath, int(stat.Uid), int(stat.Gid))
}
return os.Chown(dstPath, int(stat.Uid), int(stat.Gid))
}
func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) {
@@ -629,13 +810,12 @@ func (f *FileService) ChangeName(req request.FileRename) error {
return buserr.New("ErrInvalidChar")
}
fo := files.NewFileOp()
info, _ := files.NewFileInfo(files.FileOption{Path: req.OldName, Expand: false})
content, _ := os.ReadFile(req.OldName)
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
if err := fo.Rename(req.OldName, req.NewName); err != nil {
return err
}
if info != nil && !info.IsDir {
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, info.FileMode, req.OldName, req.NewName); histErr != nil {
if shouldRecordHistory {
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, mode, req.OldName, req.NewName); histErr != nil {
global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr)
}
}
@@ -665,21 +845,15 @@ func (f *FileService) MvFile(m request.FileMove) error {
path string
content []byte
mode os.FileMode
isDir bool
}
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
for _, oldPath := range m.OldPaths {
content, _ := os.ReadFile(oldPath)
mode := os.FileMode(0640)
isDir := false
if info, err := files.NewFileInfo(files.FileOption{Path: oldPath, Expand: false}); err == nil {
mode = info.FileMode
isDir = info.IsDir
}
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, isDir: isDir})
record bool
}
var errs []error
if m.Type == "cut" {
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
for _, oldPath := range m.OldPaths {
content, mode, record := readEditableFileHistoryContent(oldPath)
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, record: record})
}
if len(m.CoverPaths) > 0 {
for _, src := range m.CoverPaths {
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
@@ -692,7 +866,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
return err
}
for _, snapshot := range snapshots {
if !snapshot.isDir {
if snapshot.record {
targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths))
if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil {
global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr)
@@ -728,6 +902,39 @@ func (f *FileService) MvFile(m request.FileMove) error {
return nil
}
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
info, err := os.Lstat(filePath)
if err != nil {
return nil, 0640, false
}
mode := info.Mode()
if mode.IsDir() || mode&os.ModeSymlink != 0 || !mode.IsRegular() || files.IsBlockDevice(mode) || info.Size() > fileHistorySnapshotMaxSize {
return nil, mode, false
}
file, err := os.Open(filePath)
if err != nil {
return nil, mode, false
}
defer file.Close()
headBuf := make([]byte, 1024)
n, err := file.Read(headBuf)
if err != nil && err != io.EOF {
return nil, mode, false
}
if n > 0 && files.DetectBinary(headBuf[:n]) {
return nil, mode, false
}
if _, err := file.Seek(0, 0); err != nil {
return nil, mode, false
}
content, err := io.ReadAll(io.LimitReader(file, fileHistorySnapshotMaxSize+1))
if err != nil || int64(len(content)) > fileHistorySnapshotMaxSize {
return nil, mode, false
}
return content, mode, true
}
func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string {
if strings.TrimSpace(dst) == "" {
return sourcePath
+33 -4
View File
@@ -41,19 +41,24 @@ import (
func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
runtimeDir := path.Join(global.Dir.RuntimeDir, create.Type)
projectDir := path.Join(runtimeDir, create.Name)
if create.CodeDir != "" && isPathInsideOrEqual(projectDir, create.CodeDir) {
return buserr.New("ErrRuntimeProjectDirContainsCodeDir")
}
if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil {
return
}
versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir))
projectDir := path.Join(runtimeDir, create.Name)
cleanupTarget := versionDir
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
_ = fileOp.DeleteDir(cleanupTarget)
}
}()
if err = fileOp.Rename(versionDir, projectDir); err != nil {
return
}
cleanupTarget = projectDir
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil {
return
@@ -76,12 +81,36 @@ func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp
return
}
func isPathInsideOrEqual(baseDir, targetDir string) bool {
baseAbs := resolveRuntimePath(baseDir)
targetAbs := resolveRuntimePath(targetDir)
rel, err := filepath.Rel(baseAbs, targetAbs)
if err != nil {
return false
}
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
}
func resolveRuntimePath(dir string) string {
cleanDir := filepath.Clean(dir)
if realDir, err := filepath.EvalSymlinks(cleanDir); err == nil {
return realDir
}
if absDir, err := filepath.Abs(cleanDir); err == nil {
return absDir
}
return cleanDir
}
func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
runtimeDir := path.Join(global.Dir.RuntimeDir, create.Type)
projectDir := path.Join(runtimeDir, create.Name)
if create.CodeDir != "" && isPathInsideOrEqual(projectDir, create.CodeDir) {
return buserr.New("ErrRuntimeProjectDirContainsCodeDir")
}
if err = fileOp.CopyDirWithNewName(appVersionDir, runtimeDir, create.Name); err != nil {
return
}
projectDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
@@ -640,7 +669,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
for _, host := range create.ExtraHosts {
extraHosts = append(extraHosts, fmt.Sprintf("%s:%s", host.Hostname, host.IP))
}
delete(serviceValue, "extraHosts")
delete(serviceValue, "extra_hosts")
if len(extraHosts) > 0 {
serviceValue["extra_hosts"] = extraHosts
}
+25 -2
View File
@@ -10,6 +10,7 @@ import (
"os"
"path"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
@@ -46,6 +47,28 @@ import (
type WebsiteService struct {
}
func buildRedirectHostPattern(domain string) string {
labels := strings.Split(domain, ".")
patternLabels := make([]string, 0, len(labels))
for _, label := range labels {
if label == "*" {
patternLabels = append(patternLabels, `[^.]+`)
continue
}
patternLabels = append(patternLabels, regexp.QuoteMeta(label))
}
return fmt.Sprintf("^%s$", strings.Join(patternLabels, `\.`))
}
func parseRedirectHostPattern(pattern string) string {
pattern = strings.Trim(pattern, "'")
pattern = strings.TrimPrefix(pattern, "^")
pattern = strings.TrimSuffix(pattern, "$")
pattern = strings.ReplaceAll(pattern, `[^.]+`, "*")
pattern = strings.ReplaceAll(pattern, `\.`, ".")
return pattern
}
type IWebsiteService interface {
PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error)
GetWebsites() ([]response.WebsiteDTO, error)
@@ -1728,7 +1751,7 @@ func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error
for _, domain := range req.Domains {
block.Directives = append(block.Directives, &components.Directive{
Name: "if",
Parameters: []string{"($host", "~", fmt.Sprintf("'^%s')", domain)},
Parameters: []string{"($host", "~", fmt.Sprintf("'%s')", buildRedirectHostPattern(domain))},
Block: returnBlock,
})
}
@@ -1834,7 +1857,7 @@ func (w WebsiteService) GetRedirect(id uint) (res []response.NginxRedirectConfig
for _, ifDir := range dirs {
params := ifDir.GetParameters()
if len(params) > 2 && params[0] == "($host" {
domain := strings.Trim(strings.Trim(params[2], "'"), "^")
domain := parseRedirectHostPattern(params[2])
redirectConfig.Domains = append(redirectConfig.Domains, domain)
if len(redirectConfig.Domains) > 1 {
continue
+2 -2
View File
@@ -188,7 +188,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...)
}
for _, domain := range existDomains {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
@@ -220,7 +220,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n")
for _, domain := range domainArray {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
if domain != "localhost" && !common.IsValidDomain(domain) {
err = buserr.WithName("ErrDomainFormat", domain)
return nil, err
+2 -1
View File
@@ -60,7 +60,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 Pro Edition for unlimited agents'
ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Business Edition for unlimited agents'
ErrAgentWebsiteBound: 'This agent is already bound to a website'
ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound'
ErrAgentWebsiteInUse: 'This website is already bound to another agent'
@@ -181,6 +181,7 @@ ErrSSLValid: 'Certificate file is invalid'
ErrWebsiteDir: 'Select a subdirectory under the website directory'
ErrComposerFileNotFound: "composer.json file does not exist"
ErrRuntimeNoPort: 'Runtime port is not set'
ErrRuntimeProjectDirContainsCodeDir: 'The runtime directory contains the selected project directory. Please choose another project directory and create it again.'
Status: 'Status'
start: 'Start'
stop: 'Stop'
+2 -1
View File
@@ -55,7 +55,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. Actualiza a la edición Pro para usar agentes ilimitados'
ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualiza a la edición Business para usar agentes ilimitados'
ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web'
ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos'
ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente'
@@ -166,6 +166,7 @@ ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado'
ErrWebsiteDir: 'seleccione un directorio dentro del directorio del sitio web'
ErrComposerFileNotFound: 'El archivo composer.json no existe'
ErrRuntimeNoPort: 'El entorno de ejecución no tiene configurado un puerto, edítelo primero'
ErrRuntimeProjectDirContainsCodeDir: 'El directorio de ejecución incluye el directorio de proyecto seleccionado. Elija otro directorio de proyecto y créelo de nuevo.'
Status: 'Estado'
start: 'Iniciar'
stop: 'Detener'
+2 -1
View File
@@ -55,7 +55,7 @@ ErrAgentApiKeyRequired: 'APIキーが必要です'
ErrAgentComposeRequired: 'Composeが必要です'
ErrAgentIDRequired: 'エージェントIDが必要です'
ErrAgentAccountIDRequired: 'アカウントIDが必要です'
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Pro Edition にアップグレードすると無制限で利用できます'
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Business Edition にアップグレードすると無制限で利用できます'
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
@@ -166,6 +166,7 @@ ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認
ErrWebsiteDir: 'ウェブサイトディレクトリ内のディレクトリを選択してください。'
ErrComposerFileNotFound: 'composer.json ファイルが存在しません'
ErrRuntimeNoPort: 'ランタイム環境にポートが設定されていません。先にランタイム環境を編集してください。'
ErrRuntimeProjectDirContainsCodeDir: '実行環境ディレクトリに選択したプロジェクトディレクトリが含まれています。別のプロジェクトディレクトリを選択して、再作成してください。'
Status: 'ステータス'
start: '開始'
stop: '停止'
+2 -1
View File
@@ -55,7 +55,7 @@ ErrAgentApiKeyRequired: 'API 키 필요'
ErrAgentComposeRequired: 'Compose 내용 필요'
ErrAgentIDRequired: '에이전트 ID 필요'
ErrAgentAccountIDRequired: '계정 ID 필요'
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 만들 수 있습니다. Pro Edition으로 업그레이드하면 제한 없이 사용할 수 있습니다'
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 만들 수 있습니다. Business Edition으로 업그레이드하면 제한 없이 사용할 수 있습니다'
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
@@ -166,6 +166,7 @@ ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를
ErrWebsiteDir: '웹사이트 디렉토리 내의 디렉토리를 선택하세요.'
ErrComposerFileNotFound: 'composer.json 파일이 존재하지 않습니다'
ErrRuntimeNoPort: '런타임 환경에 포트가 설정되지 않았습니다. 먼저 런타임 환경을 편집하세요.'
ErrRuntimeProjectDirContainsCodeDir: '런타임 디렉터리에 선택한 프로젝트 디렉터리가 포함되어 있습니다. 다른 프로젝트 디렉터리를 선택한 후 다시 생성해 주세요.'
Status: '상태'
start: '시작'
stop: '중지'
+2 -1
View File
@@ -55,7 +55,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 Pro untuk penggunaan tanpa had'
ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke Edisi Business untuk penggunaan tanpa had'
ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web'
ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan'
ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain'
@@ -166,6 +166,7 @@ ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil'
ErrWebsiteDir: 'Sila pilih direktori dalam direktori laman web.'
ErrComposerFileNotFound: 'Fail composer.json tidak wujud'
ErrRuntimeNoPort: 'Persekitaran runtime tidak diset dengan port, sila edit persekitaran runtime terlebih dahulu.'
ErrRuntimeProjectDirContainsCodeDir: 'Direktori runtime mengandungi direktori projek yang dipilih. Sila pilih direktori projek lain dan cipta semula.'
Status: 'Status'
start: 'Mulakan'
stop: 'Berhenti'
+2 -1
View File
@@ -55,7 +55,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. Faça upgrade para a edição Pro para usar agentes ilimitados'
ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Faça upgrade para a edição Business para usar agentes ilimitados'
ErrAgentWebsiteBound: 'Este agente já está vinculado a um site'
ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados'
ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente'
@@ -166,6 +166,7 @@ ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do cert
ErrWebsiteDir: ', selecione um diretório dentro do diretório do site.'
ErrComposerFileNotFound: 'O arquivo composer.json não existe'
ErrRuntimeNoPort: 'O ambiente de tempo de execução não está configurado com uma porta, edite o ambiente de tempo de execução primeiro.'
ErrRuntimeProjectDirContainsCodeDir: 'O diretório de execução inclui o diretório de projeto selecionado. Escolha outro diretório de projeto e crie novamente.'
Status: 'Status'
start: 'Iniciar'
stop: 'Parar'
+2 -1
View File
@@ -55,7 +55,7 @@ ErrAgentApiKeyRequired: 'Нужен API-ключ'
ErrAgentComposeRequired: 'Нужен compose'
ErrAgentIDRequired: 'Нужен ID агента'
ErrAgentAccountIDRequired: 'Нужен ID аккаунта'
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Обновитесь до Pro Edition, чтобы использовать их без ограничений'
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Обновитесь до Business Edition, чтобы использовать их без ограничений'
ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом'
ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты'
ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом'
@@ -166,6 +166,7 @@ ErrSSLValid: 'Файл сертификата аномален, проверьт
ErrWebsiteDir: 'Пожалуйста, выберите директорию внутри директории сайта.'
ErrComposerFileNotFound: 'Файл composer.json не существует'
ErrRuntimeNoPort: 'Среда выполнения не настроена с портом, сначала отредактируйте среду выполнения.'
ErrRuntimeProjectDirContainsCodeDir: 'Каталог среды выполнения содержит выбранный каталог проекта. Выберите другой каталог проекта и создайте его заново.'
Status: 'Статус'
start: 'Запустить'
stop: 'Остановить'
+2 -1
View File
@@ -55,7 +55,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ırsız kullanım için Pro Sürüme yükseltin'
ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırsız kullanım için Business Sürüme yükseltin'
ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı'
ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir'
ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı'
@@ -166,6 +166,7 @@ ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edi
ErrWebsiteDir: 'Lütfen web sitesi dizini içindeki bir dizin seçin.'
ErrComposerFileNotFound: 'composer.json dosyası mevcut değil'
ErrRuntimeNoPort: 'Çalışma zamanı ortamı bir porta sahip değil, lütfen önce çalışma zamanı ortamını düzenleyin.'
ErrRuntimeProjectDirContainsCodeDir: 'Çalışma zamanı dizini, seçilen proje dizinini içeriyor. Lütfen başka bir proje dizini seçip yeniden oluşturun.'
Status: 'Durum'
start: 'Başlat'
stop: 'Durdur'
+2 -1
View File
@@ -55,7 +55,7 @@ ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
ErrAgentIDRequired: '智能體 ID 不可為空,請重試'
ErrAgentAccountIDRequired: '帳號 ID 不可為空,請重試'
ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體,升級至 Pro 版即可無限制使用'
ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體,升級至商業版即可無限制使用'
ErrAgentWebsiteBound: '該智能體已關聯網站'
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站'
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
@@ -166,6 +166,7 @@ ErrSSLValid: '憑證檔案異常,請檢查憑證狀態!'
ErrWebsiteDir: '請選擇網站目錄下的目錄'
ErrComposerFileNotFound: 'composer.json 檔案不存在'
ErrRuntimeNoPort: '執行環境未設定埠,請先編輯執行環境'
ErrRuntimeProjectDirContainsCodeDir: '執行環境目錄包含所選專案目錄,請先更換專案目錄後再重新建立'
Status: '狀態'
start: '開啟'
stop: '關閉'
+2 -1
View File
@@ -60,7 +60,7 @@ ErrAgentApiKeyRequired: "API Key 不能为空"
ErrAgentComposeRequired: "自定义编排内容不能为空"
ErrAgentIDRequired: "智能体 ID 不能为空"
ErrAgentAccountIDRequired: "账号 ID 不能为空"
ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至专业版无数量限制"
ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至商业版无数量限制"
ErrAgentWebsiteBound: "该智能体已关联网站"
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站"
ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
@@ -181,6 +181,7 @@ ErrSSLValid: '证书文件异常,请检查证书状态!'
ErrWebsiteDir: '请选择网站目录下的目录'
ErrComposerFileNotFound: 'composer.json 文件不存在'
ErrRuntimeNoPort: '运行环境未设置端口,请先编辑运行环境'
ErrRuntimeProjectDirContainsCodeDir: '运行环境目录包含所选项目目录,请先更换项目目录后再重新创建'
Status: '状态'
start: '开启'
stop: '关闭'
+1
View File
@@ -25,6 +25,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/compress", baseApi.CompressFile)
fileRouter.POST("/compress/stop", baseApi.StopCompressFile)
fileRouter.POST("/decompress", baseApi.DeCompressFile)
fileRouter.POST("/decompress/stop", baseApi.StopDeCompressFile)
fileRouter.POST("/content", baseApi.GetContent)
fileRouter.POST("/preview", baseApi.PreviewContent)
fileRouter.POST("/save", baseApi.SaveContent)
+10
View File
@@ -29,6 +29,16 @@ func SudoHandleCmd() string {
}
func Which(name string) bool {
// Prefer Go's built-in PATH lookup so we don't depend on an external
// `which` binary, which is not installed by default on minimal
// distributions (e.g. Arch Linux, some Alpine images, slim containers).
// See 1Panel-dev/1Panel#12605.
if _, err := exec.LookPath(name); err == nil {
return true
}
// Fall back to shelling out for environments where PATH inside the
// agent process differs from the user's interactive shell PATH (the
// previous behaviour, preserved for compatibility).
stdout, err := RunDefaultWithStdoutBashCf("which %s", name)
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) {
return false
+23
View File
@@ -0,0 +1,23 @@
package cmd
import "testing"
// TestWhich_ExistingBinary verifies that Which() returns true for a
// binary that is guaranteed to exist on every Unix-like build host
// (`sh`). Regression test for #12605: the previous implementation
// shelled out to `which`, which is not always available on minimal
// distributions like Arch Linux. The new implementation tries
// exec.LookPath first, so this assertion holds regardless of whether
// `which` itself is on PATH.
func TestWhich_ExistingBinary(t *testing.T) {
if !Which("sh") {
t.Errorf("Which(\"sh\") = false, want true")
}
}
func TestWhich_MissingBinary(t *testing.T) {
// A binary name that is extremely unlikely to exist on any host.
if Which("definitely-not-a-real-binary-xyzzy-1panel") {
t.Errorf("Which(\"definitely-not-a-real-binary-xyzzy-1panel\") = true, want false")
}
}
+20
View File
@@ -346,6 +346,26 @@ func IsValidIP(ip string) bool {
return net.ParseIP(ip) != nil
}
// ParseIPLoose parses an IP address, accepting bracketed IPv6 forms
// such as "[::1]" or "[fe80::1%eth0]" in addition to the bare forms
// that net.ParseIP supports natively. It also trims surrounding
// whitespace. Returns nil for non-IP input, matching net.ParseIP.
//
// This is required because some flows pass the host portion of a URL
// (e.g. "[::1]") rather than a bare IP address. Rejecting that form
// caused 1Panel-dev/1Panel#12646 — the panel SSL self-sign workflow
// reported “domain format invalid” for IPv6 hosts.
func ParseIPLoose(s string) net.IP {
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return nil
}
if len(trimmed) >= 2 && trimmed[0] == '[' && trimmed[len(trimmed)-1] == ']' {
trimmed = trimmed[1 : len(trimmed)-1]
}
return net.ParseIP(trimmed)
}
const (
b = uint64(1)
kb = 1024 * b
+36
View File
@@ -0,0 +1,36 @@
package common
import "testing"
// Regression test for 1Panel-dev/1Panel#12646: panel SSL self-signed flow
// rejected IPv6 hosts because net.ParseIP does not accept the bracketed
// form (e.g. "[::1]") and the upstream caller passes the host portion of
// a URL rather than a bare IP.
func TestParseIPLoose(t *testing.T) {
cases := []struct {
name string
in string
want bool
}{
{"bare ipv4", "127.0.0.1", true},
{"bare ipv6", "::1", true},
{"bracketed ipv6", "[::1]", true},
{"bracketed full ipv6", "[2001:db8::1]", true},
{"trimmed bare ipv6", " ::1 ", true},
{"trimmed bracketed ipv6", " [::1] ", true},
{"empty", "", false},
{"only brackets", "[]", false},
{"hostname", "example.com", false},
{"bracketed garbage", "[notanip]", false},
{"unbalanced bracket left", "[::1", false},
{"unbalanced bracket right", "::1]", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := ParseIPLoose(tc.in) != nil
if got != tc.want {
t.Errorf("ParseIPLoose(%q) ok = %v, want %v", tc.in, got, tc.want)
}
})
}
}
+1 -1
View File
@@ -260,7 +260,7 @@ func SelectInitScript(keyword string) (string, error) {
keyword = strings.TrimSuffix(keyword, ".service") + ".openrc"
case "sysvinit":
if _, err := os.Stat("/etc/rc.common"); err == nil {
keyword = strings.TrimSuffix(keyword, ".service") + ".prod"
keyword = strings.TrimSuffix(keyword, ".service") + ".procd"
} else {
keyword = strings.TrimSuffix(keyword, ".service") + ".init"
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
type ShellArchiver interface {
Extract(filePath, dstDir string, secret string) error
Extract(ctx context.Context, filePath, dstDir string, secret string) error
Compress(ctx context.Context, sourcePaths []string, dstFile string, secret string) error
}
+74 -21
View File
@@ -32,6 +32,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/mholt/archiver/v4"
"github.com/spf13/afero"
"golang.org/x/sync/singleflight"
)
const (
@@ -54,6 +55,11 @@ var protectedPaths = []string{
"/root",
}
var (
dirSizeGroup singleflight.Group
dirSizeLimiter = make(chan struct{}, 2)
)
func IsProtected(path string) bool {
real, err := filepath.EvalSymlinks(path)
if err == nil {
@@ -682,7 +688,24 @@ func (f FileOp) CopyFile(src, dst string) error {
}
func (f FileOp) GetDirSize(path string) (int64, error) {
duCmd := exec.Command("du", "-s", path)
cleanPath := filepath.Clean(path)
result, err, _ := dirSizeGroup.Do("single:"+cleanPath, func() (interface{}, error) {
dirSizeLimiter <- struct{}{}
defer func() {
<-dirSizeLimiter
}()
return f.getDirSize(cleanPath)
})
if err != nil {
return 0, err
}
return result.(int64), nil
}
func (f FileOp) getDirSize(path string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), cmdRecursiveTimeout)
defer cancel()
duCmd := exec.CommandContext(ctx, "du", "-s", path)
output, err := duCmd.Output()
if err == nil {
fields := strings.Fields(string(output))
@@ -694,6 +717,9 @@ func (f FileOp) GetDirSize(path string) (int64, error) {
}
}
}
if ctx.Err() != nil {
return 0, ctx.Err()
}
var size int64
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
@@ -717,12 +743,31 @@ type DirSize struct {
}
func (f FileOp) GetDepthDirSize(path string) ([]DirSize, error) {
cleanPath := filepath.Clean(path)
result, err, _ := dirSizeGroup.Do("depth:"+cleanPath, func() (interface{}, error) {
dirSizeLimiter <- struct{}{}
defer func() {
<-dirSizeLimiter
}()
return f.getDepthDirSize(cleanPath)
})
if err != nil {
return nil, err
}
return result.([]DirSize), nil
}
func (f FileOp) getDepthDirSize(path string) ([]DirSize, error) {
var result []DirSize
sizeMap := make(map[string]int64)
duCmd := exec.Command("du", "-k", "--max-depth=1", "--exclude=proc", path)
ctx, cancel := context.WithTimeout(context.Background(), cmdRecursiveTimeout)
defer cancel()
duCmd := exec.CommandContext(ctx, "du", "-k", "--max-depth=1", "--exclude=proc", path)
output, err := duCmd.Output()
if err == nil {
parseDUOutput(output, sizeMap)
} else if ctx.Err() != nil {
return nil, ctx.Err()
} else {
calculateDirSizeFallback(path, sizeMap)
}
@@ -743,12 +788,17 @@ func parseDUOutput(output []byte, sizeMap map[string]int64) {
if strings.TrimSpace(line) == "" {
continue
}
fields := strings.Fields(line)
if len(fields) == 2 {
if sizeKB, err := strconv.ParseInt(fields[0], 10, 64); err == nil {
dir := fields[1]
sizeMap[dir] = sizeKB * 1024
sizeText, dir, ok := strings.Cut(strings.TrimSpace(line), "\t")
if !ok {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
sizeText = fields[0]
dir = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), sizeText))
}
if sizeKB, err := strconv.ParseInt(strings.TrimSpace(sizeText), 10, 64); err == nil {
sizeMap[strings.TrimSpace(dir)] = sizeKB * 1024
}
}
}
@@ -910,13 +960,13 @@ func decodeGBK(input string) (string, error) {
return decoded, nil
}
func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType) error {
func (f FileOp) decompressWithSDK(ctx context.Context, srcFile string, dst string, cType CompressType) error {
format := getFormat(cType)
if cType == Gz {
if err := f.tryDecompressTarGz(srcFile, dst, format); err == nil {
if err := f.tryDecompressTarGz(ctx, srcFile, dst, format); err == nil {
return nil
}
return f.DecompressGzFile(srcFile, dst)
return f.DecompressGzFile(ctx, srcFile, dst)
}
type dirEntry struct {
@@ -950,7 +1000,7 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
} else {
parentDir := path.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
if err := f.Fs.MkdirAll(parentDir, constant.DirPerm); err != nil {
return err
}
}
@@ -976,7 +1026,7 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
return err
}
defer input.Close()
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
if err := format.Extract(ctx, input, nil, handler); err != nil {
return err
}
for i := len(dirs) - 1; i >= 0; i-- {
@@ -985,21 +1035,21 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
return nil
}
func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error {
func (f FileOp) Decompress(ctx context.Context, srcFile string, dst string, cType CompressType, secret string) error {
if cType == Tar || cType == Zip || cType == TarGz || cType == Rar || cType == X7z {
shellArchiver, err := NewExtractShellArchiver(cType)
if !f.Stat(dst) {
_ = f.CreateDir(dst, 0755)
}
if err == nil {
if err = shellArchiver.Extract(srcFile, dst, secret); err == nil {
if err = shellArchiver.Extract(ctx, srcFile, dst, secret); err == nil {
return nil
}
if cType == TarGz {
if strings.Contains(err.Error(), "bad decrypt") {
return buserr.New("ErrBadDecrypt")
}
if err := shellArchiver.Extract(srcFile, dst, "-"); strings.Contains(err.Error(), "bad decrypt") {
if err := shellArchiver.Extract(ctx, srcFile, dst, "-"); strings.Contains(err.Error(), "bad decrypt") {
return buserr.New("ErrBadDecrypt")
}
}
@@ -1009,7 +1059,7 @@ func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secre
}
}
}
return f.decompressWithSDK(srcFile, dst, cType)
return f.decompressWithSDK(ctx, srcFile, dst, cType)
}
func ZipFile(ctx context.Context, files []archiver.File, dst afero.File, progress func(current, total int, message string)) error {
@@ -1088,7 +1138,7 @@ func (r *contextReader) Read(p []byte) (int, error) {
}
}
func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.CompressedArchive) error {
func (f FileOp) tryDecompressTarGz(ctx context.Context, srcFile string, dst string, format archiver.CompressedArchive) error {
input, err := f.Fs.Open(srcFile)
if err != nil {
return err
@@ -1116,7 +1166,7 @@ func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.C
} else {
parentDir := filepath.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
if err := f.Fs.MkdirAll(parentDir, constant.DirPerm); err != nil {
return err
}
}
@@ -1139,7 +1189,7 @@ func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.C
return nil
}
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
if err := format.Extract(ctx, input, nil, handler); err != nil {
return err
}
if !extracted {
@@ -1151,7 +1201,7 @@ func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.C
return nil
}
func (f FileOp) DecompressGzFile(srcFile, dst string) error {
func (f FileOp) DecompressGzFile(ctx context.Context, srcFile, dst string) error {
var archiveModTime time.Time
if st, err := f.Fs.Stat(srcFile); err == nil {
archiveModTime = st.ModTime()
@@ -1163,7 +1213,7 @@ func (f FileOp) DecompressGzFile(srcFile, dst string) error {
}
defer in.Close()
gr, err := gzip.NewReader(in)
gr, err := gzip.NewReader(&contextReader{ctx: ctx, r: in})
if err != nil {
return fmt.Errorf("gzip reader creation failed: %w", err)
}
@@ -1282,6 +1332,9 @@ func (f FileOp) TarGzExtractPro(src, dst string, secret string) error {
global.LOG.Debug(commands)
}
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(dst), cmd.WithIgnoreExist1())
if len(secret) == 0 {
return cmdMgr.Run("tar", "zxvf", src)
}
return cmdMgr.RunBashC(commands)
}
func CopyCustomAppFile(srcPath, dstPath string) error {
+2 -2
View File
@@ -19,11 +19,11 @@ func NewRarArchiver() ShellArchiver {
return &RarArchiver{}
}
func (z RarArchiver) Extract(filePath, dstDir string, _ string) error {
func (z RarArchiver) Extract(ctx context.Context, filePath, dstDir string, _ string) error {
if err := checkCmdAvailability("unrar"); err != nil {
return err
}
return cmd.RunDefaultBashCf("unrar x -y -o+ %q %q", filePath, dstDir)
return cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashCf("unrar x -y -o+ %q %q", filePath, dstDir)
}
func (z RarArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) (err error) {
+2 -2
View File
@@ -18,8 +18,8 @@ func NewTarArchiver(compressType CompressType) ShellArchiver {
}
}
func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) error {
return cmd.RunDefaultBashCf("%s %s \"%s\" -C \"%s\"", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)
func (t TarArchiver) Extract(ctx context.Context, FilePath string, dstDir string, secret string) error {
return cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashCf("%s %s \"%s\" -C \"%s\"", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)
}
func (t TarArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, secret string) error {
+2 -2
View File
@@ -21,7 +21,7 @@ func NewTarGzArchiver() ShellArchiver {
return &TarGzArchiver{}
}
func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error {
func (t TarGzArchiver) Extract(ctx context.Context, filePath, dstDir string, secret string) error {
if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("failed to create destination dir: %w", err)
}
@@ -35,7 +35,7 @@ func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error {
commands = fmt.Sprintf("tar -zxvf '%s' -C '%s' > /dev/null 2>&1", filePath, dstDir)
global.LOG.Debug(commands)
}
if err = cmd.RunDefaultBashC(commands); err != nil {
if err = cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashC(commands); err != nil {
return err
}
return nil
+2 -2
View File
@@ -19,11 +19,11 @@ func NewX7zArchiver() ShellArchiver {
return &X7zArchiver{}
}
func (z X7zArchiver) Extract(filePath, dstDir string, _ string) error {
func (z X7zArchiver) Extract(ctx context.Context, filePath, dstDir string, _ string) error {
if err := checkCmdAvailability("7z"); err != nil {
return err
}
return cmd.RunDefaultBashCf("7z x -y -o%q %q", dstDir, filePath)
return cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashCf("7z x -y -o%q %q", dstDir, filePath)
}
func (z X7zArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) (err error) {
+2 -2
View File
@@ -20,11 +20,11 @@ func NewZipArchiver() ShellArchiver {
return &ZipArchiver{}
}
func (z ZipArchiver) Extract(filePath, dstDir string, secret string) error {
func (z ZipArchiver) Extract(ctx context.Context, filePath, dstDir string, secret string) error {
if err := checkCmdAvailability("unzip"); err != nil {
return err
}
return cmd.RunDefaultBashCf("unzip -qo %s -d %s", filePath, dstDir)
return cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashCf("unzip -qo %s -d %s", filePath, dstDir)
}
func (z ZipArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) error {
+38 -16
View File
@@ -10,6 +10,7 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/model"
@@ -33,6 +34,8 @@ type serviceInfo struct {
selAgentName string
}
const minUpgradeFreeSpace = 500 << 20 // 500MB
func loadServiceInfo() (serviceInfo, error) {
basePath, err := controller.GetServicePath("")
if err != nil {
@@ -139,6 +142,18 @@ func (u *UpgradeService) LoadNotes(req dto.Upgrade) (string, error) {
func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
global.LOG.Info("start to upgrade now...")
itemArch, err := loadArch()
if err != nil {
return err
}
svcInfo, err := loadServiceInfo()
if err != nil {
return err
}
if err := checkUpgradeSpace(); err != nil {
return err
}
baseDir := path.Join(global.CONF.Base.InstallDir, fmt.Sprintf("1panel/tmp/upgrade/%s", req.Version))
downloadDir := path.Join(baseDir, "downloads")
_ = os.RemoveAll(baseDir)
@@ -149,14 +164,6 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
if err := os.MkdirAll(originalDir, os.ModePerm); err != nil {
return err
}
itemArch, err := loadArch()
if err != nil {
return err
}
svcInfo, err := loadServiceInfo()
if err != nil {
return err
}
mode := global.CONF.Base.Mode
if strings.Contains(req.Version, "beta") {
@@ -206,7 +213,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
return
}
if err := files.CopyItem(false, true, path.Join(tmpDir, "1pctl"), "/usr/local/bin"); err != nil {
if err := files.CopyFileWithRename(path.Join(tmpDir, "1pctl"), "/usr/local/bin/1pctl"); err != nil {
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
u.handleRollback(originalDir, 2, svcInfo)
@@ -241,11 +248,13 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
global.LOG.Errorf("Update language files failed: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
u.handleRollback(originalDir, 4, svcInfo)
return
}
if err := files.CopyItem(false, true, path.Join(tmpDir, "GeoIP.mmdb"), path.Join(global.CONF.Base.InstallDir, "1panel/geo")); err != nil {
if err := files.CopyFileWithRename(path.Join(tmpDir, "GeoIP.mmdb"), path.Join(global.CONF.Base.InstallDir, "1panel/geo/GeoIP.mmdb")); err != nil {
global.LOG.Warnf("Update GeoIP database failed: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
u.handleRollback(originalDir, 4, svcInfo)
return
}
global.LOG.Info("upgrade successful!")
@@ -349,6 +358,19 @@ func analyzeDoc(version, content string) dto.ReleasesNotes {
return item
}
func checkUpgradeSpace() error {
dir := global.CONF.Base.InstallDir
var stat syscall.Statfs_t
if err := syscall.Statfs(dir, &stat); err != nil {
return err
}
avail := stat.Bavail * uint64(stat.Bsize)
if avail < minUpgradeFreeSpace {
return fmt.Errorf("available space of %s is %d MB, less than required 500MB", dir, avail>>20)
}
return nil
}
func (u *UpgradeService) handleBackup(originalDir string, svcInfo serviceInfo) error {
if err := files.CopyItem(false, true, "/usr/local/bin/1panel-core", originalDir); err != nil {
return err
@@ -385,25 +407,25 @@ func (u *UpgradeService) handleRollback(originalDir string, errStep int, svcInfo
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
}
}
if err := files.CopyItem(false, true, path.Join(originalDir, "1panel-core"), "/usr/local/bin"); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, "1panel-core"), "/usr/local/bin/1panel-core"); err != nil {
global.LOG.Errorf("rollback 1panel-core failed, err: %v", err)
}
if err := files.CopyItem(false, true, path.Join(originalDir, "1panel-agent"), "/usr/local/bin"); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, "1panel-agent"), "/usr/local/bin/1panel-agent"); err != nil {
global.LOG.Errorf("rollback 1panel-agent failed, err: %v", err)
}
if errStep == 1 {
return
}
if err := files.CopyItem(false, true, path.Join(originalDir, "1pctl"), "/usr/local/bin"); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, "1pctl"), "/usr/local/bin/1pctl"); err != nil {
global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
}
if errStep == 2 {
return
}
if err := files.CopyItem(false, true, path.Join(originalDir, svcInfo.coreName), svcInfo.basePath); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, svcInfo.coreName), path.Join(svcInfo.basePath, svcInfo.coreName)); err != nil {
global.LOG.Errorf("rollback %s failed, err: %v", svcInfo.coreName, err)
}
if err := files.CopyItem(false, true, path.Join(originalDir, svcInfo.agentName), svcInfo.basePath); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, svcInfo.agentName), path.Join(svcInfo.basePath, svcInfo.agentName)); err != nil {
global.LOG.Errorf("rollback %s failed, err: %v", svcInfo.agentName, err)
}
if errStep == 3 {
@@ -412,7 +434,7 @@ func (u *UpgradeService) handleRollback(originalDir string, errStep int, svcInfo
if err := files.CopyItem(true, true, path.Join(originalDir, "lang"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback language files failed, err: %v", err)
}
if err := files.CopyItem(false, true, path.Join(originalDir, "GeoIP.mmdb"), path.Join(global.CONF.Base.InstallDir, "1panel/geo")); err != nil {
if err := files.CopyFileWithRename(path.Join(originalDir, "GeoIP.mmdb"), path.Join(global.CONF.Base.InstallDir, "1panel/geo/GeoIP.mmdb")); err != nil {
global.LOG.Errorf("rollback GeoIP database failed, err: %v", err)
}
}
+3 -2
View File
@@ -70,8 +70,8 @@ ErrXpackVersion: "License validation failed, this license is version-limited, ca
ErrLicenseSave: "Failed to save license information, error {{ .err }}, retry!"
ErrLicenseSync: "License synchronization failed, no license info detected in database!"
ErrLicenseExist: "This license record already exists. You can directly go to the license page for node binding."
ErrXpackNotFound: "This section requires Pro Edition. Import a license in Panel Settings > License."
ErrXpackExceptional: "This section requires Pro Edition. Sync the license status in Panel Settings > License."
ErrXpackNotFound: "This section requires Business Edition. Import a license in Panel Settings > License."
ErrXpackExceptional: "This section requires Business Edition. Sync the license status in Panel Settings > License."
ErrXpackLost: "The license retry limit has been reached. Go to Panel Settings > License and run a manual sync."
ErrDeviceLost: "Required files for license verification are missing, check and try again!"
ErrDeviceErr: "Current environment does not match the license import environment. Edit the license and re-import."
@@ -83,6 +83,7 @@ ErrNodeBoundDelete: "This license is bound and does not support delete operation
ErrNodeBoundLimit: "The current free node has reached its limit, check and try again!"
ErrLicenseFree: "Free nodes are available only when the license is correctly bound to a node. Verify and retry."
ErrLicenseUnbind: "Community Edition nodes are still linked to this license. Unbind them in Panel Settings > License and try again."
ErrLicenseBindCount: "The new license free node count is less than the currently linked Community Edition node count. Check and try again."
ErrNoSuchNode: "Node information not found, check and retry!"
ErrNodeUnbind: "This node is not within the license binding range, check and retry!"
ErrNodeBind: "This node is already bound to a license, check and retry!"
+3 -2
View File
@@ -69,8 +69,8 @@ ErrXpackVersion: "Validación de licencia fallida, esta licencia está limitada
ErrLicenseSave: "Error al guardar la información de la licencia, error {{ .err }}, reintente"
ErrLicenseSync: "Sincronización de licencia fallida, no se detectó información de licencia en la base de datos"
ErrLicenseExist: "Este registro de licencia ya existe. Puede ir directamente a la página de licencia para la vinculación del nodo."
ErrXpackNotFound: "Esta sección requiere la edición Pro. Importa una licencia en Configuración del panel > Licencia."
ErrXpackExceptional: "Esta sección requiere la edición Pro. Sincroniza el estado de la licencia en Configuración del panel > Licencia."
ErrXpackNotFound: "Esta sección requiere la edición Business. Importa una licencia en Configuración del panel > Licencia."
ErrXpackExceptional: "Esta sección requiere la edición Business. Sincroniza el estado de la licencia en Configuración del panel > Licencia."
ErrXpackOutOfDate: "La licencia actual ha expirado, importe nuevamente la licencia en Panel > Ajustes > Licencia"
ErrXpackLost: "La licencia alcanzó el límite de reintentos. Ve a Configuración del panel > Licencia y ejecuta una sincronización manual."
ErrDeviceLost: "Faltan archivos necesarios para la verificación de licencia, verifique e intente de nuevo"
@@ -83,6 +83,7 @@ ErrNodeBoundDelete: "Esta licencia está vinculada y no admite operaciones de el
ErrNodeBoundLimit: "El nodo gratuito actual ha alcanzado su límite, verifique e intente de nuevo"
ErrLicenseFree: "Los nodos gratuitos solo pueden usarse si la licencia está correctamente vinculada a un nodo. Verifique e intente de nuevo"
ErrLicenseUnbind: "Esta licencia todavía tiene nodos de la edición Community vinculados. Desvincúlalos en Configuración del panel > Licencia y vuelve a intentarlo."
ErrLicenseBindCount: "El número de nodos gratuitos de la nueva licencia es menor que el número actual de nodos de la edición Community vinculados. Verifique e intente de nuevo."
ErrNoSuchNode: "Información del nodo no encontrada, verifique e intente de nuevo"
ErrNodeUnbind: "Este nodo no está dentro del rango de vinculación de la licencia, verifique e intente de nuevo"
ErrNodeBind: "Este nodo ya está vinculado a una licencia, verifique e intente de nuevo"
+3 -2
View File
@@ -65,8 +65,8 @@ ErrXpackVersion: "ライセンスの確認に失敗しました、このライ
ErrLicenseSave: "ライセンス情報の保存に失敗しました、エラー {{ .err }}、再試行してください"
ErrLicenseSync: "ライセンス情報の同期に失敗しました、データベースにライセンス情報が検出されませんでした"
ErrLicenseExist: "このライセンス記録は既に存在します。ライセンスページで直接ノードの紐付けが可能です。"
ErrXpackNotFound: "この機能は Pro Edition で利用できます。パネル設定 > ライセンス でライセンスをインポートしてください"
ErrXpackExceptional: "この機能は Pro Edition で利用できます。パネル設定 > ライセンス でライセンス状態を同期してください"
ErrXpackNotFound: "この機能は Business Edition で利用できます。パネル設定 > ライセンス でライセンスをインポートしてください"
ErrXpackExceptional: "この機能は Business Edition で利用できます。パネル設定 > ライセンス でライセンス状態を同期してください"
ErrXpackLost: "ライセンスの再試行回数が上限に達しました。パネル設定 > ライセンス で手動同期を実行してください"
ErrDeviceLost: "ライセンス検証に必要なファイルが失われました。確認して再試行してください!"
ErrDeviceErr: "現在の環境とライセンスのインポート環境が一致しません。ライセンスを編集して再度インポートしてください!"
@@ -78,6 +78,7 @@ ErrNodeBoundDelete: "このライセンスはバインドされており、削
ErrNodeBoundLimit: "現在の無料ノードは上限に達しました。確認して再試行してください!"
ErrLicenseFree: "ライセンスがノードに正常にバインドされている場合のみ、その無料ノードを使用できます。確認して再試行してください!"
ErrLicenseUnbind: "このライセンスには Community Edition のノードがまだ紐付いています。パネル設定 > ライセンス で解除してから、もう一度お試しください"
ErrLicenseBindCount: "新しいライセンスの無料ノード数が、現在紐付いている Community Edition ノード数より少ないです。確認して再試行してください"
ErrNoSuchNode: "そのノード情報が見つかりませんでした、確認して再試行してください"
ErrNodeUnbind: "そのノードはライセンスのバインド範囲内ではありません、確認して再試行してください"
ErrNodeBind: "そのノードはライセンスにバインドされています、確認して再試行してください"
+3 -2
View File
@@ -64,8 +64,8 @@ ErrXpackVersion: "라이선스 검증 실패, 이 라이선스는 버전 제한
ErrLicenseSave: "라이선스 정보 저장 실패, 오류 {{ .err }}, 다시 시도해 주세요"
ErrLicenseSync: "라이선스 정보 동기화 실패, 데이터베이스에서 라이선스 정보를 찾을 수 없습니다"
ErrLicenseExist: "해당 라이선스 기록이 이미 존재합니다. 라이선스 페이지에서 직접 노드 바인딩이 가능합니다."
ErrXpackNotFound: "이 기능은 Pro Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스를 가져오세요"
ErrXpackExceptional: "이 기능은 Pro Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스 상태를 동기화하세요"
ErrXpackNotFound: "이 기능은 Business Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스를 가져오세요"
ErrXpackExceptional: "이 기능은 Business Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스 상태를 동기화하세요"
ErrXpackLost: "라이선스 재시도 한도에 도달했습니다. 패널 설정 > 라이선스에서 수동 동기화를 실행하세요"
ErrDeviceLost: "라이센스 검증에 필요한 파일이 누락되었습니다. 확인 후 다시 시도해 주세요"
ErrDeviceErr: "현재 환경이 라이선스 가져오기 환경과 일치하지 않습니다. 라이선스를 편집하고 다시 가져오십시오"
@@ -77,6 +77,7 @@ ErrNodeBoundDelete: "이 라이센스는 바인딩되어 있으며 삭제 작업
ErrNodeBoundLimit: "현재 무료 노드가 한도에 도달했습니다. 확인 후 다시 시도해 주세요"
ErrLicenseFree: "라이선스가 노드에 정상적으로 바인딩된 경우에만 무료 노드를 사용할 수 있습니다. 확인 후 다시 시도하세요"
ErrLicenseUnbind: "이 라이선스에는 아직 커뮤니티 에디션 노드가 연결되어 있습니다. 패널 설정 > 라이선스에서 연결을 해제한 뒤 다시 시도해 주세요"
ErrLicenseBindCount: "새 라이선스의 무료 노드 수가 현재 연결된 커뮤니티 에디션 노드 수보다 적습니다. 확인 후 다시 시도해 주세요"
ErrNoSuchNode: "노드 정보를 찾을 수 없습니다. 다시 확인하고 시도해 주세요"
ErrNodeUnbind: "이 노드가 라이선스 바인딩 범위에 있지 않음을 감지하였습니다. 다시 확인하고 시도해 주세요"
ErrNodeBind: "이 노드가 이미 라이선스에 바인딩되어 있음을 감지하였습니다. 다시 확인하고 시도해 주세요"
+3 -2
View File
@@ -59,8 +59,8 @@ ErrXpackVersion: "Pengesahan lesen gagal, lesen ini terhad kepada versi tertentu
ErrLicenseSave: "Gagal menyimpan maklumat lesen {{ .err }}, sila cuba lagi"
ErrLicenseSync: "Penyelarasan maklumat lesen gagal, maklumat lesen tidak ditemui dalam pangkalan data"
ErrLicenseExist: "Rekod lesen ini sudah wujud. Anda boleh terus ke halaman lesen untuk mengikat nod."
ErrXpackNotFound: "Bahagian ini memerlukan Edisi Pro. Import lesen di Tetapan Panel > Lesen"
ErrXpackExceptional: "Bahagian ini memerlukan Edisi Pro. Segerakkan status lesen di Tetapan Panel > Lesen"
ErrXpackNotFound: "Bahagian ini memerlukan Edisi Business. Import lesen di Tetapan Panel > Lesen"
ErrXpackExceptional: "Bahagian ini memerlukan Edisi Business. Segerakkan status lesen di Tetapan Panel > Lesen"
ErrXpackLost: "Lesen telah mencapai had percubaan semula. Pergi ke Tetapan Panel > Lesen dan jalankan penyelarasan manual"
ErrDeviceLost: "Fail yang diperlukan untuk pengesahan lesen hilang, sila semak dan cuba lagi"
ErrDeviceErr: "Persekitaran semasa tidak sepadan dengan persekitaran import lesen. Sila edit lesen dan import semula"
@@ -72,6 +72,7 @@ ErrNodeBoundDelete: "Lisensi ini telah diikat dan tidak menyokong operasi pengha
ErrNodeBoundLimit: "Nod percuma semasa telah mencapai had, sila semak dan cuba lagi"
ErrLicenseFree: "Nod percuma hanya boleh digunakan apabila lesen terikat dengan betul pada nod. Sila semak dan cuba lagi"
ErrLicenseUnbind: "Lesen ini masih terikat pada nod Edisi Community. Nyahikat di Tetapan Panel > Lesen dan cuba lagi"
ErrLicenseBindCount: "Bilangan nod percuma lesen baharu kurang daripada bilangan nod Edisi Community yang sedang terikat. Sila semak dan cuba lagi"
ErrNoSuchNode: "Maklumat nod tidak ditemui, sila semak dan cuba lagi"
ErrNodeUnbind: "Nod di luar skop lesen dikesan, sila semak dan cuba lagi"
ErrNodeBind: "Nod ini telah diikat dengan lesen, sila semak dan cuba lagi"
+3 -2
View File
@@ -64,8 +64,8 @@ ErrXpackVersion: "Falha na verificação da licença, esta licença é restrita
ErrLicenseSave: "Falha ao salvar as informações da licença {{ .err }}, por favor tente novamente"
ErrLicenseSync: "Falha ao sincronizar as informações da licença, nenhuma informação foi encontrada no banco de dados"
ErrLicenseExist: "Este registro de licença já existe. Você pode ir diretamente para a página de licenças para vincular nós."
ErrXpackNotFound: "Este recurso requer a edição Pro. Importe uma licença em Configurações do Painel > Licença."
ErrXpackExceptional: "Este recurso requer a edição Pro. Sincronize o status da licença em Configurações do Painel > Licença."
ErrXpackNotFound: "Este recurso requer a edição Business. Importe uma licença em Configurações do Painel > Licença."
ErrXpackExceptional: "Este recurso requer a edição Business. Sincronize o status da licença em Configurações do Painel > Licença."
ErrXpackLost: "A licença atingiu o limite de tentativas. Acesse Configurações do Painel > Licença e execute uma sincronização manual."
ErrDeviceLost: "Arquivos necessários para a verificação da licença estão faltando, por favor verifique e tente novamente"
ErrDeviceErr: "O ambiente atual não corresponde ao ambiente de importação da licença. Por favor, edite a licença e reimporte"
@@ -77,6 +77,7 @@ ErrNodeBoundDelete: "Esta licença está vinculada e não suporta operações de
ErrNodeBoundLimit: "O nó gratuito atual atingiu seu limite, por favor verifique e tente novamente"
ErrLicenseFree: "Nós gratuitos só podem ser usados quando a licença está devidamente vinculada a um nó. Por favor verifique e tente novamente"
ErrLicenseUnbind: "Ainda há nós da edição Community vinculados a esta licença. Desvincule-os em Configurações do Painel > Licença e tente novamente."
ErrLicenseBindCount: "A quantidade de nós gratuitos da nova licença é menor que a quantidade de nós da edição Community vinculados atualmente. Verifique e tente novamente."
ErrNoSuchNode: "As informações deste nó não foram encontradas, por favor verifique e tente novamente"
ErrNodeUnbind: "Nó fora do escopo da licença detectado, por favor verifique e tente novamente"
ErrNodeBind: "Este nó já está vinculado a uma licença, por favor verifique e tente novamente"
+3 -2
View File
@@ -64,8 +64,8 @@ ErrXpackVersion: "Ошибка проверки лицензии, лицензи
ErrLicenseSave: "Ошибка сохранения информации о лицензии {{ .err }}, повторите попытку"
ErrLicenseSync: "Ошибка синхронизации информации о лицензии, информация не найдена в базе данных"
ErrLicenseExist: "Данная лицензия уже существует. Вы можете перейти на страницу лицензий для привязки узлов."
ErrXpackNotFound: "Этот раздел доступен в Pro Edition. Импортируйте лицензию в Настройки панели > Лицензия"
ErrXpackExceptional: "Этот раздел доступен в Pro Edition. Синхронизируйте статус лицензии в Настройки панели > Лицензия"
ErrXpackNotFound: "Этот раздел доступен в Business Edition. Импортируйте лицензию в Настройки панели > Лицензия"
ErrXpackExceptional: "Этот раздел доступен в Business Edition. Синхронизируйте статус лицензии в Настройки панели > Лицензия"
ErrXpackLost: "Достигнут лимит повторных попыток по лицензии. Перейдите в Настройки панели > Лицензия и выполните ручную синхронизацию"
ErrDeviceLost: "Необходимые файлы для проверки лицензии отсутствуют, пожалуйста, проверьте и попробуйте снова"
ErrDeviceErr: "Текущая среда не соответствует среде импорта лицензии. Отредактируйте лицензию и повторите импорт"
@@ -77,6 +77,7 @@ ErrNodeBoundDelete: "Эта лицензия привязана и не подд
ErrNodeBoundLimit: "Текущий бесплатный узел достиг предела, пожалуйста, проверьте и попробуйте снова"
ErrLicenseFree: "Бесплатные узлы доступны только при правильной привязке лицензии к узлу. Проверьте и повторите попытку"
ErrLicenseUnbind: "К этой лицензии всё ещё привязаны узлы Community Edition. Отвяжите их в Настройки панели > Лицензия и повторите попытку"
ErrLicenseBindCount: "Количество бесплатных узлов в новой лицензии меньше текущего количества привязанных узлов Community Edition. Проверьте и повторите попытку"
ErrNoSuchNode: "Информация об узле не найдена, проверьте и повторите попытку"
ErrNodeUnbind: "Обнаружен узел вне области действия лицензии, проверьте и повторите попытку"
ErrNodeBind: "Этот узел уже связан с лицензией, проверьте и повторите попытку"
+3 -2
View File
@@ -64,8 +64,8 @@ ErrXpackVersion: "Lisans doğrulama başarısız, bu lisans sürüm sınırlıd
ErrLicenseSave: "Lisans bilgilerini kaydetme başarısız, hata {{ .err }}, lütfen tekrar deneyin"
ErrLicenseSync: "Lisans senkronizasyonu başarısız, veritabanında lisans bilgisi tespit edilmedi"
ErrLicenseExist: "Bu lisans kaydı zaten mevcut. Düğüm bağlama için doğrudan lisans sayfasına gidebilirsiniz."
ErrXpackNotFound: "Bu bölüm Pro Sürüm gerektirir. Lisansı Panel Ayarları > Lisans bölümünden içe aktarın"
ErrXpackExceptional: "Bu bölüm Pro Sürüm gerektirir. Lisans durumunu Panel Ayarları > Lisans bölümünden senkronize edin"
ErrXpackNotFound: "Bu bölüm Business Sürüm gerektirir. Lisansı Panel Ayarları > Lisans bölümünden içe aktarın"
ErrXpackExceptional: "Bu bölüm Business Sürüm gerektirir. Lisans durumunu Panel Ayarları > Lisans bölümünden senkronize edin"
ErrXpackLost: "Lisans yeniden deneme sınırına ulaştı. Panel Ayarları > Lisans bölümüne gidip manuel senkronizasyon çalıştırın"
ErrDeviceLost: "Lisans doğrulama için gerekli dosyalar eksik, lütfen kontrol edip tekrar deneyin"
ErrXpackTimeout: "İstek zaman aşımı, ağ bağlantısı kararsız olabilir, lütfen daha sonra tekrar deneyin"
@@ -76,6 +76,7 @@ ErrNodeBoundDelete: "Bu lisans bağlı ve silme işlemlerini desteklemiyor. Lüt
ErrNodeBoundLimit: "Mevcut ücretsiz düğüm limitine ulaştı, lütfen kontrol edip tekrar deneyin"
ErrLicenseFree: "Ücretsiz düğümler yalnızca lisans bir düğüme düzgün şekilde bağlandığında kullanılabilir. Lütfen doğrulayın ve tekrar deneyin"
ErrLicenseUnbind: "Bu lisansa hala Community Sürümü düğümleri bağlı. Panel Ayarları > Lisans bölümünden bağlantıyı kaldırıp tekrar deneyin"
ErrLicenseBindCount: "Yeni lisansın ücretsiz düğüm sayısı mevcut bağlı Community Sürümü düğüm sayısından az. Lütfen kontrol edip tekrar deneyin"
ErrNoSuchNode: "Düğüm bilgisi bulunamadı, lütfen kontrol edip tekrar deneyin"
ErrNodeUnbind: "Bu düğüm lisans bağlama aralığında değil, lütfen kontrol edip tekrar deneyin"
ErrNodeBind: "Bu düğüm zaten bir lisansa bağlı, lütfen kontrol edip tekrar deneyin"
+3 -2
View File
@@ -64,8 +64,8 @@ ErrXpackVersion: "許可證驗證失敗,該許可證受版本限制,無法
ErrLicenseSave: "許可證資訊儲存失敗,錯誤 {{ .err }},請重試。"
ErrLicenseSync: "許可證資訊同步失敗,資料庫中未偵測到許可證資訊!"
ErrLicenseExist: "該許可證記錄已存在,您可直接前往許可證頁面進行節點綁定。"
ErrXpackNotFound: "此功能需要 Pro 版。請在 面板設定 > 許可證 匯入許可證"
ErrXpackExceptional: "此功能需要 Pro 版。請在 面板設定 > 許可證 同步許可證狀態"
ErrXpackNotFound: "此功能需要商業版。請在 面板設定 > 許可證 匯入許可證"
ErrXpackExceptional: "此功能需要商業版。請在 面板設定 > 許可證 同步許可證狀態"
ErrXpackLost: "許可證已達重試上限,請前往 面板設定 > 許可證 手動同步"
ErrDeviceLost: "許可證驗證必要檔案遺失,請檢查後重試。"
ErrDeviceErr: "目前環境與許可證匯入環境不一致,請編輯許可證重新匯入!"
@@ -77,6 +77,7 @@ ErrNodeBoundDelete: "該許可證已被綁定,不支援刪除操作,請檢
ErrNodeBoundLimit: "目前免費節點已達上限,請檢查後重試。"
ErrLicenseFree: "僅當許可證正常綁定到節點後,才能使用其免費節點,請檢查後重試。"
ErrLicenseUnbind: "此許可證仍綁定了社群版節點,請在 面板設定 > 許可證 解除綁定後再試"
ErrLicenseBindCount: "新許可證免費節點數量小於目前已綁定的社群版節點數量,請檢查後重試。"
ErrNoSuchNode: "未能找到該節點資訊,請檢查後重試。"
ErrNodeUnbind: "偵測到該節點未在許可證綁定範圍內,請檢查後重試。"
ErrNodeBind: "偵測到該節點已綁定許可證,請檢查後重試。"
+5 -4
View File
@@ -70,19 +70,20 @@ ErrXpackVersion: "许可证校验失败,该许可证受版本限制,无法
ErrLicenseSave: "许可证信息保存失败,错误 {{ .err }},请重试!"
ErrLicenseSync: "许可证信息同步失败,数据库中未检测到许可证信息!"
ErrLicenseExist: "该许可证记录已存在,您可直接前往许可证页面进行节点绑定。"
ErrXpackNotFound: "该部分为专业版功能,请先在 面板设置-许可证 界面导入许可证"
ErrXpackExceptional: "该部分为专业版功能,请先在 面板设置-许可证 界面同步许可证状态"
ErrXpackLost: "许可证已达到最大重试次数,请进入【面板设置】【许可证】页面手动点击同步按钮,以确保专业版功能正常使用"
ErrXpackNotFound: "该部分为商业版功能,请先在 面板设置-许可证 界面导入许可证"
ErrXpackExceptional: "该部分为商业版功能,请先在 面板设置-许可证 界面同步许可证状态"
ErrXpackLost: "许可证已达到最大重试次数,请进入【面板设置】【许可证】页面手动点击同步按钮,以确保商业版功能正常使用"
ErrDeviceLost: "许可证校验必要文件丢失,请检查后重试!"
ErrDeviceErr: "当前环境与许可证导入环境不一致,请编辑许可证重新导入!"
ErrXpackTimeout: "请求超时,网络连接可能不稳定,请稍后再试!"
ErrUnbindMaster: "检测到节点管理内存在专业版节点,无法解绑当前许可证,请先移除或解绑后重试!"
ErrUnbindMaster: "检测到节点管理内存在商业版节点,无法解绑当前许可证,请先移除或解绑后重试!"
ErrFreeNodeLimit: "社区版节点数量已达到免费上限,请前往 www.lxware.cn/1panel 购买后重试!"
ErrNodeBound: "该许可证已绑定到其他节点,请检查后重试!"
ErrNodeBoundDelete: "该许可证已被绑定,不支持删除操作,请检查后重试!"
ErrNodeBoundLimit: "当前免费节点已经达到上限,请检查后重试!"
ErrLicenseFree: "仅当许可证正常绑定到节点后,才能使用其免费节点,请检查后重试!"
ErrLicenseUnbind: "检测到该许可证存在社区版节点,请在 [ 面板设置 - 许可证 ] 中解绑后重试!"
ErrLicenseBindCount: "新许可证免费节点数量小于当前已绑定社区版节点数量,请检查后重试!"
ErrNoSuchNode: "未能找到该节点信息,请检查后重试!"
ErrNodeUnbind: "检测到该节点未在许可证绑定范围内,请检查后重试!"
ErrNodeBind: "检测到该节点已绑定许可证,请检查后重试!"
+16 -7
View File
@@ -50,14 +50,13 @@ func Proxy() gin.HandlerFunc {
return
}
if c.Request.URL.Path == "/api/v2/hosts/terminal" && (currentNode == "local" || len(currentNode) == 0) {
proxyLocalAgent(c)
return
}
if !strings.HasPrefix(c.Request.URL.Path, "/api/v2/core") && (currentNode == "local" || len(currentNode) == 0) {
defer func() {
if err := recover(); err != nil && err != http.ErrAbortHandler {
global.LOG.Debug(err)
}
}()
proxy.LocalAgentProxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
proxyLocalAgent(c)
return
}
xpack.Proxy(c, currentNode)
@@ -65,6 +64,16 @@ func Proxy() gin.HandlerFunc {
}
}
func proxyLocalAgent(c *gin.Context) {
defer func() {
if err := recover(); err != nil && err != http.ErrAbortHandler {
global.LOG.Debug(err)
}
}()
proxy.LocalAgentProxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
func checkSession(c *gin.Context) bool {
psession, err := global.SESSION.Get(c)
if err != nil {
+10
View File
@@ -17,6 +17,16 @@ func SudoHandleCmd() string {
}
func Which(name string) bool {
// Prefer Go's built-in PATH lookup so we don't depend on an external
// `which` binary, which is not installed by default on minimal
// distributions (e.g. Arch Linux, some Alpine images, slim containers).
// See 1Panel-dev/1Panel#12605.
if _, err := exec.LookPath(name); err == nil {
return true
}
// Fall back to shelling out for environments where PATH inside the
// agent process differs from the user's interactive shell PATH (the
// previous behaviour, preserved for compatibility).
stdout, err := RunDefaultWithStdoutBashCf("which %s", name)
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) {
return false
+23
View File
@@ -0,0 +1,23 @@
package cmd
import "testing"
// TestWhich_ExistingBinary verifies that Which() returns true for a
// binary that is guaranteed to exist on every Unix-like build host
// (`sh`). Regression test for #12605: the previous implementation
// shelled out to `which`, which is not always available on minimal
// distributions like Arch Linux. The new implementation tries
// exec.LookPath first, so this assertion holds regardless of whether
// `which` itself is on PATH.
func TestWhich_ExistingBinary(t *testing.T) {
if !Which("sh") {
t.Errorf("Which(\"sh\") = false, want true")
}
}
func TestWhich_MissingBinary(t *testing.T) {
// A binary name that is extremely unlikely to exist on any host.
if Which("definitely-not-a-real-binary-xyzzy-1panel") {
t.Errorf("Which(\"definitely-not-a-real-binary-xyzzy-1panel\") = true, want false")
}
}
+1 -1
View File
@@ -285,7 +285,7 @@ func SelectInitScript(keyword string) (string, error) {
keyword = strings.TrimSuffix(keyword, ".service") + ".openrc"
case "sysvinit":
if _, err := os.Stat("/etc/rc.common"); err == nil {
keyword = strings.TrimSuffix(keyword, ".service") + ".prod"
keyword = strings.TrimSuffix(keyword, ".service") + ".procd"
} else {
keyword = strings.TrimSuffix(keyword, ".service") + ".init"
}
-1
View File
@@ -7,7 +7,6 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
pnpm-lock.yaml
package-lock.json
node_modules
dist
+11384
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -33,10 +33,10 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"anser": "^2.3.2",
"axios": "^1.7.2",
"axios": "^1.16.0",
"codemirror": "^6.0.2",
"crypto-js": "^4.2.0",
"dompurify": "^3.3.1",
"dompurify": "^3.4.2",
"echarts": "^5.5.0",
"element-plus": "2.11.9",
"js-base64": "^3.7.7",
@@ -47,7 +47,7 @@
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^1.6.1",
"punycode": "^2.3.1",
"qs": "^6.12.1",
"qs": "^6.15.1",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.7",
"uuid": "^10.0.0",
@@ -67,27 +67,27 @@
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/compiler-sfc": "^3.5.32",
"autoprefixer": "^10.4.7",
"commitizen": "^4.2.4",
"commitizen": "^4.3.1",
"esbuild": "^0.27.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0",
"lint-staged": "^16.4.0",
"postcss": "^8.4.31",
"postcss-html": "^1.4.1",
"postcss": "^8.5.14",
"postcss-html": "^1.8.1",
"prettier": "^3.8.2",
"rollup-plugin-visualizer": "^5.5.4",
"sass": "^1.83.0",
"sass": "^1.99.0",
"standard-version": "^9.5.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.9.2",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0",
"vite": "^7.1.5",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^7.3.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint2": "^5.0.3",
"vite-svg-loader": "^5.1.0",
"vite-svg-loader": "^5.1.1",
"vue-tsc": "^2.2.12"
},
"trustedDependencies": [
+5
View File
@@ -149,6 +149,11 @@ export namespace File {
dst: string;
type: string;
secret: string;
taskID?: string;
}
export interface FileDeCompressStopReq {
taskID: string;
}
export interface FileEdit {
+1 -1
View File
@@ -97,7 +97,7 @@ export const inspect = (params: Container.ContainerInspect) => {
export const DownloadFile = (params: Container.ContainerLogInfo) => {
return http.download<BlobPart>('/containers/download/log', params, {
responseType: 'blob',
timeout: TimeoutEnum.T_40S,
timeout: TimeoutEnum.T_60S,
});
};
+9 -2
View File
@@ -57,8 +57,15 @@ export const stopCompressFile = (taskID: string) => {
return http.post('files/compress/stop', { taskID } as File.FileCompressStopReq);
};
export const deCompressFile = (form: File.FileDeCompress) => {
return http.post<File.File>('files/decompress', form, TimeoutEnum.T_10M);
export const deCompressFile = (form: File.FileDeCompress, config?: AxiosRequestConfig) => {
return http.service.post<File.File>('files/decompress', form, {
timeout: TimeoutEnum.T_10M,
...config,
});
};
export const stopDeCompressFile = (taskID: string) => {
return http.post('files/decompress/stop', { taskID } as File.FileDeCompressStopReq);
};
export const getFileContent = (params: File.ReqFile) => {
@@ -123,9 +123,12 @@ const rightClick = ref({
top: 0,
currentRow: null,
});
const selectedRows = ref<any[]>([]);
const handleRightClick = (row, column, event) => {
clearSelects();
tableRef.value.refElTable.toggleRowSelection(row);
if (!selectedRows.value.includes(row)) {
clearSelects();
tableRef.value.refElTable.toggleRowSelection(row);
}
if (!props.rightButtons) {
return;
}
@@ -140,7 +143,6 @@ const handleRightClick = (row, column, event) => {
};
const closeRightClick = () => {
rightClick.value.visible = false;
clearSelects();
document.removeEventListener('click', closeRightClick);
};
const disabled = computed(() => {
@@ -178,6 +180,7 @@ function sizeChange() {
}
function handleSelectionChange(row: any) {
selectedRows.value = row;
emit('update:selects', row);
if (row.length > 0) {
leftSelect.value = true;
@@ -304,7 +307,7 @@ onMounted(() => {
});
window.addEventListener('resize', calcHeight);
watch(
() => props.height,
() => [props.height, props.heightDiff],
() => {
calcHeight();
},
+10 -2
View File
@@ -1,5 +1,5 @@
import { ElTable } from 'element-plus';
import { computed, defineComponent, h, onMounted, ref, watch, type PropType, type VNode } from 'vue';
import { cloneVNode, computed, defineComponent, h, onMounted, ref, watch, type PropType, type VNode } from 'vue';
import {
FU_TABLE_STORAGE_PREFIX,
@@ -145,7 +145,15 @@ export default defineComponent({
);
return resolvedColumns.value
.filter((column) => column.show !== false)
.map((column) => columnNodeMap.get(column.key))
.map((column) => {
const vnode = columnNodeMap.get(column.key);
if (!vnode) {
return null;
}
return cloneVNode(vnode, {
key: column.key,
});
})
.filter((column): column is VNode => Boolean(column));
});
@@ -5,7 +5,7 @@
:trigger="trigger"
:popper-class="resolvedPopperClass"
placement="bottom-end"
:width="260"
:width="popoverWidth"
>
<template #reference>
<el-button class="fu-search-bar-button" :icon="iconComponent" @click.stop>
@@ -14,14 +14,8 @@
</template>
<div class="fu-table-column-select__panel" @click.stop>
<div class="fu-table-column-select__title">{{ resolvedTitle }}</div>
<div ref="listRef" class="fu-table-column-select__list">
<div
v-for="column in columns"
:key="column.key"
class="fu-table-column-select__item"
:data-key="column.key"
>
<el-icon class="fu-table-column-select__drag"><Rank /></el-icon>
<div class="fu-table-column-select__list">
<div v-for="column in columns" :key="column.key" class="fu-table-column-select__item">
<el-checkbox v-model="column.show" :disabled="isFixedColumn(column)">
{{ column.label }}
</el-checkbox>
@@ -33,9 +27,8 @@
</template>
<script setup lang="ts">
import Sortable from 'sortablejs';
import { Rank, Setting } from '@element-plus/icons-vue';
import { computed, nextTick, onBeforeUnmount, ref, watch, type PropType } from 'vue';
import { Setting } from '@element-plus/icons-vue';
import { computed, ref, type PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import type { FuTableColumnConfig } from './shared';
@@ -72,54 +65,22 @@ const props = defineProps({
});
const visible = ref(false);
const listRef = ref<HTMLElement | null>(null);
let sortable: Sortable | undefined;
const resolvedTitle = computed(() => props.title || t('fu.table.custom_table_rows'));
const resolvedPopperClass = computed(() =>
['fu-table-column-select__popover', props.popperClass].filter(Boolean).join(' '),
);
const iconComponent = computed(() => props.icon || Setting);
const initSortable = async () => {
await nextTick();
sortable?.destroy();
if (!listRef.value) {
return;
}
sortable = Sortable.create(listRef.value, {
animation: 150,
handle: '.fu-table-column-select__drag',
onEnd: ({ oldIndex, newIndex }) => {
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
return;
}
const nextColumns = [...props.columns];
const [movedColumn] = nextColumns.splice(oldIndex, 1);
if (!movedColumn) {
return;
}
nextColumns.splice(newIndex, 0, movedColumn);
props.columns.splice(0, props.columns.length, ...nextColumns);
},
});
const estimateTextWidth = (text: string) => {
return Array.from(text).reduce((total, char) => total + (char.charCodeAt(0) > 255 ? 14 : 8), 0);
};
const popoverWidth = computed(() => {
const texts = [resolvedTitle.value, ...props.columns.map((column) => String(column.label || ''))];
const contentWidth = texts.reduce((maxWidth, text) => Math.max(maxWidth, estimateTextWidth(text)), 0);
return Math.min(Math.max(contentWidth + 72, 180), 320);
});
const isFixedColumn = (column: FuTableColumnConfig) => {
return Boolean(column.fixed);
};
watch(visible, (value) => {
if (value) {
initSortable();
return;
}
sortable?.destroy();
sortable = undefined;
});
onBeforeUnmount(() => {
sortable?.destroy();
sortable = undefined;
});
</script>
@@ -3,7 +3,7 @@
<DialogPro v-model="open" class="level-up-pro" @close="handleClose">
<div style="text-align: center" v-loading="loading">
<span class="text-3xl font-medium title">
{{ isImport ? $t('license.importLicense') : $t('license.levelUpPro') }}
{{ $t('license.importLicense') }}
</span>
<el-row type="flex" justify="center" class="mt-6">
<el-col :span="22">
@@ -45,10 +45,10 @@
plain
@click="submit"
>
{{ isImport ? $t('commons.button.confirm') : $t('commons.button.power') }}
{{ $t('commons.button.power') }}
</el-button>
<div class="mt-3 mb-5">
<el-button text type="primary" @click="toLxware">{{ $t('license.knowMorePro') }}</el-button>
<el-button text type="primary" @click="toEdition">{{ $t('license.knowMorePro') }}</el-button>
</div>
</div>
</DialogPro>
@@ -111,9 +111,9 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.handleStart(file);
};
const toLxware = () => {
const toEdition = () => {
if (!globalStore.isIntl) {
window.open('https://www.lxware.cn/1panel' + '', '_blank', 'noopener,noreferrer');
window.open('https://1panel.cn/versions.html' + '', '_blank', 'noopener,noreferrer');
} else {
window.open('https://1panel.pro/pricing' + '', '_blank', 'noopener,noreferrer');
}
@@ -3,7 +3,7 @@
v-model="open"
:header="resource"
@close="handleClose"
:size="globalStore.isFullScreen ? 'full' : 'large'"
:size="globalStore.isFullScreen ? 'full' : '60%'"
:resource="container"
>
<template #extra v-if="!mobile">
@@ -4,7 +4,7 @@
:header="$t('commons.button.log')"
@close="handleClose"
:resource="logSearch.container"
:size="globalStore.isFullScreen ? 'full' : 'large'"
:size="globalStore.isFullScreen ? 'full' : '60%'"
>
<template #extra v-if="!mobile">
<el-tooltip :content="loadTooltip()" placement="top">
@@ -67,6 +67,7 @@ const openWithResourceID = (taskType: string, taskOperate: string, resourceID: n
const em = defineEmits(['close']);
const handleClose = () => {
em('close', true);
open.value = false;
bus.emit('refreshTask', true);
bus.emit('refreshApp', true);
};
@@ -3,6 +3,10 @@
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
<div class="flex flex-wrap gap-y-2 items-center">
<span v-if="props.footer">
<el-link type="primary" underline="never" @click="toEdition" v-if="!isFxplay">
<span class="font-normal">{{ $t('license.knowMorePro') }}</span>
</el-link>
<el-divider direction="vertical" />
<el-link type="primary" underline="never" @click="toForum" v-if="!isFxplay">
<span class="font-normal">{{ $t('setting.forum') }}</span>
</el-link>
@@ -17,14 +21,18 @@
<el-divider direction="vertical" />
</span>
<div class="flex flex-wrap items-center">
<el-link underline="never" type="primary" @click="toLxware">
<span v-if="isMasterPro">
<el-link v-if="isMasterPro" underline="never" type="primary" @click="toLxware">
<span>
{{ $t('license.pro') }}
</span>
<span v-else-if="isOffLine">
</el-link>
<el-link v-else-if="isOffLine" underline="never" type="primary" @click="to1Panel">
<span v-if="isOffLine">
{{ $t('license.offLine') }}
</span>
<span v-else>
</el-link>
<el-link v-else underline="never" type="primary" @click="toEdition">
<span>
{{ $t('license.community') }}
</span>
</el-link>
@@ -94,10 +102,6 @@ const getVersionLog = () => {
};
const toLxware = () => {
if (isOffLine.value) {
to1Panel();
return;
}
if (!globalStore.isIntl) {
window.open('https://www.lxware.cn/1panel' + '', '_blank', 'noopener,noreferrer');
} else {
@@ -114,6 +118,14 @@ const toDoc = () => {
window.open(docsUrl.value, '_blank', 'noopener,noreferrer');
};
const toEdition = () => {
if (!globalStore.isIntl) {
window.open('https://1panel.cn/versions.html' + '', '_blank', 'noopener,noreferrer');
} else {
window.open('https://1panel.pro/pricing' + '', '_blank', 'noopener,noreferrer');
}
};
const toForum = () => {
let url = globalStore.isIntl
? 'https://github.com/1Panel-dev/1Panel/discussions'
+3 -2
View File
@@ -241,8 +241,9 @@ const initWebSocket = (endpoint_: string, args: string = '') => {
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
const host = href.split('//')[1].split('/')[0];
const endpoint = endpoint_.replace(/^\/+/, '');
let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${globalStore.currentNode}`;
if (args.indexOf('&operateNode=') !== -1) {
let node = args.indexOf('id=') !== -1 ? 'local' : globalStore.currentNode;
let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${node}`;
if (args.indexOf('operateNode=') !== -1) {
conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`;
}
terminalSocket.value = new WebSocket(conn);
@@ -141,6 +141,7 @@ function initChart() {
});
}
const grid = props.option.grid || { left: '7%', right: '7%', bottom: '20%' };
const option = {
title: [
{
@@ -205,9 +206,9 @@ function initChart() {
return res;
},
},
grid: props.option.grid || { left: '7%', right: '7%', bottom: '20%' },
grid,
legend: itemSelect || {
right: 10,
right: grid.right || 10,
itemWidth: 8,
textStyle: {
color: '#646A73',
@@ -228,7 +229,14 @@ function initChart() {
},
},
series: series,
dataZoom: [{ startValue: props?.option.xData[0], show: props.dataZoom }],
dataZoom: [
{
...(props.option.xData?.[0] ? { startValue: props.option.xData[0] } : {}),
left: grid.left,
right: grid.right,
show: props.dataZoom,
},
],
};
// 渲染数据
itemChart.setOption(option, true);
+549 -66
View File
@@ -1,92 +1,575 @@
<template>
<div>
<DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
<el-row>
<el-col :span="22" :offset="1">
<el-alert :closable="false" :title="$t('file.vscodeHelper')" type="info"></el-alert>
<el-form
ref="vscodeConnectInfoForm"
label-position="top"
:model="addForm"
label-width="100px"
class="mt-4"
>
<el-form-item :label="$t('setting.systemIP')" prop="host">
<el-input v-model="addForm.host" />
</el-form-item>
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input v-model="addForm.port" />
</el-form-item>
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input v-model="addForm.username" />
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(vscodeConnectInfoForm)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DialogPro>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { FormInstance } from 'element-plus';
<DialogPro v-model="open" :title="$t('file.openWithVscode')" size="large" @close="handleClose">
<div v-loading="loading" class="vscode-open space-y-4">
<el-alert
v-if="addForm.authMode === 'key'"
:closable="false"
:title="$t('file.vscodeHelper')"
:description="$t('file.vscodeKeyHelper')"
show-icon
type="info"
/>
const open = ref();
<div class="grid gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.9fr)]">
<section class="vscode-open__panel">
<el-form ref="vscodeConnectInfoForm" :model="addForm" :rules="rules" label-position="top">
<el-form-item :label="$t('terminal.authMode')" prop="authMode" class="mb-5">
<el-radio-group v-model="addForm.authMode" @change="handleAuthModeChange">
<el-radio-button value="password">{{ $t('terminal.passwordMode') }}</el-radio-button>
<el-radio-button value="key">{{ $t('terminal.keyMode') }}</el-radio-button>
</el-radio-group>
</el-form-item>
<div class="grid gap-4 md:grid-cols-2">
<el-form-item :label="$t('terminal.ip')" prop="host">
<el-input v-model.trim="addForm.host" autocomplete="off" />
</el-form-item>
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input v-model.number="addForm.port" autocomplete="off" />
</el-form-item>
</div>
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input v-model.trim="addForm.username" autocomplete="off" />
</el-form-item>
<template v-if="addForm.authMode === 'key'">
<el-form-item :label="$t('file.vscodeSelectKey')" prop="certID">
<el-select
v-model="addForm.certID"
class="w-full"
clearable
filterable
:loading="loading"
:placeholder="$t('commons.msg.inputOrSelect')"
@change="handleCertChange"
>
<el-option
v-for="item in certOptions"
:key="item.id"
:label="formatCertLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('file.vscodeKeyPath')" prop="sshKeyPath">
<el-input
v-model.trim="addForm.sshKeyPath"
:placeholder="$t('file.vscodeKeyPathPlaceholder')"
autocomplete="off"
/>
</el-form-item>
</template>
</el-form>
</section>
<section class="vscode-open__panel">
<div v-if="addForm.authMode === 'key'" class="space-y-4">
<div class="flex items-center justify-between gap-2">
<div>
<div class="vscode-open__title">
{{ $t('file.vscodeScriptPreview') }}
</div>
<p class="vscode-open__tip">
{{ $t('file.vscodeScriptPreviewHint') }}
</p>
</div>
<div class="flex flex-wrap justify-end gap-2">
<el-button
:disabled="!canGenerate"
plain
@click="showScriptPreview = !showScriptPreview"
>
{{
showScriptPreview ? $t('commons.button.collapse') : $t('commons.button.preview')
}}
</el-button>
<el-button :disabled="!canGenerate" plain @click="copySetupScript">
{{ $t('file.vscodeCopyConfig') }}
</el-button>
</div>
</div>
<pre
v-if="showScriptPreview"
class="vscode-open__script max-h-[430px] overflow-auto p-3 text-xs leading-6"
>{{ scriptText }}</pre
>
</div>
<div v-else class="flex h-full min-h-[320px] flex-col justify-between gap-4">
<div class="space-y-3">
<div class="vscode-open__title">
{{ $t('file.vscodePasswordModeTitle') }}
</div>
<p class="vscode-open__tip">
{{ $t('file.vscodePasswordModeHint') }}
</p>
<div class="vscode-open__steps space-y-2 p-3">
<div class="flex items-center gap-2 text-sm">
<span class="vscode-open__step-index">1</span>
{{ $t('file.vscodePasswordStep1') }}
</div>
<div class="flex items-center gap-2 text-sm">
<span class="vscode-open__step-index">2</span>
{{ $t('file.vscodePasswordStep2') }}
</div>
<div class="flex items-center gap-2 text-sm">
<span class="vscode-open__step-index">3</span>
{{ $t('file.vscodePasswordStep3') }}
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<template #footer>
<span class="dialog-footer flex flex-wrap items-center justify-end gap-2">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button
v-if="addForm.authMode === 'key'"
:disabled="!selectedCert"
plain
@click="downloadPrivateKey"
>
{{ $t('commons.button.download') }}
</el-button>
<el-button v-if="addForm.authMode === 'key'" :disabled="!canGenerate" @click="copySetupScript">
{{ $t('file.vscodeCopyConfig') }}
</el-button>
<el-button :disabled="!canGenerate" type="primary" @click="submit">
{{ $t('file.openWithVscode') }}
</el-button>
</span>
</template>
</DialogPro>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import type { FormInstance, FormItemRule, FormRules } from 'element-plus';
import { Base64 } from 'js-base64';
import i18n from '@/lang';
import { GlobalStore } from '@/store';
import { getSSHInfo, searchCert } from '@/api/modules/host';
import { loadLocalConn } from '@/api/modules/terminal';
import { Host } from '@/api/interface/host';
import { Rules } from '@/global/form-rules';
import { copyText } from '@/utils/clipboard';
import { MsgError } from '@/utils/message';
const globalStore = GlobalStore();
const open = ref(false);
const loading = ref(false);
const showScriptPreview = ref(false);
const vscodeConnectInfoForm = ref<FormInstance>();
const certOptions = ref<Host.RootCertInfo[]>([]);
interface DialogProps {
path: string;
}
const vscodeConnectInfoForm = ref<FormInstance>();
const addForm = reactive({
const STORAGE_KEY = 'VscodeConnectInfo';
type AuthMode = 'key' | 'password';
const normalizeAuthMode = (value: unknown): AuthMode => (value === 'key' ? 'key' : 'password');
const defaultForm = () => ({
authMode: 'password' as AuthMode,
host: '',
port: 22,
username: 'root',
certID: '' as number | '',
sshKeyPath: '',
path: '',
});
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
if (vscodeConnectInfoForm.value) {
vscodeConnectInfoForm.value.resetFields();
const addForm = reactive(defaultForm());
const getStorageKey = () => `${STORAGE_KEY}:${globalStore.currentNode}:${globalStore.currentNodeAddr || 'local'}`;
const isKeyMode = computed(() => addForm.authMode === 'key');
const selectedCert = computed(() => certOptions.value.find((item) => String(item.id) === String(addForm.certID)));
const sshConfigValue = (value: string) =>
`"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
const normalizeHost = (value: string) => value.trim().replace(/^\[(.*)\]$/, '$1');
const formatHostForRemote = (value: string) => {
const trimmed = normalizeHost(value);
if (trimmed.includes(':') && !trimmed.startsWith('[')) {
return `[${trimmed}]`;
}
em('close', false);
return trimmed;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
const getKeyPathFileName = () => {
const keyPath = addForm.sshKeyPath.trim();
const normalizedPath = keyPath.replace(/\\/g, '/').replace(/\/+$/, '');
return normalizedPath.split('/').pop() || selectedCert.value?.name || 'ssh-private-key';
};
const getSafeKeyFileName = (name = 'ssh-private-key') =>
name
.trim()
.replace(/[/\\]/g, '-')
.replace(/\s+/g, '-')
.replace(/[^a-zA-Z0-9._-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^[.-]+|[.-]+$/g, '') || 'ssh-private-key';
const buildSshAlias = () => {
const host = normalizeHost(addForm.host);
const port = Number(addForm.port) || 22;
const username = addForm.username.trim() || 'root';
const certName = selectedCert.value?.name?.trim() || 'key';
const certID = selectedCert.value?.id ?? 0;
return `1panel-${host}-${port}-${username}-${certName}-${certID}`
.replace(/[^a-zA-Z0-9._-]+/g, '-')
.replace(/-+/g, '-');
};
const buildVscodeUrl = () => {
const remoteTarget = isKeyMode.value
? buildSshAlias()
: `${addForm.username.trim() || 'root'}@${formatHostForRemote(addForm.host)}:${Number(addForm.port) || 22}`;
const remotePath = encodeURI(addForm.path);
return `vscode://vscode-remote/ssh-remote+${encodeURIComponent(remoteTarget)}${remotePath}?windowId=_blank`;
};
const buildSetupScript = () => {
const host = normalizeHost(addForm.host);
const port = Number(addForm.port) || 22;
const username = addForm.username.trim() || 'root';
const keyPath = addForm.sshKeyPath.trim();
const alias = buildSshAlias();
const managedBegin = `# 1Panel VS Code managed block: ${alias}`;
const managedEnd = `# 1Panel VS Code managed block end: ${alias}`;
return [
'#!/usr/bin/env bash',
'set -euo pipefail',
'',
'# Run this script on the computer running VS Code.',
`SSH_CONFIG="$HOME/.ssh/config"`,
`KEY_PATH=${sshConfigValue(keyPath)}`,
`MANAGED_BEGIN=${sshConfigValue(managedBegin)}`,
`MANAGED_END=${sshConfigValue(managedEnd)}`,
'',
'mkdir -p "$(dirname "$SSH_CONFIG")"',
'chmod 700 "$(dirname "$SSH_CONFIG")" || true',
'touch "$SSH_CONFIG"',
'',
'tmp_file="$(mktemp)"',
'awk -v begin="$MANAGED_BEGIN" -v end="$MANAGED_END" \'',
' $0 == begin { skip = 1; next }',
' $0 == end { skip = 0; next }',
' !skip { print }',
'\' "$SSH_CONFIG" > "$tmp_file"',
'',
'cat <<EOF >> "$tmp_file"',
managedBegin,
`Host ${alias}`,
` HostName ${sshConfigValue(host)}`,
` User ${sshConfigValue(username)}`,
` Port ${port}`,
` IdentityFile ${sshConfigValue(keyPath)}`,
' IdentitiesOnly yes',
managedEnd,
'EOF',
'',
'mv "$tmp_file" "$SSH_CONFIG"',
'chmod 600 "$SSH_CONFIG"',
'if [ ! -f "$KEY_PATH" ]; then',
' echo "1Panel: private key not found: $KEY_PATH" >&2',
' echo "1Panel: download the private key and save it to this exact path, then rerun this script." >&2',
' exit 1',
'fi',
'chmod 600 "$KEY_PATH"',
`echo "1Panel: SSH config ready for ${alias}"`,
].join('\n');
};
const scriptText = computed(() =>
addForm.authMode === 'key' && addForm.path && canGenerate.value ? buildSetupScript() : '',
);
const canGenerate = computed(() => {
if (!addForm.host.trim() || !addForm.username.trim() || !addForm.port) {
return false;
}
if (!isKeyMode.value) {
return true;
}
return Boolean(selectedCert.value && addForm.sshKeyPath.trim());
});
const requiredKeyValidator = (_rule: FormItemRule, value: unknown, callback: (error?: Error) => void) => {
if (!isKeyMode.value) {
callback();
return;
}
if (value === '' || typeof value === 'undefined' || value === null) {
callback(new Error(String(i18n.global.t('commons.rule.requiredSelect'))));
return;
}
callback();
};
const requiredKeyPathValidator = (_rule: FormItemRule, value: unknown, callback: (error?: Error) => void) => {
if (!isKeyMode.value) {
callback();
return;
}
if (value === '' || typeof value === 'undefined' || value === null) {
callback(new Error(String(i18n.global.t('commons.rule.requiredInput'))));
return;
}
callback();
};
const rules = reactive<FormRules>({
authMode: [Rules.requiredSelect],
host: [Rules.requiredInput, Rules.ipV4V6OrDomain],
port: [Rules.requiredInput, Rules.port],
username: [Rules.requiredInput],
certID: [{ validator: requiredKeyValidator, trigger: 'change' }],
sshKeyPath: [{ validator: requiredKeyPathValidator, trigger: 'blur' }],
});
const emit = defineEmits(['close']);
const saveDraft = () => {
localStorage.setItem(
getStorageKey(),
JSON.stringify({
authMode: addForm.authMode,
host: addForm.host,
port: addForm.port,
username: addForm.username,
certID: addForm.certID,
sshKeyPath: addForm.sshKeyPath,
}),
);
};
const restoreDraft = () => {
const storedInfo = localStorage.getItem(getStorageKey());
if (!storedInfo) return;
try {
const parsed = JSON.parse(storedInfo);
addForm.authMode = normalizeAuthMode(parsed.authMode);
addForm.host = parsed.host?.trim() || addForm.host;
addForm.port = parsed.port || addForm.port;
addForm.username = parsed.username?.trim() || addForm.username;
addForm.certID = parsed.certID ?? '';
addForm.sshKeyPath = parsed.sshKeyPath?.trim() || '';
} catch {
localStorage.removeItem(getStorageKey());
}
};
const loadConnectionInfo = async () => {
if (globalStore.currentNode === 'local') {
try {
const res = await loadLocalConn();
if (res.data) {
addForm.host = res.data.addr || globalStore.currentNodeAddr || '127.0.0.1';
addForm.port = Number(res.data.port) || 22;
addForm.username = res.data.user || 'root';
return;
}
} catch {}
}
try {
const res = await getSSHInfo();
if (res.data) {
addForm.port = Number(res.data.port) || 22;
addForm.username = res.data.currentUser || 'root';
}
localStorage.setItem('VscodeConnectInfo', JSON.stringify(addForm));
} catch {}
addForm.host = globalStore.currentNodeAddr || addForm.host || '127.0.0.1';
};
const loadCertOptions = async () => {
const res = await searchCert({ page: 1, pageSize: 500 });
certOptions.value = res.data?.items || [];
};
const loadDialogData = async () => {
loading.value = true;
await Promise.allSettled([loadConnectionInfo(), loadCertOptions()]);
restoreDraft();
if (addForm.authMode === 'key' && addForm.certID && !selectedCert.value) {
addForm.certID = '';
}
loading.value = false;
};
const formatCertLabel = (item: Host.RootCertInfo) => {
const desc = item.description?.trim();
return desc ? `${item.name} (${item.encryptionMode}) - ${desc}` : `${item.name} (${item.encryptionMode})`;
};
const handleClose = () => {
saveDraft();
open.value = false;
showScriptPreview.value = false;
vscodeConnectInfoForm.value?.resetFields();
Object.assign(addForm, defaultForm());
certOptions.value = [];
emit('close', false);
};
const handleAuthModeChange = () => {
showScriptPreview.value = false;
};
const handleCertChange = () => {
if (addForm.sshKeyPath.trim() || !selectedCert.value?.name) {
return;
}
addForm.sshKeyPath = `~/.ssh/${getSafeKeyFileName(selectedCert.value.name)}`;
};
const copySetupScript = async () => {
if (addForm.authMode !== 'key') return;
if (!canGenerate.value) {
MsgError(String(i18n.global.t('commons.msg.inputOrSelect')));
return;
}
await copyText(buildSetupScript());
};
const downloadPrivateKey = () => {
if (!selectedCert.value?.privateKey) {
MsgError(String(i18n.global.t('commons.msg.noneData')));
return;
}
const content = Base64.decode(selectedCert.value.privateKey);
const fileName = getKeyPathFileName();
const downloadUrl = window.URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' }));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = fileName;
a.dispatchEvent(new MouseEvent('click'));
setTimeout(() => {
window.URL.revokeObjectURL(downloadUrl);
}, 100);
};
const submit = async () => {
if (!vscodeConnectInfoForm.value) return;
await vscodeConnectInfoForm.value.validate((valid) => {
if (!valid || !canGenerate.value) return;
saveDraft();
open.value = false;
const vscodeUrl = `vscode://vscode-remote/ssh-remote+${addForm.username}@${addForm.host}:${addForm.port}${addForm.path}?windowId=_blank`;
window.open(vscodeUrl);
window.open(buildVscodeUrl(), '_blank', 'noopener,noreferrer');
});
};
const acceptParams = async (params: DialogProps): Promise<void> => {
const vscodeConnectInfo = localStorage.getItem('VscodeConnectInfo');
if (vscodeConnectInfo) {
try {
const storedInfo = JSON.parse(vscodeConnectInfo);
addForm.host = storedInfo.host;
addForm.port = storedInfo.port;
addForm.username = storedInfo.username;
} catch (error) {}
}
Object.assign(addForm, defaultForm());
certOptions.value = [];
showScriptPreview.value = false;
addForm.path = params.path;
open.value = true;
await loadDialogData();
};
defineExpose({ acceptParams });
</script>
<style lang="scss" scoped>
.vscode-open {
:deep(.el-descriptions__label) {
width: 120px;
}
}
.vscode-open__panel {
padding: 16px;
border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-base);
background: var(--el-bg-color);
}
.vscode-open__title {
color: var(--el-text-color-primary);
font-size: 15px;
font-weight: 600;
line-height: 24px;
}
.vscode-open__tip {
margin-top: 4px;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 22px;
}
.vscode-open__script {
border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-base);
color: var(--el-text-color-primary);
background: var(--el-fill-color-lighter);
}
.vscode-open__steps {
border: 1px dashed var(--el-border-color);
border-radius: var(--el-border-radius-base);
color: var(--el-text-color-regular);
background: var(--el-fill-color-lighter);
}
.vscode-open__step-index {
display: inline-flex;
width: 22px;
height: 22px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
font-size: 12px;
font-weight: 600;
}
@media (max-width: 768px) {
.vscode-open {
:deep(.el-descriptions__table) {
display: block;
}
:deep(.el-descriptions__body),
:deep(.el-descriptions__table tbody),
:deep(.el-descriptions__table tr),
:deep(.el-descriptions__table th),
:deep(.el-descriptions__table td) {
display: block;
width: 100% !important;
}
:deep(.el-descriptions__label) {
width: 100%;
}
}
.vscode-open__panel {
padding: 12px;
}
}
</style>
+98 -34
View File
@@ -859,6 +859,36 @@ const message = {
from_remote: 'This model was not downloaded via 1Panel, no related pull logs.',
no_logs: 'The pull logs for this model have been deleted and cannot be viewed.',
vllmVersionHelper: 'For FusionXpark GB 10 servers, please select the -cu130 version.',
downloader: 'Downloader',
modelDir: 'Model directory',
manualDownload: 'Manual Download',
modelSource: 'Model source',
repoID: 'Repository ID',
modelScopeEndpoint: 'ModelScope endpoint',
modelScopeToken: 'ModelScope Token (optional)',
hfToken: 'HF Token (optional)',
hfEndpoint: 'HuggingFace acceleration endpoint',
hfEndpointHelper:
'Configure an acceleration endpoint when HuggingFace access is unstable. Search, details, and downloads will use it.',
searchModelScope: 'Search ModelScope',
searchHuggingFace: 'Search HuggingFace',
sortTrending: 'Trending',
sortLikes: 'Most liked',
sortDownloads: 'Most downloaded',
sortUpdated: 'Recently updated',
sortCreated: 'Newest',
downloadsCount: 'Downloads',
likes: 'Likes',
taskQueue: 'Download Queue',
progress: 'Progress',
localDownloaded: 'Downloaded',
modelInfo: 'Model Details',
modelCard: 'Model Card',
downloading: 'Downloading',
canceled: 'Canceled',
downloadQueued: 'Added to the download queue',
cancelTaskConfirm: 'Cancel this download task?',
deleteLocalModelConfirm: 'Delete local model {0}?',
},
proxy: {
proxy: 'AI Proxy Enhancement',
@@ -1608,7 +1638,7 @@ const message = {
clam: {
clam: 'Virus scan',
cron: 'Scheduled scan',
cronHelper: 'Pro supports scheduled scans',
cronHelper: 'Business Edition supports scheduled scans',
specErr: 'Execution schedule format error, please check and retry!',
disableMsg:
'Stopping scheduled execution will prevent this scan task from running automatically. Continue?',
@@ -1643,7 +1673,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf logs',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam logs',
alertHelper: 'Pro supports scheduled scans and SMS alerts',
alertHelper: 'Business Edition supports scheduled scans and SMS alerts',
alertTitle: 'Virus scan task 「{0}」 detected infected file alert',
},
},
@@ -1951,7 +1981,27 @@ const message = {
top: 'Go Back',
up: 'Go back',
openWithVscode: 'Open with VS Code',
vscodeHelper: 'Please make sure that VS Code is installed locally and the SSH Remote plugin is configured',
vscodeHelper:
'Confirm the authentication mode, host, port, and username. Key authentication requires SSH configuration on the computer running VS Code before use.',
vscodeCopyConfig: 'Copy setup script',
vscodeKeyHelper:
'The private key download name is derived from the configured path. Save the file to that path; the setup script will verify it and apply permissions.',
vscodeKeyPath: 'SSH private key path',
vscodeKeyPathPlaceholder: 'For example: ~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH private key',
vscodeScriptPreview: 'SSH setup script',
vscodePasswordHelper:
'Password authentication does not write to the local SSH config. Enter the password in VS Code when connecting to the remote host.',
vscodePasswordHint:
'Password authentication connects with the configured host, port, and username. No setup script or private key file is required.',
vscodeScriptPreviewHint:
'Run the setup script on the computer running VS Code to register the SSH alias for remote connections. Rerun it after changing the host, port, username, private key, or key path.',
vscodePasswordModeTitle: 'Password authentication',
vscodePasswordModeHint:
'Password authentication connects with the configured host, port, and username. No setup script or private key file is required.',
vscodePasswordStep1: 'Confirm the host, port, and username.',
vscodePasswordStep2: 'Open VS Code and enter the password when prompted.',
vscodePasswordStep3: 'No private key download is required for this mode.',
saveContentAndClose: 'The file has been modified, do you want to save and close it?',
saveAndOpenNewFile: 'The file has been modified, do you want to save and open the new file?',
noEdit: 'The file has not been modified, no need to do this!',
@@ -2121,12 +2171,12 @@ const message = {
'The address will be used for application redirection, container access, and other functions. Each node can be configured with a different address.',
proxy: 'Server proxy',
proxyHelper: 'It will be effective in the following scenarios after you set up the proxy server:',
proxyHelper1: 'Installation package downloads and App Store sync (Pro Edition only)',
proxyHelper2: 'System updates and update checks (Pro Edition only)',
proxyHelper1: 'Installation package downloads and App Store sync (Business Edition only)',
proxyHelper2: 'System updates and update checks (Business Edition only)',
proxyHelper3: 'System license verification and synchronization',
proxyHelper4: 'Network requests such as Docker image pulls go through the proxy server (Pro Edition only)',
proxyHelper5: 'Unified download and sync for system script libraries (Pro Edition)',
proxyHelper6: 'Certificate requests (Pro Edition)',
proxyHelper4: 'Network requests such as Docker image pulls go through the proxy server (Business Edition only)',
proxyHelper5: 'Unified download and sync for system script libraries (Business Edition)',
proxyHelper6: 'Certificate requests (Business Edition)',
proxyType: 'Proxy type',
proxyUrl: 'Proxy Address',
proxyPort: 'Proxy Port',
@@ -2518,7 +2568,7 @@ const message = {
offLine: 'Offline',
community: 'OSS',
oss: 'Open Source Software',
pro: 'Pro',
pro: 'Business Edition',
trial: 'Trial',
add: 'Add Community Edition',
licenseBindHelper: 'Free node quotas can only be used when the license is bound to a node',
@@ -2536,35 +2586,37 @@ const message = {
authorizationId: 'Subscription ID',
authorizedUser: 'Authorized user',
lostHelper:
'The license has reached the retry limit. Click Sync manually to make sure Pro features continue to work.',
'The license has reached the retry limit. Click Sync manually to make sure Business Edition features continue to work.',
exceptionalHelper:
'License sync verification failed. Click Sync manually to make sure Pro features work properly. Detail: ',
'License sync verification failed. Click Sync manually to make sure Business Edition features work properly. Detail: ',
quickUpdate: 'Quick update',
import: 'Import',
power: 'Authorize',
unbindHelper: 'All Pro related Settings will be cleaned after unbinding. Continue? ',
unbindHelper: 'All Business Edition related Settings will be cleaned after unbinding. Continue? ',
importLicense: 'Import license',
importHelper: 'Click or drag the license file here',
levelUpPro: 'Upgrade to Pro Edition',
levelUpPro: 'Upgrade to Business Edition',
licenseSync: 'License Sync',
knowMorePro: 'Learn More',
knowMorePro: 'Learn About Business Edition',
closeAlert: 'The current page can be closed in the panel settings',
introduce: 'Feature Introduction',
waf: 'Upgrade to Pro to unlock interception maps, logs, block records, geo-blocking, custom rules, custom block pages, and more.',
tamper: 'Upgrade to Pro to protect websites from unauthorized changes or tampering.',
waf: 'Upgrade to Business Edition to unlock interception maps, logs, block records, geo-blocking, custom rules, custom block pages, and more.',
tamper: 'Upgrade to Business Edition to protect websites from unauthorized changes or tampering.',
tamperHelper: 'Operation failed, the file or folder has tamper protection enabled. Please check and try again!',
setting: 'Upgrade to Pro to customize the panel logo, welcome message, and more.',
setting: 'Upgrade to Business Edition to customize the panel logo, welcome message, and more.',
monitor:
'Upgrade to Pro to view real-time website status, visitor trends, traffic sources, request logs, and more.',
alert: 'Upgrade to Pro to receive SMS alerts, review alert logs, and stay on top of key events.',
node: 'Upgrade to Pro Edition to manage multiple Linux servers in 1Panel.',
nodeApp: 'Upgrade to Pro Edition to upgrade multi-node app versions without switching nodes manually.',
'Upgrade to Business Edition to view real-time website status, visitor trends, traffic sources, request logs, and more.',
alert: 'Upgrade to Business Edition to receive SMS alerts, review alert logs, and stay on top of key events.',
node: 'Upgrade to Business Edition to manage multiple Linux servers in 1Panel.',
nodeApp: 'Upgrade to Business Edition to upgrade multi-node app versions without switching nodes manually.',
nodeDashboard:
'Upgrade to Pro Edition for centralized management of multi-node apps, websites, databases, and scheduled tasks.',
fileExchange: 'Upgrade to Pro Edition to transfer files quickly between servers.',
app: 'Upgrade to Pro to view service details, anomaly monitoring, and more in the mobile app.',
cluster: 'Upgrade to Pro Edition to manage MySQL/Postgres/Redis primary-replica clusters.',
vllm: 'Upgrade to Pro Edition to manage vLLM services in 1Panel with centralized deployment, configuration, lifecycle operations, and task tracking.',
'Upgrade to Business Edition for centralized management of multi-node apps, websites, databases, and scheduled tasks.',
fileExchange: 'Upgrade to Business Edition to transfer files quickly between servers.',
app: 'Upgrade to Business Edition to view service details, anomaly monitoring, and more in the mobile app.',
cluster: 'Upgrade to Business Edition to manage MySQL/Postgres/Redis primary-replica clusters.',
vllm: 'Upgrade to Business Edition to manage vLLM services in 1Panel with centralized deployment, configuration, lifecycle operations, and task tracking.',
'model-downloader':
'Upgrade to Business Edition to search, download, and manage local model files from HuggingFace, ModelScope, or compatible mirrors with centralized model directories and download tasks.',
},
clean: {
scan: 'Start scanning',
@@ -3586,12 +3638,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Friendly reminder: Your Pro trial will expire in {0} days, and all Pro features will no longer be accessible. Please renew or upgrade to the full version in a timely manner.',
'Friendly reminder: Your Business Edition trial will expire in {0} days, and all Business Edition features will no longer be accessible. Please renew or upgrade to the full version in a timely manner.',
expiresAlert:
'Friendly reminder: Your Pro license will expire in {0} days, and all Pro features will no longer be accessible. Please renew promptly to ensure continued usage.',
menu: 'Pro',
'Friendly reminder: Your Business Edition license will expire in {0} days, and all Business Edition features will no longer be accessible. Please renew promptly to ensure continued usage.',
menu: 'Business Edition',
upage: 'AI Website Builder',
proAlert: 'Upgrade to Pro to use this feature',
proAlert: 'Upgrade to Business Edition to use this feature',
app: {
app: 'APP',
title: 'Panel Alias',
@@ -4143,6 +4195,18 @@ const message = {
vllmTitle4: 'Controlled Configuration',
vllmContent4:
'Standardize ports, containers, startup commands, and Compose settings through advanced options for easier delivery and later adjustments.',
modelDownloaderTitle1: 'Model Discovery',
modelDownloaderContent1:
'Search HuggingFace model repositories directly and view downloads, likes, model details, and file lists.',
modelDownloaderTitle2: 'Accelerated Downloads',
modelDownloaderContent2:
'Configure a HuggingFace acceleration endpoint and HF Token for the official site, mirrors, and private models.',
modelDownloaderTitle3: 'Task Tracking',
modelDownloaderContent3:
'Review download queues, task status, progress, and errors, with support for canceling, retrying, and clearing records.',
modelDownloaderTitle4: 'Local Management',
modelDownloaderContent4:
'Manage local model directories in one place, including paths, sizes, timestamps, and deleting unused model files.',
},
node: {
master: 'Main Node',
@@ -4187,7 +4251,7 @@ const message = {
syncHelper: 'When master node data changes, it synchronizes to this child node in real-time',
syncBackupAccount: 'Backup account settings',
syncWithMaster:
'After upgrading to Pro, all data will be synced by default. Sync policies can be manually adjusted in node management.',
'After upgrading to Business Edition, all data will be synced by default. Sync policies can be manually adjusted in node management.',
syncProxy: 'System proxy settings',
syncProxyHelper: 'Syncing system proxy settings requires Docker restart',
syncProxyHelper1: 'Restarting Docker may affect currently running container services.',
@@ -4266,7 +4330,7 @@ const message = {
nodes: 'Nodes',
selectNode: 'Select Node',
selectNodeError: 'Select a node',
licenseHelper: 'The Pro version supports the custom application repository feature',
licenseHelper: 'The Business Edition supports the custom application repository feature',
databaseHelper: 'Application associated database, select target node database',
nodeHelper: 'Cannot select current node',
migrateHelper:
@@ -4281,7 +4345,7 @@ const message = {
alertCount: 'Alert Count',
clamHelper: 'Trigger alert when scanning infected files',
cronJobHelper: 'Trigger alert when task execution fails',
licenseHelper: 'Pro supports additional alert channels',
licenseHelper: 'Business Edition supports additional alert channels',
alertCountHelper: 'Maximum daily alarm frequency',
alert: 'SMS Alert',
logs: 'Alert Logs',
@@ -4487,7 +4551,7 @@ const message = {
feiShuConfigHelper: 'Feishu alert notification configuration',
webhookName: 'Bot name',
webhookUrl: 'Webhook URL',
alertConfigProHelper: 'Pro Edition also supports WeCom, DingTalk, Feishu, and SMS alerts.',
alertConfigProHelper: 'Business Edition also supports WeCom, DingTalk, Feishu, and SMS alerts.',
},
theme: {
lingXiaGold: 'LXware Gold',
+97 -33
View File
@@ -872,6 +872,36 @@ const message = {
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.',
downloader: 'Descargador',
modelDir: 'Directorio del modelo',
manualDownload: 'Descarga manual',
modelSource: 'Fuente del modelo',
repoID: 'ID del repositorio',
modelScopeEndpoint: 'Endpoint de ModelScope',
modelScopeToken: 'Token de ModelScope (opcional)',
hfToken: 'HF Token (opcional)',
hfEndpoint: 'Endpoint de aceleración de HuggingFace',
hfEndpointHelper:
'Configure un endpoint de aceleración cuando el acceso a HuggingFace sea inestable. La búsqueda, los detalles y las descargas lo utilizarán.',
searchModelScope: 'Buscar en ModelScope',
searchHuggingFace: 'Buscar en HuggingFace',
sortTrending: 'Tendencias',
sortLikes: 'Más valorados',
sortDownloads: 'Más descargados',
sortUpdated: 'Actualizados recientemente',
sortCreated: 'Más recientes',
downloadsCount: 'Descargas',
likes: 'Me gusta',
taskQueue: 'Cola de descargas',
progress: 'Progreso',
localDownloaded: 'Descargados',
modelInfo: 'Detalles del modelo',
modelCard: 'Tarjeta del modelo',
downloading: 'Descargando',
canceled: 'Cancelado',
downloadQueued: 'Agregado a la cola de descargas',
cancelTaskConfirm: '¿Cancelar esta tarea de descarga?',
deleteLocalModelConfirm: '¿Eliminar el modelo local {0}?',
},
proxy: {
proxy: 'Mejoras de proxy de IA',
@@ -1645,7 +1675,7 @@ const message = {
clam: {
clam: 'Escaneo de virus',
cron: 'Escaneo programado',
cronHelper: 'Pro admite escaneos programados',
cronHelper: 'Business admite escaneos programados',
specErr: 'Formato de horario incorrecto, ¡verifique e intente nuevamente!',
disableMsg:
'Detener la ejecución programada impedirá que esta tarea se ejecute automáticamente. ¿Desea continuar?',
@@ -1680,7 +1710,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf logs',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam logs',
alertHelper: 'Pro admite escaneos programados y alertas por SMS',
alertHelper: 'Business admite escaneos programados y alertas por SMS',
alertTitle: 'La tarea de escaneo de virus 「{0}」 detectó archivos infectados',
},
},
@@ -1990,7 +2020,27 @@ const message = {
top: 'Volver arriba',
up: 'Subir',
openWithVscode: 'Abrir con VS Code',
vscodeHelper: 'Asegúrese de tener instalado VS Code y configurado el plugin SSH Remote',
vscodeHelper:
'Confirme el modo de autenticación, el host, el puerto y el usuario. La autenticación con clave requiere configurar SSH en el equipo donde se ejecuta VS Code antes de usarla.',
vscodeCopyConfig: 'Copiar script de configuración',
vscodeKeyHelper:
'El nombre de descarga de la clave privada se deriva de la ruta configurada. Guarde el archivo en esa ruta; el script de configuración lo verificará y aplicará permisos.',
vscodeKeyPath: 'Ruta de la clave privada SSH',
vscodeKeyPathPlaceholder: 'Por ejemplo: ~/.ssh/id_ed25519',
vscodeSelectKey: 'Clave privada SSH',
vscodeScriptPreview: 'Script de configuración SSH',
vscodePasswordHelper:
'La autenticación por contraseña no escribe en la configuración SSH local. Introduzca la contraseña en VS Code al conectarse al host remoto.',
vscodePasswordHint:
'La autenticación por contraseña conecta con el host, puerto y usuario configurados. No requiere script de configuración ni archivo de clave privada.',
vscodeScriptPreviewHint:
'Ejecute el script de configuración en el equipo donde se ejecuta VS Code para registrar el alias SSH de conexión remota. Vuelva a ejecutarlo si cambia el host, puerto, usuario, clave privada o ruta de la clave.',
vscodePasswordModeTitle: 'Autenticación por contraseña',
vscodePasswordModeHint:
'La autenticación por contraseña conecta con el host, puerto y usuario configurados. No requiere script de configuración ni archivo de clave privada.',
vscodePasswordStep1: 'Confirme el host, el puerto y el usuario.',
vscodePasswordStep2: 'Abra VS Code e introduzca la contraseña cuando se le solicite.',
vscodePasswordStep3: 'No es necesario descargar la clave privada.',
saveContentAndClose: 'El archivo se ha modificado, ¿desea guardar y cerrar?',
saveAndOpenNewFile: 'El archivo se ha modificado, ¿desea guardar y abrir el nuevo archivo?',
noEdit: 'El archivo no ha sido modificado, ¡no es necesario hacer esto!',
@@ -2163,13 +2213,13 @@ const message = {
'La dirección se utilizará para la redirección de aplicaciones, el acceso a contenedores y otras funciones. Cada nodo puede configurarse con una dirección diferente.',
proxy: 'Proxy del servidor',
proxyHelper: 'Tendrá efecto en los siguientes escenarios después de configurar el servidor proxy:',
proxyHelper1: 'Descarga de paquetes de instalación y sincronización de la tienda de aplicaciones (solo Pro)',
proxyHelper2: 'Actualización del sistema y comprobación de actualizaciones (solo Pro)',
proxyHelper1: 'Descarga de paquetes de instalación y sincronización de la tienda de aplicaciones (solo Business)',
proxyHelper2: 'Actualización del sistema y comprobación de actualizaciones (solo Business)',
proxyHelper3: 'Verificación y sincronización de licencias del sistema',
proxyHelper4:
'Las solicitudes de red, como la descarga de imágenes de Docker, pasarán por el servidor proxy (solo Pro)',
proxyHelper5: 'Descarga y sincronización unificadas de bibliotecas de scripts del sistema (función Pro)',
proxyHelper6: 'Solicitud de certificado (función Pro)',
'Las solicitudes de red, como la descarga de imágenes de Docker, pasarán por el servidor proxy (solo Business)',
proxyHelper5: 'Descarga y sincronización unificadas de bibliotecas de scripts del sistema (función Business)',
proxyHelper6: 'Solicitud de certificado (función Business)',
proxyType: 'Tipo de proxy',
proxyUrl: 'Dirección del proxy',
proxyPort: 'Puerto del proxy',
@@ -2579,7 +2629,7 @@ const message = {
license: {
community: 'OSS',
oss: 'Software Open Source',
pro: 'Pro',
pro: 'Business',
trial: 'Prueba',
add: 'Agregar versión Community',
licenseBindHelper:
@@ -2596,35 +2646,37 @@ const message = {
trialInfo: 'Versión',
authorizationId: 'ID de autorización',
authorizedUser: 'Usuario autorizado',
lostHelper: 'La licencia alcanzó el máximo de reintentos. Sincronícela manualmente para activar funciones Pro.',
lostHelper: 'La licencia alcanzó el máximo de reintentos. Sincronícela manualmente para activar funciones Business.',
exceptionalHelper:
'Verificación de licencia anómala. Sincronícela manualmente para activar funciones Pro. detalle: ',
'Verificación de licencia anómala. Sincronícela manualmente para activar funciones Business. detalle: ',
quickUpdate: 'Actualización rápida',
import: 'Importar',
power: 'Autorizar',
unbindHelper: 'Se eliminarán todos los ajustes Pro tras desvincular. ¿Desea continuar?',
unbindHelper: 'Se eliminarán todos los ajustes Business tras desvincular. ¿Desea continuar?',
importLicense: 'Importar licencia',
importHelper: 'Haga clic o arrastre el archivo de licencia aquí',
levelUpPro: 'Actualizar a la edición Pro',
levelUpPro: 'Actualizar a la edición Business',
licenseSync: 'Sincronizar licencia',
knowMorePro: 'Ver más',
knowMorePro: 'Conocer Business',
closeAlert: 'Puede cerrar la página en la configuración del panel',
introduce: 'Introducción de funciones',
waf: 'Actualizar a Pro permite interceptación, logs, geobloqueo, reglas, páginas personalizadas, etc.',
tamper: 'Actualizar a Pro protege sitios ante modificaciones no autorizadas.',
waf: 'Actualizar a Business permite interceptación, logs, geobloqueo, reglas, páginas personalizadas, etc.',
tamper: 'Actualizar a Business protege sitios ante modificaciones no autorizadas.',
tamperHelper:
'La operación falló, el archivo/carpeta tiene protección antimanipulación. Revise e intente de nuevo.',
setting: 'Actualizar a Pro permite personalizar logo, mensaje de bienvenida y más.',
monitor: 'Pro permite ver estado web en tiempo real, tendencias, logs, etc.',
alert: 'Pro permite recibir alertas SMS y ver logs, control total de eventos clave.',
node: 'Actualizar a la edición Pro permite administrar varios servidores Linux con 1Panel.',
setting: 'Actualizar a Business permite personalizar logo, mensaje de bienvenida y más.',
monitor: 'Business permite ver estado web en tiempo real, tendencias, logs, etc.',
alert: 'Business permite recibir alertas SMS y ver logs, control total de eventos clave.',
node: 'Actualizar a la edición Business permite administrar varios servidores Linux con 1Panel.',
nodeApp:
'Actualizar a la edición Pro permite actualizar versiones de aplicaciones multinodo sin cambiar de nodo manualmente.',
'Actualizar a la edición Business permite actualizar versiones de aplicaciones multinodo sin cambiar de nodo manualmente.',
nodeDashboard:
'Actualizar a la edición Pro permite la gestión centralizada de aplicaciones, sitios web, bases de datos y tareas programadas multinodo.',
fileExchange: 'Pro permite transferir archivos entre varios servidores.',
app: 'Pro permite ver información de servicio, monitoreo, etc. vía app móvil.',
cluster: 'Actualizar a la edición Pro permite gestionar clústeres primario-réplica de MySQL/Postgres/Redis.',
'Actualizar a la edición Business permite la gestión centralizada de aplicaciones, sitios web, bases de datos y tareas programadas multinodo.',
fileExchange: 'Business permite transferir archivos entre varios servidores.',
app: 'Business permite ver información de servicio, monitoreo, etc. vía app móvil.',
cluster: 'Actualizar a la edición Business permite gestionar clústeres primario-réplica de MySQL/Postgres/Redis.',
'model-downloader':
'Actualiza a la edición Business para buscar, descargar y gestionar archivos de modelos locales desde HuggingFace, ModelScope o espejos compatibles, con directorios de modelos y tareas de descarga centralizados.',
offLine: 'Sin conexión',
},
clean: {
@@ -3628,12 +3680,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Aviso: Tu prueba de Pro expirará en {0} días y todas las funciones Pro dejarán de estar disponibles. Renueva o actualiza a la versión completa a tiempo.',
'Aviso: Tu prueba de Business expirará en {0} días y todas las funciones Business dejarán de estar disponibles. Renueva o actualiza a la versión completa a tiempo.',
expiresAlert:
'Aviso: Tu licencia Pro expirará en {0} días y todas las funciones Pro dejarán de estar disponibles. Renueva pronto para asegurar el uso continuo.',
menu: 'Pro',
'Aviso: Tu licencia Business expirará en {0} días y todas las funciones Business dejarán de estar disponibles. Renueva pronto para asegurar el uso continuo.',
menu: 'Business',
upage: 'Constructor Web con IA',
proAlert: 'Actualiza a Pro para usar esta función',
proAlert: 'Actualiza a Business para usar esta función',
app: {
app: 'APP',
title: 'Alias del Panel',
@@ -4164,6 +4216,18 @@ const message = {
clusterTitle3: 'Estado de Replicación',
clusterContent3:
'Muestra estado de replicación maestro-esclavo y retrasos, ayudando a diagnosticar problemas de sincronización',
modelDownloaderTitle1: 'Descubrimiento de modelos',
modelDownloaderContent1:
'Busca repositorios de modelos de HuggingFace directamente y consulta descargas, me gusta, detalles y archivos.',
modelDownloaderTitle2: 'Descargas aceleradas',
modelDownloaderContent2:
'Configura un endpoint de aceleración de HuggingFace y HF Token para el sitio oficial, espejos y modelos privados.',
modelDownloaderTitle3: 'Seguimiento de tareas',
modelDownloaderContent3:
'Consulta la cola, el estado, el progreso y los errores de descarga, con soporte para cancelar, reintentar y limpiar registros.',
modelDownloaderTitle4: 'Gestión local',
modelDownloaderContent4:
'Gestiona directorios de modelos locales en un solo lugar, incluyendo rutas, tamaños, fechas y eliminación de modelos no usados.',
},
node: {
master: 'Nodo Principal',
@@ -4205,7 +4269,7 @@ const message = {
'Cuando cambian los datos en el nodo principal, se sincronizan en tiempo real con este nodo secundario.',
syncBackupAccount: 'Configuración de Cuentas de Respaldo',
syncWithMaster:
'Tras actualizar a Pro, todos los datos se sincronizarán por defecto. Las políticas de sincronización se pueden ajustar manualmente en la gestión de nodos.',
'Tras actualizar a Business, todos los datos se sincronizarán por defecto. Las políticas de sincronización se pueden ajustar manualmente en la gestión de nodos.',
syncProxy: 'Configuración del Proxy del Sistema',
syncProxyHelper: 'La sincronización del proxy del sistema requiere reiniciar Docker.',
syncProxyHelper1: 'Reiniciar Docker puede afectar a los contenedores actualmente en ejecución.',
@@ -4289,7 +4353,7 @@ const message = {
nodes: 'Nodos',
selectNode: 'Seleccionar Nodo',
selectNodeError: 'Por favor, selecciona un nodo',
licenseHelper: 'La versión Pro admite la función de repositorio de aplicaciones personalizadas',
licenseHelper: 'La versión Business admite la función de repositorio de aplicaciones personalizadas',
databaseHelper: 'Base de datos asociada a la aplicación, seleccione la base de datos del nodo destino',
nodeHelper: 'No se puede seleccionar el nodo actual',
migrateHelper:
@@ -4304,7 +4368,7 @@ const message = {
alertCount: 'Número de alertas',
clamHelper: 'Generar alerta al detectar archivos infectados en escaneo',
cronJobHelper: 'Generar alerta cuando falle la ejecución de una tarea programada',
licenseHelper: 'La versión Pro soporta alertas',
licenseHelper: 'La versión Business soporta alertas',
alertCountHelper: 'Frecuencia máxima diaria de alertas',
alert: 'Alerta SMS',
logs: 'Logs de alertas',
@@ -4506,7 +4570,7 @@ const message = {
barkConfigHelper: 'Configuración de notificación de alerta Bark',
webhookName: 'Nombre del bot',
webhookUrl: 'URL de Webhook',
alertConfigProHelper: 'La edición Pro también admite alertas mediante WeCom, DingTalk, Feishu y SMS.',
alertConfigProHelper: 'La edición Business también admite alertas mediante WeCom, DingTalk, Feishu y SMS.',
},
theme: {
lingXiaGold: 'LXware Gold',
+85 -21
View File
@@ -863,6 +863,36 @@ const message = {
from_remote: 'このモデルは1Panelを介してダウンロードされておらず、関連するプルログはありません。',
no_logs: 'このモデルのプルログは削除されており、関連するログを表示できません。',
vllmVersionHelper: 'FusionXpark GB 10 サーバーでは -cu130 バージョンを選択してください。',
downloader: 'ダウンローダー',
modelDir: 'モデルディレクトリ',
manualDownload: '手動ダウンロード',
modelSource: 'モデルソース',
repoID: 'リポジトリ ID',
modelScopeEndpoint: 'ModelScope エンドポイント',
modelScopeToken: 'ModelScope Token(任意)',
hfToken: 'HF Token(任意)',
hfEndpoint: 'HuggingFace アクセラレーションエンドポイント',
hfEndpointHelper:
'HuggingFace へのアクセスが不安定な場合はアクセラレーションエンドポイントを設定してください。検索、詳細、ダウンロードで使用されます。',
searchModelScope: 'ModelScope を検索',
searchHuggingFace: 'HuggingFace を検索',
sortTrending: 'トレンド',
sortLikes: 'いいね順',
sortDownloads: 'ダウンロード数順',
sortUpdated: '最近更新',
sortCreated: '新規作成',
downloadsCount: 'ダウンロード数',
likes: 'いいね',
taskQueue: 'ダウンロードキュー',
progress: '進捗',
localDownloaded: 'ダウンロード済み',
modelInfo: 'モデル詳細',
modelCard: 'モデルカード',
downloading: 'ダウンロード中',
canceled: 'キャンセル済み',
downloadQueued: 'ダウンロードキューに追加しました',
cancelTaskConfirm: 'このダウンロードタスクをキャンセルしますか?',
deleteLocalModelConfirm: 'ローカルモデル {0} を削除しますか?',
},
proxy: {
proxy: 'AI プロキシ強化',
@@ -1618,7 +1648,7 @@ const message = {
clam: {
clam: 'ウイルススキャン',
cron: 'スケジュールされたスキャン',
cronHelper: 'Pro 版は定期スキャンに対応しています',
cronHelper: 'ビジネス版は定期スキャンに対応しています',
specErr: '実行スケジュールフォーマットエラー、チェックして再試行してください!',
disableMsg:
'スケジュールされた実行を停止すると、このスキャンタスクが自動的に実行されなくなります。続けたいですか?',
@@ -1653,7 +1683,7 @@ const message = {
clamLog: '@:toolbox.clam.clamconfログ',
freshClam: 'フレッシュクラム',
freshClamLog: '@:toolbox.clam.freshclamログ',
alertHelper: 'Pro 版は定期スキャンと SMS アラートに対応しています',
alertHelper: 'ビジネス版は定期スキャンと SMS アラートに対応しています',
alertTitle: 'ウイルススキャンタスク({0}」感染したファイルアラートが検出されました',
},
},
@@ -1960,7 +1990,26 @@ const message = {
up: '戻って行きます',
openWithVscode: 'VSコードで開く',
vscodeHelper:
'VSコードがローカルにインストールされ、SSHリモートプラグインが構成されていることを確認してください',
'認証方式、ホスト、ポート、ユーザー名を確認してください。鍵認証を使用する場合は、事前に VS Code を実行するローカルコンピューターで SSH 設定を完了してください。',
vscodeCopyConfig: 'セットアップスクリプトをコピー',
vscodeKeyHelper:
'秘密鍵のダウンロード名は設定したパスから生成されます。そのパスに保存してください。設定スクリプトがファイルを確認し、権限を設定します。',
vscodeKeyPath: 'SSH 秘密鍵のパス',
vscodeKeyPathPlaceholder: '例: ~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH 秘密鍵',
vscodeScriptPreview: 'SSH 設定スクリプト',
vscodePasswordHelper:
'パスワード認証はローカル SSH 設定に書き込まれません。リモートホストへ接続する際は、VS Code でパスワードを入力してください。',
vscodePasswordHint:
'パスワード認証は、設定したホスト、ポート、ユーザー名で接続します。設定スクリプトや秘密鍵ファイルは不要です。',
vscodeScriptPreviewHint:
'VS Code を実行するローカルコンピューターで設定スクリプトを実行すると、リモート接続用の SSH エイリアスが登録されます。ホスト、ポート、ユーザー名、秘密鍵、または鍵パスを変更した場合は再実行してください。',
vscodePasswordModeTitle: 'パスワード認証',
vscodePasswordModeHint:
'パスワード認証は、設定したホスト、ポート、ユーザー名で接続します。設定スクリプトや秘密鍵ファイルは不要です。',
vscodePasswordStep1: 'ホスト、ポート、ユーザー名を確認します。',
vscodePasswordStep2: 'VS Code を開き、接続時にパスワードを入力します。',
vscodePasswordStep3: '秘密鍵のダウンロードは不要です。',
saveContentAndClose: 'ファイルが変更されましたが、保存して閉じたいですか?',
saveAndOpenNewFile: 'ファイルが変更されましたが、新しいファイルを保存して開きますか?',
noEdit: 'ファイルは変更されておらず、これを行う必要はありません!',
@@ -2126,11 +2175,11 @@ const message = {
'アプリケーションリダイレクト、コンテナアクセスなどの機能はこのアドレスを使用して転送されます。各ノードで異なるアドレスを設定できます。',
proxy: 'サーバープロキシ',
proxyHelper: 'プロキシサーバーを設定した後、次のシナリオで効果的になります。',
proxyHelper1: 'アプリストアからのインストールパッケージのダウンロードと同期(Pro 版のみ)',
proxyHelper2: 'システムアップデートと更新情報の取得(Pro 版のみ)',
proxyHelper1: 'アプリストアからのインストールパッケージのダウンロードと同期(ビジネス版のみ)',
proxyHelper2: 'システムアップデートと更新情報の取得(ビジネス版のみ)',
proxyHelper4:
'Docker イメージの取得などのネットワークリクエストはプロキシサーバー経由で処理されます(Pro 版のみ)',
proxyHelper5: 'システム系スクリプトライブラリの一括ダウンロードと同期(Pro 版機能)',
'Docker イメージの取得などのネットワークリクエストはプロキシサーバー経由で処理されます(ビジネス版のみ)',
proxyHelper5: 'システム系スクリプトライブラリの一括ダウンロードと同期(ビジネス版機能)',
proxyHelper6: '証明書を申請する(プロ版機能)',
proxyHelper3: 'システムライセンスの確認と同期',
proxyType: 'プロキシタイプ',
@@ -2558,30 +2607,33 @@ const message = {
'ライセンスの同期の検証は失敗しました。プロのバージョン機能が適切に機能していることを確認するには、手動で同期ボタンをクリックしてください。세부사항: ',
quickUpdate: 'クイックアップデート',
power: '許可',
unbindHelper: 'すべてのPro関連設定は、バインディングを解除した後にクリーニングされます。続けたいですか?',
unbindHelper:
'すべてのビジネス版関連設定は、バインディングを解除した後にクリーニングされます。続けたいですか?',
importLicense: 'ライセンス',
importHelper: 'ここでライセンスファイルをクリックまたはドラッグしてください',
technicalAdvice: '技術的な相談',
advice: '相談',
levelUpPro: 'Pro Edition にアップグレード',
levelUpPro: 'ビジネス版 にアップグレード',
licenseSync: 'ライセンス同期',
knowMorePro: 'もっと詳しく知る',
closeAlert: '現在のページはパネル設定で閉じることができます',
introduce: '機能の紹介',
waf: 'Pro 版にアップグレードすると、インターセプトマップ、ログ、ブロック記録、地域ブロック、カスタムルール、カスタムブロックページなどを利用できます。',
tamper: 'Pro 版にアップグレードすると、Web サイトを不正な変更や改ざんから保護できます。',
setting: 'Pro 版にアップグレードすると、パネルロゴ、ウェルカムメッセージなどをカスタマイズできます。',
waf: 'ビジネス版にアップグレードすると、インターセプトマップ、ログ、ブロック記録、地域ブロック、カスタムルール、カスタムブロックページなどを利用できます。',
tamper: 'ビジネス版にアップグレードすると、Web サイトを不正な変更や改ざんから保護できます。',
setting: 'ビジネス版にアップグレードすると、パネルロゴ、ウェルカムメッセージなどをカスタマイズできます。',
monitor:
'Pro 版にアップグレードすると、Web サイトのリアルタイム状態、訪問者トレンド、流入元、リクエストログなどを確認できます。',
alert: 'Pro 版にアップグレードすると、SMS アラートの受信やアラートログの確認が可能になります。',
app: 'Pro 版にアップグレードすると、モバイルアプリでサービス情報や異常監視を確認できます。',
node: 'Pro Edition にアップグレードすると、1Panel で複数の Linux サーバーを管理できます。',
'ビジネス版にアップグレードすると、Web サイトのリアルタイム状態、訪問者トレンド、流入元、リクエストログなどを確認できます。',
alert: 'ビジネス版にアップグレードすると、SMS アラートの受信やアラートログの確認が可能になります。',
app: 'ビジネス版にアップグレードすると、モバイルアプリでサービス情報や異常監視を確認できます。',
node: 'ビジネス版 にアップグレードすると、1Panel で複数の Linux サーバーを管理できます。',
nodeApp:
'Pro Edition にアップグレードすると、ノードを手動で切り替えずにマルチノードアプリのバージョンを一括アップグレードできます。',
'ビジネス版 にアップグレードすると、ノードを手動で切り替えずにマルチノードアプリのバージョンを一括アップグレードできます。',
nodeDashboard:
'Pro Edition にアップグレードすると、マルチノードのアプリ、Web サイト、データベース、スケジュールタスクを一元管理できます。',
fileExchange: 'Pro Edition にアップグレードすると、複数サーバー間でファイルをすばやく転送できます。',
cluster: 'Pro Edition にアップグレードすると、MySQL/Postgres/Redis のプライマリ/レプリカ構成を管理できます。',
'ビジネス版 にアップグレードすると、マルチノードのアプリ、Web サイト、データベース、スケジュールタスクを一元管理できます。',
fileExchange: 'ビジネス版 にアップグレードすると、複数サーバー間でファイルをすばやく転送できます。',
cluster: 'ビジネス版 にアップグレードすると、MySQL/Postgres/Redis のプライマリ/レプリカ構成を管理できます。',
'model-downloader':
'ビジネス版にアップグレードすると、HuggingFace、ModelScope、または互換ミラーからローカルモデルファイルを検索、ダウンロード、管理できます。モデルディレクトリとダウンロードタスクを一元管理できます。',
exceptionalHelper: 'ライセンス同期検証が異常です。手動で同期して機能を確認してください。詳細: ',
tamperHelper: '操作失敗。ファイル/フォルダの改ざん保護を確認してください。',
},
@@ -4152,6 +4204,18 @@ const message = {
clusterTitle3: 'レプリケーション状態',
clusterContent3:
'マスタースレーブレプリケーション状態と遅延情報を表示し、同期の問題を解決するのに役立ちます',
modelDownloaderTitle1: 'モデル検索',
modelDownloaderContent1:
'HuggingFace のモデルリポジトリを直接検索し、ダウンロード数、いいね、モデル詳細、ファイル一覧を確認できます。',
modelDownloaderTitle2: '高速ダウンロード',
modelDownloaderContent2:
'HuggingFace アクセラレーションエンドポイントと HF Token を設定し、公式サイト、ミラー、プライベートモデルに対応します。',
modelDownloaderTitle3: 'タスク追跡',
modelDownloaderContent3:
'ダウンロードキュー、タスク状態、進捗、エラーを一元確認し、キャンセル、再試行、記録の削除を行えます。',
modelDownloaderTitle4: 'ローカル管理',
modelDownloaderContent4:
'ローカルモデルディレクトリを一元管理し、パス、サイズ、日時の確認と不要なモデルファイルの削除を行えます。',
},
node: {
master: '主ノード',
@@ -4496,7 +4560,7 @@ const message = {
barkConfigHelper: 'Barkアラート通知設定',
webhookName: 'ボット名',
webhookUrl: 'Webhook URL',
alertConfigProHelper: 'Pro 版では、WeCom、DingTalk、Feishu、SMS 通知も利用できます。',
alertConfigProHelper: 'ビジネス版では、WeCom、DingTalk、Feishu、SMS 通知も利用できます。',
},
theme: {
lingXiaGold: 'LXware Gold',
+91 -27
View File
@@ -847,6 +847,36 @@ const message = {
from_remote: '이 모델은 1Panel을 통해 다운로드되지 않았으며 관련 풀 로그가 없습니다.',
no_logs: '이 모델의 풀 로그가 삭제되어 관련 로그를 볼 수 없습니다.',
vllmVersionHelper: 'FusionXpark GB 10 서버는 -cu130 버전을 선택하세요.',
downloader: '다운로더',
modelDir: '모델 디렉토리',
manualDownload: '수동 다운로드',
modelSource: '모델 소스',
repoID: '저장소 ID',
modelScopeEndpoint: 'ModelScope 엔드포인트',
modelScopeToken: 'ModelScope Token (선택)',
hfToken: 'HF Token (선택 사항)',
hfEndpoint: 'HuggingFace 가속 엔드포인트',
hfEndpointHelper:
'HuggingFace 접속이 불안정할 때 가속 엔드포인트를 설정하세요. 검색, 상세 정보, 다운로드에 사용됩니다.',
searchModelScope: 'ModelScope 검색',
searchHuggingFace: 'HuggingFace 검색',
sortTrending: '인기 트렌드',
sortLikes: '좋아요 많은 순',
sortDownloads: '다운로드 많은 순',
sortUpdated: '최근 업데이트',
sortCreated: '최신 생성',
downloadsCount: '다운로드 수',
likes: '좋아요',
taskQueue: '다운로드 대기열',
progress: '진행률',
localDownloaded: '다운로드됨',
modelInfo: '모델 상세 정보',
modelCard: '모델 카드',
downloading: '다운로드 중',
canceled: '취소됨',
downloadQueued: '다운로드 대기열에 추가되었습니다',
cancelTaskConfirm: '이 다운로드 작업을 취소하시겠습니까?',
deleteLocalModelConfirm: '로컬 모델 {0}을(를) 삭제하시겠습니까?',
},
proxy: {
proxy: 'AI 프록시 강화',
@@ -1919,7 +1949,27 @@ const message = {
top: '처음으로 돌아가기',
up: '뒤로가기',
openWithVscode: 'VS Code 로 열기',
vscodeHelper: '로컬에 VS Code 가 설치되어 있고 SSH Remote 플러그인이 구성되어 있는지 확인하세요.',
vscodeHelper:
'인증 방식, 호스트, 포트, 사용자명을 확인하세요. 키 인증을 사용하려면 먼저 VS Code를 실행하는 로컬 컴퓨터에서 SSH 설정을 완료해야 합니다.',
vscodeCopyConfig: '설정 스크립트 복사',
vscodeKeyHelper:
'개인 키 다운로드 파일명은 설정된 경로를 기준으로 생성됩니다. 파일을 해당 경로에 저장하면 설정 스크립트가 파일을 확인하고 권한을 적용합니다.',
vscodeKeyPath: 'SSH 개인 키 경로',
vscodeKeyPathPlaceholder: '예: ~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH 개인 키',
vscodeScriptPreview: 'SSH 설정 스크립트',
vscodePasswordHelper:
'비밀번호 인증은 로컬 SSH 설정에 기록되지 않습니다. 원격 호스트에 연결할 때 VS Code에서 비밀번호를 입력해야 합니다.',
vscodePasswordHint:
'비밀번호 인증은 설정된 호스트, 포트, 사용자명으로 연결합니다. 설정 스크립트나 개인 키 파일은 필요하지 않습니다.',
vscodeScriptPreviewHint:
'VS Code를 실행하는 로컬 컴퓨터에서 설정 스크립트를 실행하면 원격 연결용 SSH 별칭이 등록됩니다. 호스트, 포트, 사용자명, 개인 키 또는 키 경로가 변경되면 다시 실행하세요.',
vscodePasswordModeTitle: '비밀번호 인증',
vscodePasswordModeHint:
'비밀번호 인증은 설정된 호스트, 포트, 사용자명으로 연결합니다. 설정 스크립트나 개인 키 파일은 필요하지 않습니다.',
vscodePasswordStep1: '호스트, 포트, 사용자명을 확인하세요.',
vscodePasswordStep2: 'VS Code를 열고 연결 시 비밀번호를 입력하세요.',
vscodePasswordStep3: '개인 키 다운로드는 필요하지 않습니다.',
saveContentAndClose: '파일이 수정되었습니다. 저장 후 닫으시겠습니까?',
saveAndOpenNewFile: '파일이 수정되었습니다. 저장 후 새 파일을 열겠습니까?',
noEdit: '파일이 수정되지 않았습니다. 이 작업은 필요하지 않습니다!',
@@ -2084,12 +2134,12 @@ const message = {
'애플리케이션 리다이렉트, 컨테이너 접근 등의 기능은 이 주소를 사용하여 라우팅됩니다. 각 노드마다 다른 주소를 설정할 수 있습니다.',
proxy: '서버 프록시',
proxyHelper: '프록시 서버를 설정한 후 다음 시나리오에서 적용됩니다:',
proxyHelper1: '설치 패키지 다운로드 및 앱 스토어 동기화 (Pro Edition 전용)',
proxyHelper2: '시스템 업데이트 및 업데이트 정보 확인 (Pro Edition 전용)',
proxyHelper4: 'Docker 이미지 pull 같은 네트워크 요청은 프록시 서버를 통해 처리됩니다 (Pro Edition 전용)',
proxyHelper1: '설치 패키지 다운로드 및 앱 스토어 동기화 (Business Edition 전용)',
proxyHelper2: '시스템 업데이트 및 업데이트 정보 확인 (Business Edition 전용)',
proxyHelper4: 'Docker 이미지 pull 같은 네트워크 요청은 프록시 서버를 통해 처리됩니다 (Business Edition 전용)',
proxyHelper3: '시스템 라이선스 인증 및 동기화',
proxyHelper5: '시스템 스크립트 라이브러리 통합 다운로드 및 동기화 (Pro Edition 기능)',
proxyHelper6: '인증서 신청 (프로 버전 기능)',
proxyHelper5: '시스템 스크립트 라이브러리 통합 다운로드 및 동기화 (Business Edition 기능)',
proxyHelper6: '인증서 신청 (Business Edition 기능)',
proxyType: '프록시 유형',
proxyUrl: '프록시 주소',
proxyPort: '프록시 포트',
@@ -2472,7 +2522,7 @@ const message = {
offLine: '오프라인 버전',
community: 'OSS',
oss: '오픈 소스 소프트웨어',
pro: 'Pro',
pro: 'Business Edition',
trial: '체험판',
add: '커뮤니티 에디션 추가',
licenseBindHelper: '라이선스가 노드에 바인딩된 경우에만 무료 노드 할당량을 사용할 수 있습니다',
@@ -2495,32 +2545,34 @@ const message = {
'라이센스 동기화 검증에 실패했습니다. 전문가 버전 기능이 제대로 작동하는지 확인하려면 동기화 버튼을 수동으로 클릭하세요.',
quickUpdate: '빠른 업데이트',
power: '권한 부여',
unbindHelper: '연결 해제 후 모든 Pro 관련 설정이 초기화됩니다. 계속하시겠습니까?',
unbindHelper: '연결 해제 후 모든 Business Edition 관련 설정이 초기화됩니다. 계속하시겠습니까?',
importLicense: '라이센스',
importHelper: '라이센스 파일을 여기에 클릭하거나 드래그하세요',
technicalAdvice: '기술 상담',
advice: '상담',
levelUpPro: 'Pro Edition으로 업그레이드',
levelUpPro: 'Business Edition으로 업그레이드',
licenseSync: '라이센스 동기화',
knowMorePro: '더 알아보기',
closeAlert: '현재 페이지는 패널 설정에서 닫을 수 있습니다.',
introduce: '기능 소개',
waf: 'Pro로 업그레이드하면 차단 맵, 로그, 차단 기록, 지역 차단, 사용자 정의 규칙, 사용자 정의 차단 페이지 등을 사용할 수 있습니다.',
tamper: 'Pro로 업그레이드하면 웹사이트를 무단 수정이나 변조로부터 보호할 수 있습니다.',
setting: 'Pro로 업그레이드하면 패널 로고, 환영 메시지 등 정보를 사용자 정의할 수 있습니다.',
waf: 'Business Edition으로 업그레이드하면 차단 맵, 로그, 차단 기록, 지역 차단, 사용자 정의 규칙, 사용자 정의 차단 페이지 등을 사용할 수 있습니다.',
tamper: 'Business Edition으로 업그레이드하면 웹사이트를 무단 수정이나 변조로부터 보호할 수 있습니다.',
setting: 'Business Edition으로 업그레이드하면 패널 로고, 환영 메시지 등 정보를 사용자 정의할 수 있습니다.',
monitor:
'Pro로 업그레이드하면 웹사이트의 실시간 상태, 방문자 추세, 유입 경로, 요청 로그 등을 확인할 수 있습니다.',
alert: 'Pro로 업그레이드하면 SMS 알림을 받고 알림 로그를 확인할 수 있습니다.',
node: 'Pro Edition으로 업그레이드하면 1Panel에서 여러 Linux 서버를 관리할 수 있습니다.',
'Business Edition으로 업그레이드하면 웹사이트의 실시간 상태, 방문자 추세, 유입 경로, 요청 로그 등을 확인할 수 있습니다.',
alert: 'Business Edition으로 업그레이드하면 SMS 알림을 받고 알림 로그를 확인할 수 있습니다.',
node: 'Business Edition으로 업그레이드하면 1Panel에서 여러 Linux 서버를 관리할 수 있습니다.',
nodeApp:
'Pro Edition으로 업그레이드하면 노드를 수동으로 전환하지 않고도 다중 노드 애플리케이션 버전을 일괄 업그레이드할 수 있습니다.',
'Business Edition으로 업그레이드하면 노드를 수동으로 전환하지 않고도 다중 노드 애플리케이션 버전을 일괄 업그레이드할 수 있습니다.',
nodeDashboard:
'Pro Edition으로 업그레이드하면 다중 노드 애플리케이션, 웹사이트, 데이터베이스, 예약 작업을 중앙에서 관리할 수 있습니다.',
fileExchange: 'Pro Edition으로 업그레이드하면 여러 서버 간에 파일을 빠르게 전송할 수 있습니다.',
app: 'Pro로 업그레이드하면 모바일 앱에서 서비스 정보와 이상 모니터링을 확인할 수 있습니다.',
cluster: 'Pro Edition으로 업그레이드하면 MySQL/Postgres/Redis 기본-복제 클러스터를 관리할 수 있습니다.',
'Business Edition으로 업그레이드하면 다중 노드 애플리케이션, 웹사이트, 데이터베이스, 예약 작업을 중앙에서 관리할 수 있습니다.',
fileExchange: 'Business Edition으로 업그레이드하면 여러 서버 간에 파일을 빠르게 전송할 수 있습니다.',
app: 'Business Edition으로 업그레이드하면 모바일 앱에서 서비스 정보와 이상 모니터링을 확인할 수 있습니다.',
cluster: 'Business Edition으로 업그레이드하면 MySQL/Postgres/Redis 기본-복제 클러스터를 관리할 수 있습니다.',
'model-downloader':
'Business Edition으로 업그레이드하면 HuggingFace, ModelScope 또는 호환 미러에서 로컬 모델 파일을 검색, 다운로드 및 관리하고 모델 디렉터리와 다운로드 작업을 통합 관리할 수 있습니다.',
exceptionalHelper:
'라이선스 동기화 검증이 비정상입니다. 수동으로 동기화해 Pro 기능이 정상적으로 동작하는지 확인하세요. 상세: ',
'라이선스 동기화 검증이 비정상입니다. 수동으로 동기화해 Business Edition 기능이 정상적으로 동작하는지 확인하세요. 상세: ',
tamperHelper: '작업 실패, 파일/폴더에 변조 방지가 활성화되어 있습니다. 확인 후 다시 시도하세요!',
},
clean: {
@@ -3531,12 +3583,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'친절한 알림: 귀하의 Pro 체험판이 {0}일 후 만료되며, 모든 Pro 기능에 더 이상 접근할 수 없습니다. 제때 갱신하거나 전체 버전으로 업그레이드하시기 바랍니다.',
'친절한 알림: 귀하의 Business Edition 체험판이 {0}일 후 만료되며, 모든 Business Edition 기능에 더 이상 접근할 수 없습니다. 제때 갱신하거나 전체 버전으로 업그레이드하시기 바랍니다.',
expiresAlert:
'친절한 알림: 귀하의 Pro 라이선스가 {0}일 후 만료되며, 모든 Pro 기능에 더 이상 접근할 수 없습니다. 지속적인 사용을 위해 신속하게 갱신하시기 바랍니다.',
menu: 'Pro',
'친절한 알림: 귀하의 Business Edition 라이선스가 {0}일 후 만료되며, 모든 Business Edition 기능에 더 이상 접근할 수 없습니다. 지속적인 사용을 위해 신속하게 갱신하시기 바랍니다.',
menu: 'Business Edition',
upage: 'AI 웹사이트 빌더',
proAlert: '이 기능을 사용하려면 Pro로 업그레이드하세요',
proAlert: '이 기능을 사용하려면 Business Edition으로 업그레이드하세요',
app: {
app: 'APP',
title: '패널 별칭',
@@ -4054,6 +4106,18 @@ const message = {
clusterTitle3: '복제 상태',
clusterContent3:
'마스터-슬레이브 복제 상태 및 지연 정보를 표시하여 동기화 문제를 해결하는 데 도움을 줍니다',
modelDownloaderTitle1: '모델 탐색',
modelDownloaderContent1:
'HuggingFace 모델 저장소를 직접 검색하고 다운로드 수, 좋아요, 모델 상세 정보, 파일 목록을 확인합니다.',
modelDownloaderTitle2: '가속 다운로드',
modelDownloaderContent2:
'HuggingFace 가속 엔드포인트와 HF Token을 설정하여 공식 사이트, 미러, 비공개 모델에 대응합니다.',
modelDownloaderTitle3: '작업 추적',
modelDownloaderContent3:
'다운로드 대기열, 작업 상태, 진행률, 오류를 한곳에서 확인하고 취소, 재시도, 기록 정리를 지원합니다.',
modelDownloaderTitle4: '로컬 관리',
modelDownloaderContent4:
'로컬 모델 디렉토리를 중앙에서 관리하고 경로, 크기, 시간을 확인하며 불필요한 모델 파일을 삭제합니다.',
},
node: {
master: '주 노드',
@@ -4097,7 +4161,7 @@ const message = {
syncHelper: '마스터 노드 데이터가 변경되면, 이 자식 노드에 실시간으로 동기화됩니다,',
syncBackupAccount: '백업 계정 설정',
syncWithMaster:
'프로 버전으로 업그레이드 후 모든 데이터가 기본적으로 동기화됩니다. 노드 관리에서 동기화 정책을 수동으로 조정할 수 있습니다.',
'Business Edition으로 업그레이드 후 모든 데이터가 기본적으로 동기화됩니다. 노드 관리에서 동기화 정책을 수동으로 조정할 수 있습니다.',
syncProxy: '시스템 프록시 설정',
syncProxyHelper: '시스템 프록시 설정 동기화에는 Docker 재시작이 필요합니다',
syncProxyHelper1: 'Docker 재시작은 현재 실행 중인 컨테이너 서비스에 영향을 줄 수 있습니다.',
@@ -4176,7 +4240,7 @@ const message = {
nodes: '노드 선택',
selectNode: '노드 선택',
selectNodeError: '노드를 선택하세요',
licenseHelper: '프로 버전은 사용자 정의 애플리케이션 저장소 기능을 지원합니다',
licenseHelper: 'Business Edition은 사용자 정의 애플리케이션 저장소 기능을 지원합니다',
databaseHelper: '애플리케이션 관련 데이터베이스, 대상 노드 데이터베이스를 선택하세요',
nodeHelper: '현재 노드는 선택할 수 없습니다',
migrateHelper:
+98 -34
View File
@@ -872,6 +872,36 @@ const message = {
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.',
downloader: 'Pemuat Turun',
modelDir: 'Direktori Model',
manualDownload: 'Muat Turun Manual',
modelSource: 'Sumber model',
repoID: 'ID Repositori',
modelScopeEndpoint: 'Endpoint ModelScope',
modelScopeToken: 'Token ModelScope (pilihan)',
hfToken: 'HF Token (pilihan)',
hfEndpoint: 'Titik akhir pecutan HuggingFace',
hfEndpointHelper:
'Konfigurasikan titik akhir pecutan apabila akses HuggingFace tidak stabil. Carian, butiran dan muat turun akan menggunakannya.',
searchModelScope: 'Cari ModelScope',
searchHuggingFace: 'Cari HuggingFace',
sortTrending: 'Trend popular',
sortLikes: 'Paling disukai',
sortDownloads: 'Paling banyak dimuat turun',
sortUpdated: 'Dikemas kini baru-baru ini',
sortCreated: 'Terbaru dibuat',
downloadsCount: 'Muat turun',
likes: 'Suka',
taskQueue: 'Baris Gilir Muat Turun',
progress: 'Kemajuan',
localDownloaded: 'Dimuat turun',
modelInfo: 'Butiran Model',
modelCard: 'Kad Model',
downloading: 'Sedang memuat turun',
canceled: 'Dibatalkan',
downloadQueued: 'Ditambah ke baris gilir muat turun',
cancelTaskConfirm: 'Batalkan tugas muat turun ini?',
deleteLocalModelConfirm: 'Padam model tempatan {0}?',
},
proxy: {
proxy: 'Peningkatan Proksi AI',
@@ -1643,7 +1673,7 @@ const message = {
clam: {
clam: 'Imbasan virus',
cron: 'Imbasan berjadual',
cronHelper: 'Edisi Pro menyokong imbasan berjadual',
cronHelper: 'Edisi Business menyokong imbasan berjadual',
specErr: 'Ralat format jadual pelaksanaan, sila semak dan cuba lagi!',
disableMsg:
'Menghentikan pelaksanaan berjadual akan menghalang tugas imbasan ini daripada berjalan secara automatik. Adakah anda mahu meneruskan?',
@@ -1678,7 +1708,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf log',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam log',
alertHelper: 'Edisi Pro menyokong imbasan berjadual dan amaran SMS',
alertHelper: 'Edisi Business menyokong imbasan berjadual dan amaran SMS',
alertTitle: 'Tugas imbasan virus 「{0}」 mengesan amaran fail dijangkiti',
},
},
@@ -1994,7 +2024,27 @@ const message = {
top: 'Pergi ke atas',
up: 'Kembali',
openWithVscode: 'Buka dengan VS Code',
vscodeHelper: 'Pastikan VS Code dipasang secara tempatan dan pemalam SSH Remote dikonfigurasikan',
vscodeHelper:
'Sahkan mod pengesahan, hos, port dan nama pengguna. Pengesahan kunci memerlukan konfigurasi SSH pada komputer yang menjalankan VS Code sebelum digunakan.',
vscodeCopyConfig: 'Salin skrip persediaan',
vscodeKeyHelper:
'Nama muat turun kunci peribadi dijana daripada laluan yang dikonfigurasi. Simpan fail ke laluan tersebut; skrip persediaan akan menyemak fail dan menetapkan kebenaran.',
vscodeKeyPath: 'Laluan kunci peribadi SSH',
vscodeKeyPathPlaceholder: 'Contohnya: ~/.ssh/id_ed25519',
vscodeSelectKey: 'Kunci peribadi SSH',
vscodeScriptPreview: 'Skrip persediaan SSH',
vscodePasswordHelper:
'Pengesahan kata laluan tidak ditulis ke konfigurasi SSH tempatan. Masukkan kata laluan dalam VS Code semasa menyambung ke hos jauh.',
vscodePasswordHint:
'Pengesahan kata laluan menyambung menggunakan hos, port dan nama pengguna yang dikonfigurasi. Tiada skrip persediaan atau fail kunci peribadi diperlukan.',
vscodeScriptPreviewHint:
'Jalankan skrip persediaan pada komputer yang menjalankan VS Code untuk mendaftar alias SSH sambungan jauh. Jalankan semula selepas hos, port, nama pengguna, kunci peribadi atau laluan kunci berubah.',
vscodePasswordModeTitle: 'Pengesahan kata laluan',
vscodePasswordModeHint:
'Pengesahan kata laluan menyambung menggunakan hos, port dan nama pengguna yang dikonfigurasi. Tiada skrip persediaan atau fail kunci peribadi diperlukan.',
vscodePasswordStep1: 'Sahkan hos, port, dan nama pengguna.',
vscodePasswordStep2: 'Buka VS Code dan masukkan kata laluan apabila diminta.',
vscodePasswordStep3: 'Tiada muat turun kunci peribadi diperlukan.',
saveContentAndClose: 'Fail telah diubah suai, adakah anda mahu menyimpan dan menutupnya?',
saveAndOpenNewFile: 'Fail telah diubah suai, adakah anda mahu menyimpan dan membuka fail baru?',
noEdit: 'Fail tidak diubah suai, tiada keperluan untuk tindakan ini!',
@@ -2166,12 +2216,12 @@ const message = {
'Penghantaran semula aplikasi, akses kontena dan fungsi lain akan menggunakan alamat ini untuk penghalaan. Setiap nod boleh ditetapkan dengan alamat yang berbeza.',
proxy: 'Proksi pelayan',
proxyHelper: 'Ia akan berkuat kuasa dalam senario berikut selepas anda menyediakan pelayan proksi:',
proxyHelper1: 'Muat turun pakej pemasangan dan penyelarasan daripada kedai aplikasi (Edisi Pro sahaja)',
proxyHelper2: 'Kemas kini sistem dan semakan maklumat kemas kini (Edisi Pro sahaja)',
proxyHelper4: 'Permintaan rangkaian seperti tarikan imej Docker akan melalui pelayan proksi (Edisi Pro sahaja)',
proxyHelper1: 'Muat turun pakej pemasangan dan penyelarasan daripada kedai aplikasi (Edisi Business sahaja)',
proxyHelper2: 'Kemas kini sistem dan semakan maklumat kemas kini (Edisi Business sahaja)',
proxyHelper4: 'Permintaan rangkaian seperti tarikan imej Docker akan melalui pelayan proksi (Edisi Business sahaja)',
proxyHelper3: 'Pengesahan dan penyelarasan lesen sistem',
proxyHelper5: 'Muat turun dan penyegerakan bersepadu untuk pustaka skrip sistem (ciri Edisi Pro)',
proxyHelper6: 'Mohon sijil (Fungsi versi Pro)',
proxyHelper5: 'Muat turun dan penyegerakan bersepadu untuk pustaka skrip sistem (ciri Edisi Business)',
proxyHelper6: 'Mohon sijil (Fungsi versi Business)',
proxyType: 'Jenis proksi',
proxyUrl: 'Alamat proksi',
proxyPort: 'Port proksi',
@@ -2581,7 +2631,7 @@ const message = {
offLine: 'Versi Luar Talian',
community: 'OSS',
oss: 'Perisian Sumber Terbuka',
pro: 'Pro',
pro: 'Business',
trial: 'Percubaan',
add: 'Tambah Edisi Komuniti',
licenseBindHelper: 'Kuota nod percuma hanya boleh digunakan apabila lesen terikat pada nod',
@@ -2599,37 +2649,39 @@ const message = {
authorizationId: 'ID Langganan',
authorizedUser: 'Pengguna yang Dibenarkan',
lostHelper:
'Lesen telah mencapai had percubaan semula. Jalankan penyegerakan secara manual untuk memastikan ciri Pro terus berfungsi dengan baik. butiran: ',
'Lesen telah mencapai had percubaan semula. Jalankan penyegerakan secara manual untuk memastikan ciri Business terus berfungsi dengan baik. butiran: ',
disableHelper:
'Pengesahan penyegerakan lesen gagal. Jalankan penyegerakan secara manual untuk memastikan ciri Pro berfungsi dengan baik. butiran: ',
'Pengesahan penyegerakan lesen gagal. Jalankan penyegerakan secara manual untuk memastikan ciri Business berfungsi dengan baik. butiran: ',
quickUpdate: 'Kemas Kini Pantas',
power: 'Kebenaran',
unbindHelper: 'Semua Tetapan berkaitan Pro akan dibersihkan selepas nyahikat. Adakah anda mahu meneruskan?',
unbindHelper: 'Semua Tetapan berkaitan Business akan dibersihkan selepas nyahikat. Adakah anda mahu meneruskan?',
importLicense: 'Lesen',
importHelper: 'Sila klik atau seret fail lesen ke sini',
technicalAdvice: 'Konsultasi Teknikal',
advice: 'Konsultasi',
levelUpPro: 'Naik taraf ke Edisi Pro',
levelUpPro: 'Naik taraf ke Edisi Business',
licenseSync: 'Penyegerakan Lesen',
knowMorePro: 'Ketahui Lebih Lanjut',
knowMorePro: 'Ketahui Business',
closeAlert: 'Halaman semasa boleh ditutup dalam tetapan panel',
introduce: 'Pengenalan Ciri',
waf: 'Naik taraf ke Edisi Pro untuk menggunakan peta sekatan, log, rekod sekatan, sekatan geografi, peraturan tersuai, halaman sekatan tersuai dan banyak lagi.',
tamper: 'Naik taraf ke Edisi Pro untuk melindungi laman web daripada perubahan atau gangguan tanpa kebenaran.',
setting: 'Naik taraf ke Edisi Pro untuk menyesuaikan logo panel, mesej alu-aluan dan maklumat lain.',
waf: 'Naik taraf ke Edisi Business untuk menggunakan peta sekatan, log, rekod sekatan, sekatan geografi, peraturan tersuai, halaman sekatan tersuai dan banyak lagi.',
tamper: 'Naik taraf ke Edisi Business untuk melindungi laman web daripada perubahan atau gangguan tanpa kebenaran.',
setting: 'Naik taraf ke Edisi Business untuk menyesuaikan logo panel, mesej alu-aluan dan maklumat lain.',
monitor:
'Naik taraf ke Edisi Pro untuk melihat status laman web masa nyata, trend pelawat, sumber trafik, log permintaan dan lain-lain.',
alert: 'Naik taraf ke Edisi Pro untuk menerima amaran SMS dan melihat log amaran.',
node: 'Naik taraf ke Edisi Pro untuk mengurus berbilang pelayan Linux dengan 1Panel.',
'Naik taraf ke Edisi Business untuk melihat status laman web masa nyata, trend pelawat, sumber trafik, log permintaan dan lain-lain.',
alert: 'Naik taraf ke Edisi Business untuk menerima amaran SMS dan melihat log amaran.',
node: 'Naik taraf ke Edisi Business untuk mengurus berbilang pelayan Linux dengan 1Panel.',
nodeApp:
'Naik taraf ke Edisi Pro untuk menaik taraf versi aplikasi berbilang nod tanpa menukar nod secara manual.',
'Naik taraf ke Edisi Business untuk menaik taraf versi aplikasi berbilang nod tanpa menukar nod secara manual.',
nodeDashboard:
'Naik taraf ke Edisi Pro untuk mengurus aplikasi, laman web, pangkalan data dan tugas berjadual berbilang nod secara berpusat.',
fileExchange: 'Naik taraf ke Edisi Pro untuk memindahkan fail dengan pantas antara pelayan.',
app: 'Naik taraf ke Edisi Pro untuk melihat maklumat perkhidmatan dan pemantauan anomali dalam aplikasi mudah alih.',
cluster: 'Naik taraf ke Edisi Pro untuk mengurus kelompok utama-replika MySQL/Postgres/Redis.',
'Naik taraf ke Edisi Business untuk mengurus aplikasi, laman web, pangkalan data dan tugas berjadual berbilang nod secara berpusat.',
fileExchange: 'Naik taraf ke Edisi Business untuk memindahkan fail dengan pantas antara pelayan.',
app: 'Naik taraf ke Edisi Business untuk melihat maklumat perkhidmatan dan pemantauan anomali dalam aplikasi mudah alih.',
cluster: 'Naik taraf ke Edisi Business untuk mengurus kelompok utama-replika MySQL/Postgres/Redis.',
'model-downloader':
'Naik taraf kepada edisi Business untuk mencari, memuat turun dan mengurus fail model tempatan daripada HuggingFace, ModelScope atau cermin serasi, dengan direktori model dan tugasan muat turun berpusat.',
exceptionalHelper:
'Pengesahan penyegerakan lesen tidak normal. Klik butang sync secara manual untuk pastikan fungsi Pro berjalan baik. butiran: ',
'Pengesahan penyegerakan lesen tidak normal. Klik butang sync secara manual untuk pastikan fungsi Business berjalan baik. butiran: ',
tamperHelper: 'Operasi gagal, fail atau folder mempunyai perlindungan gangguan. Sila semak dan cuba lagi!',
},
clean: {
@@ -3666,12 +3718,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Peringatan mesra: Percubaan Pro anda akan tamat dalam {0} hari, dan semua ciri Pro tidak lagi dapat diakses. Sila perbaharui atau naik taraf ke versi penuh tepat pada masanya.',
'Peringatan mesra: Percubaan Business anda akan tamat dalam {0} hari, dan semua ciri Business tidak lagi dapat diakses. Sila perbaharui atau naik taraf ke versi penuh tepat pada masanya.',
expiresAlert:
'Peringatan mesra: Lesen Pro anda akan tamat dalam {0} hari, dan semua ciri Pro tidak lagi dapat diakses. Sila perbaharui segera untuk memastikan penggunaan berterusan.',
menu: 'Pro',
'Peringatan mesra: Lesen Business anda akan tamat dalam {0} hari, dan semua ciri Business tidak lagi dapat diakses. Sila perbaharui segera untuk memastikan penggunaan berterusan.',
menu: 'Business',
upage: 'Pembina Laman Web AI',
proAlert: 'Tingkatkan ke Pro untuk menggunakan ciri ini',
proAlert: 'Tingkatkan ke Business untuk menggunakan ciri ini',
app: {
app: 'APP',
title: 'Nama Panel',
@@ -4213,6 +4265,18 @@ const message = {
clusterTitle3: 'Status Replikasi',
clusterContent3:
'Memaparkan status replikasi utama-hamba dan maklumat kelewatan, membantu menyelesaikan masalah sinkronisasi',
modelDownloaderTitle1: 'Penemuan Model',
modelDownloaderContent1:
'Cari repositori model HuggingFace secara langsung dan lihat muat turun, suka, butiran model dan senarai fail.',
modelDownloaderTitle2: 'Muat Turun Dipercepat',
modelDownloaderContent2:
'Konfigurasikan titik akhir pecutan HuggingFace dan HF Token untuk laman rasmi, cermin dan model persendirian.',
modelDownloaderTitle3: 'Penjejakan Tugas',
modelDownloaderContent3:
'Semak baris gilir muat turun, status tugas, kemajuan dan ralat, dengan sokongan batal, cuba semula dan bersihkan rekod.',
modelDownloaderTitle4: 'Pengurusan Tempatan',
modelDownloaderContent4:
'Urus direktori model tempatan di satu tempat, termasuk laluan, saiz, masa dan pemadaman fail model yang tidak digunakan.',
},
node: {
master: 'Nod Utama',
@@ -4257,7 +4321,7 @@ const message = {
syncHelper: 'Apabila data nod induk berubah, ia akan disegerakkan ke nod anak ini secara masa nyata,',
syncBackupAccount: 'Tetapan akaun sandaran',
syncWithMaster:
'Selepas menaik taraf ke Pro, semua data akan diselaraskan secara lalai. Dasar penyelarasan boleh disesuaikan secara manual dalam pengurusan nod.',
'Selepas menaik taraf ke Business, semua data akan diselaraskan secara lalai. Dasar penyelarasan boleh disesuaikan secara manual dalam pengurusan nod.',
syncProxy: 'Tetapan proksi sistem',
syncProxyHelper: 'Penyelarasan tetapan proksi sistem memerlukan mulakan semula Docker',
syncProxyHelper1: 'Memulakan semula Docker mungkin menjejaskan perkhidmatan kontena yang sedang berjalan.',
@@ -4337,7 +4401,7 @@ const message = {
nodes: 'Pilih Node',
selectNode: 'Pilih Node',
selectNodeError: 'Sila pilih node',
licenseHelper: 'Versi Pro menyokong fungsi gudang aplikasi tersuai',
licenseHelper: 'Versi Business menyokong fungsi gudang aplikasi tersuai',
databaseHelper: 'Pangkalan data berkaitan aplikasi, sila pilih pangkalan data nod sasaran',
nodeHelper: 'Tidak boleh memilih nod semasa',
migrateHelper:
@@ -4352,7 +4416,7 @@ const message = {
alertCount: 'Bilangan Amaran',
clamHelper: 'Hantar amaran apabila terdapat fail yang dijangkiti semasa imbasan',
cronJobHelper: 'Hantar amaran apabila pelaksanaan tugas gagal',
licenseHelper: 'Edisi Pro menyokong lebih banyak saluran amaran',
licenseHelper: 'Edisi Business menyokong lebih banyak saluran amaran',
alertCountHelper: 'Kekerapan maksimum amaran harian',
alert: 'Amaran SMS',
logs: 'Log Amaran',
@@ -4563,7 +4627,7 @@ const message = {
barkConfigHelper: 'Konfigurasi pemberitahuan amaran Bark',
webhookName: 'Nama bot',
webhookUrl: 'URL Webhook',
alertConfigProHelper: 'Edisi Pro turut menyokong amaran WeCom, DingTalk, Feishu dan SMS.',
alertConfigProHelper: 'Edisi Business turut menyokong amaran WeCom, DingTalk, Feishu dan SMS.',
},
theme: {
lingXiaGold: 'LXware Gold',
+99 -36
View File
@@ -868,6 +868,36 @@ const message = {
from_remote: 'Model ini tidak dimuat turun melalui 1Panel, tiada log pengambilan berkaitan.',
no_logs: 'Log pengambilan untuk model ini telah dipadam dan tidak dapat dilihat.',
vllmVersionHelper: 'Para servidores FusionXpark GB 10, selecione a versão -cu130.',
downloader: 'Baixador',
modelDir: 'Diretório do modelo',
manualDownload: 'Download manual',
modelSource: 'Fonte do modelo',
repoID: 'ID do repositório',
modelScopeEndpoint: 'Endpoint do ModelScope',
modelScopeToken: 'Token do ModelScope (opcional)',
hfToken: 'HF Token (opcional)',
hfEndpoint: 'Endpoint de aceleração do HuggingFace',
hfEndpointHelper:
'Configure um endpoint de aceleração quando o acesso ao HuggingFace estiver instável. A pesquisa, os detalhes e os downloads usarão esse endereço.',
searchModelScope: 'Pesquisar no ModelScope',
searchHuggingFace: 'Pesquisar no HuggingFace',
sortTrending: 'Tendências',
sortLikes: 'Mais curtidos',
sortDownloads: 'Mais baixados',
sortUpdated: 'Atualizados recentemente',
sortCreated: 'Criados recentemente',
downloadsCount: 'Downloads',
likes: 'Curtidas',
taskQueue: 'Fila de downloads',
progress: 'Progresso',
localDownloaded: 'Baixados',
modelInfo: 'Detalhes do modelo',
modelCard: 'Cartão do modelo',
downloading: 'Baixando',
canceled: 'Cancelado',
downloadQueued: 'Adicionado à fila de downloads',
cancelTaskConfirm: 'Cancelar esta tarefa de download?',
deleteLocalModelConfirm: 'Excluir o modelo local {0}?',
},
proxy: {
proxy: 'Melhoria de Proxy AI',
@@ -1645,7 +1675,7 @@ const message = {
clam: {
clam: 'Varredura de vírus',
cron: 'Varredura agendada',
cronHelper: 'A edição Pro oferece suporte a varreduras agendadas',
cronHelper: 'A edição Business oferece suporte a varreduras agendadas',
specErr: 'Formato do agendamento inválido, verifique e tente novamente!',
disableMsg: 'Parar a execução agendada impedirá a execução automática desta tarefa. Continuar?',
enableMsg:
@@ -1679,7 +1709,7 @@ const message = {
clamLog: 'Logs de @:toolbox.clam.clamConf',
freshClam: 'FreshClam',
freshClamLog: 'Logs de @:toolbox.clam.freshClam',
alertHelper: 'A edição Pro oferece suporte a varreduras agendadas e alertas por SMS',
alertHelper: 'A edição Business oferece suporte a varreduras agendadas e alertas por SMS',
alertTitle: 'Tarefa de varredura de vírus "{0}" detectou arquivo infectado',
},
},
@@ -1758,7 +1788,7 @@ const message = {
clam: {
clam: 'Scan de vírus',
cron: 'Scan agendado',
cronHelper: 'A edição Pro oferece suporte a scans agendados',
cronHelper: 'A edição Business oferece suporte a scans agendados',
specErr: 'Erro no formato do agendamento, por favor, verifique e tente novamente!',
disableMsg:
'Parar a execução agendada impedirá que esta tarefa de scan seja executada automaticamente. Deseja continuar?',
@@ -1793,7 +1823,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf logs',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam logs',
alertHelper: 'A edição Pro oferece suporte a scans agendados e alertas por SMS',
alertHelper: 'A edição Business oferece suporte a scans agendados e alertas por SMS',
alertTitle: 'Tarefa de scan de vírus 「{0}」 detectou alerta de arquivo infectado',
},
logs: {
@@ -2108,7 +2138,26 @@ const message = {
up: 'Voltar',
openWithVscode: 'Abrir com VS Code',
vscodeHelper:
'Por favor, certifique-se de que o VS Code está instalado localmente e o plugin SSH Remote está configurado',
'Confirme o modo de autenticação, host, porta e usuário. A autenticação por chave exige configurar o SSH no computador que executa o VS Code antes do uso.',
vscodeCopyConfig: 'Copiar script de configuração',
vscodeKeyHelper:
'O nome do download da chave privada é derivado do caminho configurado. Salve o arquivo nesse caminho; o script de configuração verificará o arquivo e aplicará permissões.',
vscodeKeyPath: 'Caminho da chave privada SSH',
vscodeKeyPathPlaceholder: 'Por exemplo: ~/.ssh/id_ed25519',
vscodeSelectKey: 'Chave privada SSH',
vscodeScriptPreview: 'Script de configuração SSH',
vscodePasswordHelper:
'A autenticação por senha não grava na configuração SSH local. Informe a senha no VS Code ao conectar ao host remoto.',
vscodePasswordHint:
'A autenticação por senha conecta usando host, porta e usuário configurados. Não exige script de configuração nem arquivo de chave privada.',
vscodeScriptPreviewHint:
'Execute o script de configuração no computador que executa o VS Code para registrar o alias SSH de conexão remota. Execute-o novamente se alterar host, porta, usuário, chave privada ou caminho da chave.',
vscodePasswordModeTitle: 'Autenticação por senha',
vscodePasswordModeHint:
'A autenticação por senha conecta usando host, porta e usuário configurados. Não exige script de configuração nem arquivo de chave privada.',
vscodePasswordStep1: 'Confirme host, porta e usuário.',
vscodePasswordStep2: 'Abra o VS Code e informe a senha quando solicitado.',
vscodePasswordStep3: 'Não é necessário baixar a chave privada.',
saveContentAndClose: 'O arquivo foi modificado, deseja salvar e fechar?',
saveAndOpenNewFile: 'O arquivo foi modificado, deseja salvar e abrir o novo arquivo?',
noEdit: 'O arquivo não foi modificado, não é necessário fazer isso!',
@@ -2279,14 +2328,14 @@ const message = {
'Redirecionamentos de aplicativos, acesso a containers e outras funcionalidades usarão este endereço para roteamento. Cada nó pode ser configurado com um endereço diferente.',
proxy: 'Proxy do servidor',
proxyHelper: 'Será eficaz nos seguintes cenários após configurar o servidor proxy:',
proxyHelper1: 'Download de pacotes de instalação e sincronização da loja de aplicativos (somente edição Pro)',
proxyHelper2: 'Atualizações do sistema e verificação de atualizações (somente edição Pro)',
proxyHelper1: 'Download de pacotes de instalação e sincronização da loja de aplicativos (somente edição Business)',
proxyHelper2: 'Atualizações do sistema e verificação de atualizações (somente edição Business)',
proxyHelper4:
'Solicitações de rede, como pull de imagens Docker, serão feitas pelo servidor proxy (somente edição Pro)',
'Solicitações de rede, como pull de imagens Docker, serão feitas pelo servidor proxy (somente edição Business)',
proxyHelper3: 'Verificação e sincronização da licença do sistema',
proxyHelper5:
'Download e sincronização unificados para bibliotecas de scripts do sistema (recurso da edição Pro)',
proxyHelper6: 'Solicitar certificado (Funcionalidade da versão Pro)',
'Download e sincronização unificados para bibliotecas de scripts do sistema (recurso da edição Business)',
proxyHelper6: 'Solicitar certificado (Funcionalidade da versão Business)',
proxyType: 'Tipo de proxy',
proxyUrl: 'Endereço do proxy',
proxyPort: 'Porta do proxy',
@@ -2701,7 +2750,7 @@ const message = {
offLine: 'Versão Offline',
community: 'Gratuito',
oss: 'Open Source Software',
pro: 'Pro',
pro: 'Business',
trial: 'Teste',
add: 'Adicionar Edição Comunitária',
licenseBindHelper: 'As cotas de nó gratuitas só podem ser usadas quando a licença está vinculada a um nó',
@@ -2721,38 +2770,40 @@ const message = {
authorizationId: 'ID da Assinatura',
authorizedUser: 'Usuário autorizado',
lostHelper:
'A licença atingiu o limite de tentativas. Execute a sincronização manualmente para garantir que os recursos da edição Pro continuem funcionando. detalhes: ',
'A licença atingiu o limite de tentativas. Execute a sincronização manualmente para garantir que os recursos da edição Business continuem funcionando. detalhes: ',
disableHelper:
'A verificação da sincronização da licença falhou. Execute a sincronização manualmente para garantir que os recursos da edição Pro funcionem corretamente. detalhes: ',
'A verificação da sincronização da licença falhou. Execute a sincronização manualmente para garantir que os recursos da edição Business funcionem corretamente. detalhes: ',
quickUpdate: 'Atualização rápida',
power: 'Autorizar',
unbindHelper:
'Todas as configurações relacionadas ao Pro serão limpas após a desvinculação. Você deseja continuar?',
'Todas as configurações relacionadas ao Business serão limpas após a desvinculação. Você deseja continuar?',
importLicense: 'Licença',
importHelper: 'Clique ou arraste o arquivo de licença aqui',
technicalAdvice: 'Consultoria técnica',
advice: 'Consultoria',
levelUpPro: 'Fazer upgrade para a edição Pro',
levelUpPro: 'Fazer upgrade para a edição Business',
licenseSync: 'Sincronização de Licença',
knowMorePro: 'Saiba mais',
knowMorePro: 'Saiba sobre Business',
closeAlert: 'A página atual pode ser fechada nas configurações do painel',
introduce: 'Introdução de recursos',
waf: 'Faça upgrade para a edição Pro para usar mapa de bloqueios, logs, registros de bloqueio, bloqueio geográfico, regras personalizadas, páginas de bloqueio personalizadas e mais.',
tamper: 'Faça upgrade para a edição Pro para proteger sites contra alterações ou adulterações não autorizadas.',
setting: 'Faça upgrade para a edição Pro para personalizar o logo do painel, a mensagem de boas-vindas e mais.',
waf: 'Faça upgrade para a edição Business para usar mapa de bloqueios, logs, registros de bloqueio, bloqueio geográfico, regras personalizadas, páginas de bloqueio personalizadas e mais.',
tamper: 'Faça upgrade para a edição Business para proteger sites contra alterações ou adulterações não autorizadas.',
setting: 'Faça upgrade para a edição Business para personalizar o logo do painel, a mensagem de boas-vindas e mais.',
monitor:
'Faça upgrade para a edição Pro para acompanhar o status do site em tempo real, tendências de visitantes, origens de tráfego, logs de requisições e mais.',
alert: 'Faça upgrade para a edição Pro para receber alertas por SMS e consultar os logs de alertas.',
node: 'Atualize para a edição Pro para gerenciar vários servidores Linux com o 1Panel.',
'Faça upgrade para a edição Business para acompanhar o status do site em tempo real, tendências de visitantes, origens de tráfego, logs de requisições e mais.',
alert: 'Faça upgrade para a edição Business para receber alertas por SMS e consultar os logs de alertas.',
node: 'Atualize para a edição Business para gerenciar vários servidores Linux com o 1Panel.',
nodeApp:
'Atualize para a edição Pro para atualizar versões de aplicativos multi-nó sem alternar manualmente entre os nós.',
'Atualize para a edição Business para atualizar versões de aplicativos multi-nó sem alternar manualmente entre os nós.',
nodeDashboard:
'Atualize para a edição Pro para gerenciar aplicativos, sites, bancos de dados e tarefas agendadas multi-nó de forma centralizada.',
fileExchange: 'Atualize para a edição Pro para transferir arquivos rapidamente entre vários servidores.',
app: 'Faça upgrade para a edição Pro para visualizar informações do serviço e monitoramento de anomalias no aplicativo móvel.',
cluster: 'Atualize para a edição Pro para gerenciar clusters primário-réplica de MySQL/Postgres/Redis.',
'Atualize para a edição Business para gerenciar aplicativos, sites, bancos de dados e tarefas agendadas multi-nó de forma centralizada.',
fileExchange: 'Atualize para a edição Business para transferir arquivos rapidamente entre vários servidores.',
app: 'Faça upgrade para a edição Business para visualizar informações do serviço e monitoramento de anomalias no aplicativo móvel.',
cluster: 'Atualize para a edição Business para gerenciar clusters primário-réplica de MySQL/Postgres/Redis.',
'model-downloader':
'Atualize para a edição Business para pesquisar, baixar e gerenciar arquivos de modelos locais do HuggingFace, ModelScope ou mirrors compatíveis, com diretórios de modelos e tarefas de download centralizados.',
exceptionalHelper:
'A verificação da sincronização da licença está anormal. Clique em sincronizar manualmente para garantir o funcionamento da edição Pro. detalhe: ',
'A verificação da sincronização da licença está anormal. Clique em sincronizar manualmente para garantir o funcionamento da edição Business. detalhe: ',
tamperHelper:
'Falha na operação, o arquivo ou pasta tem proteção contra alteração habilitada. Verifique e tente novamente!',
},
@@ -3807,12 +3858,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Lembrete: Sua avaliação Pro expira em {0} dias. Depois disso, todos os recursos da edição Pro deixarão de ficar disponíveis. Renove ou faça upgrade em tempo hábil.',
'Lembrete: Sua avaliação Business expira em {0} dias. Depois disso, todos os recursos da edição Business deixarão de ficar disponíveis. Renove ou faça upgrade em tempo hábil.',
expiresAlert:
'Lembrete: Sua licença Pro expira em {0} dias. Depois disso, todos os recursos da edição Pro deixarão de ficar disponíveis. Renove a licença para manter o uso contínuo.',
menu: 'Pro',
'Lembrete: Sua licença Business expira em {0} dias. Depois disso, todos os recursos da edição Business deixarão de ficar disponíveis. Renove a licença para manter o uso contínuo.',
menu: 'Business',
upage: 'Construtor de Sites com IA',
proAlert: 'Atualize para Pro para usar este recurso',
proAlert: 'Atualize para Business para usar este recurso',
app: {
app: 'APP',
title: 'Apelido do Painel',
@@ -4359,6 +4410,18 @@ const message = {
clusterTitle3: 'Estado de Replicação',
clusterContent3:
'Exibe o estado de replicação mestre-escravo e informações de atraso, auxiliando na resolução de problemas de sincronização',
modelDownloaderTitle1: 'Descoberta de modelos',
modelDownloaderContent1:
'Pesquise repositórios de modelos do HuggingFace diretamente e veja downloads, curtidas, detalhes do modelo e lista de arquivos.',
modelDownloaderTitle2: 'Downloads acelerados',
modelDownloaderContent2:
'Configure um endpoint de aceleração do HuggingFace e HF Token para o site oficial, espelhos e modelos privados.',
modelDownloaderTitle3: 'Acompanhamento de tarefas',
modelDownloaderContent3:
'Veja filas de download, status, progresso e erros, com suporte para cancelar, tentar novamente e limpar registros.',
modelDownloaderTitle4: 'Gerenciamento local',
modelDownloaderContent4:
'Gerencie diretórios de modelos locais em um só lugar, incluindo caminhos, tamanhos, horários e exclusão de modelos não usados.',
},
node: {
master: 'Nó Principal',
@@ -4405,7 +4468,7 @@ const message = {
syncHelper: 'Quando os dados do nó mestre mudam, são sincronizados em tempo real para este nó filho,',
syncBackupAccount: 'Configurações de conta de backup',
syncWithMaster:
'Após atualizar para Pro, todos os dados serão sincronizados por padrão. As políticas de sincronização podem ser ajustadas manualmente no gerenciamento de nós.',
'Após atualizar para Business, todos os dados serão sincronizados por padrão. As políticas de sincronização podem ser ajustadas manualmente no gerenciamento de nós.',
syncProxy: 'Configurações de proxy do sistema',
syncProxyHelper: 'Sincronizar configurações de proxy do sistema requer reinicialização do Docker',
syncProxyHelper1: 'Reiniciar o Docker pode afetar os serviços de contêiner em execução.',
@@ -4484,7 +4547,7 @@ const message = {
nodes: 'Selecionar Nós',
selectNode: 'Selecionar Node',
selectNodeError: 'Por favor, selecione um nó',
licenseHelper: 'A versão Pro suporta o recurso de repositório de aplicativos personalizados',
licenseHelper: 'A versão Business suporta o recurso de repositório de aplicativos personalizados',
databaseHelper: 'Banco de dados associado ao aplicativo, selecione o banco de dados do nó de destino',
nodeHelper: 'Não é possível selecionar o nó atual',
migrateHelper:
@@ -4499,7 +4562,7 @@ const message = {
alertCount: 'Contagem de Alertas',
clamHelper: 'Dispara alerta via ao detectar arquivos infectados durante a varredura',
cronJobHelper: 'Dispara alerta via ao falhar na execução de tarefas',
licenseHelper: 'A edição Pro oferece suporte a alertas via',
licenseHelper: 'A edição Business oferece suporte a alertas via',
alertCountHelper: 'Frequência máxima diária de alertas',
alert: 'Alerta por SMS',
logs: 'Registros de Alerta',
@@ -4709,7 +4772,7 @@ const message = {
barkConfigHelper: 'Configuração de notificação de alerta Bark',
webhookName: 'Nome do bot',
webhookUrl: 'URL do Webhook',
alertConfigProHelper: 'A edição Pro também oferece suporte a alertas via WeCom, DingTalk, Feishu e SMS.',
alertConfigProHelper: 'A edição Business também oferece suporte a alertas via WeCom, DingTalk, Feishu e SMS.',
},
theme: {
lingXiaGold: 'LXware Gold',
+97 -35
View File
@@ -687,8 +687,7 @@ const message = {
allowedOriginsRequired: 'Укажите хотя бы один адрес доступа',
allowedOriginsInvalid: 'Используйте формат http(s)://host-or-ip[:port]',
provider: 'Поставщик моделей',
accountModelsHelper:
'Настройте модели, которые этот аккаунт предоставляет OpenClaw для выбора и настройки',
accountModelsHelper: 'Настройте модели, которые этот аккаунт предоставляет OpenClaw для выбора и настройки',
accountModelsRequired: 'Выберите хотя бы одну модель',
accountModelsDuplicate: 'В каталоге обнаружены повторяющиеся модели',
modelPool: 'Каталог моделей',
@@ -863,6 +862,36 @@ const message = {
from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.',
no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.',
vllmVersionHelper: 'Для серверов FusionXpark GB 10 выберите версию -cu130.',
downloader: 'Загрузчик',
modelDir: 'Каталог модели',
manualDownload: 'Ручная загрузка',
modelSource: 'Источник модели',
repoID: 'ID репозитория',
modelScopeEndpoint: 'Endpoint ModelScope',
modelScopeToken: 'ModelScope Token (необязательно)',
hfToken: 'HF Token (необязательно)',
hfEndpoint: 'Endpoint ускорения HuggingFace',
hfEndpointHelper:
'Настройте endpoint ускорения, если доступ к HuggingFace нестабилен. Он будет использоваться для поиска, деталей и загрузок.',
searchModelScope: 'Поиск в ModelScope',
searchHuggingFace: 'Поиск в HuggingFace',
sortTrending: 'Популярное',
sortLikes: 'Больше всего лайков',
sortDownloads: 'Больше всего загрузок',
sortUpdated: 'Недавно обновлено',
sortCreated: 'Недавно создано',
downloadsCount: 'Загрузки',
likes: 'Лайки',
taskQueue: 'Очередь загрузок',
progress: 'Прогресс',
localDownloaded: 'Загружено',
modelInfo: 'Сведения о модели',
modelCard: 'Карточка модели',
downloading: 'Загрузка',
canceled: 'Отменено',
downloadQueued: 'Добавлено в очередь загрузок',
cancelTaskConfirm: 'Отменить эту задачу загрузки?',
deleteLocalModelConfirm: 'Удалить локальную модель {0}?',
},
proxy: {
proxy: 'Усиление AI-прокси',
@@ -1630,7 +1659,7 @@ const message = {
clam: {
clam: 'Антивирусное сканирование',
cron: 'Запланированное сканирование',
cronHelper: 'Профессиональная версия поддерживает функцию запланированного сканирования',
cronHelper: 'Коммерческая версия поддерживает функцию запланированного сканирования',
specErr: 'Ошибка формата расписания выполнения, пожалуйста, проверьте и попробуйте снова!',
disableMsg:
'Остановка запланированного выполнения предотвратит автоматический запуск этой задачи сканирования. Хотите продолжить?',
@@ -1665,7 +1694,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf логи',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam логи',
alertHelper: 'Профессиональная версия поддерживает запланированное сканирование и SMS-оповещения',
alertHelper: 'Коммерческая версия поддерживает запланированное сканирование и SMS-оповещения',
alertTitle: 'Задача антивирусного сканирования 「{0}」 обнаружила зараженный файл',
},
},
@@ -1974,7 +2003,27 @@ const message = {
top: 'Вернуться',
up: 'Назад',
openWithVscode: 'Открыть в VS Code',
vscodeHelper: 'Пожалуйста, убедитесь что VS Code установлен локально и настроен плагин SSH Remote',
vscodeHelper:
'Проверьте способ аутентификации, хост, порт и имя пользователя. Для аутентификации по ключу сначала настройте SSH на компьютере, где запущен VS Code.',
vscodeCopyConfig: 'Скопировать скрипт настройки',
vscodeKeyHelper:
'Имя файла приватного ключа формируется из указанного пути. Сохраните файл по этому пути; скрипт настройки проверит его и применит права доступа.',
vscodeKeyPath: 'Путь к SSH приватному ключу',
vscodeKeyPathPlaceholder: 'Например: ~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH приватный ключ',
vscodeScriptPreview: 'Скрипт настройки SSH',
vscodePasswordHelper:
'Парольная аутентификация не записывается в локальную SSH config. Введите пароль в VS Code при подключении к удаленному хосту.',
vscodePasswordHint:
'Парольная аутентификация подключается с указанными хостом, портом и именем пользователя. Скрипт настройки и файл приватного ключа не требуются.',
vscodeScriptPreviewHint:
'Выполните скрипт настройки на компьютере, где запущен VS Code, чтобы зарегистрировать SSH alias для удаленного подключения. Повторите выполнение после изменения хоста, порта, имени пользователя, приватного ключа или пути к ключу.',
vscodePasswordModeTitle: 'Парольная аутентификация',
vscodePasswordModeHint:
'Парольная аутентификация подключается с указанными хостом, портом и именем пользователя. Скрипт настройки и файл приватного ключа не требуются.',
vscodePasswordStep1: 'Проверьте хост, порт и имя пользователя.',
vscodePasswordStep2: 'Откройте VS Code и введите пароль при подключении.',
vscodePasswordStep3: 'Загрузка приватного ключа не требуется.',
saveContentAndClose: 'Файл был изменен, хотите сохранить и закрыть его?',
saveAndOpenNewFile: 'Файл был изменен, хотите сохранить и открыть новый файл?',
noEdit: 'Файл не был изменен, не нужно этого делать!',
@@ -2143,13 +2192,13 @@ const message = {
proxy: 'Прокси',
proxyHelper: 'После настройки прокси-сервера он будет действовать в следующих сценариях:',
proxyHelper1:
'Загрузка установочного пакета и синхронизация из магазина приложений (только профессиональная версия)',
proxyHelper2: 'Обновление системы и получение информации об обновлениях (только профессиональная версия)',
'Загрузка установочного пакета и синхронизация из магазина приложений (только коммерческая версия)',
proxyHelper2: 'Обновление системы и получение информации об обновлениях (только коммерческая версия)',
proxyHelper4:
'Сетевые запросы, такие как загрузка образов Docker, будут выполняться через прокси-сервер (только профессиональная версия)',
'Сетевые запросы, такие как загрузка образов Docker, будут выполняться через прокси-сервер (только коммерческая версия)',
proxyHelper3: 'Проверка и синхронизация системной лицензии',
proxyHelper5: 'Единая загрузка и синхронизация системных скрипт-библиотек (функция Профессиональной версии)',
proxyHelper6: 'Подать заявку на сертификат (функция профессиональной версии)',
proxyHelper5: 'Единая загрузка и синхронизация системных скрипт-библиотек (функция Коммерческой версии)',
proxyHelper6: 'Подать заявку на сертификат (функция коммерческой версии)',
proxyType: 'Тип прокси',
proxyUrl: 'Адрес прокси',
proxyPort: 'Порт прокси',
@@ -2562,7 +2611,7 @@ const message = {
offLine: 'Офлайн версия',
community: 'OSS',
oss: 'Open Source Software',
pro: 'Pro',
pro: 'Коммерческая версия',
trial: 'Пробная версия',
add: 'Добавить Community Edition',
licenseBindHelper: 'Бесплатные квоты узлов можно использовать только тогда, когда лицензия привязана к узлу',
@@ -2582,39 +2631,40 @@ const message = {
authorizationId: 'ID подписки',
authorizedUser: 'Авторизованный пользователь',
lostHelper:
'Лицензия достигла максимального количества попыток повторной проверки. Пожалуйста, вручную нажмите кнопку синхронизации, чтобы убедиться, что функции профессиональной версии работают правильно. Детали: ',
'Лицензия достигла максимального количества попыток повторной проверки. Пожалуйста, вручную нажмите кнопку синхронизации, чтобы убедиться, что функции коммерческой версии работают правильно. Детали: ',
disableHelper:
'Синхронизация лицензии не удалась. Пожалуйста, вручную нажмите кнопку синхронизации, чтобы убедиться, что функции профессиональной версии работают правильно. Детали: ',
'Синхронизация лицензии не удалась. Пожалуйста, вручную нажмите кнопку синхронизации, чтобы убедиться, что функции коммерческой версии работают правильно. Детали: ',
quickUpdate: 'Быстрое обновление',
power: 'Авторизовать',
unbindHelper: 'Все настройки Pro будут очищены после отвязки. Хотите продолжить?',
unbindHelper: 'Все настройки коммерческой версии будут очищены после отвязки. Хотите продолжить?',
importLicense: 'Лицензия',
importHelper: 'Пожалуйста, нажмите или перетащите файл лицензии сюда',
technicalAdvice: 'Техническая консультация',
advice: 'Консультация',
levelUpPro: 'Обновить до версии Pro',
levelUpPro: 'Обновить до коммерческой версии',
licenseSync: 'Синхронизация лицензии',
knowMorePro: 'Узнать больше',
closeAlert: 'Текущую страницу можно закрыть в настройках панели',
introduce: 'Описание функций',
waf: 'Обновление до профессиональной версии предоставляет такие функции, как карта перехватов, логи, записи блокировок, блокировка по географическому положению, пользовательские правила, пользовательские страницы перехвата и т.д.',
tamper: 'Обновление до профессиональной версии может защитить веб-сайты от несанкционированных изменений или подделок.',
waf: 'Обновление до коммерческой версии предоставляет такие функции, как карта перехватов, логи, записи блокировок, блокировка по географическому положению, пользовательские правила, пользовательские страницы перехвата и т.д.',
tamper: 'Обновление до коммерческой версии может защитить веб-сайты от несанкционированных изменений или подделок.',
setting:
'Обновление до профессиональной версии позволяет настраивать логотип панели, приветственное сообщение и другую информацию.',
'Обновление до коммерческой версии позволяет настраивать логотип панели, приветственное сообщение и другую информацию.',
monitor:
'Обновление до профессиональной версии позволяет просматривать статус веб-сайта в реальном времени, тенденции посещений, источники посетителей, логи запросов и другую информацию.',
alert: 'Обновление до профессиональной версии позволяет получать информацию о тревогах через SMS и просматривать логи тревог, полностью контролировать различные ключевые события и обеспечивать беспроблемную работу системы',
node: 'Обновление до профессиональной редакции позволяет управлять несколькими серверами Linux с помощью 1Panel.',
'Обновление до коммерческой версии позволяет просматривать статус веб-сайта в реальном времени, тенденции посещений, источники посетителей, логи запросов и другую информацию.',
alert: 'Обновление до коммерческой версии позволяет получать информацию о тревогах через SMS и просматривать логи тревог, полностью контролировать различные ключевые события и обеспечивать беспроблемную работу системы',
node: 'Обновление до коммерческой редакции позволяет управлять несколькими серверами Linux с помощью 1Panel.',
nodeApp:
'Обновление до профессиональной редакции позволяет унифицированно обновлять версии приложений на нескольких узлах без необходимости ручного переключения узлов.',
'Обновление до коммерческой редакции позволяет унифицированно обновлять версии приложений на нескольких узлах без необходимости ручного переключения узлов.',
nodeDashboard:
'Обновление до профессиональной редакции позволяет централизованно управлять приложениями, веб-сайтами, базами данных и запланированными задачами на нескольких узлах.',
fileExchange: 'Обновите до Профессиональной версии, чтобы быстро передавать файлы между несколькими серверами.',
app: 'Обновите до профессиональной версии, чтобы просматривать информацию о сервисах, мониторинг аномалий и т.д. через мобильное приложение.',
cluster:
'Обновление до профессиональной версии позволяет управлять кластерами мастер-слейв MySQL/Postgres/Redis.',
'Обновление до коммерческой редакции позволяет централизованно управлять приложениями, веб-сайтами, базами данных и запланированными задачами на нескольких узлах.',
fileExchange: 'Обновите до Коммерческой версии, чтобы быстро передавать файлы между несколькими серверами.',
app: 'Обновите до коммерческой версии, чтобы просматривать информацию о сервисах, мониторинг аномалий и т.д. через мобильное приложение.',
cluster: 'Обновление до коммерческой версии позволяет управлять кластерами мастер-слейв MySQL/Postgres/Redis.',
'model-downloader':
'Обновите до коммерческой версии, чтобы искать, загружать и управлять локальными файлами моделей из HuggingFace, ModelScope или совместимых зеркал, централизованно управляя каталогами моделей и задачами загрузки.',
exceptionalHelper:
'Проверка синхронизации лицензии выполнена с ошибкой. Нажмите синхронизацию вручную для корректной работы Pro. Детали: ',
'Проверка синхронизации лицензии выполнена с ошибкой. Нажмите синхронизацию вручную для корректной работы коммерческой версии. Детали: ',
tamperHelper: 'Операция не выполнена: для файла или папки включена защита от изменений. Проверьте и повторите!',
},
clean: {
@@ -3660,12 +3710,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Дружеское напоминание: ваша пробная версия Pro истечет через {0} дней, и все функции Pro станут недоступны. Пожалуйста, своевременно продлите или обновите до полной версии.',
'Дружеское напоминание: ваша пробная коммерческая версия истечет через {0} дней, и все функции коммерческой версии станут недоступны. Пожалуйста, своевременно продлите или обновите до полной версии.',
expiresAlert:
'Дружеское напоминание: ваша лицензия Pro истечет через {0} дней, и все функции Pro станут недоступны. Пожалуйста, продлите лицензию вовремя, чтобы обеспечить дальнейшее использование.',
'Дружеское напоминание: ваша коммерческая лицензия истечет через {0} дней, и все функции коммерческой версии станут недоступны. Пожалуйста, продлите лицензию вовремя, чтобы обеспечить дальнейшее использование.',
menu: 'Рro',
upage: 'AI Конструктор сайтов',
proAlert: 'Обновитесь до Pro, чтобы использовать эту функцию',
proAlert: 'Обновитесь до коммерческой версии, чтобы использовать эту функцию',
app: {
app: 'APP',
title: 'Псевдоним панели',
@@ -4208,6 +4258,18 @@ const message = {
clusterTitle3: 'Состояние репликации',
clusterContent3:
'Отображает состояние репликации мастер-слейв и информацию о задержке, помогая в устранении проблем синхронизации',
modelDownloaderTitle1: 'Поиск моделей',
modelDownloaderContent1:
'Ищите репозитории моделей HuggingFace напрямую и просматривайте загрузки, лайки, сведения о модели и список файлов.',
modelDownloaderTitle2: 'Ускоренные загрузки',
modelDownloaderContent2:
'Настройте endpoint ускорения HuggingFace и HF Token для официального сайта, зеркал и приватных моделей.',
modelDownloaderTitle3: 'Отслеживание задач',
modelDownloaderContent3:
'Просматривайте очередь загрузок, статус, прогресс и ошибки, отменяйте, повторяйте и очищайте записи.',
modelDownloaderTitle4: 'Локальное управление',
modelDownloaderContent4:
'Управляйте локальными каталогами моделей в одном месте: пути, размеры, время и удаление неиспользуемых файлов моделей.',
},
node: {
master: 'Главный узел',
@@ -4255,7 +4317,7 @@ const message = {
'При изменении данных главного узла, происходит синхронизация с этим дочерним узлом в реальном времени,',
syncBackupAccount: 'Настройки резервной учётной записи',
syncWithMaster:
'После обновления до Pro все данные будут синхронизироваться по умолчанию. Политики синхронизации можно настроить вручную в управлении узлами.',
'После обновления до коммерческой версии все данные будут синхронизироваться по умолчанию. Политики синхронизации можно настроить вручную в управлении узлами.',
syncProxy: 'Настройки системного прокси',
syncProxyHelper: 'Синхронизация настроек системного прокси требует перезапуска Docker',
syncProxyHelper1: 'Перезапуск Docker может повлиять на работающие сервисы контейнеров.',
@@ -4335,7 +4397,7 @@ const message = {
nodes: 'Выбрать узлы',
selectNode: 'Выбрать узел',
selectNodeError: 'Пожалуйста, выберите узел',
licenseHelper: 'Профессиональная версия поддерживает функцию пользовательского репозитория приложений',
licenseHelper: 'Коммерческая версия поддерживает функцию пользовательского репозитория приложений',
databaseHelper: 'База данных, связанная с приложением, выберите базу данных целевого узла',
nodeHelper: 'Нельзя выбрать текущий узел',
migrateHelper:
@@ -4350,7 +4412,7 @@ const message = {
alertCount: 'Количество оповещений',
clamHelper: 'Отправлять оповещение при обнаружении зараженных файлов',
cronJobHelper: 'Отправлять оповещение при сбое выполнения задачи',
licenseHelper: 'Профессиональная версия поддерживает оповещения',
licenseHelper: 'Коммерческая версия поддерживает оповещения',
alertCountHelper: 'Максимальная дневная частота оповещений',
alert: 'SMS Уведомление',
logs: 'Журнал Уведомлений',
@@ -4565,7 +4627,7 @@ const message = {
webhookName: 'Имя бота',
webhookUrl: 'URL Webhook',
alertConfigProHelper:
'Профессиональная версия дополнительно поддерживает уведомления через WeCom, DingTalk, Feishu и SMS.',
'Коммерческая версия дополнительно поддерживает уведомления через WeCom, DingTalk, Feishu и SMS.',
},
theme: {
lingXiaGold: 'LXware Gold',
+95 -32
View File
@@ -870,6 +870,36 @@ const message = {
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.',
downloader: 'İndirici',
modelDir: 'Model dizini',
manualDownload: 'Manuel indirme',
modelSource: 'Model kaynağı',
repoID: 'Depo ID',
modelScopeEndpoint: 'ModelScope endpointi',
modelScopeToken: 'ModelScope Token (isteğe bağlı)',
hfToken: 'HF Token (isteğe bağlı)',
hfEndpoint: 'HuggingFace hızlandırma endpointi',
hfEndpointHelper:
'HuggingFace erişimi kararsız olduğunda bir hızlandırma endpointi yapılandırın. Arama, ayrıntılar ve indirmeler bunu kullanır.',
searchModelScope: 'ModelScope ara',
searchHuggingFace: 'HuggingFace ara',
sortTrending: 'Popüler trendler',
sortLikes: 'En çok beğenilen',
sortDownloads: 'En çok indirilenler',
sortUpdated: 'Son güncellenenler',
sortCreated: 'Yeni oluşturulanlar',
downloadsCount: 'İndirmeler',
likes: 'Beğeniler',
taskQueue: 'İndirme Kuyruğu',
progress: 'İlerleme',
localDownloaded: 'İndirilenler',
modelInfo: 'Model ayrıntıları',
modelCard: 'Model kartı',
downloading: 'İndiriliyor',
canceled: 'İptal edildi',
downloadQueued: 'İndirme kuyruğuna eklendi',
cancelTaskConfirm: 'Bu indirme görevi iptal edilsin mi?',
deleteLocalModelConfirm: 'Yerel model {0} silinsin mi?',
},
proxy: {
proxy: 'AI Proxy Geliştirmesi',
@@ -1641,7 +1671,7 @@ const message = {
clam: {
clam: 'Virüs taraması',
cron: 'Zamanlanmış tarama',
cronHelper: 'Pro sürüm zamanlanmış taramayı destekler',
cronHelper: 'Business sürüm zamanlanmış taramayı destekler',
specErr: 'Yürütme programı format hatası, lütfen kontrol edin ve tekrar deneyin!',
disableMsg:
'Zamanlanmış yürütmeyi durdurmak bu tarama görevinin otomatik olarak çalışmasını engelleyecektir. Devam etmek istiyor musunuz?',
@@ -1676,7 +1706,7 @@ const message = {
clamLog: '@:toolbox.clam.clamConf logları',
freshClam: 'FreshClam',
freshClamLog: '@:toolbox.clam.freshClam logları',
alertHelper: 'Pro sürüm zamanlanmış tarama ve SMS uyarılarını destekler',
alertHelper: 'Business sürüm zamanlanmış tarama ve SMS uyarılarını destekler',
alertTitle: 'Virüs tarama görevi「{0}」enfekte dosya tespit uyarısı',
},
},
@@ -1986,7 +2016,26 @@ const message = {
up: 'Geri dön',
openWithVscode: 'VS Code ile aç',
vscodeHelper:
'Lütfen VS Code’un yerel olarak yüklü olduğundan ve SSH Remote eklentisinin yapılandırıldığından emin olun',
'Kimlik doğrulama modu, host, port ve kullanıcı adını doğrulayın. Anahtar kimlik doğrulaması için kullanmadan önce VS Code’un çalıştığı yerel bilgisayarda SSH yapılandırması tamamlanmalıdır.',
vscodeCopyConfig: 'Kurulum betiğini kopyala',
vscodeKeyHelper:
'Özel anahtar indirme adı yapılandırılan yoldan türetilir. Dosyayı bu yola kaydedin; kurulum betiği dosyayı doğrular ve izinleri uygular.',
vscodeKeyPath: 'SSH özel anahtar yolu',
vscodeKeyPathPlaceholder: 'Örneğin: ~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH özel anahtarı',
vscodeScriptPreview: 'SSH kurulum betiği',
vscodePasswordHelper:
'Parola kimlik doğrulaması yerel SSH yapılandırmasına yazılmaz. Uzak host’a bağlanırken parolayı VS Code içinde girin.',
vscodePasswordHint:
'Parola kimlik doğrulaması yapılandırılan host, port ve kullanıcı adıyla bağlanır. Kurulum betiği veya özel anahtar dosyası gerekmez.',
vscodeScriptPreviewHint:
'Uzak bağlantı SSH alias’ını kaydetmek için kurulum betiğini VS Code’un çalıştığı yerel bilgisayarda çalıştırın. Host, port, kullanıcı adı, özel anahtar veya anahtar yolu değişirse yeniden çalıştırın.',
vscodePasswordModeTitle: 'Parola kimlik doğrulaması',
vscodePasswordModeHint:
'Parola kimlik doğrulaması yapılandırılan host, port ve kullanıcı adıyla bağlanır. Kurulum betiği veya özel anahtar dosyası gerekmez.',
vscodePasswordStep1: 'Host, port ve kullanıcı adını doğrulayın.',
vscodePasswordStep2: 'VS Code’u açın ve istendiğinde parolayı girin.',
vscodePasswordStep3: 'Özel anahtar indirme gerekmez.',
saveContentAndClose: 'Dosya değiştirildi, kaydedip kapatmak istiyor musunuz?',
saveAndOpenNewFile: 'Dosya değiştirildi, kaydedip yeni dosyayı açmak istiyor musunuz?',
noEdit: 'Dosya değiştirilmedi, buna gerek yok!',
@@ -2158,12 +2207,12 @@ const message = {
'Adres, uygulama yönlendirmesi, konteyner erişimi ve diğer işlevler için kullanılacaktır. Her düğüm farklı bir adresle yapılandırılabilir.',
proxy: 'Sunucu vekili',
proxyHelper: 'Vekil sunucuyu ayarladıktan sonra aşağıdaki senaryolarda etkili olacaktır:',
proxyHelper1: 'Uygulama mağazasından kurulum paketi indirme ve senkronizasyon (yalnızca Pro sürüm)',
proxyHelper2: 'Sistem güncellemeleri ve güncelleme bilgilerini alma (yalnızca Pro sürüm)',
proxyHelper1: 'Uygulama mağazasından kurulum paketi indirme ve senkronizasyon (yalnızca Business sürüm)',
proxyHelper2: 'Sistem güncellemeleri ve güncelleme bilgilerini alma (yalnızca Business sürüm)',
proxyHelper3: 'Sistem lisansı doğrulama ve senkronizasyon',
proxyHelper4: 'Docker imajı çekme gibi ağ istekleri vekil sunucu üzerinden yapılır (yalnızca Pro sürüm)',
proxyHelper5: 'Sistem komut dosyası kütüphaneleri için birleşik indirme ve senkronizasyon (Pro özelliği)',
proxyHelper6: 'Sertifika başvurusu (Pro özelliği)',
proxyHelper4: 'Docker imajı çekme gibi ağ istekleri vekil sunucu üzerinden yapılır (yalnızca Business sürüm)',
proxyHelper5: 'Sistem komut dosyası kütüphaneleri için birleşik indirme ve senkronizasyon (Business özelliği)',
proxyHelper6: 'Sertifika başvurusu (Business özelliği)',
proxyType: 'Vekil türü',
proxyUrl: 'Vekil Adresi',
proxyPort: 'Vekil Portu',
@@ -2568,7 +2617,7 @@ const message = {
offLine: 'Çevrimdışı Sürüm',
community: 'OSS',
oss: 'Açık Kaynak Yazılım',
pro: 'Pro',
pro: 'Business',
trial: 'Deneme',
add: 'Topluluk Sürümünü Ekle',
licenseBindHelper: 'Ücretsiz düğüm kotası yalnızca lisans bir düğüme bağlı olduğunda kullanılabilir',
@@ -2588,37 +2637,39 @@ const message = {
authorizationId: 'Abonelik ID',
authorizedUser: 'Yetkili kullanıcı',
lostHelper:
'Lisans yeniden deneme sınırına ulaştı. Pro özelliklerinin düzgün çalışması için lütfen senkronizasyonu manuel olarak çalıştırın.',
'Lisans yeniden deneme sınırına ulaştı. Business özelliklerinin düzgün çalışması için lütfen senkronizasyonu manuel olarak çalıştırın.',
exceptionalHelper:
'Lisans senkronizasyon doğrulaması başarısız oldu. Pro özelliklerinin düzgün çalışması için lütfen senkronizasyonu manuel olarak çalıştırın. ayrıntı: ',
'Lisans senkronizasyon doğrulaması başarısız oldu. Business özelliklerinin düzgün çalışması için lütfen senkronizasyonu manuel olarak çalıştırın. ayrıntı: ',
quickUpdate: 'Hızlı güncelleme',
import: 'İçe aktar',
power: 'Yetkilendir',
unbindHelper: 'Bağlantı kaldırıldığında tüm Pro ile ilgili ayarlar temizlenecek. Devam etmek istiyor musunuz? ',
unbindHelper: 'Bağlantı kaldırıldığında tüm Business ile ilgili ayarlar temizlenecek. Devam etmek istiyor musunuz? ',
importLicense: 'Lisansı içe aktar',
importHelper: 'Lütfen lisans dosyasını buraya tıklayın veya sürükleyin',
levelUpPro: 'Pro Sürüme Yükselt',
levelUpPro: 'Business Sürüme Yükselt',
licenseSync: 'Lisans Senkronizasyonu',
knowMorePro: 'Daha Fazla Bilgi Edinin',
closeAlert: 'Geçerli sayfa panel ayarlarında kapatılabilir',
introduce: 'Özellik Tanıtımı',
waf: 'Pro sürüme yükselterek engelleme haritası, günlükler, engelleme kayıtları, coğrafi konum engelleme, özel kurallar ve özel engelleme sayfaları gibi özellikleri açabilirsiniz.',
tamper: 'Pro sürüme yükselterek web sitelerini yetkisiz değişikliklere ve kurcalamaya karşı koruyabilirsiniz.',
waf: 'Business sürüme yükselterek engelleme haritası, günlükler, engelleme kayıtları, coğrafi konum engelleme, özel kurallar ve özel engelleme sayfaları gibi özellikleri açabilirsiniz.',
tamper: 'Business sürüme yükselterek web sitelerini yetkisiz değişikliklere ve kurcalamaya karşı koruyabilirsiniz.',
tamperHelper:
'İşlem başarısız oldu, dosya veya klasörde kurcalama koruması etkin. Lütfen kontrol edin ve tekrar deneyin!',
setting:
'Pro sürüme yükselterek panel logosunu, hoş geldiniz mesajını ve diğer bilgileri özelleştirebilirsiniz.',
'Business sürüme yükselterek panel logosunu, hoş geldiniz mesajını ve diğer bilgileri özelleştirebilirsiniz.',
monitor:
'Pro sürüme yükselterek web sitesinin gerçek zamanlı durumunu, ziyaretçi eğilimlerini, kaynaklarını ve istek günlüklerini görüntüleyebilirsiniz.',
alert: 'Pro sürüme yükselterek SMS uyarıları alabilir, uyarı günlüklerini görüntüleyebilir ve kritik olayları takip edebilirsiniz.',
node: 'Pro Sürüme yükselterek 1Panel ile birden fazla Linux sunucusunu yönetebilirsiniz.',
'Business sürüme yükselterek web sitesinin gerçek zamanlı durumunu, ziyaretçi eğilimlerini, kaynaklarını ve istek günlüklerini görüntüleyebilirsiniz.',
alert: 'Business sürüme yükselterek SMS uyarıları alabilir, uyarı günlüklerini görüntüleyebilir ve kritik olayları takip edebilirsiniz.',
node: 'Business Sürüme yükselterek 1Panel ile birden fazla Linux sunucusunu yönetebilirsiniz.',
nodeApp:
'Pro Sürüme yükselterek düğümleri manuel değiştirmeden çok düğümlü uygulama sürümlerini toplu olarak yükseltebilirsiniz.',
'Business Sürüme yükselterek düğümleri manuel değiştirmeden çok düğümlü uygulama sürümlerini toplu olarak yükseltebilirsiniz.',
nodeDashboard:
'Pro Sürüme yükselterek çok düğümlü uygulamaları, web sitelerini, veritabanlarını ve zamanlanmış görevleri merkezi olarak yönetebilirsiniz.',
fileExchange: 'Pro Sürüme yükselterek birden fazla sunucu arasında dosyaları hızlıca aktarabilirsiniz.',
app: 'Pro sürüme yükselterek mobil uygulamada hizmet bilgilerini ve anormallik izlemeyi görüntüleyebilirsiniz.',
cluster: 'Pro Sürüme yükselterek MySQL/Postgres/Redis birincil-kopya kümelerini yönetebilirsiniz.',
'Business Sürüme yükselterek çok düğümlü uygulamaları, web sitelerini, veritabanlarını ve zamanlanmış görevleri merkezi olarak yönetebilirsiniz.',
fileExchange: 'Business Sürüme yükselterek birden fazla sunucu arasında dosyaları hızlıca aktarabilirsiniz.',
app: 'Business sürüme yükselterek mobil uygulamada hizmet bilgilerini ve anormallik izlemeyi görüntüleyebilirsiniz.',
cluster: 'Business Sürüme yükselterek MySQL/Postgres/Redis birincil-kopya kümelerini yönetebilirsiniz.',
'model-downloader':
'Business sürümüne yükselterek HuggingFace, ModelScope veya uyumlu aynalardan yerel model dosyalarını arayabilir, indirebilir ve yönetebilir; model dizinlerini ve indirme görevlerini merkezi olarak takip edebilirsiniz.',
},
clean: {
scan: 'Taramayı başlat',
@@ -3661,12 +3712,12 @@ const message = {
},
xpack: {
expiresTrialAlert:
'Nazik hatırlatma: Pro deneme sürümünüz {0} gün içinde sona erecek ve tüm Pro özellikleri kullanılamaz hale gelecektir. Lütfen zamanında yenileyin veya tam sürüme yükseltin.',
'Nazik hatırlatma: Business deneme sürümünüz {0} gün içinde sona erecek ve tüm Business özellikleri kullanılamaz hale gelecektir. Lütfen zamanında yenileyin veya tam sürüme yükseltin.',
expiresAlert:
'Nazik hatırlatma: Pro lisansınız {0} gün içinde sona erecek ve tüm Pro özellikleri kullanılamaz hale gelecektir. Lütfen devam eden kullanım için zamanında yenileyin.',
menu: 'Pro',
'Nazik hatırlatma: Business lisansınız {0} gün içinde sona erecek ve tüm Business özellikleri kullanılamaz hale gelecektir. Lütfen devam eden kullanım için zamanında yenileyin.',
menu: 'Business',
upage: 'AI Web Sitesi Oluşturucu',
proAlert: 'Bu özelliği kullanmak için Proya yükseltin',
proAlert: 'Bu özelliği kullanmak için Business’a yükseltin',
app: {
app: 'Uygulama',
title: 'Panel Takma Adı',
@@ -4207,6 +4258,18 @@ const message = {
clusterTitle3: 'Çoğaltma Durumu',
clusterContent3:
'Ana-çalışan çoğaltma durumunu ve gecikme bilgilerini görüntüleyerek senkronizasyon sorunlarını gidermeye yardımcı olur',
modelDownloaderTitle1: 'Model Keşfi',
modelDownloaderContent1:
'HuggingFace model depolarını doğrudan arayın; indirme sayısı, beğeni, model ayrıntıları ve dosya listesini görüntüleyin.',
modelDownloaderTitle2: 'Hızlandırılmış İndirme',
modelDownloaderContent2:
'Resmi site, aynalar ve özel modeller için HuggingFace hızlandırma endpointi ve HF Token yapılandırın.',
modelDownloaderTitle3: 'Görev Takibi',
modelDownloaderContent3:
'İndirme kuyruğunu, görev durumunu, ilerlemeyi ve hataları görüntüleyin; iptal, yeniden deneme ve kayıt temizlemeyi destekler.',
modelDownloaderTitle4: 'Yerel Yönetim',
modelDownloaderContent4:
'Yerel model dizinlerini tek yerden yönetin; yolları, boyutları, zamanları görüntüleyin ve kullanılmayan model dosyalarını silin.',
},
node: {
master: 'Ana Düğüm',
@@ -4252,7 +4315,7 @@ const message = {
syncHelper: 'Ana düğüm verileri değiştiğinde, bu alt düğüme gerçek zamanlı olarak senkronize edilir',
syncBackupAccount: 'Yedekleme hesabı ayarları',
syncWithMaster:
'Pro’ya yükseltildikten sonra, tüm veriler varsayılan olarak senkronize edilir. Senkronizasyon politikaları düğüm yönetiminde manuel olarak ayarlanabilir.',
'Business’a yükseltildikten sonra, tüm veriler varsayılan olarak senkronize edilir. Senkronizasyon politikaları düğüm yönetiminde manuel olarak ayarlanabilir.',
syncProxy: 'Sistem proxy ayarları',
syncProxyHelper: 'Sistem proxy ayarlarını senkronize etmek Docker yeniden başlatılmasını gerektirir',
syncProxyHelper1: 'Docker’ın yeniden başlatılması, şu anda çalışan kapsayıcı hizmetlerini etkileyebilir.',
@@ -4331,7 +4394,7 @@ const message = {
nodes: 'Düğümler',
selectNode: 'Düğüm Seç',
selectNodeError: 'Lütfen bir düğüm seçin',
licenseHelper: 'Pro sürümü, özel uygulama deposu özelliğini destekler',
licenseHelper: 'Business sürümü, özel uygulama deposu özelliğini destekler',
databaseHelper: 'Uygulama ilişkili veritabanı, lütfen hedef düğüm veritabanını seçin',
nodeHelper: 'Geçerli düğüm seçilemez',
migrateHelper:
@@ -4346,7 +4409,7 @@ const message = {
alertCount: 'Uyarı Sayısı',
clamHelper: 'Enfekte dosyalar tarandığında uyarısını tetikle',
cronJobHelper: 'Görev yürütme başarısız olduğunda uyarısını tetikle',
licenseHelper: 'Pro sürüm ek uyarı kanallarını destekler',
licenseHelper: 'Business sürüm ek uyarı kanallarını destekler',
alertCountHelper: 'Günlük maksimum uyarı sıklığı',
alert: 'SMS Uyarısı',
logs: 'Uyarı Günlükleri',
@@ -4558,7 +4621,7 @@ const message = {
barkConfigHelper: 'Bark uyarı bildirim yapılandırması',
webhookName: 'Bot adı',
webhookUrl: 'Webhook URL',
alertConfigProHelper: 'Pro sürüm ayrıca WeCom, DingTalk, Feishu ve SMS bildirimlerini destekler.',
alertConfigProHelper: 'Business sürüm ayrıca WeCom, DingTalk, Feishu ve SMS bildirimlerini destekler.',
},
theme: {
lingXiaGold: 'LXware Gold',
+87 -33
View File
@@ -811,6 +811,35 @@ const message = {
from_remote: '該模型並非透過 1Panel 下載,無相關拉取日誌。',
no_logs: '該模型的拉取日誌已被刪除,無法檢視相關日誌。',
vllmVersionHelper: 'FusionXpark GB 10 伺服器請選擇 -cu130 版本',
downloader: '下載器',
modelDir: '模型目錄',
manualDownload: '手動下載',
modelSource: '模型來源',
repoID: '倉庫 ID',
modelScopeEndpoint: 'ModelScope 地址',
modelScopeToken: 'ModelScope Token(可選)',
hfToken: 'HF Token(可選)',
hfEndpoint: 'HuggingFace 加速地址',
hfEndpointHelper: '國內存取 HuggingFace 不穩定時可設定加速地址,搜尋、詳情和下載都會使用該地址。',
searchModelScope: '搜尋 ModelScope',
searchHuggingFace: '搜尋 HuggingFace',
sortTrending: '熱門趨勢',
sortLikes: '按讚最多',
sortDownloads: '下載最多',
sortUpdated: '最近更新',
sortCreated: '最新建立',
downloadsCount: '下載量',
likes: '點讚',
taskQueue: '下載佇列',
progress: '進度',
localDownloaded: '已下載',
modelInfo: '模型詳情',
modelCard: '模型卡片',
downloading: '下載中',
canceled: '已取消',
downloadQueued: '已加入下載佇列',
cancelTaskConfirm: '確認取消該下載任務?',
deleteLocalModelConfirm: '確認刪除本地模型 {0}?',
},
proxy: {
proxy: 'AI 代理增強',
@@ -1501,7 +1530,7 @@ const message = {
clam: {
clam: '病毒掃描',
cron: '定時掃描',
cronHelper: '專業版支援定時掃描功能',
cronHelper: '商業版支援定時掃描功能',
specErr: '執行週期格式錯誤,請檢查後重試',
disableMsg: '停止定時執行會導致該掃描任務不再自動執行。是否繼續?',
enableMsg: '啟用定時執行會讓該掃描任務定期自動執行。是否繼續?',
@@ -1532,7 +1561,7 @@ const message = {
clamLog: '掃描日誌',
freshClam: '病毒庫重新整理設定',
freshClamLog: '病毒庫重新整理日誌',
alertHelper: '專業版支援定時掃描和簡訊告警功能',
alertHelper: '商業版支援定時掃描和簡訊告警功能',
alertTitle: '病毒掃描「{0}」任務偵測到感染檔案告警',
},
},
@@ -1833,7 +1862,22 @@ const message = {
top: '返回上一層',
up: '上一層',
openWithVscode: 'VS Code 開啟',
vscodeHelper: '請確保本機已安裝 VS Code 並設定了 SSH Remote 外掛程式',
vscodeHelper: '請確認認證方式、主機、埠和使用者名稱。私鑰認證需先在執行 VS Code 的本地電腦完成 SSH 設定。',
vscodeCopyConfig: '複製腳本',
vscodeKeyHelper: '私鑰下載檔名將根據填寫的路徑產生。請將檔案儲存至該路徑,設定腳本會校驗檔案並設定存取權限。',
vscodeKeyPath: 'SSH 私鑰路徑',
vscodeKeyPathPlaceholder: '例如:~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH 私鑰',
vscodeScriptPreview: 'SSH 設定腳本',
vscodePasswordHelper: '密碼認證不會寫入本地 SSH 設定,連線遠端主機時需在 VS Code 中輸入密碼。',
vscodePasswordHint: '密碼認證將使用填寫的主機、埠和使用者名稱發起連線,無需設定腳本或私鑰檔案。',
vscodeScriptPreviewHint:
'在執行 VS Code 的本地電腦執行設定腳本後,將寫入用於遠端連線的 SSH 別名。主機、埠、使用者名稱、私鑰或私鑰路徑變更後,請重新執行腳本。',
vscodePasswordModeTitle: '密碼認證',
vscodePasswordModeHint: '密碼認證將使用填寫的主機、埠和使用者名稱發起連線,無需設定腳本或私鑰檔案。',
vscodePasswordStep1: '確認主機、埠和使用者名稱。',
vscodePasswordStep2: '開啟 VS Code,連線時輸入密碼。',
vscodePasswordStep3: '不需要下載私鑰。',
saveContentAndClose: '檔案已被修改,是否儲存並關閉?',
saveAndOpenNewFile: '檔案已被修改,是否儲存並開啟新檔案?',
noEdit: '檔案未修改,無需此操作',
@@ -1989,12 +2033,12 @@ const message = {
systemIPHelper: '應用跳轉、容器存取等功能將使用此地址進行路由。每個節點可設定不同地址。',
proxy: '代理伺服器',
proxyHelper: '設定代理伺服器後,將在以下場景中生效:',
proxyHelper1: '應用商店的安裝包下載和同步(專業版功能)',
proxyHelper2: '系統版本升級及取得更新說明(專業版功能)',
proxyHelper1: '應用商店的安裝包下載和同步(商業版功能)',
proxyHelper2: '系統版本升級及取得更新說明(商業版功能)',
proxyHelper3: '系統許可證的驗證和同步',
proxyHelper4: 'Docker 拉取映像等網路請求將透過代理伺服器存取(專業版功能)',
proxyHelper5: '系統類型腳本庫的統一下載與同步(專業版功能)',
proxyHelper6: '申請憑證(專業版功能)',
proxyHelper4: 'Docker 拉取映像等網路請求將透過代理伺服器存取(商業版功能)',
proxyHelper5: '系統類型腳本庫的統一下載與同步(商業版功能)',
proxyHelper6: '申請憑證(商業版功能)',
proxyType: '代理類型',
proxyUrl: '代理地址',
proxyPort: '代理埠',
@@ -2339,7 +2383,7 @@ const message = {
offLine: '離線版',
community: '社區版',
oss: '社區版',
pro: '專業版:',
pro: '商業版',
trial: '試用',
add: '新增社區版',
licenseBindHelper: '僅當授權已綁定節點時,才能使用其免費節點額度',
@@ -2354,31 +2398,33 @@ const message = {
trialInfo: '版本',
authorizationId: '訂閱授權 ID',
authorizedUser: '被授權方',
lostHelper: '許可證已達到最大重試次數,請手動點選同步按鈕,以確保專業版功能正常使用,詳情:',
exceptionalHelper: '許可證同步驗證異常,請手動點選同步按鈕,以確保專業版功能正常使用,詳情:',
lostHelper: '許可證已達到最大重試次數,請手動點選同步按鈕,以確保商業版功能正常使用,詳情:',
exceptionalHelper: '許可證同步驗證異常,請手動點選同步按鈕,以確保商業版功能正常使用,詳情:',
quickUpdate: '快速更新',
power: '授權',
unbindHelper: '解除綁定後將清除該節點所有專業版相關設定,是否繼續?',
unbindHelper: '解除綁定後將清除該節點所有商業版相關設定,是否繼續?',
importLicense: '匯入許可證',
importHelper: '請點選或拖動許可檔案到此處',
levelUpPro: '升級至 Pro 版',
levelUpPro: '升級商業版',
licenseSync: '許可證同步',
knowMorePro: '了解更多',
knowMorePro: '了解商業版',
closeAlert: '此頁可在面板設定中關閉顯示',
introduce: '功能介紹',
waf: '升級專業版可以獲得攔截地圖、日誌、封鎖記錄、地理位置封禁、自訂規則、自訂攔截頁面等功能。',
tamper: '升級專業版可以保護網站免受未經授權的修改或篡改。',
waf: '升級商業版可以獲得攔截地圖、日誌、封鎖記錄、地理位置封禁、自訂規則、自訂攔截頁面等功能。',
tamper: '升級商業版可以保護網站免受未經授權的修改或篡改。',
tamperHelper: '操作失敗,該檔案或資料夾已經開啟防篡改,請檢查後重試',
setting: '升級專業版可以自訂面板 Logo、歡迎簡介等資訊。',
monitor: '升級專業版可以檢視網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。',
alert: '升級專業版可透過簡訊接收告警資訊,並檢視告警日誌,全面掌控各類關鍵事件,確保系統執行無憂。',
node: '升級專業版可以使用 1Panel 管理多台 Linux 伺服器。',
nodeApp: '升級專業版可以統一升級多節點應用版本,無需手動切換節點。',
nodeDashboard: '升級專業版可以集中管理多節點應用、網站、資料庫和計劃任務。',
fileExchange: '升級專業版可以在多台伺服器之間快速傳輸檔案。',
app: '升級專業版可透過手機APP,檢視服務資訊、異常監控等。',
cluster: '升級專業版可以管理 MySQL/Postgres/Redis 主從叢集。',
vllm: '升級專業版可將 vLLM 服務統一納入 1Panel 管理,集中完成部署、設定調整、狀態維運與任務追蹤,降低本地模型服務維護成本。',
setting: '升級商業版可以自訂面板 Logo、歡迎簡介等資訊。',
monitor: '升級商業版可以檢視網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。',
alert: '升級商業版可透過簡訊接收告警資訊,並檢視告警日誌,全面掌控各類關鍵事件,確保系統執行無憂。',
node: '升級商業版可以使用 1Panel 管理多台 Linux 伺服器。',
nodeApp: '升級商業版可以統一升級多節點應用版本,無需手動切換節點。',
nodeDashboard: '升級商業版可以集中管理多節點應用、網站、資料庫和計劃任務。',
fileExchange: '升級商業版可以在多台伺服器之間快速傳輸檔案。',
app: '升級商業版可透過手機APP,檢視服務資訊、異常監控等。',
cluster: '升級商業版可以管理 MySQL/Postgres/Redis 主從叢集。',
vllm: '升級商業版可將 vLLM 服務統一納入 1Panel 管理,集中完成部署、設定調整、狀態維運與任務追蹤,降低本地模型服務維護成本。',
'model-downloader':
'升級商業版可從 HuggingFace、ModelScope 或相容鏡像站搜尋、下載並管理本機模型檔案,統一維護本機模型目錄與下載任務。',
},
clean: {
scan: '開始掃描',
@@ -3322,11 +3368,11 @@ const message = {
noFail: '掛載失敗不影響系統啟動',
},
xpack: {
expiresAlert: '溫馨提醒:您的專業版許可證將在 {0} 天後到期,屆時所有專業版功能將無法繼續使用。',
name: '專業版',
expiresAlert: '溫馨提醒:您的商業版許可證將在 {0} 天後到期,屆時所有商業版功能將無法繼續使用。',
name: '商業版',
menu: '進階功能',
upage: 'AI 建站',
proAlert: '升級專業版以使用此功能',
proAlert: '升級商業版以使用此功能',
waf: {
name: 'WAF',
blackWhite: '黑白名單',
@@ -3820,6 +3866,14 @@ const message = {
vllmContent3: '在同一頁面完成建立、編輯、啟停、重啟、刪除與任務追蹤,提升本地模型服務維護效率。',
vllmTitle4: '設定可控',
vllmContent4: '結合進階設定統一管理連接埠、容器、啟動命令與 Compose 設定,便於標準化交付與後續調整。',
modelDownloaderTitle1: '模型發現',
modelDownloaderContent1: '直接搜尋 HuggingFace 模型倉庫,檢視下載量、點讚數、模型詳情與檔案列表。',
modelDownloaderTitle2: '加速下載',
modelDownloaderContent2: '支援設定 HuggingFace 加速地址與 HF Token,適配官方站、鏡像站和私有模型存取。',
modelDownloaderTitle3: '任務追蹤',
modelDownloaderContent3: '統一檢視下載佇列、任務狀態、進度、錯誤資訊,並支援取消、重試和記錄清理。',
modelDownloaderTitle4: '本地管理',
modelDownloaderContent4: '集中維護本地模型目錄,檢視模型路徑、大小和時間,支援刪除不再需要的模型檔案。',
},
node: {
master: '主節點',
@@ -3858,7 +3912,7 @@ const message = {
syncInfo: '資料同步,',
syncHelper: '當主節點資料發生變化時,即時同步到該子節點,',
syncBackupAccount: '備份帳號設定',
syncWithMaster: '升級為專業版後,將預設同步所有資料,可在節點管理中手動調整同步策略。',
syncWithMaster: '升級為商業版後,將預設同步所有資料,可在節點管理中手動調整同步策略。',
syncProxy: '系統代理設定',
syncProxyHelper: '同步系統代理設定需要重新啟動 Docker',
syncProxyHelper1: '重新啟動 Docker 可能會影響目前正在執行的容器服務。',
@@ -3928,7 +3982,7 @@ const message = {
nodes: '節點',
selectNode: '選擇節點',
selectNodeError: '請選擇節點',
licenseHelper: '專業版支援自訂應用倉庫功能',
licenseHelper: '商業版支援自訂應用倉庫功能',
databaseHelper: '應用關聯資料庫,請選擇目標節點資料庫',
nodeHelper: '不能選擇當前節點',
migrateHelper: '僅支援遷移單體應用和只關聯 MySQL、MariaDB、PostgreSQL 資料庫的應用',
@@ -3941,7 +3995,7 @@ const message = {
alertCount: '告警次數',
clamHelper: '掃描到感染檔案時觸發告警',
cronJobHelper: '定時任務執行失敗時將觸發告警',
licenseHelper: '專業版支援更多告警功能',
licenseHelper: '商業版支援更多告警功能',
alertCountHelper: '每日最大告警次數',
alert: '簡訊告警',
logs: '告警日誌',
@@ -4140,7 +4194,7 @@ const message = {
barkConfigHelper: 'Bark 告警通知設定',
webhookName: '機器人名稱',
webhookUrl: 'Webhook 位址',
alertConfigProHelper: '專業版額外支援企業微信、釘釘、飛書及簡訊告警。',
alertConfigProHelper: '商業版額外支援企業微信、釘釘、飛書及簡訊告警。',
},
theme: {
lingXiaGold: 'LXware Gold',
+86 -32
View File
@@ -805,6 +805,35 @@ const message = {
from_remote: '该模型并非通过 1Panel 下载,无相关拉取日志。',
no_logs: '该模型的拉取日志已被删除,无法查看相关日志。',
vllmVersionHelper: 'FusionXpark GB 10 服务器请选择 -cu130 版本',
downloader: '下载器',
modelDir: '模型目录',
manualDownload: '手动下载',
modelSource: '模型来源',
repoID: '仓库 ID',
modelScopeEndpoint: 'ModelScope 地址',
modelScopeToken: 'ModelScope Token(可选)',
hfToken: 'HF Token(可选)',
hfEndpoint: 'HuggingFace 加速地址',
hfEndpointHelper: '国内访问 HuggingFace 不稳定时可配置加速地址,搜索、详情和下载都会使用该地址。',
searchModelScope: '搜索 ModelScope',
searchHuggingFace: '搜索 HuggingFace',
sortTrending: '热门趋势',
sortLikes: '点赞最多',
sortDownloads: '下载最多',
sortUpdated: '最近更新',
sortCreated: '最新创建',
downloadsCount: '下载量',
likes: '点赞',
taskQueue: '下载队列',
progress: '进度',
localDownloaded: '已下载',
modelInfo: '模型详情',
modelCard: '模型卡片',
downloading: '下载中',
canceled: '已取消',
downloadQueued: '已加入下载队列',
cancelTaskConfirm: '确认取消该下载任务?',
deleteLocalModelConfirm: '确认删除本地模型 {0}?',
},
proxy: {
proxy: 'AI 代理增强',
@@ -1502,7 +1531,7 @@ const message = {
clam: {
clam: '病毒扫描',
cron: '定时扫描',
cronHelper: '专业版支持定时扫描功能 ',
cronHelper: '商业版支持定时扫描功能 ',
specErr: '执行周期格式错误,请检查后重试!',
disableMsg: '停止定时执行会导致该扫描任务不再自动执行。是否继续?',
enableMsg: '启用定时执行会让该扫描任务定期自动执行。是否继续?',
@@ -1533,7 +1562,7 @@ const message = {
clamLog: '扫描日志',
freshClam: '病毒库刷新配置',
freshClamLog: '病毒库刷新日志',
alertHelper: '专业版支持定时扫描和短信告警功能',
alertHelper: '商业版支持定时扫描和短信告警功能',
alertTitle: '病毒扫描「 {0} 」任务检测到感染文件告警',
},
},
@@ -1830,7 +1859,22 @@ const message = {
top: '返回上一级',
up: '上一级',
openWithVscode: 'VS Code 打开',
vscodeHelper: '请确保本地已安装 VS Code 并配置了 SSH Remote 插件',
vscodeHelper: '请确认认证方式、主机、端口和用户名。私钥认证需先在运行 VS Code 的本地电脑完成 SSH 配置。',
vscodeCopyConfig: '复制脚本',
vscodeKeyHelper: '私钥下载文件名将根据填写的路径生成。请将文件保存至该路径,配置脚本会校验文件并设置访问权限。',
vscodeKeyPath: 'SSH 私钥路径',
vscodeKeyPathPlaceholder: '例如:~/.ssh/id_ed25519',
vscodeSelectKey: 'SSH 私钥',
vscodeScriptPreview: 'SSH 配置脚本',
vscodePasswordHelper: '密码认证不会写入本地 SSH 配置,连接远程主机时需在 VS Code 中输入密码。',
vscodePasswordHint: '密码认证将使用填写的主机、端口和用户名发起连接,无需配置脚本或私钥文件。',
vscodeScriptPreviewHint:
'在运行 VS Code 的本地电脑执行配置脚本后,将写入用于远程连接的 SSH 别名。主机、端口、用户名、私钥或私钥路径变更后,请重新执行脚本。',
vscodePasswordModeTitle: '密码认证',
vscodePasswordModeHint: '密码认证将使用填写的主机、端口和用户名发起连接,无需配置脚本或私钥文件。',
vscodePasswordStep1: '确认主机、端口和用户名。',
vscodePasswordStep2: '打开 VS Code,连接时输入密码。',
vscodePasswordStep3: '无需下载私钥。',
saveContentAndClose: '文件已被修改,是否保存并关闭?',
saveAndOpenNewFile: '文件已被修改,是否保存并打开新文件?',
noEdit: '文件未修改,无需此操作!',
@@ -1982,12 +2026,12 @@ const message = {
systemIPHelper: '应用跳转、容器访问等功能将使用此地址进行跳转,每个节点可设置不同地址。',
proxy: '代理服务器',
proxyHelper: '设置代理服务器后,将在以下场景中生效:',
proxyHelper1: '应用商店的安装包下载和同步(专业版功能)',
proxyHelper2: '系统版本升级及获取更新说明(专业版功能)',
proxyHelper1: '应用商店的安装包下载和同步(商业版功能)',
proxyHelper2: '系统版本升级及获取更新说明(商业版功能)',
proxyHelper3: '系统许可证的验证和同步',
proxyHelper4: 'Docker 拉取镜像等网络请求将通过代理服务器访问(专业版功能)',
proxyHelper5: '系统类型脚本库的统一下载与同步(专业版功能)',
proxyHelper6: '申请证书(专业版功能)',
proxyHelper4: 'Docker 拉取镜像等网络请求将通过代理服务器访问(商业版功能)',
proxyHelper5: '系统类型脚本库的统一下载与同步(商业版功能)',
proxyHelper6: '申请证书(商业版功能)',
proxyType: '代理类型',
proxyUrl: '代理地址',
proxyPort: '代理端口',
@@ -2333,7 +2377,7 @@ const message = {
offLine: '离线版',
community: '社区版',
oss: '社区版',
pro: '专业版',
pro: '商业版',
trial: '试用',
add: '添加社区版',
licenseBindHelper: '仅当许可证已绑定节点时,才能使用其免费节点额度',
@@ -2348,30 +2392,32 @@ const message = {
trialInfo: '版本',
authorizationId: '订阅授权 ID',
authorizedUser: '被授权方',
lostHelper: '许可证已达到最大重试次数,请手动点击同步按钮,以确保专业版功能正常使用,详情:',
exceptionalHelper: '许可证同步验证异常,请手动点击同步按钮,以确保专业版功能正常使用,详情:',
lostHelper: '许可证已达到最大重试次数,请手动点击同步按钮,以确保商业版功能正常使用,详情:',
exceptionalHelper: '许可证同步验证异常,请手动点击同步按钮,以确保商业版功能正常使用,详情:',
quickUpdate: '快速更新',
unbindHelper: '解除绑定后将清除该节点所有专业版相关设置,是否继续?',
unbindHelper: '解除绑定后将清除该节点所有商业版相关设置,是否继续?',
importLicense: '导入许可证',
importHelper: '请点击或拖动许可文件到此处',
levelUpPro: '升级专业版',
levelUpPro: '升级商业版',
licenseSync: '许可证同步',
knowMorePro: '了解更多',
knowMorePro: '了解商业版',
closeAlert: '当前页面可在面板设置中关闭显示',
introduce: '功能介绍',
waf: '升级专业版可以获得拦截地图、日志、封锁记录、地理位置封禁、自定义规则、自定义拦截页面等功能。',
tamper: '升级专业版可以保护网站免受未经授权的修改或篡改。',
waf: '升级商业版可以获得拦截地图、日志、封锁记录、地理位置封禁、自定义规则、自定义拦截页面等功能。',
tamper: '升级商业版可以保护网站免受未经授权的修改或篡改。',
tamperHelper: '操作失败,该文件或文件夹已经开启防篡改,请检查后重试!',
setting: '升级专业版可以自定义面板 Logo、欢迎简介等信息。',
monitor: '升级专业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。',
alert: '升级专业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。',
node: '升级专业版可以使用 1Panel 管理多台 linux 服务器。',
nodeApp: '升级专业版可以统一升级多节点应用版本,无需手动切换节点。',
nodeDashboard: '升级专业版可以集中管理多节点应用、网站、数据库和计划任务。',
fileExchange: '升级专业版可以在多台服务器之间快速传输文件。',
app: '升级专业版可通过手机 APP,查看服务信息、异常监控等。',
cluster: '升级专业版可以管理 MySQL/Postgres/Redis 主从集群。',
vllm: '升级专业版可将 vLLM 服务统一纳入 1Panel 管理,集中完成部署、配置调整、状态运维与任务追踪,降低本地模型服务维护成本。',
setting: '升级商业版可以自定义面板 Logo、欢迎简介等信息。',
monitor: '升级商业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。',
alert: '升级商业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。',
node: '升级商业版可以使用 1Panel 管理多台 linux 服务器。',
nodeApp: '升级商业版可以统一升级多节点应用版本,无需手动切换节点。',
nodeDashboard: '升级商业版可以集中管理多节点应用、网站、数据库和计划任务。',
fileExchange: '升级商业版可以在多台服务器之间快速传输文件。',
app: '升级商业版可通过手机 APP,查看服务信息、异常监控等。',
cluster: '升级商业版可以管理 MySQL/Postgres/Redis 主从集群。',
vllm: '升级商业版可将 vLLM 服务统一纳入 1Panel 管理,集中完成部署、配置调整、状态运维与任务追踪,降低本地模型服务维护成本。',
'model-downloader':
'升级商业版可从 HuggingFace、ModelScope 或兼容镜像站搜索、下载并管理本地模型文件,统一维护本地模型目录与下载任务。',
},
clean: {
scan: '开始扫描',
@@ -3321,10 +3367,10 @@ const message = {
noFail: '挂载失败不影响系统启动',
},
xpack: {
expiresAlert: '温馨提醒:专业版试用将于 [{0}] 天后到期,届时将停止使用所有专业版功能。',
expiresAlert: '温馨提醒:商业版试用将于 [{0}] 天后到期,届时将停止使用所有商业版功能。',
menu: '高级功能',
upage: 'AI 建站',
proAlert: '升级专业版使用此功能',
proAlert: '升级商业版使用此功能',
waf: {
name: 'WAF',
blackWhite: '黑白名单',
@@ -3815,6 +3861,14 @@ const message = {
vllmContent3: '在同一页面完成创建、编辑、启停、重启、删除与任务追踪,提升本地模型服务维护效率。',
vllmTitle4: '配置可控',
vllmContent4: '结合高级设置统一管理端口、容器、启动命令与 Compose 配置,便于标准化交付与后续调整。',
modelDownloaderTitle1: '模型发现',
modelDownloaderContent1: '直接搜索 HuggingFace 模型仓库,查看下载量、点赞数、模型详情与文件列表。',
modelDownloaderTitle2: '加速下载',
modelDownloaderContent2: '支持配置 HuggingFace 加速地址与 HF Token,适配官方站、镜像站和私有模型访问。',
modelDownloaderTitle3: '任务追踪',
modelDownloaderContent3: '统一查看下载队列、任务状态、进度、错误信息,并支持取消、重试和记录清理。',
modelDownloaderTitle4: '本地管理',
modelDownloaderContent4: '集中维护本地模型目录,查看模型路径、大小和时间,支持删除不再需要的模型文件。',
},
node: {
master: '主节点',
@@ -3853,7 +3907,7 @@ const message = {
syncInfo: '数据同步',
syncHelper: '主节点数据发生变化时,实时同步到该子节点',
syncBackupAccount: '备份账号设置',
syncWithMaster: '升级为专业版后,将默认同步所有数据,可在节点管理中手动调整同步策略。',
syncWithMaster: '升级为商业版后,将默认同步所有数据,可在节点管理中手动调整同步策略。',
syncProxy: '系统代理设置',
syncProxyHelper: '同步系统代理设置需要重启 Docker',
syncProxyHelper1: '重启 Docker 可能会影响当前正在运行的容器服务。',
@@ -3922,7 +3976,7 @@ const message = {
closeHelper: '是否取消使用自定义仓库',
appStoreUrlHelper: '仅支持 .tar.gz 格式',
selectNode: '选择节点',
licenseHelper: '专业版支持自定义应用仓库功能',
licenseHelper: '商业版支持自定义应用仓库功能',
databaseHelper: '应用关联数据库,请选择目标节点数据库',
nodeHelper: '不能选择当前节点',
migrateHelper: '当前仅支持迁移单体应用和只关联 MySQL、MariaDB、PostgreSQL 数据库的应用',
@@ -3935,7 +3989,7 @@ const message = {
alertCount: '告警次数',
clamHelper: '扫描到感染文件时触发告警通知',
cronJobHelper: '定时任务执行失败时将触发告警通知',
licenseHelper: '专业版支持更多告警功能',
licenseHelper: '商业版支持更多告警功能',
alertCountHelper: '每日最大告警次数',
alert: '短信告警',
logs: '告警日志',
@@ -4134,7 +4188,7 @@ const message = {
barkConfigHelper: 'Bark 告警通知配置',
webhookName: '机器人名称',
webhookUrl: 'Webhook 地址',
alertConfigProHelper: '专业版额外支持企业微信、钉钉、飞书及短信告警。',
alertConfigProHelper: '商业版额外支持企业微信、钉钉、飞书及短信告警。',
},
theme: {
lingXiaGold: '凌霞金',
@@ -1,8 +1,6 @@
<template>
<div>
<el-popover
ref="nodeChangeRef"
v-model:visible="popoverVisible"
placement="right-end"
:show-arrow="false"
:offset="0"
@@ -14,7 +12,7 @@
<template #reference>
<div class="el-dropdown-link" v-if="!menuStore.isCollapse">
<el-badge is-dot :value="taskCount" :show-zero="false" :offset="[5, 5]">
<el-button link @click="openChangeNode" @mouseenter="openChangeNode">
<el-button link>
<SvgIcon class="icon" iconName="p-pcm" />
<span class="ellipsis-text">{{ loadCurrentName() }}</span>
</el-button>
@@ -28,14 +26,18 @@
</template>
<div class="dropdown-menu" v-loading="loading">
<div class="dropdown-item" @click="openTask">
<SvgIcon class="icon" iconName="p-renwuzhongxin1" />
{{ $t('menu.msgCenter') }}
<div class="node">
<SvgIcon class="icon" iconName="p-renwuzhongxin1" />
{{ $t('menu.msgCenter') }}
</div>
<el-tag class="msg-tag" v-if="taskCount !== 0" size="small" round>{{ taskCount }}</el-tag>
</div>
<el-divider v-if="showNodes()" class="divider" />
<div class="dropdown-item" @click="openNodeDashboard" v-if="isMasterPro">
<SvgIcon class="icon" iconName="p-gailan1" />
{{ $t('xpack.node.multiOverview') }}
<div class="node">
<SvgIcon class="icon" iconName="p-gailan1" />
{{ $t('xpack.node.multiOverview') }}
</div>
</div>
<el-divider v-if="isMasterPro" class="divider" />
@@ -89,7 +91,6 @@
<script setup lang="ts">
import { GlobalStore, MenuStore } from '@/store';
import type { PopoverInstance } from 'element-plus';
import { countExecutingTask } from '@/api/modules/log';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
@@ -108,8 +109,6 @@ const menuStore = MenuStore();
const nodes = ref([]);
const nodeOptions = ref([]);
const loading = ref();
const popoverVisible = ref(false);
const nodeChangeRef = ref<PopoverInstance>();
const props = defineProps({
version: String,
});
@@ -128,10 +127,6 @@ bus.on('refreshTask', () => {
checkTask();
});
const openChangeNode = () => {
popoverVisible.value = true;
};
const loadCurrentName = () => {
if (globalStore.currentNode) {
if (globalStore.currentNode === 'local') {
+1 -1
View File
@@ -53,7 +53,7 @@
}
.fu-table-column-select__panel {
min-width: 220px;
width: 100%;
}
.fu-table-column-select__title {
+6
View File
@@ -39,6 +39,12 @@ export async function loadMonacoLanguageSupport() {
languageSupportPromise = Promise.all([
import('monaco-editor/esm/vs/base/browser/ui/codicons/codiconStyles.js'),
import('monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js'),
import('monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js'),
import('monaco-editor/esm/vs/editor/contrib/clipboard/browser/clipboard.js'),
import('monaco-editor/esm/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.js'),
import('monaco-editor/esm/vs/editor/contrib/find/browser/findController.js'),
import('monaco-editor/esm/vs/editor/contrib/multicursor/browser/multicursor.js'),
import('monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.js'),
import('monaco-editor/esm/vs/basic-languages/monaco.contribution'),
import('monaco-editor/esm/vs/language/json/monaco.contribution'),
import('monaco-editor/esm/vs/language/css/monaco.contribution'),
@@ -108,6 +108,6 @@ defineExpose({
<style lang="scss" scoped>
.terminal {
height: calc(100vh - 180px);
height: calc(100vh - 240px);
}
</style>
+11 -3
View File
@@ -28,10 +28,14 @@ import { useRoute, useRouter } from 'vue-router';
import OllamaView from '@/views/ai/model/ollama/index.vue';
import TensorRTView from '@/views/ai/model/tensorrt/index.vue';
import { loadOptionalComponent } from '@/extensions/optional';
import i18n from '@/lang';
const VllmView = defineAsyncComponent(() => loadOptionalComponent('/src/xpack/views/vllm/index.vue'));
const ModelDownloaderView = defineAsyncComponent(() =>
loadOptionalComponent('/src/xpack/views/model-downloader/index.vue'),
);
type LocalTab = 'ollama' | 'vllm' | 'tensorrt';
type LocalTab = 'ollama' | 'vllm' | 'tensorrt' | 'downloader';
const route = useRoute();
const router = useRouter();
@@ -40,17 +44,19 @@ const tabLabels: Record<LocalTab, string> = {
ollama: 'Ollama',
vllm: 'vLLM',
tensorrt: 'TensorRT LLM',
downloader: i18n.global.t('aiTools.model.downloader'),
};
const buttons = [
const buttons: Array<{ label: string; value: LocalTab }> = [
{ label: tabLabels.ollama, value: 'ollama' },
{ label: tabLabels.vllm, value: 'vllm' },
{ label: tabLabels.tensorrt, value: 'tensorrt' },
{ label: tabLabels.downloader, value: 'downloader' },
];
const currentTab = computed<LocalTab>(() => {
const tab = route.query.tab;
if (tab === 'vllm' || tab === 'tensorrt') {
if (tab === 'vllm' || tab === 'tensorrt' || tab === 'downloader') {
return tab;
}
return 'ollama';
@@ -62,6 +68,8 @@ const currentComponent = computed(() => {
return VllmView;
case 'tensorrt':
return TensorRTView;
case 'downloader':
return ModelDownloaderView;
default:
return OllamaView;
}
@@ -105,8 +105,11 @@ const initEditor = async () => {
readOnly: false,
automaticLayout: true,
folding: true,
glyphMargin: true,
roundedSelection: false,
overviewRulerBorder: false,
renderMarginRevertIcon: true,
renderGutterMenu: false,
});
editor.setModel({
+71 -64
View File
@@ -7,17 +7,7 @@
path: '/',
},
]"
>
<template #route-button>
<div class="router-button" v-if="!isOffLine">
<template v-if="!isProductPro">
<el-button link type="primary" @click="toUpload">
{{ $t('license.levelUpPro') }}
</el-button>
</template>
</div>
</template>
</RouterButton>
/>
<el-alert
v-if="!isSafety && globalStore.showEntranceWarn"
@@ -76,7 +66,6 @@
<CardWithHeader
:header="$t('menu.monitor')"
class="card-interval chart-card"
v-loading="!chartsOption['networkChart']"
@mouseenter="refreshOptionsOnHover"
>
<template #header-r>
@@ -119,7 +108,7 @@
</template>
<template #body>
<div style="position: relative; margin-top: 60px">
<div class="monitor-tags" v-if="chartOption === 'network'">
<div class="monitor-tags" :style="monitorTagsStyle" v-if="chartOption === 'network'">
<el-tag>
{{ $t('monitor.up') }}: {{ computeSizeFromKBs(currentChartInfo.netBytesSent) }}
</el-tag>
@@ -129,7 +118,7 @@
<el-tag>{{ $t('home.totalSend') }}: {{ computeSize(currentInfo.netBytesSent) }}</el-tag>
<el-tag>{{ $t('home.totalRecv') }}: {{ computeSize(currentInfo.netBytesRecv) }}</el-tag>
</div>
<div class="monitor-tags" v-if="chartOption === 'io'">
<div class="monitor-tags" :style="monitorTagsStyle" v-if="chartOption === 'io'">
<el-tag>{{ $t('monitor.read') }}: {{ currentChartInfo.ioReadBytes }} MB</el-tag>
<el-tag>{{ $t('monitor.write') }}: {{ currentChartInfo.ioWriteBytes }} MB</el-tag>
<el-tag>
@@ -145,7 +134,6 @@
id="ioChart"
type="line"
:option="chartsOption['ioChart']"
v-if="chartsOption['ioChart']"
:dataZoom="true"
/>
</div>
@@ -155,7 +143,6 @@
id="networkChart"
type="line"
:option="chartsOption['networkChart']"
v-if="chartsOption['networkChart']"
:dataZoom="true"
/>
</div>
@@ -171,6 +158,7 @@
height="368px"
indicator-position=""
arrow="never"
:autoplay="!showMemoCarousel || !memoEditing"
>
<el-carousel-item key="systemInfo">
<CardWithHeader :header="$t('home.systemInfo')">
@@ -353,9 +341,11 @@
/>
<div v-else class="memo-content">
<MarkDownEditor v-if="memoContent" :content="memoContent" />
<span v-else class="memo-placeholder">
{{ $t('home.memoPlaceholder') }}
</span>
<div v-else class="memo-empty">
<span class="memo-placeholder">
{{ $t('home.memoPlaceholder') }}
</span>
</div>
</div>
</div>
</el-scrollbar>
@@ -414,7 +404,6 @@
</el-col>
</el-row>
<LicenseImport ref="licenseRef" />
<QuickJump @search="onLoadBaseInfo(false, 'all')" ref="quickJumpRef" />
<DialogPro v-model="welcomeOpen" size="w-70" id="welcomeDialog">
@@ -428,7 +417,6 @@ import { onMounted, onBeforeUnmount, ref, reactive, computed, nextTick } from 'v
import SystemStatus from '@/views/home/status/index.vue';
import AppLauncher from '@/views/home/app/index.vue';
import VCharts from '@/components/v-charts/index.vue';
import LicenseImport from '@/components/license-import/index.vue';
import QuickJump from '@/views/home/quick/index.vue';
import CardWithHeader from '@/components/card-with-header/index.vue';
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
@@ -451,7 +439,6 @@ import {
updateSetting,
} from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
import { routerToFileWithPath, routerToNameWithQuery, routerToPath } from '@/utils/router';
import { getWelcomePage } from '@/api/modules/auth';
import {
@@ -469,6 +456,46 @@ const DASHBOARD_CACHE_TTL = {
netOptions: 60 * 60 * 1000,
ioOptions: 60 * 60 * 1000,
};
const monitorChartGrid = { left: 65, right: 65, bottom: '20%' };
const monitorTagsStyle = {
left: `${monitorChartGrid.left}px`,
right: `${monitorChartGrid.right}px`,
};
const monitorChartEmptyLength = 20;
const loadMonitorEmptyData = () => Array.from({ length: monitorChartEmptyLength }, () => null);
const loadMonitorEmptyTime = () => Array.from({ length: monitorChartEmptyLength }, () => '');
const loadMonitorChartData = (data: Array<number>) => (data.length === 0 ? loadMonitorEmptyData() : data);
const loadMonitorChartTime = (data: Array<string>) => (data.length === 0 ? loadMonitorEmptyTime() : data);
const loadIOChartOption = () => ({
xData: loadMonitorChartTime(timeIODatas.value),
yData: [
{
name: i18n.global.t('monitor.read'),
data: loadMonitorChartData(ioReadBytes.value),
},
{
name: i18n.global.t('monitor.write'),
data: loadMonitorChartData(ioWriteBytes.value),
},
],
grid: monitorChartGrid,
formatStr: 'MB',
});
const loadNetworkChartOption = () => ({
xData: loadMonitorChartTime(timeNetDatas.value),
yData: [
{
name: i18n.global.t('monitor.up'),
data: loadMonitorChartData(netBytesSents.value),
},
{
name: i18n.global.t('monitor.down'),
data: loadMonitorChartData(netBytesRecvs.value),
},
],
grid: monitorChartGrid,
formatStr: 'KB/s',
});
const statusRef = ref();
const appRef = ref();
@@ -501,9 +528,7 @@ const netOptionsFromCache = ref(false);
const ioOptionsFromCache = ref(false);
const hasRefreshedOptionsOnHover = ref(false);
const licenseRef = ref();
const quickJumpRef = ref();
const { isProductPro, isOffLine } = storeToRefs(globalStore);
const searchInfo = reactive({
ioOption: 'all',
@@ -604,7 +629,10 @@ const currentChartInfo = reactive({
});
const skipNextCurrentInfoDelta = ref(false);
const chartsOption = ref({ ioChart1: null, networkChart: null });
const chartsOption = ref({
ioChart: loadIOChartOption(),
networkChart: loadNetworkChartOption(),
});
const updateCurrentInfo = (data: Dashboard.CurrentInfo) => {
currentInfo.value = {
@@ -678,14 +706,21 @@ const onLoadIOOptions = async (force?: boolean) => {
};
const onLoadBaseInfo = async (isInit: boolean, range: string) => {
let resetChartData = false;
if (range === 'all' || range === 'io') {
ioReadBytes.value = [];
ioWriteBytes.value = [];
timeIODatas.value = [];
} else if (range === 'all' || range === 'network') {
resetChartData = true;
}
if (range === 'all' || range === 'network') {
netBytesSents.value = [];
netBytesRecvs.value = [];
timeNetDatas.value = [];
resetChartData = true;
}
if (resetChartData) {
loadData();
}
const res = await loadBaseInfo(searchInfo.ioOption, searchInfo.netOption);
baseInfo.value = res.data;
@@ -910,35 +945,9 @@ const saveMemo = async () => {
const loadData = async () => {
if (chartOption.value === 'io') {
chartsOption.value['ioChart'] = {
xData: timeIODatas.value,
yData: [
{
name: i18n.global.t('monitor.read'),
data: ioReadBytes.value,
},
{
name: i18n.global.t('monitor.write'),
data: ioWriteBytes.value,
},
],
formatStr: 'MB',
};
chartsOption.value['ioChart'] = loadIOChartOption();
} else {
chartsOption.value['networkChart'] = {
xData: timeNetDatas.value,
yData: [
{
name: i18n.global.t('monitor.up'),
data: netBytesSents.value,
},
{
name: i18n.global.t('monitor.down'),
data: netBytesRecvs.value,
},
],
formatStr: 'KB/s',
};
chartsOption.value['networkChart'] = loadNetworkChartOption();
}
};
@@ -1008,10 +1017,6 @@ const onBlur = () => {
isActive.value = false;
};
const toUpload = () => {
licenseRef.value.acceptParams();
};
const refreshOptionsOnHover = async () => {
if (hasRefreshedOptionsOnHover.value) return;
if (!netOptionsFromCache.value && !ioOptionsFromCache.value) return;
@@ -1198,12 +1203,9 @@ onBeforeUnmount(() => {
.monitor-tags {
position: absolute;
top: -10px;
left: 20px;
:deep(.el-tag) {
margin-right: 10px;
margin-bottom: 10px;
}
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.version {
@@ -1255,6 +1257,11 @@ onBeforeUnmount(() => {
}
}
.memo-empty {
min-height: 275px;
padding-top: 15px;
}
.memo-placeholder {
color: var(--el-text-color-placeholder);
display: inline-block;
@@ -62,7 +62,11 @@
<template #header>
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<el-tabs v-model="scope" type="card" class="flex-1" @tab-change="handleScopeChange">
<el-tab-pane :label="$t('file.historyCurrentScope')" name="current" />
<el-tab-pane
:label="$t('file.historyCurrentScope')"
name="current"
:disabled="!canSwitchToCurrentScope"
/>
<el-tab-pane :label="$t('file.historyAllScope')" name="all" />
</el-tabs>
<div class="flex flex-wrap items-center gap-2">
@@ -336,12 +340,13 @@ const isVersionRecord = (operation: string) => operation === 'save' || operation
const canRestoreSelected = computed(() => isVersionRecord(selectedHistory.value?.operation || ''));
const isRestoringCurrentFile = computed(() => selectedHistory.value?.path === currentFile.value.path);
const getCurrentTargetPath = (row: File.FileHistoryInfo) => row.currentPath || row.path;
const canSwitchToCurrentScope = computed(() => !!currentFile.value.path || historyItems.value.length > 0);
const syncCurrentFileContext = (history?: File.FileHistoryInfo | null) => {
if (currentFile.value.path) {
return;
}
const target = history || selectedHistory.value || historyItems.value[0];
const target = history || historyItems.value[0];
if (!target) {
return;
}
@@ -439,8 +444,11 @@ const renderDiff = async () => {
readOnly: true,
automaticLayout: true,
folding: true,
glyphMargin: true,
roundedSelection: false,
overviewRulerBorder: false,
renderMarginRevertIcon: true,
renderGutterMenu: false,
});
diffEditor.setModel({
original: originalModel,
@@ -573,8 +581,12 @@ const deleteSelected = async (row: File.FileHistoryInfo | null) => {
};
const handleScopeChange = async () => {
if (scope.value === 'current' && !canSwitchToCurrentScope.value) {
scope.value = 'all';
return;
}
if (scope.value === 'current') {
syncCurrentFileContext();
syncCurrentFileContext(historyItems.value[0]);
}
await loadHistoryList(true);
};
@@ -629,6 +641,9 @@ const acceptParams = async (params: HistoryDrawerProps): Promise<void> => {
currentFile.value = params;
operationFilter.value = '';
scope.value = params.scope || 'current';
if (scope.value === 'current' && !params.path) {
scope.value = 'all';
}
drawerVisible.value = true;
if (scope.value === 'current') {
syncCurrentFileContext();
@@ -205,15 +205,13 @@ const loadTaskInfo = async () => {
emitTaskChange();
if (!taskInfo.value || taskInfo.value.status !== 'Executing') {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
} catch (error) {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
};
@@ -1,49 +1,109 @@
<template>
<DrawerPro v-model="open" :header="$t('file.deCompress')" :resource="name" @close="handleClose" size="normal">
<el-form
ref="fileForm"
label-position="top"
:model="form"
label-width="100px"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.name')">
<el-input v-model="name" disabled></el-input>
</el-form-item>
<el-form-item :label="$t('file.deCompressDst')" prop="dst">
<el-input v-model="form.dst">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ path: form.dst, dir: true })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('setting.compressPassword')" prop="secret" v-if="name.includes('tar.gz')">
<el-input v-model="form.secret"></el-input>
</el-form-item>
</el-form>
<DrawerPro v-model="open" :header="title" @close="handleClose" size="large">
<div class="space-y-4">
<div
v-if="showTaskStatus"
class="rounded-lg border border-[var(--el-border-color-light)] bg-[var(--el-fill-color-light)] px-4 py-3"
>
<div class="flex items-center justify-between gap-3">
<div class="space-y-1">
<div class="text-sm font-medium">
{{ $t('commons.status.executing') }}
</div>
<div class="text-xs text-[var(--el-text-color-secondary)]">
{{ taskInfo?.name || $t('file.deCompress') }}
</div>
</div>
<el-button link type="primary" @click="openTaskLog" :disabled="!currentTaskID">
{{ $t('commons.button.log') }}
</el-button>
</div>
<el-progress
class="mt-3"
:percentage="100"
:indeterminate="true"
:duration="1"
:stroke-width="8"
:show-text="false"
/>
</div>
<el-form
v-if="!currentTaskID"
ref="fileForm"
label-position="top"
:model="form"
label-width="100px"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.name')">
<el-input v-model="name" disabled></el-input>
</el-form-item>
<el-form-item :label="$t('file.deCompressDst')" prop="dst">
<el-input v-model="form.dst">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ path: form.dst, dir: true })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('setting.compressPassword')" prop="secret" v-if="name.includes('tar.gz')">
<el-input v-model="form.secret"></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(fileForm)">{{ $t('commons.button.confirm') }}</el-button>
<template v-if="!currentTaskID">
<el-button :type="loading ? 'danger' : 'default'" :loading="canceling" @click="handleClose">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button v-if="!loading" type="primary" @click="submit(fileForm)">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
<template v-else>
<el-button
v-if="isTaskExecuting || loading"
type="danger"
:loading="stopping"
@click="stopCurrentTask"
>
{{ $t('commons.button.cancel') }}
</el-button>
<el-button v-if="currentTaskID" @click="openTaskLog">
{{ $t('commons.button.log') }}
</el-button>
<el-button type="default" @click="closeDrawer">
{{ $t('commons.button.close') }}
</el-button>
</template>
</span>
</template>
</DrawerPro>
<FileList ref="fileRef" @choose="getLinkPath" />
<TaskLog ref="taskLogRef" />
</template>
<script setup lang="ts">
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { File } from '@/api/interface/file';
import { Log } from '@/api/interface/log';
import { FormInstance, FormRules } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { deCompressFile } from '@/api/modules/files';
import { deCompressFile, stopDeCompressFile } from '@/api/modules/files';
import { searchTasks } from '@/api/modules/log';
import FileList from '@/components/file-list/index.vue';
import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import { MsgError } from '@/utils/message';
import { getErrorMessage } from '@/utils/misc';
import { newUUID } from '@/utils/id';
import { GlobalStore } from '@/store';
interface CompressProps {
files: Array<any>;
const globalStore = GlobalStore();
interface DecompressProps {
dst: string;
name: string;
path: string;
@@ -55,20 +115,164 @@ const rules = reactive<FormRules>({
});
const fileForm = ref<FormInstance>();
let loading = ref(false);
let form = ref<File.FileDeCompress>({ type: 'zip', dst: '', path: '', secret: '' });
let open = ref(false);
let name = ref('');
const loading = ref(false);
const canceling = ref(false);
const stopping = ref(false);
const abortController = ref<AbortController | null>(null);
const form = ref<File.FileDeCompress>({ type: 'zip', dst: '', path: '', secret: '' });
const open = ref(false);
const title = ref('');
const fileRef = ref();
const taskLogRef = ref<InstanceType<typeof TaskLog> | null>(null);
const currentTaskID = ref('');
const taskInfo = ref<Log.Task | null>(null);
let taskTimer: ReturnType<typeof setInterval> | null = null;
const decompressTaskKey = 'file-management-decompress-task';
const name = ref('');
const em = defineEmits(['close']);
const em = defineEmits<{
(e: 'close', value: boolean): void;
(
e: 'task-change',
value: {
taskID: string;
status: string;
},
): void;
}>();
const handleClose = () => {
const isTaskExecuting = computed(() => taskInfo.value?.status === 'Executing');
const showTaskStatus = computed(() =>
Boolean(currentTaskID.value || loading.value || canceling.value || stopping.value),
);
const emitTaskChange = () => {
const payload = {
taskID: currentTaskID.value,
status: taskInfo.value?.status || (currentTaskID.value || loading.value ? 'Executing' : ''),
};
if (payload.taskID && payload.status) {
localStorage.setItem(decompressTaskKey, JSON.stringify(payload));
} else {
localStorage.removeItem(decompressTaskKey);
}
em('task-change', payload);
};
const stopTaskPolling = () => {
if (taskTimer) {
clearInterval(taskTimer);
taskTimer = null;
}
};
const loadTaskInfo = async () => {
if (!currentTaskID.value) {
taskInfo.value = null;
emitTaskChange();
return;
}
try {
const res = await searchTasks(
{
taskID: currentTaskID.value,
type: '',
status: '',
page: 1,
pageSize: 1,
},
globalStore.currentNode,
);
taskInfo.value = res.data.items?.[0] || null;
emitTaskChange();
if (!taskInfo.value || taskInfo.value.status !== 'Executing') {
stopTaskPolling();
resetDrawerState();
em('close', false);
}
} catch {
stopTaskPolling();
resetDrawerState();
em('close', false);
}
};
const startTaskPolling = () => {
stopTaskPolling();
void loadTaskInfo();
taskTimer = setInterval(() => {
void loadTaskInfo();
}, 1500);
};
const resetDrawerState = () => {
if (fileForm.value) {
fileForm.value.resetFields();
}
abortController.value = null;
canceling.value = false;
loading.value = false;
taskInfo.value = null;
currentTaskID.value = '';
stopTaskPolling();
emitTaskChange();
open.value = false;
em('close', open.value);
};
const closeDrawer = () => {
if (currentTaskID.value && isTaskExecuting.value) {
stopTaskPolling();
open.value = false;
em('close', false);
return;
}
resetDrawerState();
em('close', false);
};
const resumeTask = () => {
if (!currentTaskID.value) {
return;
}
open.value = true;
};
const stopCurrentTask = async () => {
if (stopping.value || (!loading.value && !currentTaskID.value)) {
return;
}
stopping.value = true;
try {
if (loading.value) {
if (abortController.value) {
canceling.value = true;
abortController.value.abort();
}
} else if (currentTaskID.value) {
await stopDeCompressFile(currentTaskID.value);
}
open.value = false;
em('close', false);
} catch (err) {
MsgError(getErrorMessage(err));
} finally {
stopping.value = false;
}
};
const handleClose = () => {
if (loading.value) {
stopCurrentTask();
return;
}
closeDrawer();
};
const openTaskLog = () => {
if (!currentTaskID.value) {
return;
}
taskLogRef.value?.openWithTaskID(currentTaskID.value, true, globalStore.currentNode);
};
const getLinkPath = (path: string) => {
@@ -81,25 +285,111 @@ const submit = async (formEl: FormInstance | undefined) => {
if (!valid) {
return;
}
const taskID = newUUID();
form.value.taskID = taskID;
canceling.value = false;
abortController.value = new AbortController();
loading.value = true;
deCompressFile(form.value)
deCompressFile(form.value, {
signal: abortController.value?.signal,
})
.then(() => {
MsgSuccess(i18n.global.t('file.deCompressSuccess'));
handleClose();
currentTaskID.value = taskID;
loading.value = false;
taskInfo.value = null;
emitTaskChange();
startTaskPolling();
})
.catch((error) => {
const message = getErrorMessage(error).toLowerCase();
if (canceling.value || message.includes('canceled') || message.includes('errshutdown')) {
currentTaskID.value = '';
emitTaskChange();
return;
}
currentTaskID.value = '';
emitTaskChange();
MsgError(getErrorMessage(error));
})
.finally(() => {
loading.value = false;
const wasCanceling = canceling.value;
abortController.value = null;
canceling.value = false;
if (wasCanceling) {
closeDrawer();
}
});
});
};
const acceptParams = (props: CompressProps) => {
const acceptParams = (props: DecompressProps) => {
if (currentTaskID.value) {
open.value = true;
if (!taskTimer) {
startTaskPolling();
}
emitTaskChange();
return;
}
form.value.type = props.type;
form.value.dst = props.dst;
form.value.path = props.path;
form.value.secret = '';
name.value = props.name;
abortController.value = null;
canceling.value = false;
stopping.value = false;
taskInfo.value = null;
currentTaskID.value = '';
emitTaskChange();
stopTaskPolling();
open.value = true;
title.value = i18n.global.t('file.deCompress');
};
defineExpose({ acceptParams });
const restoreRunningTask = () => {
const taskText = localStorage.getItem(decompressTaskKey);
if (!taskText) {
return;
}
try {
const task = JSON.parse(taskText) as {
taskID?: string;
status?: string;
};
if (!task.taskID) {
localStorage.removeItem(decompressTaskKey);
return;
}
currentTaskID.value = task.taskID;
if (task.status === 'Executing') {
emitTaskChange();
} else {
localStorage.removeItem(decompressTaskKey);
}
} catch {
localStorage.removeItem(decompressTaskKey);
}
};
onMounted(() => {
restoreRunningTask();
});
watch(open, (val) => {
if (val && currentTaskID.value) {
startTaskPolling();
} else {
stopTaskPolling();
}
});
onUnmounted(() => {
stopTaskPolling();
});
defineExpose({ acceptParams, resumeTask });
</script>
File diff suppressed because it is too large Load Diff
@@ -243,8 +243,11 @@ const onSaveSSL = async (formEl: FormInstance | undefined) => {
cert: form.cert,
key: form.key,
};
let href = window.location.href;
param.domain = href.split('//')[1].split(':')[0];
// window.location.hostname yields the bare host name and strips
// surrounding brackets from IPv6 addresses (e.g. '[::1]' -> '::1'),
// unlike `href.split('//')[1].split(':')[0]` which incorrectly
// returns '[' for IPv6 URLs. See 1Panel-dev/1Panel#12646.
param.domain = window.location.hostname;
await updateSSL(param).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
let href = window.location.href;