From 66e6153345074277ffab31712183eed0f22fba7f Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Thu, 14 May 2026 09:55:35 +0800 Subject: [PATCH] chore: refactor cmd base caller --- cmd/builtininstall/builtininstall.go | 8 +-- cmd/clibase/clibase.go | 45 +++++++++++++ cmd/clibase/cliclient.go | 40 ++++++++++++ cmd/clibase/clilog.go | 71 ++++++++++++++++++++ cmd/reposync/reposync.go | 70 ++------------------ cmd/resetpwd/resetpwd.go | 19 ++---- cmd/restore/restore.go | 10 +-- cmd/task/task.go | 96 ++++++---------------------- 8 files changed, 190 insertions(+), 169 deletions(-) create mode 100644 cmd/clibase/clibase.go create mode 100644 cmd/clibase/cliclient.go create mode 100644 cmd/clibase/clilog.go diff --git a/cmd/builtininstall/builtininstall.go b/cmd/builtininstall/builtininstall.go index b34df8e..49a2091 100644 --- a/cmd/builtininstall/builtininstall.go +++ b/cmd/builtininstall/builtininstall.go @@ -2,22 +2,18 @@ package builtininstall import ( "flag" - "fmt" "os" "os/exec" "path/filepath" "runtime" + "github.com/engigu/baihu-panel/cmd/clibase" "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/utils" ) func printHelp() { - fmt.Fprintf(os.Stderr, "\n白虎面板内建依赖安装工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu builtininstall\n\n") - fmt.Fprintf(os.Stderr, "说明:\n") - fmt.Fprintf(os.Stderr, " 此命令将为系统中已安装的 Node.js 和 Python 环境安装白虎面板所需的内建依赖包。\n\n") + clibase.PrintSubCommandUsage("白虎面板内建依赖安装工具", "baihu builtininstall", "", nil) } // Run 执行内建包安装逻辑 diff --git a/cmd/clibase/clibase.go b/cmd/clibase/clibase.go new file mode 100644 index 0000000..2715bbc --- /dev/null +++ b/cmd/clibase/clibase.go @@ -0,0 +1,45 @@ +package clibase + +import ( + "flag" + "fmt" + "os" + + "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/internal/services" +) + +// InitContext 统一封装命令行所需的初始化上下文逻辑 +func InitContext(requireSettings bool) error { + bootstrap.InitBasicForCmd() + if requireSettings { + settingsService := services.NewSettingsService() + if err := settingsService.InitSettings(); err != nil { + return fmt.Errorf("初始化系统设置失败: %w", err) + } + } + return nil +} + +// PrintDBConfigHint 打印标准化的连接或检索失败时的排查指引 +func PrintDBConfigHint(commandExample string) { + fmt.Println(">> 提示: 程序当前可能连接到了默认的空 SQLite 数据库。") + fmt.Println(">> 若您的生产环境使用的是 MySQL 或指定路径配置,请在执行命令时携带配置文件路径环境变量,例如:") + fmt.Printf(">> BH_CONFIG_PATH=/app/data/config.ini baihu %s\n", commandExample) +} + +// PrintSubCommandUsage 打印一致风格的子程序帮助信息 +func PrintSubCommandUsage(title, usageStr, exampleStr string, fs *flag.FlagSet) { + fmt.Fprintf(os.Stderr, "\n%s\n\n", title) + fmt.Fprintf(os.Stderr, "用法:\n") + fmt.Fprintf(os.Stderr, " %s\n\n", usageStr) + if fs != nil { + fmt.Fprintf(os.Stderr, "参数说明:\n") + fs.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n") + } + if exampleStr != "" { + fmt.Fprintf(os.Stderr, "示例:\n") + fmt.Fprintf(os.Stderr, "%s\n\n", exampleStr) + } +} diff --git a/cmd/clibase/cliclient.go b/cmd/clibase/cliclient.go new file mode 100644 index 0000000..4e016a8 --- /dev/null +++ b/cmd/clibase/cliclient.go @@ -0,0 +1,40 @@ +package clibase + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/engigu/baihu-panel/internal/bootstrap" +) + +// CallInternalAPI 封装底层进程间 HTTP 通信,统一处理网络连接错误及业务级异常提取 +func CallInternalAPI(method, endpoint string, payload any) ([]byte, error) { + bodyBytes, statusCode, err := bootstrap.SendInternalRequest(method, endpoint, payload) + if err != nil { + return nil, fmt.Errorf("无法连接到主程序后台服务: %w", err) + } + + if statusCode != 200 { + return bodyBytes, fmt.Errorf("后台服务响应异常 (状态码: %d): %s", statusCode, strings.TrimSpace(string(bodyBytes))) + } + + // 尝试通用结构体嗅探,提取业务级逻辑拒绝原因 + var res struct { + Data struct { + Success *bool `json:"success"` + Error string `json:"error"` + } `json:"data"` + } + if err := json.Unmarshal(bodyBytes, &res); err == nil { + if res.Data.Success != nil && !*res.Data.Success { + errReason := res.Data.Error + if errReason == "" { + errReason = strings.TrimSpace(string(bodyBytes)) + } + return bodyBytes, fmt.Errorf("%s", errReason) + } + } + + return bodyBytes, nil +} diff --git a/cmd/clibase/clilog.go b/cmd/clibase/clilog.go new file mode 100644 index 0000000..1002ae0 --- /dev/null +++ b/cmd/clibase/clilog.go @@ -0,0 +1,71 @@ +package clibase + +import ( + "bytes" + "io" + "regexp" + "strings" +) + +// AnsiRegex 匹配终端 ANSI 控制序列的通用正则表达式 +var AnsiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") + +// CleanWriter 过滤输出流中的终端回车符覆写及 ANSI 色彩代码 +type CleanWriter struct { + out io.Writer + buf []byte +} + +// NewCleanWriter 构造输出清洗器 +func NewCleanWriter(out io.Writer) *CleanWriter { + return &CleanWriter{out: out} +} + +func (c *CleanWriter) Write(p []byte) (n int, err error) { + c.buf = append(c.buf, p...) + + for { + idx := bytes.IndexAny(c.buf, "\r\n") + if idx == -1 { + break + } + + if c.buf[idx] == '\r' && idx == len(c.buf)-1 { + // 跨块截断的回车,等待下一块 + break + } + + char := c.buf[idx] + line := string(c.buf[:idx]) + c.buf = c.buf[idx+1:] + + if char == '\r' && len(c.buf) > 0 && c.buf[0] == '\n' { + c.buf = c.buf[1:] + char = '\n' + } + + s := AnsiRegex.ReplaceAllString(line, "") + + if char == '\r' { + continue // 忽略终端进度条的同行覆盖 + } + + if s != "" { + c.out.Write([]byte(s + "\n")) + } + } + return len(p), nil +} + +// Flush 输出末尾缓冲 +func (c *CleanWriter) Flush() { + if len(c.buf) > 0 { + s := string(c.buf) + s = strings.TrimSuffix(s, "\r") + s = AnsiRegex.ReplaceAllString(s, "") + if s != "" { + c.out.Write([]byte(s + "\n")) + } + c.buf = nil + } +} diff --git a/cmd/reposync/reposync.go b/cmd/reposync/reposync.go index 0295fdd..07ebf7e 100644 --- a/cmd/reposync/reposync.go +++ b/cmd/reposync/reposync.go @@ -1,7 +1,6 @@ package reposync import ( - "bytes" "flag" "fmt" "io" @@ -13,7 +12,7 @@ import ( "strings" "time" - "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/cmd/clibase" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/services/repo" "github.com/engigu/baihu-panel/internal/utils" @@ -170,20 +169,16 @@ func getActualRepoDir(cfg Config) string { } func notifyMainServerToSyncRepoTasks(repoID string, upsertedIDs []string, deletedIDs []string) { - _, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/sync-repo-status", map[string]interface{}{ + _, err := clibase.CallInternalAPI("POST", "/internal/tasks/sync-repo-status", map[string]interface{}{ "repo_id": repoID, "upserted_ids": upsertedIDs, "deleted_ids": deletedIDs, }) if err != nil { - fmt.Printf(">> [通知] 无法连接到主程序进行增量刷新: %v\n", err) + fmt.Printf(">> [通知] 调度器同步失败: %v\n", err) return } - if statusCode == 200 { - fmt.Println(">> [通知] 已成功将变动任务增量同步至主程序调度器") - } else { - fmt.Printf(">> [通知] 调度器刷新异常,主程序响应状态码: %d\n", statusCode) - } + fmt.Println(">> [通知] 已成功将变动任务增量同步至主程序调度器") } func syncGit(cfg Config) { @@ -443,68 +438,13 @@ func isRawFileURL(url string) bool { return false } -var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") - -type cleanWriter struct { - out io.Writer - buf []byte -} - -func (c *cleanWriter) Write(p []byte) (n int, err error) { - c.buf = append(c.buf, p...) - - for { - idx := bytes.IndexAny(c.buf, "\r\n") - if idx == -1 { - break - } - - if c.buf[idx] == '\r' && idx == len(c.buf)-1 { - // Ends with \r across a chunk, wait for next. - break - } - - char := c.buf[idx] - line := string(c.buf[:idx]) - c.buf = c.buf[idx+1:] - - if char == '\r' && len(c.buf) > 0 && c.buf[0] == '\n' { - c.buf = c.buf[1:] - char = '\n' - } - - s := ansiRegex.ReplaceAllString(line, "") - - if char == '\r' { - continue // filter out terminal progress overwrites - } - - if s != "" { - c.out.Write([]byte(s + "\n")) - } - } - return len(p), nil -} - -func (c *cleanWriter) Flush() { - if len(c.buf) > 0 { - s := string(c.buf) - s = strings.TrimSuffix(s, "\r") - s = ansiRegex.ReplaceAllString(s, "") - if s != "" { - c.out.Write([]byte(s + "\n")) - } - c.buf = nil - } -} - func runCmd(args []string, dir string, env []string) { fmt.Printf(">> %s\n", strings.Join(args, " ")) cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir cmd.Env = env - cw := &cleanWriter{out: os.Stdout} + cw := clibase.NewCleanWriter(os.Stdout) cmd.Stdout = cw cmd.Stderr = cw diff --git a/cmd/resetpwd/resetpwd.go b/cmd/resetpwd/resetpwd.go index c69510a..f4c98bd 100644 --- a/cmd/resetpwd/resetpwd.go +++ b/cmd/resetpwd/resetpwd.go @@ -7,17 +7,13 @@ import ( "os" "strings" - "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/cmd/clibase" "github.com/engigu/baihu-panel/internal/services" "github.com/engigu/baihu-panel/internal/utils" ) func printHelp() { - fmt.Fprintf(os.Stderr, "\n白虎面板用户密码重置工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu resetpwd [用户名]\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu resetpwd admin\n\n") + clibase.PrintSubCommandUsage("白虎面板用户密码重置工具", "baihu resetpwd [用户名]", " baihu resetpwd admin", nil) } func Run(args []string) { @@ -33,11 +29,8 @@ func Run(args []string) { return } - // 基础环境初始化 - bootstrap.InitBasicForCmd() - settingsService := services.NewSettingsService() - if err := settingsService.InitSettings(); err != nil { - fmt.Printf("初始化系统设置失败: %v\n", err) + if err := clibase.InitContext(true); err != nil { + fmt.Println(err) return } userService := services.NewUserService() @@ -71,9 +64,7 @@ func Run(args []string) { user := userService.GetUserByUsername(username) if user == nil { fmt.Printf("找不到用户 [%s]\n", username) - fmt.Println(">> 提示: 程序当前可能连接到了默认的空 SQLite 数据库。") - fmt.Println(">> 若您的生产环境使用的是 MySQL 或指定路径配置,请在执行命令时携带配置文件路径环境变量,例如:") - fmt.Println(">> BH_CONFIG_PATH=/app/data/config.ini baihu resetpwd " + username) + clibase.PrintDBConfigHint("resetpwd " + username) return } diff --git a/cmd/restore/restore.go b/cmd/restore/restore.go index f300a7f..80e7d30 100644 --- a/cmd/restore/restore.go +++ b/cmd/restore/restore.go @@ -6,16 +6,12 @@ import ( "os" "path/filepath" - "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/cmd/clibase" "github.com/engigu/baihu-panel/internal/services" ) func printHelp() { - fmt.Fprintf(os.Stderr, "\n白虎面板系统数据恢复工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu restore <备份文件.zip>\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu restore backup_20231027.zip\n\n") + clibase.PrintSubCommandUsage("白虎面板系统数据恢复工具", "baihu restore <备份文件.zip>", " baihu restore backup_20231027.zip", nil) } func Run(args []string) { @@ -51,7 +47,7 @@ func Run(args []string) { } // 必须初始化环境与数据库才能恢复数据 - bootstrap.InitBasicForCmd() + clibase.InitContext(false) backupService := services.NewBackupService() fmt.Printf("正在从 '%s' 恢复系统数据,请勿强制中断...\n", absPath) diff --git a/cmd/task/task.go b/cmd/task/task.go index b2e758d..4f181e5 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -1,14 +1,12 @@ package task import ( - "encoding/json" "flag" "fmt" "os" - "regexp" "strings" - "github.com/engigu/baihu-panel/internal/bootstrap" + "github.com/engigu/baihu-panel/cmd/clibase" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/models" @@ -65,23 +63,14 @@ func runList(args []string) { sizePtr := fs.Int("size", 20, "每页展示条数") fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板任务列表查询工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu task list [参数]\n\n") - fmt.Fprintf(os.Stderr, "参数说明:\n") - fs.PrintDefaults() - fmt.Fprintf(os.Stderr, "\n示例:\n") - fmt.Fprintf(os.Stderr, " baihu task list\n") - fmt.Fprintf(os.Stderr, " baihu task list -page 2 -size 10\n") - fmt.Fprintf(os.Stderr, " baihu task list -name \"签到\"\n\n") + clibase.PrintSubCommandUsage("白虎面板任务列表查询工具", "baihu task list [参数]", " baihu task list\n baihu task list -page 2 -size 10\n baihu task list -name \"签到\"", fs) } if err := fs.Parse(args); err != nil { return } - // 基础环境初始化 - bootstrap.InitBasicForCmd() + clibase.InitContext(false) var total int64 query := database.DB.Model(&models.Task{}) @@ -132,11 +121,7 @@ func runList(args []string) { func runExecute(args []string) { fs := flag.NewFlagSet("run", flag.ExitOnError) fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板手动任务触发工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu task run <任务ID>\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu task run a1b2c3d4\n\n") + clibase.PrintSubCommandUsage("白虎面板手动任务触发工具", "baihu task run <任务ID>", " baihu task run a1b2c3d4", nil) } if err := fs.Parse(args); err != nil { @@ -151,35 +136,16 @@ func runExecute(args []string) { } taskID := parsedArgs[0] - bootstrap.InitBasicForCmd() + clibase.InitContext(false) - bodyBytes, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/execute/"+taskID, map[string]interface{}{}) + _, err := clibase.CallInternalAPI("POST", "/internal/tasks/execute/"+taskID, map[string]interface{}{}) if err != nil { - fmt.Printf(">> 触发请求失败: %v\n", err) + fmt.Printf(">> 任务触发失败: %v\n", err) return } - if statusCode == 200 { - var res struct { - Data struct { - Success bool `json:"success"` - Error string `json:"error"` - } `json:"data"` - } - _ = json.Unmarshal(bodyBytes, &res) - if res.Data.Success { - fmt.Printf(">> 任务 [%s] 触发指令下发成功!已进入后台调度队列排队或执行。\n", taskID) - fmt.Printf(">> 提示: 可以使用 'baihu task status %s' 查看近期执行输出。\n", taskID) - } else { - errReason := res.Data.Error - if errReason == "" { - errReason = "未知调度拒绝原因 (原始返回: " + strings.TrimSpace(string(bodyBytes)) + ")" - } - fmt.Printf(">> 任务触发被拒绝,原因: %s\n", errReason) - } - } else { - fmt.Printf(">> 触发失败,后台返回状态码: %d\n响应内容: %s\n", statusCode, string(bodyBytes)) - } + fmt.Printf(">> 任务 [%s] 触发指令下发成功!已进入后台调度队列排队或执行。\n", taskID) + fmt.Printf(">> 提示: 可以使用 'baihu task status %s' 查看近期执行输出。\n", taskID) } func runToggle(action string, args []string) { @@ -192,11 +158,7 @@ func runToggle(action string, args []string) { } fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板任务%s工具\n\n", actionName) - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu task %s <任务ID>\n\n", action) - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu task %s a1b2c3d4\n\n", action) + clibase.PrintSubCommandUsage(fmt.Sprintf("白虎面板任务%s工具", actionName), fmt.Sprintf("baihu task %s <任务ID>", action), fmt.Sprintf(" baihu task %s a1b2c3d4", action), nil) } if err := fs.Parse(args); err != nil { @@ -211,36 +173,23 @@ func runToggle(action string, args []string) { } taskID := parsedArgs[0] - bootstrap.InitBasicForCmd() + clibase.InitContext(false) - bodyBytes, statusCode, err := bootstrap.SendInternalRequest("POST", "/internal/tasks/toggle/"+taskID, map[string]interface{}{ + _, err := clibase.CallInternalAPI("POST", "/internal/tasks/toggle/"+taskID, map[string]interface{}{ "enabled": targetEnabled, }) if err != nil { - fmt.Printf(">> 切换状态请求失败: %v\n", err) + fmt.Printf(">> 切换状态操作失败: %v\n", err) return } - if statusCode == 200 { - fmt.Printf(">> 任务 [%s] 已成功%s!\n", taskID, actionName) - } else { - fmt.Printf(">> 操作失败,后台返回状态码: %d\n响应内容: %s\n", statusCode, string(bodyBytes)) - } + fmt.Printf(">> 任务 [%s] 已成功%s!\n", taskID, actionName) } -var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") - func runStatus(args []string) { fs := flag.NewFlagSet("status", flag.ExitOnError) fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板任务执行状态与日志查看工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu task status <任务ID> [日志ID]\n\n") - fmt.Fprintf(os.Stderr, "说明:\n") - fmt.Fprintf(os.Stderr, " 默认查看该任务最新的一条日志记录。若指定了具体的日志ID,则查看特定历史日志的内容。\n\n") - fmt.Fprintf(os.Stderr, "示例:\n") - fmt.Fprintf(os.Stderr, " baihu task status a1b2c3d4\n") - fmt.Fprintf(os.Stderr, " baihu task status a1b2c3d4 log_123456\n\n") + clibase.PrintSubCommandUsage("白虎面板任务执行状态与日志查看工具", "baihu task status <任务ID> [日志ID]", " baihu task status a1b2c3d4\n baihu task status a1b2c3d4 log_123456", nil) } if err := fs.Parse(args); err != nil { @@ -259,7 +208,7 @@ func runStatus(args []string) { specificLogID = parsedArgs[1] } - bootstrap.InitBasicForCmd() + clibase.InitContext(false) var taskLog models.TaskLog query := database.DB.Where("task_id = ?", taskID) @@ -313,7 +262,7 @@ func runStatus(args []string) { } else { // 清理多余回车和终端 ANSI 转义字符 cleanText := strings.ReplaceAll(decompressed, "\r\n", "\n") - cleanText = ansiRegex.ReplaceAllString(cleanText, "") + cleanText = clibase.AnsiRegex.ReplaceAllString(cleanText, "") fmt.Println(strings.TrimSpace(cleanText)) } @@ -329,14 +278,7 @@ func runHistory(args []string) { limitPtr := fs.Int("limit", 10, "展示的最近历史记录条数") fs.Usage = func() { - fmt.Fprintf(os.Stderr, "\n白虎面板任务执行历史查看工具\n\n") - fmt.Fprintf(os.Stderr, "用法:\n") - fmt.Fprintf(os.Stderr, " baihu task history <任务ID> [参数]\n\n") - fmt.Fprintf(os.Stderr, "参数说明:\n") - fs.PrintDefaults() - fmt.Fprintf(os.Stderr, "\n示例:\n") - fmt.Fprintf(os.Stderr, " baihu task history a1b2c3d4\n") - fmt.Fprintf(os.Stderr, " baihu task history a1b2c3d4 -limit 20\n\n") + clibase.PrintSubCommandUsage("白虎面板任务执行历史查看工具", "baihu task history <任务ID> [参数]", " baihu task history a1b2c3d4\n baihu task history a1b2c3d4 -limit 20", fs) } if err := fs.Parse(args); err != nil { @@ -351,7 +293,7 @@ func runHistory(args []string) { } taskID := parsedArgs[0] - bootstrap.InitBasicForCmd() + clibase.InitContext(false) var task models.Task database.DB.Where("id = ?", taskID).Limit(1).Find(&task)