diff --git a/cmd/reposync/reposync.go b/cmd/reposync/reposync.go index 2c3d71c..a551e8b 100644 --- a/cmd/reposync/reposync.go +++ b/cmd/reposync/reposync.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/engigu/baihu-panel/internal/services/tasks" "github.com/engigu/baihu-panel/internal/utils" ) @@ -31,6 +32,9 @@ type Config struct { Blacklist string // Script filter blacklist keywords, vertical line separated Dependence string // Script dependence file keywords, vertical line separated Extensions string // Script file extensions, vertical line separated + TaskID string + TaskLanguages string + TaskTimeout int } func Run(args []string) { @@ -50,26 +54,38 @@ func Run(args []string) { fs.StringVar(&cfg.Blacklist, "blacklist", "", "Script filter blacklist keywords (| separated)") fs.StringVar(&cfg.Dependence, "dependence", "", "Script dependence keywords (| separated)") fs.StringVar(&cfg.Extensions, "extensions", "", "Script extensions (| separated)") + fs.StringVar(&cfg.TaskID, "task-id", "", "Task ID for metadata") + fs.StringVar(&cfg.TaskLanguages, "task-langs", "", "Configured languages (JSON)") + fs.StringVar(&cfg.TaskID, "repo-task-id", "", "Original Task ID") + fs.IntVar(&cfg.TaskTimeout, "task-timeout", 30, "Task timeout (minutes)") fs.Parse(args) - if cfg.SourceURL == "" || cfg.TargetPath == "" { - fmt.Println("错误: 缺少 --source-url 或 --target-path 参数") - os.Exit(1) - } - - fmt.Printf("参数: %s\n", strings.Join(args, " ")) + fmt.Println("========================================") + fmt.Println(" 仓库同步任务开始 ") + fmt.Println("========================================") + fmt.Printf("[1/3] 解析同步参数: %s\n", strings.Join(args, " ")) if cfg.SourceType == "git" { + fmt.Printf("[2/3] 正在通过 Git 同步内容...\n") syncGit(cfg) } else { + fmt.Printf("[2/3] 正在通过 URL 下载内容...\n") syncURL(cfg) } // 执行脚本过滤(仅限 git 模式,url 加载通常为单文件,暂不处理过滤) if cfg.SourceType == "git" { + fmt.Printf("[3/3] 正在执行脚本过滤与文件清理...\n") filterFiles(cfg) + + if cfg.TaskID != "" { + tasks.ParseRepoScriptsAndAddCron(nil, cfg.TaskID, os.Stdout) + } } + fmt.Println("\n========================================") + fmt.Println(" 仓库同步任务完成 ") + fmt.Println("========================================") } func syncGit(cfg Config) { @@ -692,5 +708,6 @@ func cleanEmptyDirs(root string) { } } } + } diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 63b7155..3b7b18d 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -29,17 +29,22 @@ func New() *App { return app } -// InitBasic 初始化基础环境(配置和数据库),不启动后台服务和路由 func InitBasic() *App { app := &App{} utils.InitRuntime() - app.initConfig() + + // 自动加载配置 (内部会自动处理 BH_CONFIG_PATH 环境变量与默认路径的优先级) + app.initConfigWithPath("") app.initDatabase() return app } func (a *App) initConfig() { - cfg, err := services.LoadConfig(constant.ConfigPath) + a.initConfigWithPath(constant.ConfigPath) +} + +func (a *App) initConfigWithPath(path string) { + cfg, err := services.LoadConfig(path) if err != nil { logger.Fatalf("Failed to load config: %v", err) } diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index ac78aea..154fd03 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -3,6 +3,7 @@ package controllers import ( "encoding/json" "path/filepath" + "strings" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/models" @@ -39,6 +40,9 @@ func resolveWorkDir(workDir string) string { return absPath } // 如果已经是绝对路径,直接返回 + if strings.HasPrefix(workDir, "$SCRIPTS_DIR$") { + return workDir + } if filepath.IsAbs(workDir) { return workDir } diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 6827bf2..e9f7c01 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -67,6 +67,15 @@ func getEnvInt(key string, target *int) { } func LoadConfig(path string) (*AppConfig, error) { + // 路径发现逻辑:参数优先 -> 环境变量优先 -> 默认常量 + if path == "" { + if envPath := os.Getenv("BH_CONFIG_PATH"); envPath != "" { + path = envPath + } else { + path = constant.ConfigPath + } + } + // 初始化默认配置 Config = &AppConfig{ Server: ServerConfig{ diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 008e499..417f079 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -241,9 +241,7 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, // 处理任务完成(更新统计、清理旧日志等) h.es.taskLogService.ProcessTaskCompletion(taskLog) - if task.Type == constant.TaskTypeRepo && result.Status == constant.TaskStatusSuccess { - go ParseRepoScriptsAndAddCron(h.es, task) - } + // 更新内存缓冲 h.es.UpdateResult(*result) @@ -966,6 +964,13 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) if config.Extensions != "" { args = append(args, "--extensions", config.Extensions) } + + // 传递任务 ID,以便 reposync 内部直接处理脚本注册并输出日志 + args = append(args, "--task-id", task.ID) + args = append(args, "--task-timeout", fmt.Sprintf("%d", task.Timeout)) + if langData, err := json.Marshal(task.Languages); err == nil { + args = append(args, "--task-langs", string(langData)) + } // 为了防止 shell 解释特殊字符(如 |),对每个参数进行转义/加引号 quotedArgs := make([]string, len(args)) @@ -974,7 +979,8 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" } - return "'" + strings.ReplaceAll(exePath, "'", "'\\''") + "' " + strings.Join(quotedArgs, " "), filepath.Dir(exePath) + cmdStr := "'" + strings.ReplaceAll(exePath, "'", "'\\''") + "' " + strings.Join(quotedArgs, " ") + return buildRepoCommandEnvPrefix()+cmdStr, filepath.Dir(exePath) } // loadEnvVars 加载环境变量,支持全局注入及重名合并 @@ -1007,6 +1013,45 @@ func (es *ExecutorService) loadEnvVars(taskID string, envIDs string) []string { } func (es *ExecutorService) ResolvePath(path string) string { - absScriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir) + absScriptsDir := resolveAbsScriptsDir() return strings.ReplaceAll(path, "$SCRIPTS_DIR$", absScriptsDir) } + +func buildRepoCommandEnvPrefix() string { + absConfig, err := filepath.Abs(constant.ConfigPath) + if err != nil { + absConfig = constant.ConfigPath + } + + absScriptsDir := resolveAbsScriptsDir() + return "BH_CONFIG_PATH='" + strings.ReplaceAll(absConfig, "'", "'\\''") + "' BH_SCRIPTS_DIR='" + strings.ReplaceAll(absScriptsDir, "'", "'\\''") + "' " +} + +func resolveAbsScriptsDir() string { + if scriptsDir := os.Getenv("BH_SCRIPTS_DIR"); scriptsDir != "" { + if filepath.IsAbs(scriptsDir) { + return filepath.Clean(scriptsDir) + } + if absScriptsDir, err := filepath.Abs(scriptsDir); err == nil { + return absScriptsDir + } + return filepath.Clean(scriptsDir) + } + + if configPath := os.Getenv("BH_CONFIG_PATH"); configPath != "" { + if !filepath.IsAbs(configPath) { + if absConfigPath, err := filepath.Abs(configPath); err == nil { + configPath = absConfigPath + } + } + + projectRoot := filepath.Dir(filepath.Dir(configPath)) + return filepath.Clean(filepath.Join(projectRoot, constant.ScriptsWorkDir)) + } + + if absScriptsDir, err := filepath.Abs(constant.ScriptsWorkDir); err == nil { + return absScriptsDir + } + + return filepath.Clean(constant.ScriptsWorkDir) +} diff --git a/internal/services/tasks/ql_repo_parser.go b/internal/services/tasks/ql_repo_parser.go index d20ff51..2ed71c0 100644 --- a/internal/services/tasks/ql_repo_parser.go +++ b/internal/services/tasks/ql_repo_parser.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "fmt" + "io" "io/fs" "os" "path/filepath" @@ -23,8 +24,25 @@ var ( ) // ParseRepoScriptsAndAddCron scans the repo dir for scripts, parses cron and env comments, and registers tasks -func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { - if repoTask == nil || repoTask.Type != constant.TaskTypeRepo { +func ParseRepoScriptsAndAddCron(es *ExecutorService, taskID string, logWriter io.Writer) { + // help print logs to writer if provided + log := func(format string, a ...interface{}) { + msg := fmt.Sprintf(format, a...) + if !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + if logWriter != nil { + logWriter.Write([]byte(msg)) + } + // logger.Info(msg) + } + + var repoTask models.Task + if err := database.DB.Where("id = ?", taskID).First(&repoTask).Error; err != nil { + return + } + + if repoTask.Type != constant.TaskTypeRepo { return } @@ -41,10 +59,13 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { targetPath := repoCfg.TargetPath if targetPath == "" { targetPath = repoTask.WorkDir + } else if !filepath.IsAbs(targetPath) { + targetPath = filepath.Join(resolveAbsScriptsDir(), targetPath) } if targetPath == "" { return } + targetPath = filepath.Clean(targetPath) // We might have appended a repo id to targetPath repoId := utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) @@ -81,7 +102,13 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { } } + log("\n----------------------------------------") + log(" 开始扫描脚本并自动注册定时任务 ") + log("----------------------------------------") + foundSourceIDs := make(map[string]bool) + newTaskCount := 0 + updateTaskCount := 0 filepath.WalkDir(targetPath, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { @@ -234,7 +261,8 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { if taskName != "" && taskCron != "" { // 获取脚本相对于数据目录的路径 - absScriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir) + absScriptsDir := resolveAbsScriptsDir() + absTargetPath, _ := filepath.Abs(targetPath) absPath, _ := filepath.Abs(path) // 计算 SourceID: 相对于脚本目录的完整路径,并清洗特殊符号 @@ -245,9 +273,11 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { displayPath := path displayWorkDir := targetPath if strings.HasPrefix(absPath, absScriptsDir) { - displayPath = filepath.Join("$SCRIPTS_DIR$", relPath) + if relCommandPath, err := filepath.Rel(absTargetPath, absPath); err == nil && relCommandPath != "" { + displayPath = filepath.Clean(relCommandPath) + } // 获取目录路径 - relDir, _ := filepath.Rel(absScriptsDir, targetPath) + relDir, _ := filepath.Rel(absScriptsDir, absTargetPath) displayWorkDir = filepath.Join("$SCRIPTS_DIR$", relDir) } @@ -259,15 +289,12 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { // See if task exists (优先通过 SourceID 匹配) var existing models.Task - err := database.DB.Where("source_id = ?", sourceID).First(&existing).Error - if err != nil { - // 降级使用 command + tag 匹配 (兼容旧数据) - err = database.DB.Where("command = ? AND tags LIKE ?", command, "%"+tag+"%").First(&existing).Error - } + tx := database.DB.Where("source_id = ? AND repo_task_id = ?", sourceID, repoTask.ID).Limit(1).Find(&existing) - if err == nil { + if tx.RowsAffected > 0 { // update existing.Name = taskName + existing.Command = models.BigText(command) existing.Schedule = normalizeCron(taskCron) existing.Languages = repoTask.Languages existing.SourceID = sourceID @@ -286,6 +313,8 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { if existing.Enabled && es != nil { es.AddCronTask(&existing) } + log("[更新] 任务: %s (%s)", taskName, filename) + updateTaskCount++ foundSourceIDs[sourceID] = true } else { // create new @@ -310,6 +339,8 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { if es != nil { es.AddCronTask(newTask) } + log("[新增] 任务: %s (%s)", taskName, filename) + newTaskCount++ foundSourceIDs[sourceID] = true } } @@ -318,10 +349,13 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { }) // 清理该仓库任务下不再存在的旧脚本任务 + deletedTaskCount := 0 var oldTasks []models.Task if err := database.DB.Where("repo_task_id = ?", repoTask.ID).Find(&oldTasks).Error; err == nil { for _, ot := range oldTasks { if !foundSourceIDs[ot.SourceID] { + log("[移除] 脚本已不存在,删除对应任务: %s", ot.Name) + deletedTaskCount++ if es != nil { if es.taskService != nil { es.taskService.DeleteTask(ot.ID) @@ -334,6 +368,9 @@ func ParseRepoScriptsAndAddCron(es *ExecutorService, repoTask *models.Task) { } } } + + log("\n扫描完成: [新增 %d] [更新 %d] [移除 %d]", newTaskCount, updateTaskCount, deletedTaskCount) + log("----------------------------------------") } func sanitizeIdentifier(s string) string { diff --git a/internal/services/tasks/tiny_log.go b/internal/services/tasks/tiny_log.go index a785f3e..ba252b5 100644 --- a/internal/services/tasks/tiny_log.go +++ b/internal/services/tasks/tiny_log.go @@ -139,6 +139,11 @@ func (l *TinyLog) Write(p []byte) (n int, err error) { return originalInputLen, nil } +// WriteString 方便地写入字符串 +func (l *TinyLog) WriteString(s string) (n int, err error) { + return l.Write([]byte(s)) +} + // Subscribe 返回一个实时接收日志块的通道 func (l *TinyLog) Subscribe() chan []byte { l.mu.Lock() diff --git a/main.go b/main.go index 132045b..77d4a53 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ func main() { } if handler, ok := cmd.Handlers[commandName]; ok { + bootstrap.InitBasic() // 启动基础环境(配置和数据库) handler(os.Args[2:]) return } diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index f15a8a3..8534cd1 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -1,15 +1,17 @@