@opentiny/next-sdk 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/agent/AgentModelProvider.ts +78 -81
  2. package/agent/type.ts +6 -9
  3. package/agent/utils/getAISDKTools.ts +0 -1
  4. package/agent/utils/getBuiltinMcpTools.ts +7 -7
  5. package/core.ts +0 -3
  6. package/dist/SimulatorMask-BHVXyogh-CARX3Rff.js +361 -0
  7. package/dist/agent/type.d.ts +4 -12
  8. package/dist/agent/utils/getBuiltinMcpTools.d.ts +3 -3
  9. package/dist/core.d.ts +0 -1
  10. package/dist/core.js +16 -17
  11. package/dist/index-R_HIbfUX.js +6604 -0
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.js +76 -4969
  14. package/dist/{initialize-builtin-WebMCP-HgObT902.js → initialize-builtin-WebMCP-JaoKwVlm.js} +1156 -1037
  15. package/dist/page-tools/a11y/build.d.ts +10 -0
  16. package/dist/page-tools/a11y/config.d.ts +96 -0
  17. package/dist/page-tools/a11y/constants.d.ts +11 -0
  18. package/dist/page-tools/a11y/search.d.ts +17 -0
  19. package/dist/page-tools/a11y/types.d.ts +95 -0
  20. package/dist/page-tools/a11y/utils.d.ts +55 -0
  21. package/dist/page-tools/a11y/vnode.d.ts +40 -0
  22. package/dist/page-tools/a11y-tree.d.ts +9 -99
  23. package/dist/page-tools/configs/console-cloud.d.ts +6 -0
  24. package/dist/page-tools/constants.d.ts +10 -0
  25. package/dist/page-tools/context.d.ts +40 -0
  26. package/dist/page-tools/handlers/browserState.d.ts +8 -0
  27. package/dist/page-tools/handlers/click.d.ts +8 -0
  28. package/dist/page-tools/handlers/executeJavascript.d.ts +8 -0
  29. package/dist/page-tools/handlers/fill.d.ts +8 -0
  30. package/dist/page-tools/handlers/scroll.d.ts +8 -0
  31. package/dist/page-tools/handlers/searchTree.d.ts +9 -0
  32. package/dist/page-tools/handlers/select.d.ts +8 -0
  33. package/dist/page-tools/page-agent-highlight/index.d.ts +21 -0
  34. package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +16 -0
  35. package/dist/page-tools/page-agent-mask/checkDarkMode.d.ts +5 -0
  36. package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
  37. package/dist/page-tools/page-agent-tool.d.ts +3 -8
  38. package/dist/page-tools/schema.d.ts +44 -0
  39. package/dist/page-tools/tool-config.d.ts +50 -0
  40. package/dist/page-tools/utils/dom.d.ts +6 -0
  41. package/dist/page-tools/utils/scroll.d.ts +15 -0
  42. package/dist/runtime.d.ts +7 -0
  43. package/dist/runtime.js +732 -0
  44. package/dist/utils/builtinProxy.d.ts +1 -1
  45. package/dist/vitest.config.d.ts +2 -0
  46. package/index.ts +35 -5
  47. package/package.json +23 -29
  48. package/page-tools/a11y/build.ts +74 -0
  49. package/page-tools/a11y/config.ts +465 -0
  50. package/page-tools/a11y/constants.ts +131 -0
  51. package/page-tools/a11y/search.ts +127 -0
  52. package/page-tools/a11y/types.ts +105 -0
  53. package/page-tools/a11y/utils.ts +239 -0
  54. package/page-tools/a11y/vnode.ts +439 -0
  55. package/page-tools/a11y-tree.ts +9 -527
  56. package/page-tools/bridge.ts +23 -3
  57. package/page-tools/configs/console-cloud.ts +172 -0
  58. package/page-tools/constants.ts +12 -0
  59. package/page-tools/context.ts +50 -0
  60. package/page-tools/handlers/browserState.ts +12 -0
  61. package/page-tools/handlers/click.ts +30 -0
  62. package/page-tools/handlers/executeJavascript.ts +22 -0
  63. package/page-tools/handlers/fill.ts +65 -0
  64. package/page-tools/handlers/scroll.ts +66 -0
  65. package/page-tools/handlers/searchTree.ts +27 -0
  66. package/page-tools/handlers/select.ts +39 -0
  67. package/page-tools/page-agent-highlight/index.ts +245 -0
  68. package/page-tools/page-agent-mask/SimulatorMask.module.css +14 -0
  69. package/page-tools/page-agent-mask/SimulatorMask.ts +299 -0
  70. package/page-tools/page-agent-mask/checkDarkMode.ts +181 -0
  71. package/page-tools/page-agent-mask/cursor-border.svg +3 -0
  72. package/page-tools/page-agent-mask/cursor-fill.svg +5 -0
  73. package/page-tools/page-agent-mask/cursor.module.css +70 -0
  74. package/page-tools/page-agent-mask/hauwei.svg +25 -0
  75. package/page-tools/page-agent-prompt.md +34 -18
  76. package/page-tools/page-agent-tool-event.ts +113 -0
  77. package/page-tools/page-agent-tool.ts +146 -162
  78. package/page-tools/schema.ts +52 -0
  79. package/page-tools/tool-config.ts +100 -0
  80. package/page-tools/utils/dom.ts +158 -0
  81. package/page-tools/utils/scroll.ts +58 -0
  82. package/runtime.ts +44 -0
  83. package/test/page-tools/a11y/build.test.ts +638 -0
  84. package/test/page-tools/a11y/config.test.ts +370 -0
  85. package/test/page-tools/configs/console-cloud.test.ts +168 -0
  86. package/test/page-tools/page-agent-highlight.test.ts +110 -0
  87. package/test/page-tools/page-agent-tool-dispatch.test.ts +208 -0
  88. package/test/page-tools/page-agent-tool.test.ts +102 -0
  89. package/test/page-tools/tool-config.test.ts +112 -0
  90. package/test/page-tools/utils/dom.test.ts +122 -0
  91. package/utils/builtinProxy.ts +45 -13
  92. package/vite.config.runtime.ts +22 -0
  93. package/vite.config.ts +52 -8
  94. package/vitest.config.ts +10 -0
  95. package/McpSdk.ts +0 -14
  96. package/WebAgent.ts +0 -5
  97. package/WebMcp.ts +0 -26
  98. package/Zod.ts +0 -1
  99. package/dist/McpSdk.d.ts +0 -14
  100. package/dist/SimulatorMask-BHVXyogh-BFEGpD5S.js +0 -1048
  101. package/dist/SimulatorMask-BHVXyogh-CCYbrb84.js +0 -801
  102. package/dist/WebAgent.d.ts +0 -5
  103. package/dist/WebMcp.d.ts +0 -23
  104. package/dist/Zod.d.ts +0 -1
  105. package/dist/index.es.dev.js +0 -59017
  106. package/dist/index.es.js +0 -46795
  107. package/dist/index.umd.dev.js +0 -60355
  108. package/dist/index.umd.js +0 -1248
  109. package/dist/mcpsdk@1.25.3.dev.js +0 -22780
  110. package/dist/mcpsdk@1.25.3.es.dev.js +0 -22778
  111. package/dist/mcpsdk@1.25.3.es.js +0 -16960
  112. package/dist/mcpsdk@1.25.3.js +0 -48
  113. package/dist/transport/ExtensionClientTransport.d.ts +0 -24
  114. package/dist/transport/ExtensionContentServerTransport.d.ts +0 -39
  115. package/dist/transport/ExtensionPageServerTransport.d.ts +0 -36
  116. package/dist/transport/messages.d.ts +0 -9
  117. package/dist/vite.config.mcpSdk.d.ts +0 -2
  118. package/dist/vite.config.webAgent.d.ts +0 -2
  119. package/dist/vite.config.webMcp.d.ts +0 -2
  120. package/dist/vite.config.webMcpFull.d.ts +0 -2
  121. package/dist/vite.config.zod.d.ts +0 -2
  122. package/dist/webagent.dev.js +0 -49360
  123. package/dist/webagent.es.dev.js +0 -49071
  124. package/dist/webagent.es.js +0 -39219
  125. package/dist/webagent.js +0 -642
  126. package/dist/webmcp-full.dev.js +0 -31336
  127. package/dist/webmcp-full.es.dev.js +0 -30283
  128. package/dist/webmcp-full.es.js +0 -22889
  129. package/dist/webmcp-full.js +0 -645
  130. package/dist/webmcp.dev.js +0 -9572
  131. package/dist/webmcp.es.dev.js +0 -8518
  132. package/dist/webmcp.es.js +0 -6727
  133. package/dist/webmcp.js +0 -602
  134. package/dist/zod@3.25.76.dev.js +0 -4037
  135. package/dist/zod@3.25.76.es.dev.js +0 -4033
  136. package/dist/zod@3.25.76.es.js +0 -2945
  137. package/dist/zod@3.25.76.js +0 -1
  138. package/transport/ExtensionClientTransport.ts +0 -100
  139. package/transport/ExtensionContentServerTransport.ts +0 -162
  140. package/transport/ExtensionPageServerTransport.ts +0 -149
  141. package/transport/messages.ts +0 -63
  142. package/vite-build-tsc.ts +0 -63
  143. package/vite-env.d.ts +0 -10
  144. package/vite.config.mcpSdk.ts +0 -28
  145. package/vite.config.webAgent.ts +0 -19
  146. package/vite.config.webMcp.ts +0 -40
  147. package/vite.config.webMcpFull.ts +0 -19
  148. package/vite.config.zod.ts +0 -23
  149. /package/dist/{vite-build-tsc.d.ts → vite.config.runtime.d.ts} +0 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * a11y/config.ts
3
+ *
4
+ * 统一无障碍配置(A11yConfig):把角色推断、状态推断、白/黑名单、自定义属性、
5
+ * 弹窗选择器等配置项收敛为一个对象,并提供:
6
+ * 1. 声明式规则(roles/states)的合并与匹配逻辑
7
+ * 2. 底层的逐元素解析函数(resolveA11yRole/resolveA11yStates/resolveA11yInfo)
8
+ *
9
+ * 运行期读写 API 统一由 ../tool-config.ts 的 getPageAgentToolConfig/setPageAgentToolConfig 提供
10
+ * (a11yConfig 是其中一个字段),这里不再单独维护一套 get/set。
11
+ */
12
+
13
+ import { TAG_ROLE_MAP, INPUT_TYPE_ROLE, DEFAULT_ERROR_SELECTORS, DEFAULT_WARNING_SELECTORS, DEFAULT_DIALOG_SELECTORS } from './constants'
14
+
15
+ // ─── 类型定义 ────────────────────────────────────────────────────────────
16
+
17
+ export interface A11yMatcher {
18
+ /**
19
+ * 标准 CSS 选择器。优先使用 selector,仅当选择器表达不了时再写 match。
20
+ * - **states**:用 `closest` 判断自身或祖先是否命中(适合 error/selected 等挂在容器上的状态)
21
+ * - **roles**:用 `matches` 仅判断自身是否命中(避免角色传染给子孙节点)
22
+ * 支持标签/属性/id/组合选择器,也支持字符串数组(任意一个命中即可)。
23
+ */
24
+ selector?: string | string[]
25
+ /** 自定义判断函数,优先级高于 selector,用于读取计算样式、组合条件等选择器表达不了的场景 */
26
+ match?: (el: Element) => boolean
27
+ }
28
+
29
+ export type A11yStateName =
30
+ | 'checked' | 'selected' | 'pressed' | 'current' | 'expanded' | 'hasPopup'
31
+ | 'disabled' | 'readonly' | 'required' | 'invalid' | 'busy'
32
+ | 'error' | 'warning'
33
+ | (string & {}) // 允许任意自定义状态名,同时保留标准值的自动补全
34
+
35
+ export interface A11yRoleRule extends A11yMatcher {
36
+ /** 命中后赋予的 ARIA 角色,如 'tab' | 'tabpanel' | 'switch' | 'treeitem' */
37
+ role: string
38
+ /** 为 true 时覆盖元素已有的显式 role 属性,默认 false(不覆盖开发者显式设置) */
39
+ force?: boolean
40
+ }
41
+
42
+ export interface A11yConfig {
43
+ /** 角色推断规则:用于弥补页面缺失的语义 role(如自定义 Tab 组件没有 role=tab) */
44
+ roles?: A11yRoleRule[]
45
+ /** 状态推断规则:key 为状态名,value 为一条或多条规则(命中任意一条即成立),与标准 aria-* 检测结果取"或" */
46
+ states?: Partial<Record<A11yStateName, A11yMatcher | A11yMatcher[]>>
47
+ /** 白名单:强制识别为可交互元素并纳入无障碍树。支持 Element 引用或 CSS 选择器字符串(字符串每次动态解析,适配 SPA 重渲染) */
48
+ whitelist?: Array<Element | string>
49
+ /** 黑名单:强制从无障碍树中排除,规则同上 */
50
+ blacklist?: Array<Element | string>
51
+ /** 额外暴露的自定义 DOM 属性(作为 token 输出,如 [data-testid="xxx"]) */
52
+ exposedAttributes?: string[]
53
+ /** 模态弹窗 CSS 选择器(用于 detectPageDialog 检测阻塞交互的弹窗) */
54
+ dialogSelectors?: string[]
55
+ }
56
+
57
+ /** 单个元素解析出的完整无障碍信息 */
58
+ export interface A11yInfo {
59
+ role: string
60
+ tokens: string[]
61
+ }
62
+
63
+ /** 标记已与默认值合并并规整过的配置,避免热路径上二次 merge 导致默认规则翻倍 */
64
+ const RESOLVED_A11Y_BRAND = Symbol.for('@opentiny/next-sdk:ResolvedA11yConfig')
65
+
66
+ /**
67
+ * 合并/规整后的完整无障碍配置:与用户书写的 {@link A11yConfig} 的唯一区别是
68
+ * states 的每个状态名都统一规范化为数组(不再是 `A11yMatcher | A11yMatcher[]`)。
69
+ * resolveA11yInfo 内部与 getPageAgentToolConfig().a11yConfig 读取到的都是这个类型。
70
+ */
71
+ export interface ResolvedA11yConfig {
72
+ roles: A11yRoleRule[]
73
+ states: Partial<Record<A11yStateName, A11yMatcher[]>>
74
+ whitelist: Array<Element | string>
75
+ blacklist: Array<Element | string>
76
+ exposedAttributes: string[]
77
+ dialogSelectors: string[]
78
+ readonly [RESOLVED_A11Y_BRAND]?: true
79
+ }
80
+
81
+ function markResolved(config: ResolvedA11yConfig): ResolvedA11yConfig {
82
+ return Object.assign(config, { [RESOLVED_A11Y_BRAND]: true as const })
83
+ }
84
+
85
+ /** 是否为 {@link mergeA11yConfig}/{@link mergeA11yConfigs} 产出的已规整配置 */
86
+ export function isResolvedA11yConfig(
87
+ config?: A11yConfig | ResolvedA11yConfig | null,
88
+ ): config is ResolvedA11yConfig {
89
+ return !!config && (config as ResolvedA11yConfig)[RESOLVED_A11Y_BRAND] === true
90
+ }
91
+
92
+ /** 内置已知状态名(用于区分"标准状态"与"用户自定义状态",避免重复输出 token) */
93
+ const STANDARD_STATE_NAMES: string[] = [
94
+ 'checked', 'selected', 'pressed', 'current', 'expanded', 'hasPopup',
95
+ 'disabled', 'readonly', 'required', 'invalid', 'busy',
96
+ 'error', 'warning',
97
+ ]
98
+
99
+ // ─── 默认配置 ────────────────────────────────────────────────────────────
100
+
101
+ // 检测 CSS 激活/选中状态类名(用于未使用标准 ARIA 的 Tab/选项组件,如按钮组/镜像选择)
102
+ // 仅当作为独立 class 词或有连字符前缀时匹配,避免误匹配 "interactive" 等
103
+ const DEFAULT_SELECTED_CLASS_RE = /\b(is-active|isActive|is-selected|isSelected|is-current|isCurrent|active-item|activeItem|tab-active|tabActive|active|selected|current)\b/
104
+
105
+ function defaultSelectedMatch(el: Element): boolean {
106
+ const cls = typeof (el as HTMLElement).className === 'string' ? (el as HTMLElement).className : ''
107
+ if (!cls || !DEFAULT_SELECTED_CLASS_RE.test(cls)) return false
108
+ // 只对有明确角色的元素输出,避免太多噪音
109
+ const role = el.getAttribute('role') || el.tagName.toLowerCase()
110
+ return ['button', 'option', 'a', 'li', 'generic'].includes(role) || role.startsWith('tab')
111
+ }
112
+
113
+ /** 默认生效的无障碍配置:零配置即可覆盖 ARIA 标准 + 主流 UI 框架的常见错误/警告/选中态检测 */
114
+ export const DEFAULT_A11Y_CONFIG: ResolvedA11yConfig = markResolved({
115
+ roles: [],
116
+ states: {
117
+ selected: [
118
+ { match: defaultSelectedMatch },
119
+ // Tiny3 按钮组/单选按钮组:选中 class 在容器上(.ti3-btn-item-container.ti3-active),
120
+ // 内部 button 自身无 aria-selected / ti3-active,需靠 closest 命中
121
+ { selector: '.ti3-btn-item-container.ti3-active' },
122
+ ],
123
+ error: [{ selector: DEFAULT_ERROR_SELECTORS }],
124
+ warning: [{ selector: DEFAULT_WARNING_SELECTORS }],
125
+ },
126
+ whitelist: [],
127
+ blacklist: [],
128
+ exposedAttributes: [],
129
+ dialogSelectors: DEFAULT_DIALOG_SELECTORS,
130
+ })
131
+
132
+ // ─── 匹配辅助 ────────────────────────────────────────────────────────────
133
+
134
+ /** 将 selector 规整为单个 CSS 选择器字符串:数组按逗号拼接为选择器列表,语义等价于"任意一个命中即可" */
135
+ function normalizeSelector(selector?: string | string[]): string | undefined {
136
+ if (!selector) return undefined
137
+ const joined = Array.isArray(selector) ? selector.filter(Boolean).join(', ') : selector
138
+ return joined || undefined
139
+ }
140
+
141
+ /**
142
+ * 通用匹配:match 优先;selector 用 closest(自身或祖先)。
143
+ * 用于 states:容器上的 error/selected class 需要能命中内部元素。
144
+ */
145
+ function matchesRule(el: Element, rule: A11yMatcher): boolean {
146
+ if (rule.match) {
147
+ try {
148
+ return !!rule.match(el)
149
+ } catch {
150
+ return false
151
+ }
152
+ }
153
+ const selector = normalizeSelector(rule.selector)
154
+ if (selector) {
155
+ try {
156
+ return !!el.closest(selector)
157
+ } catch {
158
+ // 忽略非法选择器
159
+ return false
160
+ }
161
+ }
162
+ return false
163
+ }
164
+
165
+ /**
166
+ * 角色匹配:match 优先;selector 用 matches(仅自身)。
167
+ * 避免 `{ role: 'tab', selector: '.tab-item' }` 把 tab 角色传染给子孙节点。
168
+ */
169
+ function matchesRoleRule(el: Element, rule: A11yMatcher): boolean {
170
+ if (rule.match) {
171
+ try {
172
+ return !!rule.match(el)
173
+ } catch {
174
+ return false
175
+ }
176
+ }
177
+ const selector = normalizeSelector(rule.selector)
178
+ if (selector) {
179
+ try {
180
+ return !!el.matches(selector)
181
+ } catch {
182
+ return false
183
+ }
184
+ }
185
+ return false
186
+ }
187
+
188
+ function matchesAnyRule(el: Element, rules?: A11yMatcher | A11yMatcher[]): boolean {
189
+ if (!rules) return false
190
+ const list = Array.isArray(rules) ? rules : [rules]
191
+ return list.some((rule) => matchesRule(el, rule))
192
+ }
193
+
194
+ /** 从状态规则中提取纯 CSS 选择器列表(忽略只有 match 函数、没有 selector 的规则;数组 selector 会被展开),供页面级选择器扫描场景复用 */
195
+ export function extractSelectors(rules?: A11yMatcher | A11yMatcher[]): string[] {
196
+ if (!rules) return []
197
+ const list = Array.isArray(rules) ? rules : [rules]
198
+ return list.flatMap((r) => (Array.isArray(r.selector) ? r.selector.filter(Boolean) : r.selector ? [r.selector] : []))
199
+ }
200
+
201
+ // ─── 合并逻辑 ────────────────────────────────────────────────────────────
202
+
203
+ function concatArr<T>(a?: T[], b?: T[]): T[] {
204
+ return [...(a ?? []), ...(b ?? [])]
205
+ }
206
+
207
+ function normalizeMatcherList(value?: A11yMatcher | A11yMatcher[]): A11yMatcher[] {
208
+ if (!value) return []
209
+ return Array.isArray(value) ? value : [value]
210
+ }
211
+
212
+ function mergeStates(
213
+ base?: A11yConfig['states'],
214
+ patch?: A11yConfig['states'],
215
+ ): ResolvedA11yConfig['states'] {
216
+ const result: NonNullable<ResolvedA11yConfig['states']> = {}
217
+ // 无论某个状态名是否被 patch 触及,都统一规范化为数组,保证 states.<name> 的返回类型一致,
218
+ // 避免"只有被 patch 过的 key 才是数组,其余仍是单个 matcher 对象"的不一致行为
219
+ const keys = new Set<string>([...Object.keys(base ?? {}), ...Object.keys(patch ?? {})])
220
+ for (const key of keys) {
221
+ result[key] = [...normalizeMatcherList(base?.[key]), ...normalizeMatcherList(patch?.[key])]
222
+ }
223
+ return result
224
+ }
225
+
226
+ /** 合并两份 A11yConfig:数组类字段拼接(additive,不丢失 base 中已有的规则),states 按 key 独立合并 */
227
+ export function mergeA11yConfigs(base: A11yConfig, patch: A11yConfig): ResolvedA11yConfig {
228
+ return markResolved({
229
+ roles: concatArr(base.roles, patch.roles),
230
+ states: mergeStates(base.states, patch.states),
231
+ whitelist: concatArr(base.whitelist, patch.whitelist),
232
+ blacklist: concatArr(base.blacklist, patch.blacklist),
233
+ exposedAttributes: concatArr(base.exposedAttributes, patch.exposedAttributes),
234
+ dialogSelectors: concatArr(base.dialogSelectors, patch.dialogSelectors),
235
+ })
236
+ }
237
+
238
+ /**
239
+ * 将用户配置与默认配置合并(additive),得到最终生效的完整配置。
240
+ * 若入参已是 {@link ResolvedA11yConfig},直接返回(幂等,避免默认规则被再次拼接)。
241
+ */
242
+ export function mergeA11yConfig(user?: A11yConfig | ResolvedA11yConfig): ResolvedA11yConfig {
243
+ if (isResolvedA11yConfig(user)) return user
244
+ return mergeA11yConfigs(DEFAULT_A11Y_CONFIG, user ?? {})
245
+ }
246
+
247
+ /**
248
+ * 保证拿到已与默认值合并的配置:已规整则原样返回(同引用,零开销),否则与默认配置合并。
249
+ * buildA11yTree / resolveA11yInfo 热路径应优先走此函数,避免对运行期配置反复 merge。
250
+ */
251
+ export function ensureResolvedA11yConfig(config?: A11yConfig | ResolvedA11yConfig): ResolvedA11yConfig {
252
+ if (isResolvedA11yConfig(config)) return config
253
+ return mergeA11yConfig(config)
254
+ }
255
+
256
+ /** 恒等函数,仅用于书写配置时获得 TS 类型提示/校验(风格对齐 defineConfig) */
257
+ export function defineA11yConfig(config: A11yConfig): A11yConfig {
258
+ return config
259
+ }
260
+
261
+ // ─── 角色 / 状态解析 ─────────────────────────────────────────────────────
262
+
263
+ function computeRole(el: Element, resolved: ResolvedA11yConfig): string {
264
+ const explicit = el.getAttribute('role')
265
+ const hasExplicit = !!explicit && explicit !== 'presentation' && explicit !== 'none'
266
+
267
+ for (const rule of resolved.roles) {
268
+ if (hasExplicit && !rule.force) continue
269
+ if (matchesRoleRule(el, rule)) return rule.role
270
+ }
271
+
272
+ if (hasExplicit) return explicit as string
273
+
274
+ const tag = el.tagName.toLowerCase()
275
+ if (tag === 'input') {
276
+ const inputType = (el as HTMLInputElement).type?.toLowerCase() ?? 'text'
277
+ return INPUT_TYPE_ROLE[inputType] ?? 'textbox'
278
+ }
279
+ return TAG_ROLE_MAP[tag] ?? 'generic'
280
+ }
281
+
282
+ function computeStates(el: Element, resolved: ResolvedA11yConfig): string[] {
283
+ const tokens: string[] = []
284
+ const aria = (k: string) => el.getAttribute(k)
285
+ const states = resolved.states
286
+ const has = (name: A11yStateName) => matchesAnyRule(el, states[name])
287
+
288
+ // checked:aria-checked 三态(true/false/mixed)优先,其次原生 input.checked / label[for] 关联,最后自定义规则兜底
289
+ const ariaChecked = aria('aria-checked')
290
+ if (ariaChecked === 'true') {
291
+ tokens.push('checked')
292
+ } else if (ariaChecked === 'mixed') {
293
+ tokens.push('checked=mixed')
294
+ } else if (ariaChecked === 'false') {
295
+ tokens.push('unchecked')
296
+ } else {
297
+ const elTag = el.tagName.toLowerCase()
298
+ let nativeChecked: boolean | undefined
299
+ if (elTag === 'input' && ((el as HTMLInputElement).type === 'checkbox' || (el as HTMLInputElement).type === 'radio')) {
300
+ nativeChecked = (el as HTMLInputElement).checked
301
+ } else if (elTag === 'label' && el.hasAttribute('for')) {
302
+ const target = document.getElementById(el.getAttribute('for')!)
303
+ if (target instanceof HTMLInputElement && (target.type === 'checkbox' || target.type === 'radio')) {
304
+ nativeChecked = target.checked
305
+ }
306
+ }
307
+ if (nativeChecked !== undefined) {
308
+ tokens.push(nativeChecked ? 'checked' : 'unchecked')
309
+ } else if (has('checked')) {
310
+ tokens.push('checked')
311
+ }
312
+ }
313
+
314
+ // selected:aria-selected 或自定义规则(内置默认规则覆盖常见 class 命名的 Tab/选项选中态)
315
+ if (aria('aria-selected') === 'true' || has('selected')) {
316
+ tokens.push('selected')
317
+ }
318
+
319
+ // pressed:toggle 按钮三态(true/false/mixed)
320
+ const ariaPressed = aria('aria-pressed')
321
+ if (ariaPressed === 'true') {
322
+ tokens.push('pressed')
323
+ } else if (ariaPressed === 'mixed') {
324
+ tokens.push('pressed=mixed')
325
+ } else if (ariaPressed !== 'false' && has('pressed')) {
326
+ tokens.push('pressed')
327
+ }
328
+
329
+ // current:当前步骤/当前页(面包屑、分页、向导),支持 page/step/location/date/time 等取值
330
+ const ariaCurrent = aria('aria-current')
331
+ if (ariaCurrent && ariaCurrent !== 'false') {
332
+ tokens.push(ariaCurrent === 'true' ? 'current' : `current=${ariaCurrent}`)
333
+ } else if (has('current')) {
334
+ tokens.push('current')
335
+ }
336
+
337
+ // disabled
338
+ if (aria('aria-disabled') === 'true' || (el as HTMLInputElement).disabled || has('disabled')) {
339
+ tokens.push('disabled')
340
+ }
341
+
342
+ // hasPopup
343
+ const ariaHasPopup = aria('aria-haspopup')
344
+ if ((ariaHasPopup && ariaHasPopup !== 'false') || has('hasPopup')) {
345
+ tokens.push('hasPopup')
346
+ }
347
+
348
+ // expanded
349
+ if (aria('aria-expanded') === 'true' || has('expanded')) {
350
+ tokens.push('expanded')
351
+ }
352
+
353
+ // invalid:校验失败(元素自身状态,区别于基于选择器判断的 [error] 容器级 token)
354
+ const ariaInvalid = aria('aria-invalid')
355
+ if (ariaInvalid && ariaInvalid !== 'false') {
356
+ tokens.push(ariaInvalid === 'true' ? 'invalid' : `invalid=${ariaInvalid}`)
357
+ } else if (has('invalid')) {
358
+ tokens.push('invalid')
359
+ }
360
+
361
+ // readonly
362
+ if (aria('aria-readonly') === 'true' || (el as HTMLInputElement).readOnly || has('readonly')) {
363
+ tokens.push('readonly')
364
+ }
365
+
366
+ // required
367
+ if (aria('aria-required') === 'true' || (el as HTMLInputElement).required || has('required')) {
368
+ tokens.push('required')
369
+ }
370
+
371
+ // busy:加载中
372
+ if (aria('aria-busy') === 'true' || has('busy')) {
373
+ tokens.push('busy')
374
+ }
375
+
376
+ // orientation:滑块/tablist/toolbar 方向
377
+ const orientation = aria('aria-orientation')
378
+ if (orientation) tokens.push(`orientation=${orientation}`)
379
+
380
+ // sort:表头排序方向
381
+ const sort = aria('aria-sort')
382
+ if (sort && sort !== 'none') tokens.push(`sort=${sort}`)
383
+
384
+ // multiselectable
385
+ if (aria('aria-multiselectable') === 'true') tokens.push('multiselectable')
386
+
387
+ // heading level(h1-h6 或 aria-level)
388
+ const headingMatch = el.tagName.match(/^H([1-6])$/)
389
+ if (headingMatch) tokens.push(`level=${headingMatch[1]}`)
390
+ const ariaLevel = aria('aria-level')
391
+ if (ariaLevel && !headingMatch) tokens.push(`level=${ariaLevel}`)
392
+
393
+ // cursor=pointer 表示"视觉上可点击"
394
+ try {
395
+ const style = window.getComputedStyle(el as HTMLElement)
396
+ if (style.cursor === 'pointer') tokens.push('cursor=pointer')
397
+ } catch {
398
+ // 某些元素 getComputedStyle 可能抛异常,忽略
399
+ }
400
+
401
+ // 记录输入元素的值,以便在 fill/输入后在 A11y 树中显示并产生 Diff
402
+ const tag = el.tagName.toLowerCase()
403
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
404
+ const val = (el as HTMLInputElement).value
405
+ if (val !== undefined && val !== '') {
406
+ tokens.push(`value="${val}"`)
407
+ }
408
+ }
409
+ const valuenow = aria('aria-valuenow')
410
+ if (valuenow) tokens.push(`valuenow="${valuenow}"`)
411
+ // aria-valuetext 覆盖 aria-valuenow 的可读文案(如滑块显示"中"而非 50)
412
+ const valuetext = aria('aria-valuetext')
413
+ if (valuetext) tokens.push(`valuetext="${valuetext}"`)
414
+
415
+ // link 元素:暴露原生 target 属性值,提示 Agent 该链接会在新标签页打开
416
+ if (tag === 'a') {
417
+ const target = aria('target')
418
+ if (target) tokens.push(`target=${target}`)
419
+ }
420
+
421
+ // 校验错误/警告状态(ARIA 标准 + 主流 UI 框架,可配置),error 优先于 warning
422
+ if (has('error')) {
423
+ tokens.push('error')
424
+ } else if (has('warning')) {
425
+ tokens.push('warning')
426
+ }
427
+
428
+ // 自定义状态名(非标准 key),命中则直接输出同名 token,支持完全自定义状态(如 [highlighted])
429
+ for (const key of Object.keys(states)) {
430
+ if (STANDARD_STATE_NAMES.includes(key)) continue
431
+ if (matchesAnyRule(el, states[key])) tokens.push(key)
432
+ }
433
+
434
+ // 额外暴露的自定义属性白名单
435
+ for (const attr of resolved.exposedAttributes) {
436
+ const val = el.getAttribute(attr)
437
+ if (val !== null) tokens.push(`${attr}="${val}"`)
438
+ }
439
+
440
+ return Array.from(new Set(tokens))
441
+ }
442
+
443
+ export function resolveA11yRole(el: Element, config?: A11yConfig | ResolvedA11yConfig): string {
444
+ return computeRole(el, ensureResolvedA11yConfig(config))
445
+ }
446
+
447
+ export function resolveA11yStates(el: Element, config?: A11yConfig | ResolvedA11yConfig): string[] {
448
+ return computeStates(el, ensureResolvedA11yConfig(config))
449
+ }
450
+
451
+ /**
452
+ * 统一入口(供用户直接调用的底层函数):读取配置 -> 依据 roles/states 规则计算出该元素的
453
+ * 完整无障碍信息(角色 + 状态 token)。buildA11yTree 内部对每个 DOM 节点也是调用这一个函数,
454
+ * 是声明式规则与树生成结果之间唯一的桥接点;用户也可以直接调用它来调试/复用同一套解析逻辑。
455
+ *
456
+ * 若传入已是 {@link ResolvedA11yConfig}(如 getPageAgentToolConfig().a11yConfig),则跳过合并,
457
+ * 避免在整树遍历时对每个节点重复 array spread / 默认规则翻倍。
458
+ */
459
+ export function resolveA11yInfo(el: Element, config?: A11yConfig | ResolvedA11yConfig): A11yInfo {
460
+ const resolved = ensureResolvedA11yConfig(config)
461
+ return {
462
+ role: computeRole(el, resolved),
463
+ tokens: computeStates(el, resolved),
464
+ }
465
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * a11y/constants.ts
3
+ *
4
+ * 存放无障碍树依赖的静态常量配置、默认选择器及角色映射表。
5
+ */
6
+
7
+ // ─── 默认校验错误/警告选择器(ARIA 标准 + 主流 UI 框架) ──────────────────
8
+
9
+ export const DEFAULT_ERROR_SELECTORS = [
10
+ '[role="alert"]', '[aria-invalid="true"]',
11
+ '.ti3-unifyvalid-error', '.ti3-error', '.ti-error',
12
+ '.lego-text-error', '.lego-error',
13
+ '.el-form-item__error',
14
+ '.ant-form-item-explain-error',
15
+ '.is-invalid', '.invalid-feedback',
16
+ '.ng-invalid',
17
+ '.error-msg', '.error-message', '.error-text',
18
+ '.field-error', '.form-error',
19
+ '.is-error', '.has-error',
20
+ '.validate-error', '.valid-error',
21
+ ]
22
+
23
+ export const DEFAULT_WARNING_SELECTORS = [
24
+ '.ti3-warning', '.ti-warning', '.lego-text-warning',
25
+ '.warning-msg', '.warning-text', '.is-warning', '.has-warning',
26
+ ]
27
+
28
+ /** 模态弹窗默认选择器:ARIA 标准 + 主流 UI 框架(唯一来源,顶层 constants.ts 从此处重新导出) */
29
+ export const DEFAULT_DIALOG_SELECTORS = [
30
+ // W3C ARIA 标准
31
+ '[role="dialog"]',
32
+ '[role="alertdialog"]',
33
+ // Tiny3 / Lego(云控制台)
34
+ '[class*="ti3-modal"]',
35
+ '[class*="ti3-message-box"]',
36
+ // Element UI / Element Plus
37
+ '[class*="el-dialog"]',
38
+ '[class*="el-message-box"]',
39
+ // Ant Design
40
+ '[class*="ant-modal"]',
41
+ // Bootstrap
42
+ '[class*="modal-content"]',
43
+ // Vuetify
44
+ '[class*="v-dialog"]',
45
+ // Naive UI
46
+ '[class*="n-modal"]',
47
+ ]
48
+
49
+ // ─── ARIA 隐式角色静态映射表(覆盖页面 95%+ 的常用标签)───────────────────────
50
+
51
+ export const TAG_ROLE_MAP: Record<string, string> = {
52
+ a: 'link',
53
+ article: 'article',
54
+ aside: 'complementary',
55
+ button: 'button',
56
+ caption: 'caption',
57
+ cell: 'cell',
58
+ checkbox: 'checkbox',
59
+ code: 'code',
60
+ columnheader: 'columnheader',
61
+ combobox: 'combobox',
62
+ datalist: 'listbox',
63
+ dd: 'definition',
64
+ details: 'group',
65
+ dialog: 'dialog',
66
+ dt: 'term',
67
+ em: 'emphasis',
68
+ fieldset: 'group',
69
+ figure: 'figure',
70
+ footer: 'contentinfo',
71
+ form: 'form',
72
+ h1: 'heading',
73
+ h2: 'heading',
74
+ h3: 'heading',
75
+ h4: 'heading',
76
+ h5: 'heading',
77
+ h6: 'heading',
78
+ header: 'banner',
79
+ hr: 'separator',
80
+ img: 'img',
81
+ input: 'textbox', // 默认,具体 type 下面会覆盖
82
+ li: 'listitem',
83
+ link: 'link',
84
+ main: 'main',
85
+ mark: 'mark',
86
+ math: 'math',
87
+ menu: 'list',
88
+ menuitem: 'menuitem',
89
+ meter: 'meter',
90
+ nav: 'navigation',
91
+ ol: 'list',
92
+ option: 'option',
93
+ output: 'status',
94
+ p: 'paragraph',
95
+ progress: 'progressbar',
96
+ rowheader: 'rowheader',
97
+ search: 'search',
98
+ section: 'region',
99
+ select: 'listbox',
100
+ strong: 'strong',
101
+ summary: 'button',
102
+ table: 'table',
103
+ tbody: 'rowgroup',
104
+ td: 'cell',
105
+ textarea: 'textbox',
106
+ tfoot: 'rowgroup',
107
+ th: 'columnheader',
108
+ thead: 'rowgroup',
109
+ time: 'time',
110
+ tr: 'row',
111
+ ul: 'list',
112
+ }
113
+
114
+ // input[type=*] 的角色覆盖
115
+ export const INPUT_TYPE_ROLE: Record<string, string> = {
116
+ button: 'button',
117
+ checkbox: 'checkbox',
118
+ color: 'textbox',
119
+ email: 'textbox',
120
+ file: 'textbox',
121
+ image: 'button',
122
+ number: 'spinbutton',
123
+ radio: 'radio',
124
+ range: 'slider',
125
+ reset: 'button',
126
+ search: 'searchbox',
127
+ submit: 'button',
128
+ tel: 'textbox',
129
+ text: 'textbox',
130
+ url: 'textbox',
131
+ }