Repository: 1Panel-dev/1Panel
Pull request #8085: fix: Split the system log files
Author: login: ssongliu
Labels: name: do-not-merge/release-note-label-needed
name: dev-v2
Created: 2025-03-06T09:25:16Z | Merged: 2025-03-06T09:40:07Z



Diff:
diff --git a/agent/app/service/file.go b/agent/app/service/file.go
index a41ebc919a25..7bb69a8d239a 100644
--- a/agent/app/service/file.go
+++ b/agent/app/service/file.go
@@ -446,10 +446,17 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
 		logFilePath = ssl.GetLogPath()
 	case constant.TypeSystem:
 		fileName := ""
-		if len(req.Name) == 0 || req.Name == time.Now().Format("2006-01-02") {
+		if len(req.Name) == 0 {
 			fileName = "1Panel.log"
 		} else {
-			fileName = "1Panel-" + req.Name + ".log"
+			if strings.HasSuffix(req.Name, time.Now().Format("2006-01-02")) {
+				fileName = "1Panel.log"
+				if strings.HasPrefix(req.Name, "Core-") {
+					fileName = "1Panel-Core.log"
+				}
+			} else {
+				fileName = "1Panel-" + req.Name + ".log"
+			}
 		}
 		logFilePath = path.Join(global.Dir.DataDir, "log", fileName)
 		if _, err := os.Stat(logFilePath); err != nil {
diff --git a/agent/app/service/logs.go b/agent/app/service/logs.go
index 64c4d30a78d9..97def0acca5f 100644
--- a/agent/app/service/logs.go
+++ b/agent/app/service/logs.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"os"
 	"path"
-	"path/filepath"
 	"sort"
 	"strings"
 	"time"
@@ -25,35 +24,45 @@ func NewILogService() ILogService {
 }
 
 func (u *LogService) ListSystemLogFile() ([]string, error) {
-	var files []string
-	if err := filepath.Walk(global.Dir.LogDir, func(pathItem string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel") {
-			if info.Name() == "1Panel.log" {
-				files = append(files, time.Now().Format("2006-01-02"))
-				return nil
+	var listFile []string
+	files, err := os.ReadDir(global.Dir.LogDir)
+	if err != nil {
+		return nil, err
+	}
+	listMap := make(map[string]struct{})
+	for _, item := range files {
+		if !item.IsDir() && strings.HasPrefix(item.Name(), "1Panel") {
+			if item.Name() == "1Panel.log" || item.Name() == "1Panel-Core.log" {
+				itemName := time.Now().Format("2006-01-02")
+				if _, ok := listMap[itemName]; ok {
+					continue
+				}
+				listMap[itemName] = struct{}{}
+				listFile = append(listFile, itemName)
+				continue
 			}
-			itemFileName := strings.TrimPrefix(info.Name(), "1Panel-")
+			itemFileName := strings.TrimPrefix(item.Name(), "1Panel-Core-")
+			itemFileName = strings.TrimPrefix(itemFileName, "1Panel-")
 			itemFileName = strings.TrimSuffix(itemFileName, ".gz")
 			itemFileName = strings.TrimSuffix(itemFileName, ".log")
-			files = append(files, itemFileName)
-			return nil
+			if len(itemFileName) == 0 {
+				continue
+			}
+			if _, ok := listMap[itemFileName]; ok {
+				continue
+			}
+			listMap[itemFileName] = struct{}{}
+			listFile = append(listFile, itemFileName)
 		}
-		return nil
-	}); err != nil {
-		return nil, err
 	}
-
-	if len(files) < 2 {
-		return files, nil
+	if len(listFile) < 2 {
+		return listFile, nil
 	}
-	sort.Slice(files, func(i, j int) bool {
-		return files[i] > files[j]
+	sort.Slice(listFile, func(i, j int) bool {
+		return listFile[i] > listFile[j]
 	})
 
-	return files, nil
+	return listFile, nil
 }
 
 func (u *LogService) LoadSystemLog(name string) (string, error) {
diff --git a/core/cmd/server/conf/app.yaml b/core/cmd/server/conf/app.yaml
index ec5b0d326eb7..6c6977682bd9 100644
--- a/core/cmd/server/conf/app.yaml
+++ b/core/cmd/server/conf/app.yaml
@@ -16,6 +16,6 @@ remote_url:
 log:
   level: debug
   time_zone: Asia/Shanghai
-  log_name: 1Panel
+  log_name: 1Panel-Core
   log_suffix: .log
   max_backup: 10
\ No newline at end of file
diff --git a/frontend/src/components/system-upgrade/index.vue b/frontend/src/components/system-upgrade/index.vue
index 1a13ae33ec91..8b97934521c4 100644
--- a/frontend/src/components/system-upgrade/index.vue
+++ b/frontend/src/components/system-upgrade/index.vue
@@ -108,15 +108,10 @@ const onLoadUpgradeInfo = async () => {
                 upgradeInfo.value = res.data;
                 if (upgradeInfo.value.newVersion) {
                     upgradeVersion.value = upgradeInfo.value.newVersion;
-                    return;
-                }
-                if (upgradeInfo.value.latestVersion) {
+                } else if (upgradeInfo.value.latestVersion) {
                     upgradeVersion.value = upgradeInfo.value.latestVersion;
-                    return;
-                }
-                if (upgradeInfo.value.testVersion) {
+                } else if (upgradeInfo.value.testVersion) {
                     upgradeVersion.value = upgradeInfo.value.testVersion;
-                    return;
                 }
                 upgradeRef.value.acceptParams({ upgradeInfo: upgradeInfo.value, upgradeVersion: upgradeVersion.value });
             } else {
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 5b96700a4593..5d37792c2f07 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -1253,6 +1253,8 @@ const message = {
         },
     },
     logs: {
+        core: 'Panel Service',
+        agent: 'Node Monitoring',
         panelLog: 'Panel Logs',
         operation: 'Operation Logs',
         login: 'Login Logs',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index 0ae0955285b9..4fe143b8007a 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -1190,6 +1190,8 @@ const message = {
         },
     },
     logs: {
+        core: 'パネルサービス',
+        agent: 'ノード監視',
         panelLog: 'パネルログ',
         operation: '操作ログ',
         login: 'ログインログ',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index 29c888606add..22e6875c0067 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -1178,6 +1178,8 @@ const message = {
         },
     },
     logs: {
+        core: '패널 서비스',
+        agent: '노드 모니터링',
         panelLog: '패널 로그',
         operation: '작업 로그',
         login: '로그인 로그',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index 93adac56cd85..1968b39bfe49 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -1233,6 +1233,8 @@ const message = {
         },
     },
     logs: {
+        core: 'Perkhidmatan Panel',
+        agent: 'Pemantauan Nod',
         panelLog: 'Log Panel',
         operation: 'Log Operasi',
         login: 'Log Masuk',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index a59cfd3ce79d..56582ed4ea76 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -1216,6 +1216,8 @@ const message = {
         alertTitle: 'Tarefa de scan de vírus 「{0}」 detectou alerta de arquivo infectado',
     },
     logs: {
+        core: 'Serviço de Painel',
+        agent: 'Monitoramento de Nós',
         panelLog: 'Logs do painel',
         operation: 'Logs de operação',
         login: 'Logs de login',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 5c5121c3c77e..a1ce45b1dbd5 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -1224,6 +1224,8 @@ const message = {
         },
     },
     logs: {
+        core: 'Сервис панели',
+        agent: 'Мониторинг узлов',
         panelLog: 'Логи панели',
         operation: 'Логи операций',
         login: 'Логи входа',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 3d88002d0fb2..e7a39749da2b 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -1183,6 +1183,8 @@ const message = {
         },
     },
     logs: {
+        core: '面板服務',
+        agent: '節點監控',
         panelLog: '面板日誌',
         operation: '操作日誌',
         login: '訪問日誌',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 6a9cd998e6c8..60520fe515ce 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -1181,6 +1181,8 @@ const message = {
         },
     },
     logs: {
+        core: '面板服务',
+        agent: '节点监控',
         panelLog: '面板日志',
         operation: '操作日志',
         login: '访问日志',
diff --git a/frontend/src/views/log/system/index.vue b/frontend/src/views/log/system/index.vue
index d70a827609f3..0a9a648bb909 100644
--- a/frontend/src/views/log/system/index.vue
+++ b/frontend/src/views/log/system/index.vue
@@ -5,7 +5,7 @@
                 <LogRouter current="SystemLog" />
             </template>
             <template #leftToolBar>
-                <el-select class="p-w-200 mr-2.5" v-model="logConfig.name" @change="search()">
+                <el-select class="p-w-200 mr-2.5" v-model="itemName" @change="search()">
                     <template #prefix>{{ $t('commons.table.date') }}</template>
                     <el-option v-for="(item, index) in fileList" :key="index" :label="item" :value="item" />
                 </el-select>
@@ -14,6 +14,10 @@
                         {{ $t('commons.button.watch') }}
                     </el-checkbox>
                 </el-button>
+                <el-radio-group class="ml-2" @change="search()" v-model="itemType">
+                    <el-radio-button :label="$t('logs.agent')" value="Agent" />
+                    <el-radio-button :label="$t('logs.core')" value="Core" />
+                </el-radio-group>
             </template>
             <template #main>
                 <LogFile
@@ -48,6 +52,8 @@ const logConfig = reactive({
     colorMode: 'system',
 });
 const showLog = ref(false);
+const itemName = ref();
+const itemType = ref('Agent');
 
 const changeTail = () => {
     logRef.value.changeTail(true);
@@ -57,12 +63,13 @@ const loadFiles = async () => {
     const res = await getSystemFiles();
     fileList.value = res.data || [];
     if (fileList.value) {
-        logConfig.name = fileList.value[0];
+        itemName.value = fileList.value[0];
         search();
     }
 };
 
 const search = () => {
+    logConfig.name = itemType.value === 'Agent' ? itemName.value : 'Core-' + itemName.value;
     showLog.value = false;
     nextTick(() => {
         showLog.value = true;


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:25:41Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:25:51Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:25:58Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:40:01Z

Review comments:
diff_hunk: @@ -25,35 +24,45 @@ func NewILogService() ILogService {
 }
 
 func (u *LogService) ListSystemLogFile() ([]string, error) {
-	var files []string
-	if err := filepath.Walk(global.Dir.LogDir, func(pathItem string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel") {
-			if info.Name() == "1Panel.log" {
-				files = append(files, time.Now().Format("2006-01-02"))
-				return nil
+	var listFile []string
+	files, err := os.ReadDir(global.Dir.LogDir)
+	if err != nil {
+		return nil, err
+	}
+	listMap := make(map[string]struct{})
+	for _, item := range files {
+		if !item.IsDir() && strings.HasPrefix(item.Name(), "1Panel") {
+			if item.Name() == "1Panel.log" || item.Name() == "1Panel-Core.log" {
+				itemName := time.Now().Format("2006-01-02")
+				if _, ok := listMap[itemName]; ok {
+					continue
+				}
+				listMap[itemName] = struct{}{}
+				listFile = append(listFile, itemName)
+				continue
 			}
-			itemFileName := strings.TrimPrefix(info.Name(), "1Panel-")
+			itemFileName := strings.TrimPrefix(item.Name(), "1Panel-Core-")
+			itemFileName = strings.TrimPrefix(itemFileName, "1Panel-")
 			itemFileName = strings.TrimSuffix(itemFileName, ".gz")
 			itemFileName = strings.TrimSuffix(itemFileName, ".log")
-			files = append(files, itemFileName)
-			return nil
+			if len(itemFileName) == 0 {
+				continue
+			}
+			if _, ok := listMap[itemFileName]; ok {
+				continue
+			}
+			listMap[itemFileName] = struct{}{}
+			listFile = append(listFile, itemFileName)
 		}
-		return nil
-	}); err != nil {
-		return nil, err
 	}
-
-	if len(files) < 2 {
-		return files, nil
+	if len(listFile) < 2 {
+		return listFile, nil
 	}
-	sort.Slice(files, func(i, j int) bool {
-		return files[i] > files[j]
+	sort.Slice(listFile, func(i, j int) bool {
+		return listFile[i] > listFile[j]
 	})
 
-	return files, nil
+	return listFile, nil
 }
 
 func (u *LogService) LoadSystemLog(name string) (string, error) {
path: agent/app/service/logs.go
user: login: wanghe-fit2cloud
body: The provided code appears to have several issues and areas for improvement:

1. **Error Handling**: The logic for reading files from `global.Dir.LogDir` uses both `filepath.Walk` and `os.ReadDir`, with an unnecessary error chain at the end of each function.

2. **Repetition**: There is repeated code where similar file filtering and appending operations are performed using different methods (`filepath.Walk` and `os.ReadDir`). This can be simplified.

3. **Logging File Names**: The filenames used for sorting are generated based on current dates but not used further to filter logs. Consider including other criteria like log size for more meaningful sorting.

4. **Concurrency Issues**: If this function were designed to handle multiple systems or directories concurrently from within a service, it should use Go's concurrency mechanisms (e.g., goroutines) to improve performance and responsiveness.

Here are some suggestions to address these points:

### Cleaned Up Version

```go
import (
	"fmt"
	"go.uber.org/zap"
	"io/ioutil"
	"path/filepath"
	"os"
	"strings"
	"time"
)

func ListSysLogs(dir string) ([]string, error) {
	var logs []*syslogEntry

	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		name := info.Name()
		fname := strings.TrimPrefix(name, "1Panel")
		switch fname {
		case "1Panel.log", "1Panel-Core.log":
			timestamp := time.Now().Format("2006-01-02")
			logs = append(logs, &syslogEntry{
				Time: timestamp,
				Name: name,
			})
		}
		return nil
	})

	if err != nil {
		return nil, err
	}

	sort.Slice(logs, func(i, j int) bool { return logs[i].Time > logs[j].Time })
	lists := make([]string, len(logs))
	for i, l := range logs {
		lists[i] = l.Name
	}

	return lists, nil
}

type syslogEntry struct {
	Time string
	Name string
}

func LoadSysLog(ctx context.Context, dir, fileName string, logger *zap.SugaredLogger) (string, error) {
	entryPath := filepath.Join(dir, fileName)
	data, err := ioutil.ReadFile(entryPath)
	if err != nil {
		logger.Errorw("Failed to read system log", zap.String("file", entryPath), zap.Any("error", err))
		return "", fmt.Errorf("failed to read system log %v: %w", entryPath, err)
	}

	return string(data), nil
}
```

### Key Changes and Improvements:

- **Unified Error Handling**: Simplified error handling by consistently returning errors if they occur early.
- **Efficient Filtering**: Combined two distinct walking approaches into one loop to reduce repetition.
- **Detailed Timestamps**: Created a custom type `syslogEntry` to store timestamps alongside log names for easier comparison and processing.
- **Concurrency Management**: For larger datasets or concurrent requests, consider adding context-aware loading of files and improving logging verbosity.
- **Context Dependency**: Introduced a `context.Context` parameter in the loading function to pass around cancellations, timeouts, etc.

This refactored version aims to make the code cleaner, more efficient, and better prepared for potential enhancements and integration with other parts of a larger application.
created_at: 2025-03-06T09:25:41Z
author_association: MEMBER
diff_hunk: @@ -446,10 +446,17 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
 		logFilePath = ssl.GetLogPath()
 	case constant.TypeSystem:
 		fileName := ""
-		if len(req.Name) == 0 || req.Name == time.Now().Format("2006-01-02") {
+		if len(req.Name) == 0 {
 			fileName = "1Panel.log"
 		} else {
-			fileName = "1Panel-" + req.Name + ".log"
+			if strings.HasSuffix(req.Name, time.Now().Format("2006-01-02")) {
+				fileName = "1Panel.log"
+				if strings.HasPrefix(req.Name, "Core-") {
+					fileName = "1Panel-Core.log"
+				}
+			} else {
+				fileName = "1Panel-" + req.Name + ".log"
+			}
 		}
 		logFilePath = path.Join(global.Dir.DataDir, "log", fileName)
 		if _, err := os.Stat(logFilePath); err != nil {
path: agent/app/service/file.go
user: login: wanghe-fit2cloud
body: The provided code contains several potential issues and areas for improvement:

1. **Error Handling**: The check `if _, err := os.Stat(logFilePath); err != nil` does not handle errors appropriately. It only logs them but doesn't return or use the error value to control program flow.

2. **Variable Naming**: Some variable names could be improved for clarity. For example, `fileName` can be renamed to something like `logFileName` to make it more descriptive of its purpose.

3. **String Suffix Check**: The condition for setting `fileName` when `req.Name` ends with the current date format is redundant if it's already set based on whether `req.Name` is empty.

4. **Conditional Logic**: The conditional logic inside the else block can be simplified using logical operators, making it more readable.

Here’s an optimized version of the code with these improvements:

```go
func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileReadByLineRes, error) {
	logFilePath := ssl.GetLogPath()
	
	switch req.Type {
	case constant.TypeUser:
		logFilePath = path.Join(global.Dir.UserDir, logFilePath)
	default:
		return nil, fmt.Errorf("unsupported type %s", req.Type)
	}

	var fileName string
	if len(req.Name) == 0 {
		fileName = "1Panel.log"
	} else {
		if strings.HasSuffix(req.Name, time.Now().Format("2006-01-02")) && strings.HasPrefix(req.Name, "Core-") {
			fileName = "1Panel-Core.log"
		} else {
			fileName = "1Panel-" + req.Name + ".log"
		}
	}

	err := osStatAndLogErr(logFilePath)
	if err != nil {
		return &response.FileReadByLineRes{}, err
	}

	// Assume osStatAndLogErr() returns true if a non-nil error occurred
	if err {
		return &response.FileReadByLineRes{}, io.EOF // Assuming we want to mimic EOF after reading a non-existent file
	}

	// Further processing here...

	return &response.FileReadByLineRes{Contents: []byte()}, nil
}

func osStatAndLogErr(filePath string) bool {
	_, err := os.Stat(filePath)
	if err != nil {
		fmt.Println("Failed to stat:", filePath)
		return false
	}
	return true
}
```

### Key Points:
- **Error Handling**: The function now properly handles errors by returning an error from the method signature.
- **Code Simplification**: The conditional blocks have been cleaned up and made more efficient.
- **Logging Errors**: Logs the error message to standard out, which helps in debugging.
- **Function Names**: Improved naming conventions for better readability.
created_at: 2025-03-06T09:25:51Z
author_association: MEMBER
diff_hunk: @@ -57,12 +63,13 @@ const loadFiles = async () => {
     const res = await getSystemFiles();
     fileList.value = res.data || [];
     if (fileList.value) {
-        logConfig.name = fileList.value[0];
+        itemName.value = fileList.value[0];
         search();
     }
 };
 
 const search = () => {
+    logConfig.name = itemType.value === 'Agent' ? itemName.value : 'Core-' + itemName.value;
     showLog.value = false;
     nextTick(() => {
         showLog.value = true;
path: frontend/src/views/log/system/index.vue
user: login: wanghe-fit2cloud
body: ### Code Differences Summary

1. **Template Changes**:
   - **Left Toolbar Element**: Changed from `LogRouter.current` to `logConfig.name`.
   - **Additional Filter Inputs**: Added an input group for selecting between "Agent" and "Core".

2. **Data Model Updates**:
   - **New Variable**: Introduced `itemName` of type `ref()` to store the currently selected value from the dropdown (either a file name or prefixed with "Core").
   - **Updated Variable**: Replaced `log_config.name` with `itemType`.

3. **Search Functionality Update**:
   - **Conditioned Log Name**: The `search()` function now dynamically sets `logConfig.name` based on whether `itemType` is "Agent". If not, it appends a prefix.
   
These changes suggest that the component might be handling different types of logs ("Agents" vs. "Cores") within its configuration. The new radio buttons allow users to choose which type of log they want to view, impacting how their searches are conducted.

## Additional Suggestions

1. **Improved Search Logic**:
   - Ensure that when `itemType` switches to "Core", the search should correctly filter only those logs that start with “Core-” instead of adding this prefix before each selection if the user selects another option first.

2. **Error Handling**:
   - Add error handling around asynchronous operations like loading files (`getSystemFiles`) using try/catch blocks to manage network errors gracefully.

3. **User Feedback**:
   - Provide clear feedback to the user (e.g., a toast message) whenever there's no data available in the fileList.

4. **Responsive Design**:
   - Consider adjusting the layout and styles to ensure responsiveness across various screen sizes, especially given the added filter inputs.

5. **Documentation**:
   - Update the component's documentation to reflect these changes, including information about the new `itemType` parameter and any additional fields (`itemName`). 

Overall, these adjustments will enhance the functionality and usability of the logging system while maintaining flexibility and correctness.
created_at: 2025-03-06T09:25:58Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-03-06T09:25:20Z
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: f2c-ci-robot[bot]
created_at: 2025-03-06T09:25:24Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from [ssongliu](https://github.com/ssongliu). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

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

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":["ssongliu"]} -->
author_association: NONE