From d648c9476d5f7a8952d1191950e834a5cfb4e289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E5=A4=B4?= <3301352378@qq.com> Date: Thu, 9 Jul 2026 15:06:02 +0800 Subject: [PATCH] feat: add N13 UIA selector catalog --- internal/uiaselector/catalog.go | 148 +++++++++++++++++++++++++++ internal/uiaselector/catalog_test.go | 60 +++++++++++ 2 files changed, 208 insertions(+) create mode 100644 internal/uiaselector/catalog.go create mode 100644 internal/uiaselector/catalog_test.go diff --git a/internal/uiaselector/catalog.go b/internal/uiaselector/catalog.go new file mode 100644 index 0000000..701108e --- /dev/null +++ b/internal/uiaselector/catalog.go @@ -0,0 +1,148 @@ +package uiaselector + +const AllowedUseReadOnlyLocate = "read_only_locate" + +type Selector struct { + ID string `json:"id"` + Description string `json:"description"` + AllowedUse string `json:"allowed_use"` + Required Criteria `json:"required"` + Preferred PreferredCriteria `json:"preferred"` + Fallback []FallbackCriteria `json:"fallback"` + StabilityScore int `json:"stability_score"` + Evidence Evidence `json:"evidence"` +} + +type Criteria struct { + AutomationID string `json:"automation_id,omitempty"` + ControlType string `json:"control_type,omitempty"` + FrameworkID string `json:"framework_id,omitempty"` + ClassName string `json:"class_name,omitempty"` + ClassPrefix string `json:"class_name_prefix,omitempty"` + AncestorID string `json:"ancestor_selector,omitempty"` +} + +type PreferredCriteria struct { + ClassName string `json:"class_name,omitempty"` + ClassPrefix string `json:"class_name_prefix,omitempty"` + PathHint string `json:"path_hint,omitempty"` + AncestorID string `json:"ancestor_selector,omitempty"` +} + +type FallbackCriteria struct { + AncestorID string `json:"ancestor_selector,omitempty"` + ControlType string `json:"control_type,omitempty"` + ClassPrefix string `json:"class_name_prefix,omitempty"` + ChildAutomationIDContains []string `json:"child_automation_id_contains,omitempty"` + ChildControlTypeContains []string `json:"child_control_type_contains,omitempty"` +} + +type Evidence struct { + CaptureID string `json:"capture_id"` + UIAPath string `json:"uia_path"` +} + +func DefaultCatalog() []Selector { + return []Selector{ + { + ID: "main_window", + Description: "iSphere / IMPlatformClient 主窗口", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "frmMain", ControlType: "Window", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{ClassPrefix: "WindowsForms10.Window.8.app.0", PathHint: "root"}, + Fallback: []FallbackCriteria{{ControlType: "Window", ClassPrefix: "WindowsForms10.Window.8.app.0", ChildAutomationIDContains: []string{"panelLeft", "ucPluginTabControl1"}}}, + StabilityScore: 95, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root"}, + }, + { + ID: "main_plugin_tab", + Description: "主功能 tab 容器", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "ucPluginTabControl1", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/3", AncestorID: "main_window"}, + Fallback: []FallbackCriteria{{AncestorID: "main_window", ControlType: "Pane", ChildAutomationIDContains: []string{"FrmHomePage", "ucTabPage"}}}, + StabilityScore: 90, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/3"}, + }, + { + ID: "home_page_window", + Description: "首页内容窗口", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "FrmHomePage", ControlType: "Window", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/3/0/0", AncestorID: "main_plugin_tab"}, + Fallback: []FallbackCriteria{{AncestorID: "main_plugin_tab", ControlType: "Window", ClassPrefix: "WindowsForms10.Window.8.app.0"}}, + StabilityScore: 82, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/3/0/0"}, + }, + { + ID: "tab_page_content", + Description: "tab 页内容窗口", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "FrmTabPageContent", ControlType: "Window", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/3/1/0", AncestorID: "main_plugin_tab"}, + Fallback: []FallbackCriteria{{AncestorID: "main_plugin_tab", ControlType: "Window", ChildControlTypeContains: []string{"TitleBar"}}}, + StabilityScore: 82, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/3/1/0"}, + }, + { + ID: "left_panel", + Description: "左侧主面板", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "panelLeft", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/4", AncestorID: "main_window"}, + Fallback: []FallbackCriteria{{AncestorID: "main_window", ControlType: "Pane", ChildAutomationIDContains: []string{"ucPnlRoster1", "ucFuzzyQuery1"}}}, + StabilityScore: 92, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4"}, + }, + { + ID: "roster_panel", + Description: "组织/联系人容器外层", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "ucPnlRoster1", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/4/0/0", AncestorID: "left_panel"}, + Fallback: []FallbackCriteria{{AncestorID: "left_panel", ControlType: "Pane", ChildAutomationIDContains: []string{"ucRoster1"}}}, + StabilityScore: 88, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4/0/0"}, + }, + { + ID: "roster_view", + Description: "组织/联系人列表区域", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "ucRoster1", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/4/0/0/0", AncestorID: "roster_panel"}, + Fallback: []FallbackCriteria{{AncestorID: "roster_panel", ControlType: "Pane", ClassPrefix: "WindowsForms10.Window.8.app.0"}}, + StabilityScore: 86, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4/0/0/0"}, + }, + { + ID: "fuzzy_query_panel", + Description: "模糊搜索组件外层", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "ucFuzzyQuery1", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/4/1/0", AncestorID: "left_panel"}, + Fallback: []FallbackCriteria{{AncestorID: "left_panel", ControlType: "Pane", ChildAutomationIDContains: []string{"FrmFuzzyQuery"}}}, + StabilityScore: 90, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4/1/0"}, + }, + { + ID: "fuzzy_query_window", + Description: "模糊搜索内部窗口", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "FrmFuzzyQuery", ControlType: "Pane", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{PathHint: "root/4/1/0/0", AncestorID: "fuzzy_query_panel"}, + Fallback: []FallbackCriteria{{AncestorID: "fuzzy_query_panel", ControlType: "Pane", ChildAutomationIDContains: []string{"skinAlphaTxt"}}}, + StabilityScore: 88, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4/1/0/0"}, + }, + { + ID: "fuzzy_search_edit", + Description: "模糊搜索输入框", + AllowedUse: AllowedUseReadOnlyLocate, + Required: Criteria{AutomationID: "skinAlphaTxt", ControlType: "Edit", FrameworkID: "WinForm"}, + Preferred: PreferredCriteria{ClassName: "WindowsForms10.EDIT.app.0.d3a00f_r29_ad1", PathHint: "root/4/1/0/0/2", AncestorID: "fuzzy_query_window"}, + Fallback: []FallbackCriteria{{AncestorID: "fuzzy_query_window", ControlType: "Edit", ClassPrefix: "WindowsForms10.EDIT.app.0"}}, + StabilityScore: 93, + Evidence: Evidence{CaptureID: "2026-07-08-internal-sandbox-a", UIAPath: "root/4/1/0/0/2"}, + }, + } +} diff --git a/internal/uiaselector/catalog_test.go b/internal/uiaselector/catalog_test.go new file mode 100644 index 0000000..b252a53 --- /dev/null +++ b/internal/uiaselector/catalog_test.go @@ -0,0 +1,60 @@ +package uiaselector + +import ( + "strings" + "testing" +) + +func TestDefaultCatalogContainsInitialN13Selectors(t *testing.T) { + catalog := DefaultCatalog() + wantIDs := []string{ + "main_window", + "main_plugin_tab", + "home_page_window", + "tab_page_content", + "left_panel", + "roster_panel", + "roster_view", + "fuzzy_query_panel", + "fuzzy_query_window", + "fuzzy_search_edit", + } + + byID := map[string]Selector{} + for _, selector := range catalog { + byID[selector.ID] = selector + } + + for _, id := range wantIDs { + selector, ok := byID[id] + if !ok { + t.Fatalf("DefaultCatalog missing selector %q", id) + } + if selector.AllowedUse != AllowedUseReadOnlyLocate { + t.Fatalf("selector %q allowed_use = %q, want %q", id, selector.AllowedUse, AllowedUseReadOnlyLocate) + } + if selector.Required.AutomationID == "" { + t.Fatalf("selector %q must require automation_id", id) + } + if selector.Required.ControlType == "" { + t.Fatalf("selector %q must require control_type", id) + } + if selector.StabilityScore < 80 { + t.Fatalf("selector %q stability_score = %d, want >= 80", id, selector.StabilityScore) + } + } +} + +func TestDefaultCatalogDoesNotDescribeActionTools(t *testing.T) { + for _, selector := range DefaultCatalog() { + if selector.AllowedUse != AllowedUseReadOnlyLocate { + t.Fatalf("selector %q allowed_use = %q, want read-only", selector.ID, selector.AllowedUse) + } + lower := strings.ToLower(selector.ID + " " + selector.Description) + for _, forbidden := range []string{"send", "upload", "download", "receive", "conversation", "login", "file_transfer"} { + if strings.Contains(lower, forbidden) { + t.Fatalf("selector %q contains action word %q", selector.ID, forbidden) + } + } + } +}