Files
Ant-Browser/backend/automation_script_target_resolver.go
T

499 lines
16 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 backend
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"ant-chrome/backend/internal/automation"
"ant-chrome/backend/internal/browser"
)
const defaultAutomationCreateNameTemplate = "${templateName}-${timestamp}"
func (a *App) resolveAutomationEffectiveSelector(script automation.ScriptRecord, input automation.ScriptRunRequest, required bool) (map[string]any, string, error) {
overrideSelectorText := strings.TrimSpace(input.SelectorText)
if automationScriptTargetMode(script) == "manual" && !input.UseScriptSelector && overrideSelectorText != "" {
selector, err := parseAutomationJSONObject(overrideSelectorText, required)
return selector, "", err
}
if automationScriptTargetMode(script) != "manual" {
targetInput := input.TargetInput
if targetInput == nil && !input.UseScriptSelector && overrideSelectorText != "" {
selector, err := parseAutomationJSONObject(overrideSelectorText, false)
if err != nil {
return nil, "", err
}
targetInput = selector
}
input.TargetInput = targetInput
effectiveScript, err := applyAutomationRunTargetInput(script, input.TargetInput)
if err != nil {
return nil, "", err
}
return a.resolveAutomationScriptTarget(effectiveScript)
}
selectorText := resolveAutomationRunJSONText(input.SelectorText, script.SelectorText, input.UseScriptSelector)
selector, err := parseAutomationJSONObject(selectorText, required)
return selector, "", err
}
func automationScriptTargetMode(script automation.ScriptRecord) string {
mode := strings.ToLower(strings.TrimSpace(script.TargetConfig.Mode))
switch mode {
case "existing", "create", "rotate":
return mode
default:
return "manual"
}
}
func applyAutomationRunTargetInput(script automation.ScriptRecord, value any) (automation.ScriptRecord, error) {
if value == nil {
return script, nil
}
payload, err := marshalAutomationRunTargetInput(value)
if err != nil {
return script, err
}
if len(payload) == 0 {
return script, nil
}
switch automationScriptTargetMode(script) {
case "existing":
selector, err := decodeAutomationRunTargetSelector(payload)
if err != nil {
return script, fmt.Errorf("已有实例配置无效: %w", err)
}
script.TargetConfig.Selector = selector
case "rotate":
selector, err := decodeAutomationRunTargetSelector(payload)
if err != nil {
return script, fmt.Errorf("条件轮询配置无效: %w", err)
}
script.TargetConfig.Selector = selector
case "create":
var input struct {
TemplateSelector automation.ScriptTargetSelector `json:"templateSelector"`
Selector automation.ScriptTargetSelector `json:"selector"`
CreateNameTemplate string `json:"createNameTemplate"`
ProfileName string `json:"profileName"`
}
if err := json.Unmarshal(payload, &input); err != nil {
return script, fmt.Errorf("targetInput must be a JSON object")
}
selector := input.TemplateSelector
if automationTargetSelectorEmpty(normalizeAutomationTargetSelector(selector)) {
selector = input.Selector
}
script.TargetConfig.TemplateSelector = selector
if name := strings.TrimSpace(input.CreateNameTemplate); name != "" {
script.TargetConfig.CreateNameTemplate = name
} else if name := strings.TrimSpace(input.ProfileName); name != "" {
script.TargetConfig.CreateNameTemplate = name
}
}
return script, nil
}
func marshalAutomationRunTargetInput(value any) ([]byte, error) {
data, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("targetInput must be a JSON object")
}
var object map[string]any
if err := json.Unmarshal(data, &object); err != nil || object == nil {
return nil, fmt.Errorf("targetInput must be a JSON object")
}
if len(object) == 0 {
return nil, nil
}
return data, nil
}
func decodeAutomationRunTargetSelector(payload []byte) (automation.ScriptTargetSelector, error) {
var selector automation.ScriptTargetSelector
if err := json.Unmarshal(payload, &selector); err != nil {
return selector, fmt.Errorf("targetInput must be a JSON object")
}
return selector, nil
}
func (a *App) resolveAutomationScriptTarget(script automation.ScriptRecord) (map[string]any, string, error) {
switch strings.ToLower(strings.TrimSpace(script.TargetConfig.Mode)) {
case "existing":
profile, err := a.resolveAutomationExactTargetProfile(script.TargetConfig.Selector, "使用已有实例")
if err != nil {
return nil, "", err
}
return automationProfileSelector(profile.ProfileId), automationProfileLabel(profile), nil
case "rotate":
profiles, err := a.resolveAutomationTargetProfiles(script.TargetConfig.Selector, "按条件轮询实例")
if err != nil {
return nil, "", err
}
profile := a.pickAutomationRoundRobinTarget(script.ID, script.TargetConfig.Selector, profiles)
return automationProfileSelector(profile.ProfileId), fmt.Sprintf("轮询实例 %s", automationProfileLabel(profile)), nil
case "create":
templateProfile, err := a.resolveAutomationExactTargetProfile(script.TargetConfig.TemplateSelector, "按模板新建实例")
if err != nil {
return nil, "", err
}
createdName := buildAutomationCreatedProfileName(script.TargetConfig.CreateNameTemplate, script, templateProfile)
createdProfile, err := a.browserMgr.Copy(templateProfile.ProfileId, createdName)
if err != nil {
return nil, "", fmt.Errorf("按模板新建实例失败: %w", err)
}
if createdProfile == nil {
return nil, "", fmt.Errorf("按模板新建实例失败:未返回新实例")
}
return automationProfileSelector(createdProfile.ProfileId), fmt.Sprintf("新建实例 %s", automationProfileLabel(*createdProfile)), nil
default:
return map[string]any{}, "", nil
}
}
func (a *App) resolveAutomationExactTargetProfile(selector automation.ScriptTargetSelector, actionLabel string) (browser.Profile, error) {
if profile, ok := a.findAutomationTargetProfileByIDOrCode(selector); ok {
return profile, nil
}
return a.resolveAutomationTargetProfile(selector, actionLabel)
}
func (a *App) resolveAutomationTargetProfile(selector automation.ScriptTargetSelector, actionLabel string) (browser.Profile, error) {
profiles, err := a.resolveAutomationTargetProfiles(selector, actionLabel)
if err != nil {
return browser.Profile{}, err
}
if len(profiles) > 1 {
return browser.Profile{}, fmt.Errorf("%s失败:%s", actionLabel, buildAutomationTargetAmbiguousError(profiles))
}
return profiles[0], nil
}
func (a *App) resolveAutomationTargetProfiles(selector automation.ScriptTargetSelector, actionLabel string) ([]browser.Profile, error) {
if a.browserMgr == nil {
return nil, fmt.Errorf("%s失败:实例管理器未初始化", actionLabel)
}
normalized := normalizeAutomationTargetSelector(selector)
if automationTargetSelectorEmpty(normalized) {
return nil, fmt.Errorf("%s失败:请至少填写一个实例条件", actionLabel)
}
snapshots := a.browserMgr.List()
if len(snapshots) == 0 {
return nil, fmt.Errorf("%s失败:当前没有可用实例", actionLabel)
}
if normalized.Code != "" {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return strings.EqualFold(strings.TrimSpace(item.LaunchCode), normalized.Code)
})
}
if normalized.ProfileID != "" {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return strings.TrimSpace(item.ProfileId) == normalized.ProfileID
})
}
if normalized.ProfileName != "" {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return strings.EqualFold(strings.TrimSpace(item.ProfileName), normalized.ProfileName)
})
}
if normalized.GroupID != "" {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return strings.TrimSpace(item.GroupId) == normalized.GroupID
})
}
if len(normalized.Tags) > 0 {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return automationProfileHasAllTags(item, normalized.Tags)
})
}
if len(normalized.Keywords) > 0 {
snapshots = filterAutomationProfiles(snapshots, func(item browser.Profile) bool {
return automationProfileMatchesAllKeywordQueries(item, normalized.Keywords)
})
}
if len(snapshots) == 0 {
return nil, fmt.Errorf("%s失败:没有匹配到实例", actionLabel)
}
sortAutomationProfilesForTarget(snapshots)
return snapshots, nil
}
func normalizeAutomationTargetSelector(selector automation.ScriptTargetSelector) automation.ScriptTargetSelector {
return automation.ScriptTargetSelector{
Code: strings.ToUpper(strings.TrimSpace(selector.Code)),
ProfileID: strings.TrimSpace(selector.ProfileID),
ProfileName: strings.TrimSpace(selector.ProfileName),
GroupID: strings.TrimSpace(selector.GroupID),
Keywords: normalizeAutomationTargetTerms(selector.Keywords),
Tags: normalizeAutomationTargetTerms(selector.Tags),
}
}
func (a *App) findAutomationTargetProfileByIDOrCode(selector automation.ScriptTargetSelector) (browser.Profile, bool) {
if a.browserMgr == nil {
return browser.Profile{}, false
}
normalizedProfileID := strings.TrimSpace(selector.ProfileID)
normalizedCode := strings.ToUpper(strings.TrimSpace(selector.Code))
if normalizedProfileID == "" && normalizedCode == "" {
return browser.Profile{}, false
}
snapshots := a.browserMgr.List()
if normalizedProfileID != "" {
for _, item := range snapshots {
if strings.TrimSpace(item.ProfileId) == normalizedProfileID {
return item, true
}
}
}
if normalizedCode != "" {
for _, item := range snapshots {
if strings.EqualFold(strings.TrimSpace(item.LaunchCode), normalizedCode) {
return item, true
}
}
}
return browser.Profile{}, false
}
func (a *App) enrichAutomationExactTargetSelector(selector automation.ScriptTargetSelector) automation.ScriptTargetSelector {
normalized := normalizeAutomationTargetSelector(selector)
profile, ok := a.findAutomationTargetProfileByIDOrCode(normalized)
if !ok {
return normalized
}
normalized.ProfileID = strings.TrimSpace(profile.ProfileId)
if code := strings.ToUpper(strings.TrimSpace(profile.LaunchCode)); code != "" {
normalized.Code = code
}
return normalized
}
func normalizeAutomationTargetTerms(items []string) []string {
if len(items) == 0 {
return nil
}
seen := make(map[string]struct{}, len(items))
result := make([]string, 0, len(items))
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
key := strings.ToLower(trimmed)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
result = append(result, trimmed)
}
if len(result) == 0 {
return nil
}
return result
}
func automationTargetSelectorEmpty(selector automation.ScriptTargetSelector) bool {
return selector.Code == "" &&
selector.ProfileID == "" &&
selector.ProfileName == "" &&
selector.GroupID == "" &&
len(selector.Keywords) == 0 &&
len(selector.Tags) == 0
}
func filterAutomationProfiles(items []browser.Profile, keep func(browser.Profile) bool) []browser.Profile {
filtered := make([]browser.Profile, 0, len(items))
for _, item := range items {
if keep(item) {
filtered = append(filtered, item)
}
}
return filtered
}
func automationProfileHasAllTags(profile browser.Profile, required []string) bool {
if len(required) == 0 {
return true
}
if len(profile.Tags) == 0 {
return false
}
for _, want := range required {
found := false
for _, tag := range profile.Tags {
if strings.EqualFold(strings.TrimSpace(tag), want) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func automationProfileMatchesAllKeywordQueries(profile browser.Profile, queries []string) bool {
if len(queries) == 0 {
return true
}
if len(profile.Keywords) == 0 {
return false
}
for _, query := range queries {
queryLower := strings.ToLower(strings.TrimSpace(query))
found := false
for _, keyword := range profile.Keywords {
if strings.Contains(strings.ToLower(strings.TrimSpace(keyword)), queryLower) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func sortAutomationProfilesForTarget(items []browser.Profile) {
sort.Slice(items, func(i, j int) bool {
leftName := strings.ToLower(strings.TrimSpace(items[i].ProfileName))
rightName := strings.ToLower(strings.TrimSpace(items[j].ProfileName))
if leftName != rightName {
return leftName < rightName
}
return items[i].ProfileId < items[j].ProfileId
})
}
func buildAutomationTargetAmbiguousError(items []browser.Profile) string {
const maxPreview = 5
parts := make([]string, 0, minAutomationInt(len(items), maxPreview))
for i := 0; i < len(items) && i < maxPreview; i++ {
parts = append(parts, automationProfileLabel(items[i]))
}
suffix := ""
if len(items) > maxPreview {
suffix = fmt.Sprintf(" 等 %d 个实例", len(items))
}
return fmt.Sprintf("命中了多个实例:%s%s。请改用 code/profileId,或继续加分组、标签、关键字缩小范围", strings.Join(parts, ""), suffix)
}
func automationProfileLabel(profile browser.Profile) string {
label := strings.TrimSpace(profile.ProfileName)
if label == "" {
label = strings.TrimSpace(profile.ProfileId)
}
if code := strings.TrimSpace(profile.LaunchCode); code != "" {
return fmt.Sprintf("%s[id=%s, code=%s]", label, profile.ProfileId, code)
}
return fmt.Sprintf("%s[id=%s]", label, profile.ProfileId)
}
func automationProfileSelector(profileID string) map[string]any {
return map[string]any{
"profileId": strings.TrimSpace(profileID),
}
}
func (a *App) pickAutomationRoundRobinTarget(scriptID string, selector automation.ScriptTargetSelector, profiles []browser.Profile) browser.Profile {
rotationKey := buildAutomationTargetRotationKey(scriptID, selector)
a.automationTargetMu.Lock()
defer a.automationTargetMu.Unlock()
lastProfileID := strings.TrimSpace(a.automationTargetCursor[rotationKey])
nextIndex := 0
if lastProfileID != "" {
for idx, profile := range profiles {
if profile.ProfileId == lastProfileID {
nextIndex = (idx + 1) % len(profiles)
break
}
}
}
selected := profiles[nextIndex]
a.automationTargetCursor[rotationKey] = selected.ProfileId
return selected
}
func buildAutomationTargetRotationKey(scriptID string, selector automation.ScriptTargetSelector) string {
payload := normalizeAutomationTargetSelector(selector)
data, err := json.Marshal(payload)
if err != nil {
return strings.TrimSpace(scriptID)
}
return strings.TrimSpace(scriptID) + ":" + string(data)
}
func buildAutomationCreatedProfileName(template string, script automation.ScriptRecord, source browser.Profile) string {
now := time.Now()
pattern := strings.TrimSpace(template)
if pattern == "" {
pattern = defaultAutomationCreateNameTemplate
}
replacements := map[string]string{
"${timestamp}": now.Format("20060102-150405"),
"${date}": now.Format("20060102"),
"${time}": now.Format("150405"),
"${templateName}": strings.TrimSpace(source.ProfileName),
"${scriptName}": strings.TrimSpace(script.Name),
}
for placeholder, value := range replacements {
pattern = strings.ReplaceAll(pattern, placeholder, value)
}
pattern = strings.TrimSpace(pattern)
if pattern != "" {
return pattern
}
templateName := strings.TrimSpace(source.ProfileName)
if templateName == "" {
templateName = "自动化实例"
}
return fmt.Sprintf("%s-%s", templateName, now.Format("20060102-150405"))
}
func appendAutomationRunSummary(summary string, targetSummary string) string {
summary = strings.TrimSpace(summary)
targetSummary = strings.TrimSpace(targetSummary)
if targetSummary == "" {
return summary
}
if summary == "" {
return targetSummary
}
return summary + " · " + targetSummary
}
func minAutomationInt(a, b int) int {
if a < b {
return a
}
return b
}