mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 06:47:30 +08:00
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
package scheduling
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
)
|
||
|
||
var (
|
||
reBracketResourceKeys = regexp.MustCompile(`(?i)\[\s*resource[_\s-]*keys?\s*:\s*([^\]]+)\]`)
|
||
reBracketKeys = regexp.MustCompile(`(?i)\[\s*keys?\s*:\s*([^\]]+)\]`)
|
||
reLineResourceKeys = regexp.MustCompile(`(?i)^\s*resource[_\s-]*keys?\s*[:=]\s*(.+)$`)
|
||
)
|
||
|
||
// DeriveResourceKeys derives lock keys from content.
|
||
// Explicit directives win; otherwise lightweight heuristic extraction is used.
|
||
func DeriveResourceKeys(content string) []string {
|
||
raw := strings.TrimSpace(content)
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
if explicit := ParseExplicitResourceKeys(raw); len(explicit) > 0 {
|
||
return explicit
|
||
}
|
||
|
||
lower := strings.ToLower(raw)
|
||
keys := make([]string, 0, 8)
|
||
for _, token := range strings.Fields(lower) {
|
||
t := strings.Trim(token, "`'\"()[]{}:;,,。!?")
|
||
if t == "" {
|
||
continue
|
||
}
|
||
if strings.Contains(t, "gitea.") || strings.Contains(t, "github.com") || strings.Count(t, "/") >= 1 {
|
||
if strings.Contains(t, "github.com/") || strings.Contains(t, "gitea.") {
|
||
keys = append(keys, "repo:"+t)
|
||
}
|
||
}
|
||
if strings.Contains(t, "/") || strings.HasSuffix(t, ".go") || strings.HasSuffix(t, ".md") || strings.HasSuffix(t, ".json") || strings.HasSuffix(t, ".yaml") || strings.HasSuffix(t, ".yml") {
|
||
keys = append(keys, "file:"+t)
|
||
}
|
||
if t == "main" || strings.HasPrefix(t, "branch:") {
|
||
keys = append(keys, "branch:"+strings.TrimPrefix(t, "branch:"))
|
||
}
|
||
}
|
||
for _, topic := range deriveTopicKeys(lower) {
|
||
keys = append(keys, topic)
|
||
}
|
||
if len(keys) == 0 {
|
||
keys = append(keys, "scope:general")
|
||
}
|
||
return NormalizeResourceKeys(keys)
|
||
}
|
||
|
||
func deriveTopicKeys(lower string) []string {
|
||
if strings.TrimSpace(lower) == "" {
|
||
return nil
|
||
}
|
||
type topicRule struct {
|
||
name string
|
||
keywords []string
|
||
}
|
||
rules := []topicRule{
|
||
{name: "webui", keywords: []string{"webui", "ui", "frontend", "前端", "页面", "界面"}},
|
||
{name: "docs", keywords: []string{"readme", "doc", "docs", "文档", "说明"}},
|
||
{name: "release", keywords: []string{"release", "tag", "version", "版本", "发版", "打版本"}},
|
||
{name: "git", keywords: []string{"git", "branch", "commit", "push", "merge", "分支", "提交", "推送"}},
|
||
{name: "config", keywords: []string{"config", "配置", "参数"}},
|
||
{name: "test", keywords: []string{"test", "tests", "testing", "测试", "回归"}},
|
||
{name: "task", keywords: []string{"task", "tasks", "任务", "调度", "并发"}},
|
||
{name: "memory", keywords: []string{"memory", "记忆"}},
|
||
{name: "cron", keywords: []string{"cron", "schedule", "scheduled", "定时", "定时任务"}},
|
||
{name: "log", keywords: []string{"log", "logs", "日志"}},
|
||
}
|
||
out := make([]string, 0, 3)
|
||
for _, r := range rules {
|
||
for _, kw := range r.keywords {
|
||
if strings.Contains(lower, kw) {
|
||
out = append(out, "topic:"+r.name)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ParseExplicitResourceKeys parses directive-style keys from content.
|
||
func ParseExplicitResourceKeys(content string) []string {
|
||
raw := strings.TrimSpace(content)
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
if m := reBracketResourceKeys.FindStringSubmatch(raw); len(m) == 2 {
|
||
return ParseResourceKeyList(m[1])
|
||
}
|
||
if m := reBracketKeys.FindStringSubmatch(raw); len(m) == 2 {
|
||
return ParseResourceKeyList(m[1])
|
||
}
|
||
for _, line := range strings.Split(raw, "\n") {
|
||
m := reLineResourceKeys.FindStringSubmatch(strings.TrimSpace(line))
|
||
if len(m) == 2 {
|
||
return ParseResourceKeyList(m[1])
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ExtractResourceKeysDirective returns explicit keys and content without directive text.
|
||
func ExtractResourceKeysDirective(content string) (keys []string, cleaned string, found bool) {
|
||
raw := strings.TrimSpace(content)
|
||
if raw == "" {
|
||
return nil, "", false
|
||
}
|
||
if m := reBracketResourceKeys.FindStringSubmatch(raw); len(m) == 2 {
|
||
keys = ParseResourceKeyList(m[1])
|
||
if len(keys) == 0 {
|
||
return nil, raw, false
|
||
}
|
||
cleaned = strings.TrimSpace(reBracketResourceKeys.ReplaceAllString(raw, ""))
|
||
return keys, cleaned, true
|
||
}
|
||
if m := reBracketKeys.FindStringSubmatch(raw); len(m) == 2 {
|
||
keys = ParseResourceKeyList(m[1])
|
||
if len(keys) == 0 {
|
||
return nil, raw, false
|
||
}
|
||
cleaned = strings.TrimSpace(reBracketKeys.ReplaceAllString(raw, ""))
|
||
return keys, cleaned, true
|
||
}
|
||
lines := strings.Split(raw, "\n")
|
||
for i, line := range lines {
|
||
m := reLineResourceKeys.FindStringSubmatch(strings.TrimSpace(line))
|
||
if len(m) != 2 {
|
||
continue
|
||
}
|
||
keys = ParseResourceKeyList(m[1])
|
||
if len(keys) == 0 {
|
||
break
|
||
}
|
||
trimmed := make([]string, 0, len(lines)-1)
|
||
trimmed = append(trimmed, lines[:i]...)
|
||
trimmed = append(trimmed, lines[i+1:]...)
|
||
cleaned = strings.TrimSpace(strings.Join(trimmed, "\n"))
|
||
return keys, cleaned, true
|
||
}
|
||
return nil, raw, false
|
||
}
|
||
|
||
// ParseResourceKeyList parses comma/newline/space separated keys.
|
||
func ParseResourceKeyList(raw string) []string {
|
||
if strings.TrimSpace(raw) == "" {
|
||
return nil
|
||
}
|
||
replacer := strings.NewReplacer("\n", ",", ";", ",", ";", ",", ",", ",")
|
||
normalized := replacer.Replace(raw)
|
||
parts := strings.Split(normalized, ",")
|
||
keys := make([]string, 0, len(parts))
|
||
for _, p := range parts {
|
||
k := strings.TrimSpace(strings.Trim(p, "`'\""))
|
||
if k == "" {
|
||
continue
|
||
}
|
||
if !strings.Contains(k, ":") {
|
||
k = "file:" + k
|
||
}
|
||
keys = append(keys, k)
|
||
}
|
||
if len(keys) == 0 {
|
||
for _, p := range strings.Fields(raw) {
|
||
if !strings.Contains(p, ":") {
|
||
p = "file:" + p
|
||
}
|
||
keys = append(keys, p)
|
||
}
|
||
}
|
||
return NormalizeResourceKeys(keys)
|
||
}
|
||
|
||
// NormalizeResourceKeys lowercases, trims and deduplicates keys.
|
||
func NormalizeResourceKeys(keys []string) []string {
|
||
if len(keys) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(keys))
|
||
seen := make(map[string]struct{}, len(keys))
|
||
for _, k := range keys {
|
||
n := strings.ToLower(strings.TrimSpace(k))
|
||
if n == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[n]; ok {
|
||
continue
|
||
}
|
||
seen[n] = struct{}{}
|
||
out = append(out, n)
|
||
}
|
||
return out
|
||
}
|