Files
isphere-ai-bridge/internal/uiaselector/matcher.go
2026-07-09 15:10:01 +08:00

187 lines
4.7 KiB
Go

package uiaselector
import "strings"
type MatchResult struct {
OK bool `json:"ok"`
SelectorID string `json:"selector_id"`
Matched bool `json:"matched"`
MatchCount int `json:"match_count"`
Matches []NodeMatch `json:"matches"`
Warnings []string `json:"warnings"`
Error MatchError `json:"error,omitempty"`
}
type MatchError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type NodeMatch struct {
Path string `json:"path"`
ControlType string `json:"control_type"`
AutomationID string `json:"automation_id"`
ClassName string `json:"class_name"`
FrameworkID string `json:"framework_id"`
HasName bool `json:"has_name"`
NameLength int `json:"name_length"`
Name string `json:"-"`
}
type nodeRef struct {
path string
node *Node
}
func MatchSelector(root *Node, catalog []Selector, selectorID string) MatchResult {
selector, ok := findSelector(catalog, selectorID)
if !ok {
return MatchResult{OK: false, SelectorID: selectorID, Error: MatchError{Code: "UNKNOWN_SELECTOR", Message: "selector is not in catalog"}}
}
if selector.AllowedUse != AllowedUseReadOnlyLocate {
return MatchResult{OK: false, SelectorID: selectorID, Error: MatchError{Code: "INVALID_SELECTOR", Message: "selector is not read-only"}}
}
refs := flatten(root)
matches := matchCriteria(refs, selector.Required)
if len(matches) == 0 {
for _, fallback := range selector.Fallback {
matches = matchFallback(refs, fallback)
if len(matches) > 0 {
break
}
}
}
result := MatchResult{OK: true, SelectorID: selectorID, Matched: len(matches) > 0, MatchCount: len(matches), Warnings: []string{}}
for _, ref := range matches {
result.Matches = append(result.Matches, summarizeNode(ref))
}
if !result.Matched {
result.Warnings = append(result.Warnings, "selector_not_found")
}
return result
}
func findSelector(catalog []Selector, selectorID string) (Selector, bool) {
for _, selector := range catalog {
if selector.ID == selectorID {
return selector, true
}
}
return Selector{}, false
}
func flatten(root *Node) []nodeRef {
refs := []nodeRef{}
var walk func(node *Node, path string)
walk = func(node *Node, path string) {
if node == nil {
return
}
refs = append(refs, nodeRef{path: path, node: node})
for i := range node.Children {
walk(&node.Children[i], path+"/"+itoa(i))
}
}
walk(root, "root")
return refs
}
func itoa(value int) string {
if value == 0 {
return "0"
}
digits := []byte{}
for value > 0 {
digits = append([]byte{byte('0' + value%10)}, digits...)
value = value / 10
}
return string(digits)
}
func matchCriteria(refs []nodeRef, criteria Criteria) []nodeRef {
matches := []nodeRef{}
for _, ref := range refs {
if criteria.AutomationID != "" && ref.node.AutomationID != criteria.AutomationID {
continue
}
if criteria.ControlType != "" && ref.node.ControlType != criteria.ControlType {
continue
}
if criteria.FrameworkID != "" && ref.node.FrameworkID != criteria.FrameworkID {
continue
}
if criteria.ClassName != "" && ref.node.ClassName != criteria.ClassName {
continue
}
if criteria.ClassPrefix != "" && !strings.HasPrefix(ref.node.ClassName, criteria.ClassPrefix) {
continue
}
matches = append(matches, ref)
}
return matches
}
func matchFallback(refs []nodeRef, fallback FallbackCriteria) []nodeRef {
matches := []nodeRef{}
for _, ref := range refs {
if fallback.ControlType != "" && ref.node.ControlType != fallback.ControlType {
continue
}
if fallback.ClassPrefix != "" && !strings.HasPrefix(ref.node.ClassName, fallback.ClassPrefix) {
continue
}
if !hasChildAutomationIDs(ref.node, fallback.ChildAutomationIDContains) {
continue
}
if !hasChildControlTypes(ref.node, fallback.ChildControlTypeContains) {
continue
}
matches = append(matches, ref)
}
return matches
}
func hasChildAutomationIDs(node *Node, ids []string) bool {
for _, id := range ids {
found := false
for i := range node.Children {
if node.Children[i].AutomationID == id {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func hasChildControlTypes(node *Node, controlTypes []string) bool {
for _, controlType := range controlTypes {
found := false
for i := range node.Children {
if node.Children[i].ControlType == controlType {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func summarizeNode(ref nodeRef) NodeMatch {
return NodeMatch{
Path: ref.path,
ControlType: ref.node.ControlType,
AutomationID: ref.node.AutomationID,
ClassName: ref.node.ClassName,
FrameworkID: ref.node.FrameworkID,
HasName: ref.node.Name != "",
NameLength: len(ref.node.Name),
}
}