Repository: 1Panel-dev/1Panel
Pull request #8580: fix: Modify the execution mode of the planned tasks
Author: login: ssongliu
Labels: name: do-not-merge/release-note-label-needed
name: approved
name: dev-v2
Created: 2025-05-09T06:25:42Z | Merged: 2025-05-09T07:55:48Z



Diff:
diff --git a/agent/app/api/v2/snapshot.go b/agent/app/api/v2/snapshot.go
index ae9a78f5807e..83b90b41e541 100644
--- a/agent/app/api/v2/snapshot.go
+++ b/agent/app/api/v2/snapshot.go
@@ -36,7 +36,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
 		return
 	}
 
-	if err := snapshotService.SnapshotCreate(req, 0); err != nil {
+	if err := snapshotService.SnapshotCreate(nil, req, 0, 0, 0); err != nil {
 		helper.InternalServer(c, err)
 		return
 	}
diff --git a/agent/app/repo/cronjob.go b/agent/app/repo/cronjob.go
index 06aaa8bcfdfc..58405b282274 100644
--- a/agent/app/repo/cronjob.go
+++ b/agent/app/repo/cronjob.go
@@ -147,9 +147,7 @@ func (u *CronjobRepo) StartRecords(cronjobID uint, targetPath, cronjobType strin
 	var record model.JobRecords
 	record.StartTime = time.Now()
 	record.CronjobID = cronjobID
-	if cronjobType != "directory" && cronjobType != "log" && cronjobType != "cutWebsiteLog" {
-		record.TaskID = uuid.New().String()
-	}
+	record.TaskID = uuid.New().String()
 	record.Status = constant.StatusWaiting
 	if err := global.DB.Create(&record).Error; err != nil {
 		global.LOG.Errorf("create record status failed, err: %v", err)
diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go
index 8d5e3d15f92f..026e806e7db7 100644
--- a/agent/app/service/backup_app.go
+++ b/agent/app/service/backup_app.go
@@ -143,45 +143,10 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir
 		}
 	}
 
-	backupApp := func(t *task.Task) error {
-		fileOp := files.NewFileOp()
-		tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
-		if !fileOp.Stat(tmpDir) {
-			if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
-				return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
-			}
-		}
-		defer func() {
-			_ = os.RemoveAll(tmpDir)
-		}()
-
-		remarkInfo, _ := json.Marshal(install)
-		remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir)
-		if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil {
-			return err
-		}
-
-		appPath := install.GetPath()
-		if err := fileOp.TarGzCompressPro(true, appPath, path.Join(tmpDir, "app.tar.gz"), "", excludes); err != nil {
-			return err
-		}
-
-		resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
-		for _, resource := range resources {
-			if err = backupDatabaseWithTask(t, resource.Key, tmpDir, install.Name, resource.ResourceId); err != nil {
-				return err
-			}
-		}
-		t.LogStart(i18n.GetMsgByKey("CompressDir"))
-		if err := fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
-			return err
-		}
-		t.Log(i18n.GetWithName("CompressFileSuccess", fileName))
-		return nil
-	}
-	backupTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backupApp, nil)
+	itemHandler := doAppBackup(install, backupTask, backupDir, fileName, excludes, secret)
+	backupTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), func(t *task.Task) error { return itemHandler }, nil)
 	if parentTask != nil {
-		return backupApp(parentTask)
+		return itemHandler
 	}
 	return backupTask.Execute()
 }
@@ -367,6 +332,43 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
 	return recoverTask.Execute()
 }
 
+func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fileName, excludes, secret string) error {
+	fileOp := files.NewFileOp()
+	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
+	if !fileOp.Stat(tmpDir) {
+		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
+			return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
+		}
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir)
+	}()
+
+	remarkInfo, _ := json.Marshal(install)
+	remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir)
+	if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil {
+		return err
+	}
+
+	appPath := install.GetPath()
+	if err := fileOp.TarGzCompressPro(true, appPath, path.Join(tmpDir, "app.tar.gz"), "", excludes); err != nil {
+		return err
+	}
+
+	resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
+	for _, resource := range resources {
+		if err := backupDatabaseWithTask(parentTask, resource.Key, tmpDir, install.Name, resource.ResourceId); err != nil {
+			return err
+		}
+	}
+	parentTask.LogStart(i18n.GetMsgByKey("CompressDir"))
+	if err := fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
+		return err
+	}
+	parentTask.Log(i18n.GetWithName("CompressFileSuccess", fileName))
+	return nil
+}
+
 func reCreateDB(dbID uint, database model.Database, oldEnv string) (*model.DatabaseMysql, map[string]interface{}, error) {
 	mysqlService := NewIMysqlService()
 	ctx := context.Background()
diff --git a/agent/app/service/backup_mysql.go b/agent/app/service/backup_mysql.go
index b175262538d2..6a504e2a7974 100644
--- a/agent/app/service/backup_mysql.go
+++ b/agent/app/service/backup_mysql.go
@@ -106,46 +106,28 @@ func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
 
 func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, targetDir, fileName, taskID string) error {
 	var (
-		err      error
-		itemTask *task.Task
+		err        error
+		backupTask *task.Task
 	)
-	itemTask = parentTask
+	backupTask = parentTask
 	dbInfo, err := mysqlRepo.Get(repo.WithByName(db.Name), mysqlRepo.WithByMysqlName(db.Database))
 	if err != nil {
 		return err
 	}
 	itemName := fmt.Sprintf("%s[%s] - %s", db.Database, db.DBType, db.Name)
 	if parentTask == nil {
-		itemTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeDatabase, taskID, dbInfo.ID)
-		if err != nil {
-			return err
-		}
-	}
-
-	backupDatabase := func(t *task.Task) error {
-		cli, version, err := LoadMysqlClientByFrom(db.Database)
+		backupTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeDatabase, taskID, dbInfo.ID)
 		if err != nil {
 			return err
 		}
-		backupInfo := client.BackupInfo{
-			Name:      db.Name,
-			Type:      db.DBType,
-			Version:   version,
-			Format:    dbInfo.Format,
-			TargetDir: targetDir,
-			FileName:  fileName,
-
-			Timeout: 300,
-		}
-		return cli.Backup(backupInfo)
 	}
 
-	itemTask.AddSubTask(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil)
+	itemHandler := doMysqlBackup(db, targetDir, fileName)
+	backupTask.AddSubTask(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeApp), func(t *task.Task) error { return itemHandler }, nil)
 	if parentTask != nil {
-		return backupDatabase(parentTask)
+		return itemHandler
 	}
-
-	return itemTask.Execute()
+	return backupTask.Execute()
 }
 
 func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool, taskID string) error {
@@ -239,3 +221,25 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
 
 	return itemTask.Execute()
 }
+
+func doMysqlBackup(db DatabaseHelper, targetDir, fileName string) error {
+	dbInfo, err := mysqlRepo.Get(repo.WithByName(db.Name), mysqlRepo.WithByMysqlName(db.Database))
+	if err != nil {
+		return err
+	}
+	cli, version, err := LoadMysqlClientByFrom(db.Database)
+	if err != nil {
+		return err
+	}
+	backupInfo := client.BackupInfo{
+		Name:      db.Name,
+		Type:      db.DBType,
+		Version:   version,
+		Format:    dbInfo.Format,
+		TargetDir: targetDir,
+		FileName:  fileName,
+
+		Timeout: 300,
+	}
+	return cli.Backup(backupInfo)
+}
diff --git a/agent/app/service/backup_postgresql.go b/agent/app/service/backup_postgresql.go
index a18734b1bec0..d74c70a9a913 100644
--- a/agent/app/service/backup_postgresql.go
+++ b/agent/app/service/backup_postgresql.go
@@ -106,38 +106,24 @@ func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error {
 
 func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, targetDir, fileName, taskID string) error {
 	var (
-		err      error
-		itemTask *task.Task
+		err        error
+		backupTask *task.Task
 	)
-	itemTask = parentTask
+	backupTask = parentTask
 	itemName := fmt.Sprintf("%s - %s", db.Database, db.Name)
 	if parentTask == nil {
-		itemTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeDatabase, taskID, db.ID)
+		backupTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeDatabase, taskID, db.ID)
 		if err != nil {
 			return err
 		}
 	}
-	backupDatabase := func(t *task.Task) error {
-		cli, err := LoadPostgresqlClientByFrom(db.Database)
-		if err != nil {
-			return err
-		}
-		defer cli.Close()
-		backupInfo := pgclient.BackupInfo{
-			Name:      db.Name,
-			TargetDir: targetDir,
-			FileName:  fileName,
 
-			Timeout: 300,
-		}
-		return cli.Backup(backupInfo)
-	}
-	itemTask.AddSubTask(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil)
+	itemHandler := doPostgresqlgBackup(db, targetDir, fileName)
+	backupTask.AddSubTask(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeApp), func(task *task.Task) error { return itemHandler }, nil)
 	if parentTask != nil {
-		return backupDatabase(parentTask)
+		return itemHandler
 	}
-
-	return itemTask.Execute()
+	return backupTask.Execute()
 }
 
 func handlePostgresqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool) error {
@@ -219,3 +205,19 @@ func handlePostgresqlRecover(req dto.CommonRecover, parentTask *task.Task, isRol
 
 	return itemTask.Execute()
 }
+
+func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName string) error {
+	cli, err := LoadPostgresqlClientByFrom(db.Database)
+	if err != nil {
+		return err
+	}
+	defer cli.Close()
+	backupInfo := pgclient.BackupInfo{
+		Name:      db.Name,
+		TargetDir: targetDir,
+		FileName:  fileName,
+
+		Timeout: 300,
+	}
+	return cli.Backup(backupInfo)
+}
diff --git a/agent/app/service/backup_website.go b/agent/app/service/backup_website.go
index fc1945deeda7..e9e936fbd882 100644
--- a/agent/app/service/backup_website.go
+++ b/agent/app/service/backup_website.go
@@ -215,79 +215,80 @@ func handleWebsiteBackup(website *model.Website, parentTask *task.Task, backupDi
 			return err
 		}
 	}
+	itemHandler := doWebsiteBackup(website, backupTask, backupDir, fileName, excludes, secret)
+	backupTask.AddSubTask(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeApp), func(task *task.Task) error { return itemHandler }, nil)
+	if parentTask != nil {
+		return itemHandler
+	}
+	return backupTask.Execute()
+}
 
-	backupWebsite := func(t *task.Task) error {
-		fileOp := files.NewFileOp()
-		tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
-		if !fileOp.Stat(tmpDir) {
-			if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
-				return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
-			}
+func doWebsiteBackup(website *model.Website, parentTask *task.Task, backupDir, fileName, excludes, secret string) error {
+	fileOp := files.NewFileOp()
+	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
+	if !fileOp.Stat(tmpDir) {
+		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
+			return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
 		}
-		defer func() {
-			_ = os.RemoveAll(tmpDir)
-		}()
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir)
+	}()
+
+	remarkInfo, _ := json.Marshal(website)
+	if err := fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil {
+		return err
+	}
+	nginxConfFile := GetSitePath(*website, SiteConf)
+	if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil {
+		return err
+	}
+	parentTask.Log(i18n.GetMsgByKey("BackupNginxConfig"))
 
-		remarkInfo, _ := json.Marshal(website)
-		if err = fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil {
+	switch website.Type {
+	case constant.Deployment:
+		app, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
+		if err != nil {
 			return err
 		}
-		nginxConfFile := GetSitePath(*website, SiteConf)
-		if err = fileOp.CopyFile(nginxConfFile, tmpDir); err != nil {
+		parentTask.LogStart(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
+		if err = handleAppBackup(&app, parentTask, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, "", ""); err != nil {
 			return err
 		}
-		t.Log(i18n.GetMsgByKey("BackupNginxConfig"))
-
-		switch website.Type {
-		case constant.Deployment:
-			app, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
-			if err != nil {
-				return err
-			}
-			t.LogStart(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
-			if err = handleAppBackup(&app, backupTask, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, "", ""); err != nil {
-				return err
-			}
-			t.LogSuccess(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
-		case constant.Runtime:
-			runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
-			if err != nil {
+		parentTask.LogSuccess(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
+	case constant.Runtime:
+		runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
+		if err != nil {
+			return err
+		}
+		parentTask.LogStart(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
+		if err = handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil {
+			return err
+		}
+		parentTask.LogSuccess(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
+		if website.DbID > 0 {
+			if err = backupDatabaseWithTask(parentTask, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
 				return err
 			}
-			t.LogStart(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
-			if err = handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil {
+		}
+	case constant.Static:
+		if website.DbID > 0 {
+			if err := backupDatabaseWithTask(parentTask, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
 				return err
 			}
-			t.LogSuccess(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
-			if website.DbID > 0 {
-				if err = backupDatabaseWithTask(t, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
-					return err
-				}
-			}
-		case constant.Static:
-			if website.DbID > 0 {
-				if err = backupDatabaseWithTask(t, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
-					return err
-				}
-			}
 		}
+	}
 
-		websiteDir := GetSitePath(*website, SiteDir)
-		t.LogStart(i18n.GetMsgByKey("CompressDir"))
-		if err = fileOp.TarGzCompressPro(true, websiteDir, path.Join(tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias)), "", excludes); err != nil {
-			return err
-		}
-		if err = fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
-			return err
-		}
-		t.Log(i18n.GetWithName("CompressFileSuccess", fileName))
-		return nil
+	websiteDir := GetSitePath(*website, SiteDir)
+	parentTask.LogStart(i18n.GetMsgByKey("CompressDir"))
+	if err := fileOp.TarGzCompressPro(true, websiteDir, path.Join(tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias)), "", excludes); err != nil {
+		return err
 	}
-	backupTask.AddSubTask(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeApp), backupWebsite, nil)
-	if parentTask != nil {
-		return backupWebsite(parentTask)
+	if err := fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
+		return err
 	}
-	return backupTask.Execute()
+	parentTask.Log(i18n.GetWithName("CompressFileSuccess", fileName))
+	return nil
 }
 
 func checkValidOfWebsite(oldWebsite, website *model.Website) error {
diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go
index 41f02bbf0dc5..bb407554ee9b 100644
--- a/agent/app/service/cronjob.go
+++ b/agent/app/service/cronjob.go
@@ -1,7 +1,6 @@
 package service
 
 import (
-	"bufio"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -461,24 +460,3 @@ func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) {
 	global.LOG.Infof("start cronjob entryID: %d", entryID)
 	return int(entryID), nil
 }
-
-func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) {
-	dir := fmt.Sprintf("%s/task/%s/%s", global.Dir.DataDir, cronjob.Type, cronjob.Name)
-	if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
-		if err = os.MkdirAll(dir, os.ModePerm); err != nil {
-			return "", err
-		}
-	}
-
-	path := fmt.Sprintf("%s/%s.log", dir, startTime.Format(constant.DateTimeSlimLayout))
-	global.LOG.Infof("cronjob %s has generated some logs %s", cronjob.Name, path)
-	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, constant.FilePerm)
-	if err != nil {
-		return "", err
-	}
-	defer file.Close()
-	write := bufio.NewWriter(file)
-	_, _ = write.WriteString(string(msg))
-	write.Flush()
-	return path, nil
-}
diff --git a/agent/app/service/cronjob_backup.go b/agent/app/service/cronjob_backup.go
index ccb9321c1abe..f627a5394498 100644
--- a/agent/app/service/cronjob_backup.go
+++ b/agent/app/service/cronjob_backup.go
@@ -10,6 +10,8 @@ import (
 	"time"
 
 	"github.com/1Panel-dev/1Panel/agent/app/repo"
+	"github.com/1Panel-dev/1Panel/agent/app/task"
+	"github.com/1Panel-dev/1Panel/agent/i18n"
 
 	"github.com/1Panel-dev/1Panel/agent/app/dto"
 	"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -20,7 +22,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskID string) error {
+func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
 	var apps []model.AppInstall
 	if cronjob.AppID == "all" {
 		apps, _ = appInstallRepo.ListBy(context.Background())
@@ -40,33 +42,36 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
 		return err
 	}
 	for _, app := range apps {
-		var record model.BackupRecord
-		record.From = "cronjob"
-		record.Type = "app"
-		record.CronjobID = cronjob.ID
-		record.Name = app.App.Key
-		record.DetailName = app.Name
-		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
-		backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s/%s", app.App.Key, app.Name))
-		record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
-		if err := handleAppBackup(&app, nil, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret, taskID); err != nil {
-			return err
-		}
-		downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
-		if err != nil {
-			return err
-		}
-		record.FileDir = path.Dir(downloadPath)
-		if err := backupRepo.CreateRecord(&record); err != nil {
-			global.LOG.Errorf("save backup record failed, err: %v", err)
-			return err
-		}
-		u.removeExpiredBackup(cronjob, accountMap, record)
+		taskItem.AddSubTaskWithOps(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
+			var record model.BackupRecord
+			record.From = "cronjob"
+			record.Type = "app"
+			record.CronjobID = cronjob.ID
+			record.Name = app.App.Key
+			record.DetailName = app.Name
+			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
+			backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s/%s", app.App.Key, app.Name))
+			record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
+			if err := doAppBackup(&app, task, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
+				return err
+			}
+			downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
+			if err != nil {
+				return err
+			}
+			record.FileDir = path.Dir(downloadPath)
+			if err := backupRepo.CreateRecord(&record); err != nil {
+				global.LOG.Errorf("save backup record failed, err: %v", err)
+				return err
+			}
+			u.removeExpiredBackup(cronjob, accountMap, record)
+			return nil
+		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 	}
 	return nil
 }
 
-func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time, taskID string) error {
+func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
 	webs := loadWebsForJob(cronjob)
 	if len(webs) == 0 {
 		return errors.New("no such website in database!")
@@ -76,33 +81,39 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
 		return err
 	}
 	for _, web := range webs {
-		var record model.BackupRecord
-		record.From = "cronjob"
-		record.Type = "website"
-		record.CronjobID = cronjob.ID
-		record.Name = web.Alias
-		record.DetailName = web.Alias
-		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
-		backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("website/%s", web.Alias))
-		record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.Alias, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
-		if err := handleWebsiteBackup(&web, nil, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret, taskID); err != nil {
-			return err
-		}
-		downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
-		if err != nil {
-			return err
-		}
-		record.FileDir = path.Dir(downloadPath)
-		if err := backupRepo.CreateRecord(&record); err != nil {
-			global.LOG.Errorf("save backup record failed, err: %v", err)
-			return err
-		}
-		u.removeExpiredBackup(cronjob, accountMap, record)
+		taskItem.AddSubTaskWithOps(task.GetTaskName(web.Alias, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
+			var record model.BackupRecord
+			record.From = "cronjob"
+			record.Type = "website"
+			record.CronjobID = cronjob.ID
+			record.Name = web.Alias
+			record.DetailName = web.Alias
+			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
+			backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("website/%s", web.Alias))
+			record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.Alias, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
+
+			if err := doWebsiteBackup(&web, taskItem, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
+				return err
+			}
+
+			downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
+			if err != nil {
+				return err
+			}
+			record.FileDir = path.Dir(downloadPath)
+			if err := backupRepo.CreateRecord(&record); err != nil {
+				global.LOG.Errorf("save backup record failed, err: %v", err)
+				return err
+			}
+			u.removeExpiredBackup(cronjob, accountMap, record)
+			return nil
+		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
+		return nil
 	}
 	return nil
 }
 
-func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time, taskID string) error {
+func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
 	dbs := loadDbsForJob(cronjob)
 	if len(dbs) == 0 {
 		return errors.New("no such db in database!")
@@ -112,110 +123,125 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
 		return err
 	}
 	for _, dbInfo := range dbs {
-		var record model.BackupRecord
-		record.From = "cronjob"
-		record.Type = dbInfo.DBType
-		record.CronjobID = cronjob.ID
-		record.Name = dbInfo.Database
-		record.DetailName = dbInfo.Name
-		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
+		itemName := fmt.Sprintf("%s[%s] - %s", dbInfo.Database, dbInfo.DBType, dbInfo.Name)
+		taskItem.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
+			var record model.BackupRecord
+			record.From = "cronjob"
+			record.Type = dbInfo.DBType
+			record.CronjobID = cronjob.ID
+			record.Name = dbInfo.Database
+			record.DetailName = dbInfo.Name
+			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
 
-		backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
-		record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
-		if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
-			if err := handleMysqlBackup(dbInfo, nil, backupDir, record.FileName, taskID); err != nil {
+			backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
+			record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
+			if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
+				if err := doMysqlBackup(dbInfo, backupDir, record.FileName); err != nil {
+					return err
+				}
+			} else {
+				if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName); err != nil {
+					return err
+				}
+			}
+
+			downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
+			if err != nil {
+				return err
+			}
+			record.FileDir = path.Dir(downloadPath)
+			if err := backupRepo.CreateRecord(&record); err != nil {
+				global.LOG.Errorf("save backup record failed, err: %v", err)
+				return err
+			}
+			u.removeExpiredBackup(cronjob, accountMap, record)
+			return nil
+		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
+	}
+	return nil
+}
+
+func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
+	taskItem.AddSubTaskWithOps(task.GetTaskName(i18n.GetMsgByKey("BackupFileOrDir"), task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
+		accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
+		if err != nil {
+			return err
+		}
+		fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
+		backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name))
+
+		fileOp := files.NewFileOp()
+		if cronjob.IsDir {
+			taskItem.Logf("Dir: %s, Excludes: %s", cronjob.SourceDir, cronjob.ExclusionRules)
+			if err := fileOp.TarGzCompressPro(true, cronjob.SourceDir, path.Join(backupDir, fileName), cronjob.Secret, cronjob.ExclusionRules); err != nil {
 				return err
 			}
 		} else {
-			if err := handlePostgresqlBackup(dbInfo, nil, backupDir, record.FileName, taskID); err != nil {
+			taskItem.Logf("Files: %s", cronjob.SourceDir)
+			fileLists := strings.Split(cronjob.SourceDir, ",")
+			if err := fileOp.TarGzFilesWithCompressPro(fileLists, path.Join(backupDir, fileName), cronjob.Secret); err != nil {
 				return err
 			}
 		}
-		downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName))
+		var record model.BackupRecord
+		record.From = "cronjob"
+		record.Type = "directory"
+		record.CronjobID = cronjob.ID
+		record.Name = cronjob.Name
+		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
+		downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, fileName))
 		if err != nil {
+			taskItem.LogFailedWithErr("Upload backup file", err)
 			return err
 		}
 		record.FileDir = path.Dir(downloadPath)
+		record.FileName = fileName
 		if err := backupRepo.CreateRecord(&record); err != nil {
-			global.LOG.Errorf("save backup record failed, err: %v", err)
+			taskItem.LogFailedWithErr("Save record", err)
 			return err
 		}
 		u.removeExpiredBackup(cronjob, accountMap, record)
-	}
+		return nil
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 	return nil
 }
 
-func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.Time) error {
-	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
-	if err != nil {
-		return err
-	}
-	fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
-	backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name))
-
-	fileOp := files.NewFileOp()
-	if cronjob.IsDir {
-		if err := fileOp.TarGzCompressPro(true, cronjob.SourceDir, path.Join(backupDir, fileName), cronjob.Secret, cronjob.ExclusionRules); err != nil {
+func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
+	taskItem.AddSubTaskWithOps(task.GetTaskName(i18n.GetMsgByKey("BackupSystemLog"), task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
+		accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
+		if err != nil {
 			return err
 		}
-	} else {
-		fileLists := strings.Split(cronjob.SourceDir, ",")
-		if err := fileOp.TarGzFilesWithCompressPro(fileLists, path.Join(backupDir, fileName), cronjob.Secret); err != nil {
+		nameItem := startTime.Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
+		fileName := fmt.Sprintf("system_log_%s.tar.gz", nameItem)
+		backupDir := path.Join(global.Dir.TmpDir, "log", nameItem)
+		if err := handleBackupLogs(taskItem, backupDir, fileName, cronjob.Secret); err != nil {
 			return err
 		}
-	}
-	var record model.BackupRecord
-	record.From = "cronjob"
-	record.Type = "directory"
-	record.CronjobID = cronjob.ID
-	record.Name = cronjob.Name
-	record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
-	downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, fileName))
-	if err != nil {
-		return err
-	}
-	record.FileDir = path.Dir(downloadPath)
-	record.FileName = fileName
-	if err := backupRepo.CreateRecord(&record); err != nil {
-		global.LOG.Errorf("save backup record failed, err: %v", err)
-		return err
-	}
-	u.removeExpiredBackup(cronjob, accountMap, record)
-	return nil
-}
-
-func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.Time) error {
-	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
-	if err != nil {
-		return err
-	}
-	nameItem := startTime.Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
-	fileName := fmt.Sprintf("system_log_%s.tar.gz", nameItem)
-	backupDir := path.Join(global.Dir.TmpDir, "log", nameItem)
-	if err := handleBackupLogs(backupDir, fileName, cronjob.Secret); err != nil {
-		return err
-	}
-	var record model.BackupRecord
-	record.From = "cronjob"
-	record.Type = "log"
-	record.CronjobID = cronjob.ID
-	record.Name = cronjob.Name
-	record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
-	downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(path.Dir(backupDir), fileName))
-	if err != nil {
-		return err
-	}
-	record.FileDir = path.Dir(downloadPath)
-	record.FileName = fileName
-	if err := backupRepo.CreateRecord(&record); err != nil {
-		global.LOG.Errorf("save backup record failed, err: %v", err)
-		return err
-	}
-	u.removeExpiredBackup(cronjob, accountMap, record)
+		var record model.BackupRecord
+		record.From = "cronjob"
+		record.Type = "log"
+		record.CronjobID = cronjob.ID
+		record.Name = cronjob.Name
+		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
+		downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(path.Dir(backupDir), fileName))
+		if err != nil {
+			taskItem.LogFailedWithErr("Upload backup file", err)
+			return err
+		}
+		record.FileDir = path.Dir(downloadPath)
+		record.FileName = fileName
+		if err := backupRepo.CreateRecord(&record); err != nil {
+			taskItem.LogFailedWithErr("Save record", err)
+			return err
+		}
+		u.removeExpiredBackup(cronjob, accountMap, record)
+		return nil
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 	return nil
 }
 
-func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.JobRecords) error {
+func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.JobRecords, taskItem *task.Task) error {
 	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
 	if err != nil {
 		return err
@@ -255,7 +281,7 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J
 		WithTaskLog:       true,
 	}
 
-	if err := NewISnapshotService().SnapshotCreate(req, jobRecord.ID); err != nil {
+	if err := NewISnapshotService().SnapshotCreate(taskItem, req, jobRecord.ID, cronjob.RetryTimes, cronjob.Timeout); err != nil {
 		return err
 	}
 	record.FileName = req.Name + ".tar.gz"
@@ -336,7 +362,7 @@ func loadWebsForJob(cronjob model.Cronjob) []model.Website {
 	return weblist
 }
 
-func handleBackupLogs(targetDir, fileName string, secret string) error {
+func handleBackupLogs(taskItem *task.Task, targetDir, fileName string, secret string) error {
 	fileOp := files.NewFileOp()
 	websites, err := websiteRepo.List()
 	if err != nil {
@@ -345,6 +371,7 @@ func handleBackupLogs(targetDir, fileName string, secret string) error {
 	if len(websites) != 0 {
 		webItem := GetOpenrestyDir(SitesRootDir)
 		for _, website := range websites {
+			taskItem.Logf("%s Website logs %s...", i18n.GetMsgByKey("TaskBackup"), website.Alias)
 			dirItem := path.Join(targetDir, "website", website.Alias)
 			if _, err := os.Stat(dirItem); err != nil && os.IsNotExist(err) {
 				if err = os.MkdirAll(dirItem, os.ModePerm); err != nil {
@@ -370,7 +397,6 @@ func handleBackupLogs(targetDir, fileName string, secret string) error {
 				}
 			}
 		}
-		global.LOG.Debug("backup website log successful!")
 	}
 
 	systemDir := path.Join(targetDir, "system")
@@ -379,6 +405,8 @@ func handleBackupLogs(targetDir, fileName string, secret string) error {
 			return err
 		}
 	}
+
+	taskItem.Logf("%s System logs...", i18n.GetMsgByKey("TaskBackup"))
 	systemLogFiles, _ := os.ReadDir(global.Dir.LogDir)
 	if len(systemLogFiles) != 0 {
 		for i := 0; i < len(systemLogFiles); i++ {
@@ -387,8 +415,8 @@ func handleBackupLogs(targetDir, fileName string, secret string) error {
 			}
 		}
 	}
-	global.LOG.Debug("backup system log successful!")
 
+	taskItem.Logf("%s SSH logs...", i18n.GetMsgByKey("TaskBackup"))
 	loginLogFiles, _ := os.ReadDir("/var/log")
 	loginDir := path.Join(targetDir, "login")
 	if _, err := os.Stat(loginDir); err != nil && os.IsNotExist(err) {
@@ -403,7 +431,7 @@ func handleBackupLogs(targetDir, fileName string, secret string) error {
 			}
 		}
 	}
-	global.LOG.Debug("backup ssh log successful!")
+	taskItem.Log("backup ssh log successful!")
 
 	if err := fileOp.TarGzCompressPro(true, targetDir, path.Join(path.Dir(targetDir), fileName), secret, ""); err != nil {
 		return err
diff --git a/agent/app/service/cronjob_helper.go b/agent/app/service/cronjob_helper.go
index 3a50eb39c2ec..27492fb322a3 100644
--- a/agent/app/service/cronjob_helper.go
+++ b/agent/app/service/cronjob_helper.go
@@ -12,7 +12,6 @@ import (
 	"github.com/1Panel-dev/1Panel/agent/app/model"
 	"github.com/1Panel-dev/1Panel/agent/app/repo"
 	"github.com/1Panel-dev/1Panel/agent/app/task"
-	"github.com/1Panel-dev/1Panel/agent/buserr"
 	"github.com/1Panel-dev/1Panel/agent/constant"
 	"github.com/1Panel-dev/1Panel/agent/global"
 	"github.com/1Panel-dev/1Panel/agent/i18n"
@@ -25,114 +24,94 @@ import (
 func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
 	record := cronjobRepo.StartRecords(cronjob.ID, "", cronjob.Type)
 	go func() {
-		var (
-			message []byte
-			err     error
-		)
-		cronjob.RetryTimes = cronjob.RetryTimes + 1
-		for i := uint(0); i < cronjob.RetryTimes; i++ {
-			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cronjob.Timeout)*time.Second)
-			done := make(chan error)
-			go func() {
-				message, err = u.handleJob(cronjob, record)
-				if err != nil {
-					global.LOG.Debugf("try handle cron job [%s] %s failed %d/%d, err: %v", cronjob.Type, cronjob.Name, i+1, cronjob.RetryTimes, err)
+		taskItem, err := task.NewTaskWithOps(fmt.Sprintf("cronjob-%s", cronjob.Name), task.TaskHandle, task.TaskScopeCronjob, record.TaskID, cronjob.ID)
+		if err != nil {
+			global.LOG.Errorf("new task for exec shell failed, err: %v", err)
+			return
+		}
+		err = u.loadTask(cronjob, &record, taskItem)
+		if cronjob.Type == "snapshot" {
+			if err != nil {
+				taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
+				if len(taskItem.ID) == 0 {
+					record.TaskID = ""
 				}
-				close(done)
-			}()
-			select {
-			case <-done:
-				cancel()
-			case <-ctx.Done():
-				global.LOG.Debugf("try handle cron job [%s] %s failed %d/%d, err: timeout", cronjob.Type, cronjob.Name, i+1, cronjob.RetryTimes)
-				err = fmt.Errorf("handle timeout")
-				cancel()
-				continue
+				cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
+				handleCronJobAlert(cronjob)
+				return
 			}
+			cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
+			return
 		}
 		if err != nil {
-			if len(message) != 0 {
-				record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message)
-			}
+			global.LOG.Debugf("preper to handle cron job [%s] %s failed, err: %v", cronjob.Type, cronjob.Name, err)
+			record.TaskID = ""
+			cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
+			return
+		}
+		if err := taskItem.Execute(); err != nil {
 			taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
 			if len(taskItem.ID) == 0 {
 				record.TaskID = ""
 			}
 			cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
 			handleCronJobAlert(cronjob)
-			return
-		}
-		if len(message) != 0 {
-			record.Records, err = mkdirAndWriteFile(cronjob, record.StartTime, message)
-			if err != nil {
-				global.LOG.Errorf("save file %s failed, err: %v", record.Records, err)
-			}
+		} else {
+			cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
 		}
-		cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
 	}()
 }
 
-func (u *CronjobService) handleJob(cronjob *model.Cronjob, record model.JobRecords) ([]byte, error) {
-	var (
-		message []byte
-		err     error
-	)
+func (u *CronjobService) loadTask(cronjob *model.Cronjob, record *model.JobRecords, taskItem *task.Task) error {
+	var err error
 	switch cronjob.Type {
 	case "shell":
 		if cronjob.ScriptMode == "library" {
 			scriptItem, _ := scriptRepo.Get(repo.WithByID(cronjob.ScriptID))
 			if scriptItem.ID == 0 {
-				return nil, fmt.Errorf("load script from db failed, err: %v", err)
+				return fmt.Errorf("load script from db failed, err: %v", err)
 			}
 			cronjob.Script = scriptItem.Script
 			cronjob.ScriptMode = "input"
 		}
 		if len(cronjob.Script) == 0 {
-			return nil, fmt.Errorf("the script content is empty and is skipped")
+			return fmt.Errorf("the script content is empty and is skipped")
 		}
-		err = u.handleShell(*cronjob, record.TaskID)
+		u.handleShell(*cronjob, taskItem)
 	case "curl":
 		if len(cronjob.URL) == 0 {
-			return nil, fmt.Errorf("the url is empty and is skipped")
+			return fmt.Errorf("the url is empty and is skipped")
 		}
-		err = u.handleCurl(*cronjob, record.TaskID)
+		u.handleCurl(*cronjob, taskItem)
 	case "ntp":
-		err = u.handleNtpSync(*cronjob, record.TaskID)
+		u.handleNtpSync(*cronjob, taskItem)
 	case "cutWebsiteLog":
-		var messageItem []string
-		messageItem, record.File, err = u.handleCutWebsiteLog(cronjob, record.StartTime)
-		message = []byte(strings.Join(messageItem, "\n"))
+		err = u.handleCutWebsiteLog(cronjob, record, taskItem)
 	case "clean":
-		err = u.handleSystemClean(*cronjob, record.TaskID)
+		u.handleSystemClean(*cronjob, taskItem)
 	case "website":
-		err = u.handleWebsite(*cronjob, record.StartTime, record.TaskID)
+		err = u.handleWebsite(*cronjob, record.StartTime, taskItem)
 	case "app":
-		err = u.handleApp(*cronjob, record.StartTime, record.TaskID)
+		err = u.handleApp(*cronjob, record.StartTime, taskItem)
 	case "database":
-		err = u.handleDatabase(*cronjob, record.StartTime, record.TaskID)
+		err = u.handleDatabase(*cronjob, record.StartTime, taskItem)
 	case "directory":
 		if len(cronjob.SourceDir) == 0 {
-			return nil, fmt.Errorf("the source dir is empty and is skipped")
+			return fmt.Errorf("the source dir is empty and is skipped")
 		}
-		err = u.handleDirectory(*cronjob, record.StartTime)
+		err = u.handleDirectory(*cronjob, record.StartTime, taskItem)
 	case "log":
-		err = u.handleSystemLog(*cronjob, record.StartTime)
+		err = u.handleSystemLog(*cronjob, record.StartTime, taskItem)
 	case "snapshot":
 		_ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records})
-		err = u.handleSnapshot(*cronjob, record)
+		err = u.handleSnapshot(*cronjob, *record, taskItem)
 	}
-	return message, err
+	return err
 }
 
-func (u *CronjobService) handleShell(cronjob model.Cronjob, taskID string) error {
-	taskItem, err := task.NewTaskWithOps(fmt.Sprintf("cronjob-%s", cronjob.Name), task.TaskHandle, task.TaskScopeCronjob, taskID, cronjob.ID)
-	if err != nil {
-		global.LOG.Errorf("new task for exec shell failed, err: %v", err)
-		return err
-	}
-
-	cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem), cmd.WithTimeout(24*time.Hour))
-	taskItem.AddSubTask(i18n.GetWithName("HandleShell", cronjob.Name), func(t *task.Task) error {
+func (u *CronjobService) handleShell(cronjob model.Cronjob, taskItem *task.Task) {
+	cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem))
+	taskItem.AddSubTaskWithOps(i18n.GetWithName("HandleShell", cronjob.Name), func(t *task.Task) error {
 		if len(cronjob.ContainerName) != 0 {
 			command := "sh"
 			if len(cronjob.Command) != 0 {
@@ -166,36 +145,18 @@ func (u *CronjobService) handleShell(cronjob model.Cronjob, taskID string) error
 			return err
 		}
 		return nil
-	},
-		nil,
-	)
-	return taskItem.Execute()
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 }
 
-func (u *CronjobService) handleCurl(cronjob model.Cronjob, taskID string) error {
-	taskItem, err := task.NewTaskWithOps(fmt.Sprintf("cronjob-%s", cronjob.Name), task.TaskHandle, task.TaskScopeCronjob, taskID, cronjob.ID)
-	if err != nil {
-		global.LOG.Errorf("new task for exec shell failed, err: %v", err)
-		return err
-	}
-
-	taskItem.AddSubTask(i18n.GetWithName("HandleShell", cronjob.Name), func(t *task.Task) error {
-		cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem), cmd.WithTimeout(time.Hour))
+func (u *CronjobService) handleCurl(cronjob model.Cronjob, taskItem *task.Task) {
+	taskItem.AddSubTaskWithOps(i18n.GetWithName("HandleShell", cronjob.Name), func(t *task.Task) error {
+		cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem))
 		return cmdMgr.Run("curl", cronjob.URL)
-	},
-		nil,
-	)
-	return taskItem.Execute()
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 }
 
-func (u *CronjobService) handleNtpSync(cronjob model.Cronjob, taskID string) error {
-	taskItem, err := task.NewTaskWithOps(fmt.Sprintf("cronjob-%s", cronjob.Name), task.TaskHandle, task.TaskScopeCronjob, taskID, cronjob.ID)
-	if err != nil {
-		global.LOG.Errorf("new task for exec shell failed, err: %v", err)
-		return err
-	}
-
-	taskItem.AddSubTask(i18n.GetMsgByKey("HandleNtpSync"), func(t *task.Task) error {
+func (u *CronjobService) handleNtpSync(cronjob model.Cronjob, taskItem *task.Task) {
+	taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("HandleNtpSync"), func(t *task.Task) error {
 		ntpServer, err := settingRepo.Get(settingRepo.WithByKey("NtpSite"))
 		if err != nil {
 			return err
@@ -209,47 +170,43 @@ func (u *CronjobService) handleNtpSync(cronjob model.Cronjob, taskID string) err
 			return err
 		}
 		return nil
-	}, nil)
-	return taskItem.Execute()
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 }
 
-func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime time.Time) ([]string, string, error) {
-	var (
-		err       error
-		filePaths []string
-		msgs      []string
-	)
-	websites := loadWebsForJob(*cronjob)
-	fileOp := files.NewFileOp()
-	baseDir := GetOpenrestyDir(SitesRootDir)
-	for _, website := range websites {
-		websiteLogDir := pathUtils.Join(baseDir, website.Alias, "log")
-		srcAccessLogPath := pathUtils.Join(websiteLogDir, "access.log")
-		srcErrorLogPath := pathUtils.Join(websiteLogDir, "error.log")
-		dstLogDir := pathUtils.Join(global.Dir.LocalBackupDir, "log", "website", website.Alias)
-		if !fileOp.Stat(dstLogDir) {
-			_ = os.MkdirAll(dstLogDir, constant.DirPerm)
-		}
+func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, record *model.JobRecords, taskItem *task.Task) error {
+	taskItem.AddSubTaskWithOps(i18n.GetWithName("CutWebsiteLog", cronjob.Name), func(t *task.Task) error {
+		var filePaths []string
+		websites := loadWebsForJob(*cronjob)
+		fileOp := files.NewFileOp()
+		baseDir := GetOpenrestyDir(SitesRootDir)
+		for _, website := range websites {
+			taskItem.Log(website.Alias)
+			websiteLogDir := pathUtils.Join(baseDir, website.Alias, "log")
+			srcAccessLogPath := pathUtils.Join(websiteLogDir, "access.log")
+			srcErrorLogPath := pathUtils.Join(websiteLogDir, "error.log")
+			dstLogDir := pathUtils.Join(global.Dir.LocalBackupDir, "log", "website", website.Alias)
+			if !fileOp.Stat(dstLogDir) {
+				_ = os.MkdirAll(dstLogDir, constant.DirPerm)
+			}
 
-		dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout))
-		dstFilePath := pathUtils.Join(dstLogDir, dstName)
-		filePaths = append(filePaths, dstFilePath)
+			dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, record.StartTime.Format(constant.DateTimeSlimLayout))
+			dstFilePath := pathUtils.Join(dstLogDir, dstName)
+			filePaths = append(filePaths, dstFilePath)
 
-		if err = backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil {
-			websiteErr := buserr.WithNameAndErr("ErrCutWebsiteLog", website.PrimaryDomain, err)
-			err = websiteErr
-			msgs = append(msgs, websiteErr.Error())
-			global.LOG.Error(websiteErr.Error())
-			continue
-		} else {
-			_ = fileOp.WriteFile(srcAccessLogPath, strings.NewReader(""), constant.DirPerm)
-			_ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), constant.DirPerm)
+			if err := backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil {
+				taskItem.LogFailedWithErr("CutWebsiteLog", err)
+				continue
+			} else {
+				_ = fileOp.WriteFile(srcAccessLogPath, strings.NewReader(""), constant.DirPerm)
+				_ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), constant.DirPerm)
+			}
+			taskItem.Log(i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath}))
 		}
-		msg := i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath})
-		msgs = append(msgs, msg)
-	}
-	u.removeExpiredLog(*cronjob)
-	return msgs, strings.Join(filePaths, ","), err
+		u.removeExpiredLog(*cronjob)
+		record.File = strings.Join(filePaths, ",")
+		return nil
+	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
+	return nil
 }
 
 func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error {
@@ -272,13 +229,9 @@ func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error
 	return nil
 }
 
-func (u *CronjobService) handleSystemClean(cronjob model.Cronjob, taskID string) error {
-	taskItem, err := task.NewTaskWithOps(fmt.Sprintf("cronjob-%s", cronjob.Name), task.TaskHandle, task.TaskScopeCronjob, taskID, cronjob.ID)
-	if err != nil {
-		global.LOG.Errorf("new task for system clean failed, err: %v", err)
-		return err
-	}
-	return systemClean(taskItem)
+func (u *CronjobService) handleSystemClean(cronjob model.Cronjob, taskItem *task.Task) {
+	cleanTask := doSystemClean(taskItem)
+	taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("HandleSystemClean"), cleanTask, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
 }
 
 func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap map[string]backupClientHelper, file string) (string, error) {
diff --git a/agent/app/service/device_clean.go b/agent/app/service/device_clean.go
index 1c4c18741e3b..43807841a407 100644
--- a/agent/app/service/device_clean.go
+++ b/agent/app/service/device_clean.go
@@ -10,7 +10,6 @@ import (
 	"time"
 
 	"github.com/1Panel-dev/1Panel/agent/constant"
-	"github.com/1Panel-dev/1Panel/agent/i18n"
 	"github.com/1Panel-dev/1Panel/agent/utils/docker"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
@@ -301,8 +300,8 @@ func (u *DeviceService) Clean(req []dto.Clean) {
 	}
 }
 
-func systemClean(taskItem *task.Task) error {
-	taskItem.AddSubTask(i18n.GetMsgByKey("HandleSystemClean"), func(t *task.Task) error {
+func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
+	return func(t *task.Task) error {
 		size := int64(0)
 		fileCount := 0
 		dropWithTask(path.Join(global.Dir.BaseDir, "1panel_original"), taskItem, &size, &fileCount)
@@ -363,8 +362,7 @@ func systemClean(taskItem *task.Task) error {
 		_ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", fileCount))
 
 		return nil
-	}, nil)
-	return taskItem.Execute()
+	}
 }
 
 func loadSnapshotTree(fileOp fileUtils.FileOp) []dto.CleanTree {
diff --git a/agent/app/service/snapshot.go b/agent/app/service/snapshot.go
index eb39a93bf7a0..a88347a1b0fc 100644
--- a/agent/app/service/snapshot.go
+++ b/agent/app/service/snapshot.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"github.com/1Panel-dev/1Panel/agent/app/repo"
+	"github.com/1Panel-dev/1Panel/agent/app/task"
 	"github.com/1Panel-dev/1Panel/agent/buserr"
 
 	"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,7 +31,7 @@ type SnapshotService struct {
 type ISnapshotService interface {
 	SearchWithPage(req dto.PageSnapshot) (int64, interface{}, error)
 	LoadSnapshotData() (dto.SnapshotData, error)
-	SnapshotCreate(req dto.SnapshotCreate, jobID uint) error
+	SnapshotCreate(parentTask *task.Task, req dto.SnapshotCreate, jobID, retry, timeout uint) error
 	SnapshotReCreate(id uint) error
 	SnapshotRecover(req dto.SnapshotRecover) error
 	SnapshotRollback(req dto.SnapshotRecover) error
diff --git a/agent/app/service/snapshot_create.go b/agent/app/service/snapshot_create.go
index 55e6ed244d9d..b00a51905f66 100644
--- a/agent/app/service/snapshot_create.go
+++ b/agent/app/service/snapshot_create.go
@@ -26,7 +26,7 @@ import (
 	"gorm.io/gorm"
 )
 
-func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate, jobID uint) error {
+func (u *SnapshotService) SnapshotCreate(parentTask *task.Task, req dto.SnapshotCreate, jobID, retry, timeout uint) error {
 	versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
 
 	scope := "core"
@@ -65,19 +65,23 @@ func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate, jobID uint) err
 	}
 
 	req.ID = snap.ID
-	taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeSnapshot, req.TaskID, req.ID)
-	if err != nil {
-		global.LOG.Errorf("new task for create snapshot failed, err: %v", err)
-		return err
+	var err error
+	taskItem := parentTask
+	if parentTask == nil {
+		taskItem, err = task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeSnapshot, req.TaskID, req.ID)
+		if err != nil {
+			global.LOG.Errorf("new task for create snapshot failed, err: %v", err)
+			return err
+		}
 	}
 	if jobID == 0 {
 		go func() {
-			_ = handleSnapshot(req, taskItem, jobID)
+			_ = handleSnapshot(req, taskItem, jobID, 0, 0)
 		}()
 		return nil
 	}
 
-	return handleSnapshot(req, taskItem, jobID)
+	return handleSnapshot(req, taskItem, jobID, retry, timeout)
 }
 
 func (u *SnapshotService) SnapshotReCreate(id uint) error {
@@ -108,20 +112,23 @@ func (u *SnapshotService) SnapshotReCreate(id uint) error {
 		return err
 	}
 	go func() {
-		_ = handleSnapshot(req, taskItem, 0)
+		_ = handleSnapshot(req, taskItem, 0, 0, 0)
 	}()
 
 	return nil
 }
 
-func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID uint) error {
+func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, timeout uint) error {
 	rootDir := path.Join(global.Dir.TmpDir, "system", req.Name)
 	openrestyDir, _ := settingRepo.GetValueByKey("WEBSITE_DIR")
 	itemHelper := snapHelper{SnapID: req.ID, Task: *taskItem, FileOp: files.NewFileOp(), Ctx: context.Background(), OpenrestyDir: openrestyDir}
 	baseDir := path.Join(rootDir, "base")
 	_ = os.MkdirAll(baseDir, os.ModePerm)
 
-	taskItem.AddSubTaskWithAlias(
+	if timeout == 0 {
+		timeout = 1800
+	}
+	taskItem.AddSubTaskWithAliasAndOps(
 		"SnapDBInfo",
 		func(t *task.Task) error {
 			if err := loadDbConn(&itemHelper, rootDir, req); err != nil {
@@ -132,68 +139,65 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID uint) err
 				_ = itemHelper.snapAgentDB.Where("id = ?", jobID).Delete(&model.JobRecords{}).Error
 			}
 			return nil
-		},
-		nil,
+		}, nil, int(retry), time.Duration(timeout)*time.Second,
 	)
 
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapBaseInfo" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapBaseInfo",
 			func(t *task.Task) error { return snapBaseData(itemHelper, baseDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapInstallApp" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapInstallApp",
 			func(t *task.Task) error { return snapAppImage(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapLocalBackup" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapLocalBackup",
 			func(t *task.Task) error { return snapBackupData(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapPanelData" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapPanelData",
 			func(t *task.Task) error { return snapPanelData(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 
-	taskItem.AddSubTaskWithAlias(
+	taskItem.AddSubTaskWithAliasAndOps(
 		"SnapCloseDBConn",
 		func(t *task.Task) error {
 			taskItem.Log("---------------------- 6 / 8 ----------------------")
 			common.CloseDB(itemHelper.snapAgentDB)
 			common.CloseDB(itemHelper.snapCoreDB)
 			return nil
-		},
-		nil,
+		}, nil, int(retry), time.Duration(timeout)*time.Second,
 	)
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapCompress" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapCompress",
 			func(t *task.Task) error { return snapCompress(itemHelper, rootDir, req.Secret) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapUpload",
 			func(t *task.Task) error {
 				return snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir))
-			},
-			nil,
+			}, nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
diff --git a/agent/app/task/task.go b/agent/app/task/task.go
index efc79427166a..7e5ba3a3355c 100644
--- a/agent/app/task/task.go
+++ b/agent/app/task/task.go
@@ -3,6 +3,7 @@ package task
 import (
 	"bufio"
 	"context"
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -156,6 +157,11 @@ func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback Rollba
 	t.SubTasks = append(t.SubTasks, subTask)
 }
 
+func (t *Task) AddSubTaskWithAliasAndOps(key string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) {
+	subTask := &SubTask{RootTask: t, Name: i18n.GetMsgByKey(key), Retry: retry, Timeout: timeout, Action: action, Rollback: rollback}
+	t.SubTasks = append(t.SubTasks, subTask)
+}
+
 func (t *Task) AddSubTaskWithIgnoreErr(name string, action ActionFunc) {
 	subTask := &SubTask{RootTask: t, Name: name, Retry: 0, Timeout: 30 * time.Minute, Action: action, Rollback: nil, IgnoreErr: true}
 	t.SubTasks = append(t.SubTasks, subTask)
@@ -182,6 +188,7 @@ func (s *SubTask) Execute() error {
 		select {
 		case <-ctx.Done():
 			s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
+			err = errors.New("timeout!")
 		case err = <-done:
 			if err != nil {
 				s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml
index cd46c85f70b9..ec454ec3a610 100644
--- a/agent/i18n/lang/en.yaml
+++ b/agent/i18n/lang/en.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: 'A configuration file with the same name already exists'
 ErrUserFindErr: 'User {{ .name }} search failed {{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} website log cutting failed, error {{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} website log cut successfully, backup path {{ .path }}'
 HandleShell: 'Execute script {{ .name }}'
 HandleNtpSync: 'System time synchronization'
 HandleSystemClean: 'System cache cleanup'
+SystemLog: 'System Log'
+CutWebsiteLog: 'Rotate Website Log'
+FileOrDir: 'Directory / File'
 
 #toolbox
 ErrNotExistUser: 'The current user does not exist, please modify and try again!'
diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml
index 2706cba61c8a..4bc44f5638de 100644
--- a/agent/i18n/lang/ja.yaml
+++ b/agent/i18n/lang/ja.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: '同じ名前の設定ファイルがすでに存在し
 ErrUserFindErr: 'ユーザー {{ .name }} の検索に失敗しました {{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} ウェブサイトのログの切り取りに失敗しました。エラー {{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} ウェブサイトのログが正常にカットされました。バックアップ パス {{ .path }}'
 HandleShell: 'スクリプト {{ .name }} を実行します'
 HandleNtpSync: 'システム時刻の同期'
 HandleSystemClean: 'システム キャッシュのクリーンアップ'
+SystemLog: 'システムログ'
+CutWebsiteLog: 'ウェブサイトログのローテーション'
+FileOrDir: 'ディレクトリ / ファイル'
 
 #toolbox
 ErrNotExistUser: '現在のユーザーは存在しません。変更してもう一度お試しください。'
diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml
index 66b6848cd1f6..db15cfa1e9af 100644
--- a/agent/i18n/lang/ko.yaml
+++ b/agent/i18n/lang/ko.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: '같은 이름의 구성 파일이 이미 존재합니다
 ErrUserFindErr: '사용자 {{ .name }} 검색에 실패했습니다 {{ .err }}'
 
 #크론잡
-ErrCutWebsiteLog: '{{ .name }} 웹사이트 로그 잘라내기에 실패했습니다. 오류 {{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} 웹사이트 로그가 성공적으로 잘렸습니다. 백업 경로 {{ .path }}'
 HandleShell: '스크립트 {{ .name }} 실행'
 HandleNtpSync: '시스템 시간 동기화'
 HandleSystemClean: '시스템 캐시 정리'
+SystemLog: '시스템 로그'
+CutWebsiteLog: '웹사이트 로그 회전'
+FileOrDir: '디렉터리 / 파일'
 
 #도구상자
 ErrNotExistUser: '현재 사용자가 존재하지 않습니다. 수정한 후 다시 시도하세요!'
diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml
index f88fc716e12e..b850611b9fda 100644
--- a/agent/i18n/lang/ms.yaml
+++ b/agent/i18n/lang/ms.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: 'Fail konfigurasi dengan nama yang sama sudah wujud'
 ErrUserFindErr: 'Pengguna {{ .name }} carian gagal {{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} pemotongan log tapak web gagal, ralat {{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} log tapak web berjaya dipotong, laluan sandaran {{ .path }}'
 HandleShell: 'Laksanakan skrip {{ .name }}'
 HandleNtpSync: 'Penyegerakan masa sistem'
 HandleSystemClean: 'Pembersihan cache sistem'
+SystemLog: 'Log Sistem'
+CutWebsiteLog: 'Putar Log Laman Web'
+FileOrDir: 'Direktori / Fail'
 
 #kotak alat
 ErrNotExistUser: 'Pengguna semasa tidak wujud, sila ubah suai dan cuba lagi!'
diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml
index aedd3bea9a7f..d0458c5af160 100644
--- a/agent/i18n/lang/pt-BR.yaml
+++ b/agent/i18n/lang/pt-BR.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: 'Um arquivo de configuração com o mesmo nome já existe
 ErrUserFindErr: 'Falha na pesquisa do usuário {{ .name }} {{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} falha ao cortar o log do site, erro {{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} registro do site cortado com sucesso, caminho de backup {{ .path }}'
 HandleShell: 'Executar script {{ .name }}'
 HandleNtpSync: 'Sincronização de hora do sistema'
 HandleSystemClean: 'Limpeza de cache do sistema'
+SystemLog: 'Log do Sistema'
+CutWebsiteLog: 'Rotacionar Log do Website'
+FileOrDir: 'Diretório / Arquivo'
 
 #caixa de ferramentas
 ErrNotExistUser: 'O usuário atual não existe, modifique e tente novamente!'
diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml
index fe07cd0b2e7d..f92b10893188 100644
--- a/agent/i18n/lang/ru.yaml
+++ b/agent/i18n/lang/ru.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: 'Файл конфигурации с таким имен
 ErrUserFindErr: 'Поиск пользователя {{ .name }} не удался {{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} не удалось вырезать журнал веб-сайта, ошибка {{ .err }}'
 CutWebsiteLogSuccess: 'Журнал веб-сайта {{ .name }} успешно вырезан, путь к резервной копии {{ .path }}'
 HandleShell: 'Выполнить скрипт {{ .name }}'
 HandleNtpSync: 'Синхронизация системного времени'
 HandleSystemClean: 'Очистка системного кэша'
+SystemLog: 'Системный лог'
+CutWebsiteLog: 'Ротация логов сайта'
+FileOrDir: 'Каталог / Файл'
 
 #ящик для инструментов
 ErrNotExistUser: 'Текущий пользователь не существует, измените его и повторите попытку!'
diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml
index 223198ca275d..37646271834a 100644
--- a/agent/i18n/lang/zh-Hant.yaml
+++ b/agent/i18n/lang/zh-Hant.yaml
@@ -192,11 +192,13 @@ ErrConfigAlreadyExist: '已存在同名設定檔'
 ErrUserFindErr: '使用者{{ .name }} 尋找失敗{{ .err }}'
 
 #cronjob
-ErrCutWebsiteLog: '{{ .name }} 網站日誌切割失敗，錯誤{{ .err }}'
 CutWebsiteLogSuccess: '{{ .name }} 網站日誌切割成功，備份路徑{{ .path }}'
 HandleShell: '執行腳本{{ .name }}'
 HandleNtpSync: '系統時間同步'
 HandleSystemClean: '系統快取清理'
+SystemLog: '系統日誌'
+CutWebsiteLog: '切割網站日誌'
+FileOrDir: '目錄 / 檔案'
 
 #toolbox
 ErrNotExistUser: '目前使用者不存在，請修改後重試！'
diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml
index 7722679e373b..d6e47f4cfe54 100644
--- a/agent/i18n/lang/zh.yaml
+++ b/agent/i18n/lang/zh.yaml
@@ -191,11 +191,13 @@ ErrConfigAlreadyExist: "已存在同名配置文件"
 ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}"
 
 #cronjob
-ErrCutWebsiteLog: "{{ .name }} 网站日志切割失败，错误 {{ .err }}"
 CutWebsiteLogSuccess: "{{ .name }} 网站日志切割成功，备份路径 {{ .path }}"
 HandleShell: "执行脚本 {{ .name }}"
 HandleNtpSync: "系统时间同步"
 HandleSystemClean: "系统缓存清理"
+SystemLog: "系统日志"
+CutWebsiteLog: "切割网站日志"
+FileOrDir: "目录 / 文件"
 
 #toolbox
 ErrNotExistUser: "当前用户不存在，请修改后重试！"
diff --git a/agent/utils/files/file_op.go b/agent/utils/files/file_op.go
index f017f60f20ef..c7460c9509ea 100644
--- a/agent/utils/files/file_op.go
+++ b/agent/utils/files/file_op.go
@@ -742,7 +742,7 @@ func (f FileOp) TarGzCompressPro(withDir bool, src, dst, secret, exclusionRules
 		itemPrefix = ""
 	}
 	if len(secret) != 0 {
-		commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|%s/|') -zcf - %s | openssl enc -aes-256-cbc -salt -k '%s' -out %s", src, "%P\n", itemPrefix, srcItem, secret, dst)
+		commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read %s --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|%s/|') -zcf - %s | openssl enc -aes-256-cbc -salt -k '%s' -out %s", exStr, src, "%P\n", itemPrefix, srcItem, secret, dst)
 		global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
 	} else {
 		commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|%s/|') -zcf %s %s %s", src, "%P\n", itemPrefix, dst, exStr, srcItem)


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T06:26:29Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T06:26:49Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T06:26:56Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:53:40Z

Review comments:
diff_hunk: @@ -215,79 +215,80 @@ func handleWebsiteBackup(website *model.Website, parentTask *task.Task, backupDi
 			return err
 		}
 	}
+	itemHandler := doWebsiteBackup(website, backupTask, backupDir, fileName, excludes, secret)
+	backupTask.AddSubTask(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeApp), func(task *task.Task) error { return itemHandler }, nil)
+	if parentTask != nil {
+		return itemHandler
+	}
+	return backupTask.Execute()
+}
 
-	backupWebsite := func(t *task.Task) error {
-		fileOp := files.NewFileOp()
-		tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
-		if !fileOp.Stat(tmpDir) {
-			if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
-				return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
-			}
+func doWebsiteBackup(website *model.Website, parentTask *task.Task, backupDir, fileName, excludes, secret string) error {
+	fileOp := files.NewFileOp()
+	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
+	if !fileOp.Stat(tmpDir) {
+		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
+			return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
 		}
-		defer func() {
-			_ = os.RemoveAll(tmpDir)
-		}()
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir)
+	}()
+
+	remarkInfo, _ := json.Marshal(website)
+	if err := fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil {
+		return err
+	}
+	nginxConfFile := GetSitePath(*website, SiteConf)
+	if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil {
+		return err
+	}
+	parentTask.Log(i18n.GetMsgByKey("BackupNginxConfig"))
 
-		remarkInfo, _ := json.Marshal(website)
-		if err = fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil {
+	switch website.Type {
+	case constant.Deployment:
+		app, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
+		if err != nil {
 			return err
 		}
-		nginxConfFile := GetSitePath(*website, SiteConf)
-		if err = fileOp.CopyFile(nginxConfFile, tmpDir); err != nil {
+		parentTask.LogStart(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
+		if err = handleAppBackup(&app, parentTask, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, "", ""); err != nil {
 			return err
 		}
-		t.Log(i18n.GetMsgByKey("BackupNginxConfig"))
-
-		switch website.Type {
-		case constant.Deployment:
-			app, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
-			if err != nil {
-				return err
-			}
-			t.LogStart(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
-			if err = handleAppBackup(&app, backupTask, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, "", ""); err != nil {
-				return err
-			}
-			t.LogSuccess(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
-		case constant.Runtime:
-			runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
-			if err != nil {
+		parentTask.LogSuccess(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeApp))
+	case constant.Runtime:
+		runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
+		if err != nil {
+			return err
+		}
+		parentTask.LogStart(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
+		if err = handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil {
+			return err
+		}
+		parentTask.LogSuccess(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
+		if website.DbID > 0 {
+			if err = backupDatabaseWithTask(parentTask, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
 				return err
 			}
-			t.LogStart(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
-			if err = handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil {
+		}
+	case constant.Static:
+		if website.DbID > 0 {
+			if err := backupDatabaseWithTask(parentTask, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
 				return err
 			}
-			t.LogSuccess(task.GetTaskName(runtime.Name, task.TaskBackup, task.TaskScopeRuntime))
-			if website.DbID > 0 {
-				if err = backupDatabaseWithTask(t, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
-					return err
-				}
-			}
-		case constant.Static:
-			if website.DbID > 0 {
-				if err = backupDatabaseWithTask(t, website.DbType, tmpDir, website.Alias, website.DbID); err != nil {
-					return err
-				}
-			}
 		}
+	}
 
-		websiteDir := GetSitePath(*website, SiteDir)
-		t.LogStart(i18n.GetMsgByKey("CompressDir"))
-		if err = fileOp.TarGzCompressPro(true, websiteDir, path.Join(tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias)), "", excludes); err != nil {
-			return err
-		}
-		if err = fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
-			return err
-		}
-		t.Log(i18n.GetWithName("CompressFileSuccess", fileName))
-		return nil
+	websiteDir := GetSitePath(*website, SiteDir)
+	parentTask.LogStart(i18n.GetMsgByKey("CompressDir"))
+	if err := fileOp.TarGzCompressPro(true, websiteDir, path.Join(tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias)), "", excludes); err != nil {
+		return err
 	}
-	backupTask.AddSubTask(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeApp), backupWebsite, nil)
-	if parentTask != nil {
-		return backupWebsite(parentTask)
+	if err := fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
+		return err
 	}
-	return backupTask.Execute()
+	parentTask.Log(i18n.GetWithName("CompressFileSuccess", fileName))
+	return nil
 }
 
 func checkValidOfWebsite(oldWebsite, website *model.Website) error {
path: agent/app/service/backup_website.go
user: login: wanghe-fit2cloud
body: The provided code has a significant restructuring of how `doWebsiteBackup` is handled and organized. Here's a detailed review:

### Issues and Recommendations

1. **Single Function Handling Everything**: The initial version split the workflow into two functions:
    - `handleWebsiteBackup`
    - `backupWebsite`

   However, these became intertwined due to the introduction of multiple conditional branches within `backupWebsite`.

2. **Task Management**:
    - The original code used deferred calls to remove directories and log tasks but didn't fully integrate them with the task management system.

3. **Error Propagation**:
    - The logic for handling different types (deployment/runtime/static) needs improvement as it repeats similar operations.
   
4. **Code Duplication**:
    - There’s a lot of repeated code block that can be abstracted and reduced to make the code cleaner and easier to maintain.

5. **Security Concerns**:
    - Ensure that sensitive information like secrets are properly passed and handled securely.

6. **Comments and Readability**:
    - Improve comments and readability by adding more explanations where necessary.

Here is an updated version with suggested improvements:

```go
import (
	"fmt"
 "os"
)

const (
	SiteDir         = "site_dir"
	SiteConf        = "site_conf"
	TaskScopeApp      = "app_scope"
	TaskScopeRuntime = "runtime_scope"
	Deploy          = "deploy_type"
	Runtime         = "runtime_type"
	Static          = "static_type"
)

// doWebsiteBackup handles the backup process for a single website based on its type.
func doWebsiteBackup(website *model.Website, parentTask *task.Task, backupDir, fileName string, excludes []string, includeSecret bool) error {
	fileOp := files.NewFileOp()
	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))

	// Create temporary directory if it doesn't exist.
	if !fileOp.Stat(tmpDir) {
		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
			return fmt.Errorf("mkdir %s failed, err: %v", tmpDir, err)
		}
		defer func() {
			err := os.RemoveAll(tmpDir)
			if err != nil && !os.IsNotExist(err) { // Avoid logging errors from not finding the dir during cleanup.
				task.LogError(parentTask, err, i18n.GetMsgByKey("CleanupTemporaryDirectoryError"), tmpDir)
			}
		}()
	}

	// Save website details to disk.
	remarkInfo, _ := json.Marshal(website)
	if err := fileOp.SaveFile(filepath.Join(tmpDir, "website.json"), string(remarkInfo), fs.ModePerm); err != nil {
		return err
	}

	// Copy Nginx configuration file to the backup area.
	var nginxConfFile string
	switch website.Type {
	case Deploy:
		runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
		if err != nil {
			task.LogError(parentTask, err, i18n.GetMsgByKey("RetrieveRuntimeException"))
			return err
		}
		nginxConfFile = GetSitePath(*website, SiteConf)
	default:
		nginxConfFile = filepath.Join(tmpDir, "default.conf")
	endIf runtimeType == Runtime

	if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil {
		task.LogError(parentTask, err, i18n.GetMsgByKey("CopyNginxConfigError"), nginxConfFile)
		return err
	}

	parentTask.Log(i18n.GetMsgByKey("BackupWebserverConfiguration"))

	handleBackupTask := func(task *task.Task) error {
		switch website.Type {
		case Deploy:
			app, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
			if err != nil {
				task.LogError(parentTask, err, i18n.GetMsgByKey("RetrieveApplication"))
				return err
			}
			logName := appendLogNamesByParentAndAction("", task.GetName())
			subTaskContext := &task.Context{
				Name:             logName,
				SubtasksCompleted: 0,
				TotalTasks:       len(tasks),
			}
			childTask, childCtx := createChildTask(logName, subTaskContext)
			err := handleAppBackup(app, childTask, tmpDir, fmt.Sprintf("%s.app.tar.gz", webAlias), excludes, "", "")
			if err != nil {
				childCtx.AbortWithError(err, i18n.GetWithName("HandleAppBackupFailure", website.Name))
				return err
			}
			err = childTask.WaitWithTimeout(ctx, time.Hour*4) // Timeout after 4 hours per database.
			if err != nil {
				task.LogError(childTask, "", i18n.GetWithName("WaitForDatabaseBackupFailed", website.Alias))
			}
			break
				// Similar cases for other types (Runtime, Static).
			default:
				// No specific actions needed for static websites.
		}
		parentTask.LogStart(task.GetName())

		return nil
	}

	// Add sub-task for application-specific backups.
	parentTask.AddSubTask(i18n.Sprint("{Website} Backup Sub Task", website.Name), handleBackupTask, &struct{}{})

	// Execute all sub-tasks concurrently.
	return executeParallel(parentTask, []*task.SubTask{parentTask}, context.TODO(), true)
}

// helper function to run tasks sequentially
func executeSequential(task *task.Task, ctx context.Context) error {
	for _, subTask := range task.Tasks() {
		if err := subTask.Run(); err != nil {
			return err
		}
	}
	return nil
}

// Handle concurrent execution of tasks
func executeParallel(task *task.Task, children []*task.SubTask, ctx context.Context, waitForCompletion bool) error {
	wg := sync.WaitGroup{}

	for _, child := range children {
		wg.Add(len(child.Tasks()))
		go func(subTask *task.SubTask) {
			if err := executeSequential(child, ctx); err != nil {
				// Log each error separately when parallelizing and waiting to complete all.
				task.LogError(child, "", err, i18n.GetMsgByKey("ExecutionError"))
			}
			wg.Done()
		}(child)
	}

	if waitForCompletion {
		wg.Wait() // Wait until all goroutines/children finish their work before returning.
	}

	return nil
}
```

These changes help improve readability, manage task flow better using concurrency control mechanisms and proper error handling.
created_at: 2025-05-09T06:26:29Z
author_association: MEMBER
diff_hunk: @@ -367,6 +332,43 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
 	return recoverTask.Execute()
 }
 
+func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fileName, excludes, secret string) error {
+	fileOp := files.NewFileOp()
+	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
+	if !fileOp.Stat(tmpDir) {
+		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
+			return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err)
+		}
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir)
+	}()
+
+	remarkInfo, _ := json.Marshal(install)
+	remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir)
+	if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil {
+		return err
+	}
+
+	appPath := install.GetPath()
+	if err := fileOp.TarGzCompressPro(true, appPath, path.Join(tmpDir, "app.tar.gz"), "", excludes); err != nil {
+		return err
+	}
+
+	resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
+	for _, resource := range resources {
+		if err := backupDatabaseWithTask(parentTask, resource.Key, tmpDir, install.Name, resource.ResourceId); err != nil {
+			return err
+		}
+	}
+	parentTask.LogStart(i18n.GetMsgByKey("CompressDir"))
+	if err := fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, ""); err != nil {
+		return err
+	}
+	parentTask.Log(i18n.GetWithName("CompressFileSuccess", fileName))
+	return nil
+}
+
 func reCreateDB(dbID uint, database model.Database, oldEnv string) (*model.DatabaseMysql, map[string]interface{}, error) {
 	mysqlService := NewIMysqlService()
 	ctx := context.Background()
path: agent/app/service/backup_app.go
user: login: wanghe-fit2cloud
body: The code you provided involves refactoring the `handleAppBackup` function into a separate helper function called `doAppBackup`. This change reduces duplicated code and makes it easier to maintain.

### Potential Irregularities:
1. **Variable Scope**: The new variable `itemHandler` has a narrower scope, which might be more appropriate for this particular use case where a sub-task is added with this handler rather than executing the backup directly from within the current task.
   
2. **Error Handling**: Both functions have similar error handling patterns, but using different variables (`err` vs. `_`) can lead to confusion when debugging.

3. **Logging**: There's no explicit logging call for "Starting Backup" at the begin of the `doAppBackup` function. It would be beneficial to log an informational message if possible to indicate start and end points.

4. **Documentation Improvements**: Adding comments above both functions could enhance readability and help other developers understand their responsibilities.

### Optimization Suggestions (General):
- Consider abstracting common logic between backup and recovery processes into shared methods or utility objects to decrease repetition and improve performance.
- Use named return values for clarity, especially when returning multiple items together in a single return statement.

Here’s the revised version with these considerations:

```go
// Function to handle application backups, utilizing separate function for business logic encapsulation.
func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fileName, excludes, secret string) error {
	fileOp := files.NewFileOp()

	// Temporary directory setup for backing up files and databases.
	tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", ""))
	if !fileOp.Stat(tmpDir) {
		if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
			return fmt.Errorf("failed to create temporary dir: %w", err)
		}
	}

	// Store metadata of the installed application.
	remarkInfo, _ := json.Marshal(install)
	err := fileOp.SaveFile(fmt.Sprintf("%s/app.json", tmpDir), string(remarkInfo), fs.ModePerm)
	if err != nil {
		return err
	}

	appPath := install.GetPath()
	err = fileOp.TarGzCompressPro(true, appPath, path.Join(tmpDir, "app.tar.gz"), "", excludes)
	if err != nil {
		return err
	}

	// Back up associated databases.
	databases, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
	for _, db := range databases {
		if err := backupDatabaseWithTask(parentTask, db.Key, tmpDir, install.Name, db.ResourceId); err != nil {
			return err
		}
	}

	// Log that compression for data directories started.
	parentTask.LogStart(i18n.GetMsgByKey("CompressDir"))

	// Compress the entire folder back into a ZIP archive.
	err = fileOp.TarGzCompressPro(true, tmpDir, path.Join(backupDir, fileName), secret, "")
	if err != nil {
		return err
	}

	// Log completion of compressing the backup file.
	parentTask.Log(i18n.GetWithName("CompressFileSuccess", fileName))

	// Clean up the temporary directory after backup process completes successfully.
	return os.RemoveAll(tmpDir) // Ensure cleanup on success only.
}
```

This approach improves modularity, readability, and scalability without introducing significant complexity.
created_at: 2025-05-09T06:26:49Z
author_association: MEMBER
diff_hunk: @@ -132,68 +139,65 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID uint) err
 				_ = itemHelper.snapAgentDB.Where("id = ?", jobID).Delete(&model.JobRecords{}).Error
 			}
 			return nil
-		},
-		nil,
+		}, nil, int(retry), time.Duration(timeout)*time.Second,
 	)
 
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapBaseInfo" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapBaseInfo",
 			func(t *task.Task) error { return snapBaseData(itemHelper, baseDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapInstallApp" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapInstallApp",
 			func(t *task.Task) error { return snapAppImage(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapLocalBackup" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapLocalBackup",
 			func(t *task.Task) error { return snapBackupData(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapPanelData" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapPanelData",
 			func(t *task.Task) error { return snapPanelData(itemHelper, req, rootDir) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 
-	taskItem.AddSubTaskWithAlias(
+	taskItem.AddSubTaskWithAliasAndOps(
 		"SnapCloseDBConn",
 		func(t *task.Task) error {
 			taskItem.Log("---------------------- 6 / 8 ----------------------")
 			common.CloseDB(itemHelper.snapAgentDB)
 			common.CloseDB(itemHelper.snapCoreDB)
 			return nil
-		},
-		nil,
+		}, nil, int(retry), time.Duration(timeout)*time.Second,
 	)
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapCompress" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapCompress",
 			func(t *task.Task) error { return snapCompress(itemHelper, rootDir, req.Secret) },
-			nil,
+			nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
 	if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
-		taskItem.AddSubTaskWithAlias(
+		taskItem.AddSubTaskWithAliasAndOps(
 			"SnapUpload",
 			func(t *task.Task) error {
 				return snapUpload(itemHelper, req.SourceAccountIDs, fmt.Sprintf("%s.tar.gz", rootDir))
-			},
-			nil,
+			}, nil, int(retry), time.Duration(timeout)*time.Second,
 		)
 		req.InterruptStep = ""
 	}
path: agent/app/service/snapshot_create.go
user: login: wanghe-fit2cloud
body: 1. **Function Overload**:
   - The `SnapshotCreate` function now has an additional parameter `parentTask`. This can be removed or used as needed.

2. **Logging in `handleSnapshot` Function**:
   - Consider adding more specific logging messages within the sub-tasks instead of just "----------------------".

3. **Retry Loop and Timeout Handling**:
   - Currently, each sub-task is retried once without considering the `retry` value unless specified via the `taskItem`.
   - Ensure that retries are appropriately handled in the actual implementation based on these parameters.

4. **Timeout Logic**:
   - Added default timeout to 1800 seconds if `timeout` parameter isn't provided.
   - Each sub-task should ideally have its own retry logic considering total elapsed time.

5. **Code Style**:
   - There’s no significant issue with code style but ensure consistency throughout (`indentation`, `spacing`) and readability improvements where applicable.
created_at: 2025-05-09T06:26:56Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-05-09T06:25:46Z
body: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T06:27:23Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8580) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [10 New issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8580&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8580&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=1Panel-dev_1Panel&pullRequest=8580&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8580&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8580&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8580)


author_association: NONE
user: login: wanghe-fit2cloud
created_at: 2025-05-09T07:53:51Z
body: /approve
author_association: MEMBER
user: login: f2c-ci-robot[bot]
created_at: 2025-05-09T07:53:59Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/1Panel-dev/1Panel/pull/8580#issuecomment-2865534308" title="Approved">wanghe-fit2cloud</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=1Panel-dev%2F1Panel).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/1Panel-dev/1Panel/blob/dev-v2/OWNERS)~~ [wanghe-fit2cloud]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE