Files
clawgo/pkg/scheduling/resource_keys.go

197 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}