mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
400 lines
13 KiB
Go
400 lines
13 KiB
Go
package backend
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"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) {
|
|
if mode := automationScriptRunTargetMode(script, input); mode != automationScriptTargetMode(script) {
|
|
script.TargetConfig.Mode = mode
|
|
}
|
|
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 automationScriptRunTargetMode(script automation.ScriptRecord, input automation.ScriptRunRequest) string {
|
|
mode := strings.ToLower(strings.TrimSpace(input.TargetMode))
|
|
switch mode {
|
|
case "manual", "existing", "create", "rotate":
|
|
return mode
|
|
default:
|
|
return automationScriptTargetMode(script)
|
|
}
|
|
}
|
|
|
|
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 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"))
|
|
}
|