252 lines
7.2 KiB
Go
252 lines
7.2 KiB
Go
package repo
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"github.com/engigu/baihu-panel/internal/utils"
|
|
"github.com/robfig/cron/v3"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// envRegex 匹配脚本中的环境名称设置,如 Env("名称")
|
|
envRegex = regexp.MustCompile(`(?i)(?:new[ \t]+)?Env\(['"]?([^'"]+)['"]?\)`)
|
|
// cronRegex 匹配脚本中的 cron 表达式设置
|
|
cronRegex = regexp.MustCompile(`(?i)(?:cron[ \t]*[:=][ \t]*['"]?([^'"\r\n]+))|(?:(?:^|[ \t\*\/])(([0-9\*\/\-,L?#]+[ \t]+){4,5}[0-9\*\/\-,L?#]+))`)
|
|
// cronFormatRegex 用于校验提取出的字符串是否符合 Cron 表达式格式 (5位或6位)
|
|
cronFormatRegex = regexp.MustCompile(`^(([0-9\*\/\-,L?#]+)[ \t]+){4,5}([0-9\*\/\-,L?#]+)$`)
|
|
)
|
|
|
|
// ExtractScriptMeta 读取文件以提取任务名称和 cron 表达式
|
|
func ExtractScriptMeta(path string, ext string) (taskName string, taskCron string) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
var firstCommentLine string
|
|
inBlockComment := false
|
|
|
|
// 特殊处理:针对当前文件名的 Cron 关联正则表达式 (对标青龙 perl 逻辑)
|
|
// 寻找类似 "// 0 0 * * * jd_task.js" 的行
|
|
fileNameEscaped := regexp.QuoteMeta(filepath.Base(path))
|
|
associatedCronRegex := regexp.MustCompile(fmt.Sprintf(`(?i)(?:^|[ \t\*\//])(([0-9\*\/\-,L?#]+[ \t]+){4,5}[0-9\*\/\-,L?#]+)[ \t,"]+.*%s`, fileNameEscaped))
|
|
|
|
for i := 0; i < 15 && scanner.Scan(); i++ { // 限制扫描范围,避免误匹配代码深处的属性
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// 处理块注释开始/结束
|
|
if strings.HasPrefix(line, "/*") {
|
|
inBlockComment = true
|
|
line = strings.TrimPrefix(line, "/*")
|
|
line = strings.TrimPrefix(line, "*")
|
|
line = strings.TrimSpace(line)
|
|
}
|
|
if strings.HasSuffix(line, "*/") {
|
|
inBlockComment = false
|
|
line = strings.TrimSuffix(line, "*/")
|
|
line = strings.TrimSpace(line)
|
|
}
|
|
|
|
// 1. 尝试提取任务名称 (优先使用 Env)
|
|
if taskName == "" {
|
|
if envMatch := envRegex.FindStringSubmatch(line); len(envMatch) > 1 {
|
|
taskName = strings.TrimSpace(envMatch[1])
|
|
} else if strings.Contains(line, "name:") {
|
|
// 兼容 name: "xxx" 格式
|
|
nameRegex := regexp.MustCompile(`(?i)name:[ \t]*['"]([^'"]+)['"]`)
|
|
if nameMatch := nameRegex.FindStringSubmatch(line); len(nameMatch) > 1 {
|
|
taskName = strings.TrimSpace(nameMatch[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果还没找到名称,且在注释中,记录第一行非空注释作为备选名称
|
|
if taskName == "" && (inBlockComment || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "*") || strings.HasPrefix(line, "#")) {
|
|
cleanLine := line
|
|
if strings.HasPrefix(line, "//") {
|
|
cleanLine = strings.TrimPrefix(line, "//")
|
|
} else if strings.HasPrefix(line, "#") {
|
|
cleanLine = strings.TrimPrefix(line, "#")
|
|
} else if strings.HasPrefix(line, "*") {
|
|
cleanLine = strings.TrimPrefix(line, "*")
|
|
}
|
|
cleanLine = strings.TrimSpace(cleanLine)
|
|
|
|
// 排除掉包含 "Env" 或 "cron" 的行, 且排除掉可能是路径或URL的行
|
|
if cleanLine != "" && !strings.Contains(strings.ToLower(cleanLine), "env") &&
|
|
!strings.Contains(strings.ToLower(cleanLine), "cron") &&
|
|
!strings.Contains(cleanLine, "http") &&
|
|
!strings.Contains(cleanLine, "/") &&
|
|
firstCommentLine == "" {
|
|
// 且排除掉纯 cron 表达式
|
|
if !cronRegex.MatchString(cleanLine) {
|
|
firstCommentLine = cleanLine
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. 提取 Cron
|
|
if taskCron == "" {
|
|
// A. 优先查找关联了当前文件名的 Cron (对标 QL)
|
|
if assocMatch := associatedCronRegex.FindStringSubmatch(line); len(assocMatch) > 1 {
|
|
tempCron := strings.Trim(strings.TrimSpace(assocMatch[1]), "\"' \t")
|
|
if isLikelyCron(tempCron) {
|
|
taskCron = tempCron
|
|
}
|
|
}
|
|
|
|
// B. 如果没找到,尝试普通的 cron: "..." 或 cron 表达式
|
|
if taskCron == "" {
|
|
if cronMatch := cronRegex.FindStringSubmatch(line); len(cronMatch) > 0 {
|
|
for _, m := range cronMatch[1:] {
|
|
if m != "" {
|
|
tempCron := strings.Trim(strings.TrimSpace(m), "\"' \t")
|
|
if isLikelyCron(tempCron) {
|
|
taskCron = tempCron
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if taskName != "" && taskCron != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
// 如果最后还是没找到 taskName,使用备选名称或文件名
|
|
if taskName == "" {
|
|
if firstCommentLine != "" {
|
|
taskName = firstCommentLine
|
|
} else {
|
|
taskName = strings.TrimSuffix(filepath.Base(path), ext)
|
|
}
|
|
}
|
|
|
|
return taskName, taskCron
|
|
}
|
|
|
|
// isLikelyCron 校验字符串是否符合 Cron 表达式的格式特征且数值合法
|
|
func isLikelyCron(s string) bool {
|
|
if !cronFormatRegex.MatchString(s) {
|
|
return false
|
|
}
|
|
|
|
// 语义校验:确保数值在合法范围内(解决如 JS 数组数字超出 Cron 范围的问题)
|
|
fields := strings.Fields(s)
|
|
testCron := s
|
|
if len(fields) == 5 {
|
|
testCron = "0 " + s
|
|
}
|
|
|
|
// 使用与面板执行器一致的 Parser 进行校验
|
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
|
_, err := parser.Parse(testCron)
|
|
return err == nil
|
|
}
|
|
|
|
// splitKeywords 按竖线或逗号分割字符串
|
|
func splitKeywords(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
var parts []string
|
|
if strings.Contains(s, "|") {
|
|
parts = strings.Split(s, "|")
|
|
} else if strings.Contains(s, ",") {
|
|
parts = strings.Split(s, ",")
|
|
} else {
|
|
parts = []string{s}
|
|
}
|
|
|
|
var res []string
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
res = append(res, p)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// getValidExtensions 返回支持的文件扩展名列表,优先考虑自定义扩展名
|
|
func getValidExtensions(customExtensions string) []string {
|
|
exts := []string{".js", ".py", ".ts", ".sh"}
|
|
if customExtensions != "" {
|
|
customExts := splitKeywords(customExtensions)
|
|
if len(customExts) > 0 {
|
|
exts = nil
|
|
for _, e := range customExts {
|
|
e = strings.TrimSpace(e)
|
|
if e != "" {
|
|
if !strings.HasPrefix(e, ".") {
|
|
e = "." + e
|
|
}
|
|
exts = append(exts, e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return exts
|
|
}
|
|
|
|
// sanitizeIdentifier 将非字母数字字符替换为下划线
|
|
func sanitizeIdentifier(s string) string {
|
|
reg := regexp.MustCompile(`[^a-zA-Z0-9]+`)
|
|
res := reg.ReplaceAllString(s, "_")
|
|
return strings.ToLower(strings.Trim(res, "_"))
|
|
}
|
|
|
|
// normalizeCron 确保 cron 表达式具有 6 个字段
|
|
func normalizeCron(cron string) string {
|
|
fields := strings.Fields(cron)
|
|
if len(fields) == 5 {
|
|
return "0 " + cron
|
|
}
|
|
return cron
|
|
}
|
|
|
|
// pathExists 检查路径是否存在
|
|
func pathExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// isDir 检查路径是否为目录
|
|
func isDir(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return info.IsDir()
|
|
}
|
|
|
|
// getCommandByExt 根据文件扩展名返回默认执行命令
|
|
func getCommandByExt(ext, path string) string {
|
|
quotedPath := utils.QuotePath(path)
|
|
switch ext {
|
|
case ".js", ".ts":
|
|
return fmt.Sprintf("node %s", quotedPath)
|
|
case ".py":
|
|
return fmt.Sprintf("python %s", quotedPath)
|
|
case ".sh":
|
|
return fmt.Sprintf("bash %s", quotedPath)
|
|
case ".php":
|
|
return fmt.Sprintf("php %s", quotedPath)
|
|
case ".cs":
|
|
return fmt.Sprintf("dotnet run %s", quotedPath)
|
|
}
|
|
return quotedPath
|
|
}
|