@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,127 @@
1
+ /**
2
+ * a11y/search.ts
3
+ *
4
+ * 在无障碍树内执行基于文本或引用的搜索操作。
5
+ */
6
+
7
+ import type { SearchA11yTreeOptions, SearchA11yTreeResult, A11ySearchMatch } from './types'
8
+ import { buildA11yTree } from './build'
9
+
10
+ /**
11
+ * 在无障碍树中按关键词搜索,返回带行号的匹配结果和上下文
12
+ *
13
+ * 支持的搜索维度(均对同一个 query 字符串做包含匹配):
14
+ * - role:如 `button`、`link`、`heading`
15
+ * - accessible name:节点的语义化名称(引号内文本)
16
+ * - state token:如 `checked`、`disabled`、`expanded`
17
+ * - ref 索引:如 `#5`
18
+ *
19
+ * @example
20
+ * searchA11yTree('button') // 找全部按钮
21
+ * searchA11yTree('提交') // 找名称含"提交"的节点
22
+ * searchA11yTree('#3') // 找 ref #3
23
+ */
24
+ export function searchA11yTree(
25
+ query: string,
26
+ root: Element = document.body,
27
+ options?: SearchA11yTreeOptions,
28
+ ): SearchA11yTreeResult {
29
+ const {
30
+ contextLines = 2,
31
+ caseInsensitive = true,
32
+ maxMatches = 20,
33
+ ...treeOptions
34
+ } = options ?? {}
35
+
36
+ const safeContextLines = Math.max(0, contextLines)
37
+ const safeMaxMatches = Math.max(1, maxMatches)
38
+
39
+ // 复用 buildA11yTree 生成完整树,直接取 lines 数组(不重复构建 DOM 遍历)
40
+ const { lines, refMap, yaml } = buildA11yTree(root, treeOptions)
41
+
42
+ const needle = caseInsensitive ? query.toLowerCase() : query
43
+ const totalLines = lines.length
44
+
45
+ const isRefQuery = /^#\d+$/.test(query)
46
+ const refRegex = isRefQuery ? new RegExp(`\\s${query}(?:\\s|[\\[]|$)`) : null
47
+
48
+ // 找出所有命中行的下标(0-based)
49
+ const hitIndices: number[] = []
50
+ for (let i = 0; i < lines.length; i++) {
51
+ let matched = false
52
+ if (refRegex) {
53
+ matched = refRegex.test(lines[i])
54
+ } else {
55
+ const haystack = caseInsensitive ? lines[i].toLowerCase() : lines[i]
56
+ matched = haystack.includes(needle)
57
+ }
58
+ if (matched) {
59
+ hitIndices.push(i)
60
+ }
61
+ }
62
+
63
+ // 合并重叠的上下文区间,避免重复输出行
64
+ const mergedRanges: Array<{ start: number; end: number; hits: number[] }> = []
65
+ let isTruncated = false
66
+ for (const idx of hitIndices) {
67
+ const start = Math.max(0, idx - safeContextLines)
68
+ const end = Math.min(totalLines - 1, idx + safeContextLines)
69
+ const last = mergedRanges[mergedRanges.length - 1]
70
+ if (last && start <= last.end + 1) {
71
+ // 区间重叠或紧邻,合并
72
+ last.end = Math.max(last.end, end)
73
+ last.hits.push(idx)
74
+ } else {
75
+ if (mergedRanges.length >= safeMaxMatches) {
76
+ isTruncated = true
77
+ break
78
+ }
79
+ mergedRanges.push({ start, end, hits: [idx] })
80
+ }
81
+ }
82
+
83
+ // 构建结构化结果
84
+ const matches: A11ySearchMatch[] = mergedRanges.map((range) => ({
85
+ lineNumber: range.hits[0] + 1,
86
+ line: lines[range.hits[0]],
87
+ context: Array.from({ length: range.end - range.start + 1 }, (_, k) => ({
88
+ lineNumber: range.start + k + 1,
89
+ line: lines[range.start + k],
90
+ })),
91
+ }))
92
+
93
+ // 格式化为可读文本(模仿 grep -n -C 风格,便于 LLM 直接理解)
94
+ const textParts: string[] = [
95
+ `无障碍树搜索结果 — 关键词: "${query}" | 总行数: ${totalLines} | 命中: ${hitIndices.length} 行 | 返回分组: ${matches.length}`,
96
+ '',
97
+ ]
98
+
99
+ if (matches.length === 0) {
100
+ textParts.push('(未找到匹配项)')
101
+ } else {
102
+ matches.forEach((match, m) => {
103
+ const range = mergedRanges[m]
104
+ textParts.push(`── 分组 ${m + 1}(第 ${range.start + 1}–${range.end + 1} 行)──`)
105
+ match.context.forEach(({ lineNumber, line }) => {
106
+ const isHit = range.hits.includes(lineNumber - 1)
107
+ const ln = String(lineNumber).padStart(4)
108
+ // 命中行用 >>> 标记,上下文行用普通行号前缀
109
+ textParts.push(isHit ? `>>>${ln} | ${line}` : ` ${ln} | ${line}`)
110
+ })
111
+ textParts.push('')
112
+ })
113
+ if (isTruncated) {
114
+ textParts.push(`⚠️ 命中过多,已截断至前 ${safeMaxMatches} 个分组,建议缩小搜索范围`)
115
+ }
116
+ textParts.push(`提示:如需操作命中元素,使用其 #N 索引;如需查看完整树,请使用 browserState。`)
117
+ }
118
+
119
+ return {
120
+ text: textParts.join('\n'),
121
+ matches,
122
+ totalLines,
123
+ matchCount: hitIndices.length,
124
+ refMap,
125
+ yaml,
126
+ }
127
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * a11y/types.ts
3
+ *
4
+ * 定义无障碍树模块(A11y Tree)的核心接口和类型。
5
+ */
6
+
7
+ import type { A11yConfig } from './config'
8
+
9
+ /** ref 索引 → HTMLElement 映射,供 click/fill/select 操作使用 */
10
+ export type RefMap = Map<number, HTMLElement>
11
+
12
+ /** 内部中间态节点,与 DOM 解耦,便于剪枝和序列化 */
13
+ export interface VNode {
14
+ /** 节点的 ARIA 角色 (如 'button', 'link', 'textbox') */
15
+ role: string
16
+ /** W3C AccName 算法计算出的语义化名称 */
17
+ name: string
18
+ /** 元素相关的状态和属性标记 (如 'checked', 'disabled', 'value="xxx"') */
19
+ tokens: string[]
20
+ /** 只有交互节点才有 ref (内部索引,用于标识可在其上执行交互动作的元素) */
21
+ ref?: number
22
+ /** 关联的 DOM 元素实例 */
23
+ el: HTMLElement
24
+ /** 包含的子节点列表 */
25
+ children: VNode[]
26
+ }
27
+
28
+ /**
29
+ * 构建无障碍树时的配置选项。
30
+ * 继承统一的 A11yConfig(角色/状态规则、白/黑名单、自定义暴露属性、弹窗选择器),
31
+ * 并额外提供两个控制树"形状"(而非无障碍语义)的选项。
32
+ */
33
+ export interface A11yTreeOptions extends A11yConfig {
34
+ /**
35
+ * 是否启用剪枝:无 ref 且无 accessible name 的节点透明穿透
36
+ * 默认 true(推荐)
37
+ */
38
+ pruneUnnamed?: boolean
39
+ /**
40
+ * 强制保留的角色列表,即使无 name 也不穿透(优先级最高)
41
+ * 例如:['table', 'row'] 用于保留表格结构
42
+ */
43
+ preserveRoles?: string[]
44
+ }
45
+
46
+ /** 序列化 YAML 树时实际用到的形状选项(与无障碍语义解析无关,独立于 A11yConfig) */
47
+ export interface A11yTreeShapeOptions {
48
+ pruneUnnamed: boolean
49
+ preserveRoles: string[]
50
+ }
51
+
52
+ /** 构建无障碍树的返回结果 */
53
+ export interface A11yTreeResult {
54
+ /** 语义化 YAML 文本(供 AI 阅读和 Diff 计算) */
55
+ yaml: string
56
+ /** ref 索引 → HTMLElement 映射(供后续操作使用) */
57
+ refMap: RefMap
58
+ /** 可交互元素总数 */
59
+ interactiveCount: number
60
+ /** 原始行数组(不含 yaml 代码块包裹),供搜索使用 */
61
+ lines: string[]
62
+ }
63
+
64
+ /** 关键词搜索选项 */
65
+ export interface SearchA11yTreeOptions extends A11yTreeOptions {
66
+ /**
67
+ * 每个匹配行前后保留的上下文行数(类似 grep -C N)
68
+ * 默认 2
69
+ */
70
+ contextLines?: number
71
+ /**
72
+ * 是否大小写不敏感,默认 true
73
+ */
74
+ caseInsensitive?: boolean
75
+ /**
76
+ * 最大返回匹配分组数(防止命中过多撑爆上下文),默认 20
77
+ */
78
+ maxMatches?: number
79
+ }
80
+
81
+ /** 单个搜索匹配的分组结构 */
82
+ export interface A11ySearchMatch {
83
+ /** 主命中行行号(1-based) */
84
+ lineNumber: number
85
+ /** 主命中行内容 */
86
+ line: string
87
+ /** 含上下文的行列表(带行号) */
88
+ context: Array<{ lineNumber: number; line: string }>
89
+ }
90
+
91
+ /** searchA11yTree 函数的返回结果 */
92
+ export interface SearchA11yTreeResult {
93
+ /** 格式化后可直接发给 LLM 的文本 */
94
+ text: string
95
+ /** 结构化匹配列表 */
96
+ matches: A11ySearchMatch[]
97
+ /** 无障碍树总行数 */
98
+ totalLines: number
99
+ /** 原始命中行数(去重前) */
100
+ matchCount: number
101
+ /** 返回搜索时的 ref 映射 */
102
+ refMap: RefMap
103
+ /** 返回搜索时的 yaml 状态,用于更新缓存 */
104
+ yaml: string
105
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * a11y/utils.ts
3
+ *
4
+ * 存放生成无障碍树时的通用工具函数:可见性判断、纯文本兜底、Shadow DOM 组合树遍历等。
5
+ * 角色推断与状态 token 解析已统一迁移到 ./config(resolveA11yRole/resolveA11yStates/resolveA11yInfo)。
6
+ */
7
+
8
+ import { isTabbable } from 'tabbable'
9
+ import { resolveA11yRole, type ResolvedA11yConfig } from './config'
10
+ import { deepQuerySelectorAll } from '../utils/dom'
11
+
12
+ /**
13
+ * 非内容元素:不应进入无障碍树,其内部文本也不得被 AccName / textContent 兜底吸收。
14
+ * 云控制台常见在 body 内联 `<style>@font-face { src: url(data:font/...;base64,...) }</style>`,
15
+ * 若不剔除,整段 base64 会变成 generic 的 name,严重浪费 LLM token。
16
+ */
17
+ const NON_CONTENT_TAGS = new Set([
18
+ 'script',
19
+ 'style',
20
+ 'noscript',
21
+ 'template',
22
+ 'link',
23
+ 'meta',
24
+ 'head',
25
+ 'title',
26
+ ])
27
+
28
+ export function isNonContentElement(el: Element): boolean {
29
+ return NON_CONTENT_TAGS.has(el.tagName.toLowerCase())
30
+ }
31
+
32
+ /**
33
+ * 判断元素是否"自身"呈现 cursor:pointer(而非从祖先继承而来)。
34
+ *
35
+ * 背景:CSS 的 `cursor` 属性会向子孙继承。可点击容器(如 <a>、自定义卡片 .card-wrapper)
36
+ * 一旦设置 cursor:pointer,其内部所有子孙的 computed cursor 都会变成 pointer。
37
+ * 若仅凭「computed cursor === pointer」判定交互性,会让容器的可点击性"传染"给全部子孙,
38
+ * 产生大量误报 ref(父容器 cursor:pointer 泛滥到每个子节点)。
39
+ *
40
+ * 业界主流 DOM 提取器(如 browser-use 的 doesElementHaveInteractivePointer)同样以
41
+ * cursor:pointer 作为可点击性的核心兜底信号,并通过与父元素对比来定位真正的可点击"边界元素"。
42
+ * 因此这里的判定为:元素自身 computed cursor 为 pointer,且其父元素不是 pointer —— 说明
43
+ * 该元素(或命中它的 CSS 规则)主动声明了指针手势,是真正意义上的可点击目标。
44
+ *
45
+ * 无父元素(如 shadow host 边界、游离节点)时,没有可继承来源,视为自身声明。
46
+ */
47
+ export function hasOwnPointerCursor(el: Element): boolean {
48
+ try {
49
+ if (window.getComputedStyle(el as HTMLElement).cursor !== 'pointer') return false
50
+ const parent = el.parentElement
51
+ if (!parent) return true
52
+ return window.getComputedStyle(parent).cursor !== 'pointer'
53
+ } catch {
54
+ // 忽略跨域 iframe 等无法访问 style 的场景
55
+ return false
56
+ }
57
+ }
58
+
59
+ /** 交互态伪类:cursor 常只在这些状态下声明为 pointer(如卡片 hover 时才显示手势) */
60
+ const INTERACTION_PSEUDO_RE = /:(hover|focus|focus-visible|focus-within|active)\b/gi
61
+
62
+ /**
63
+ * 递归遍历样式表规则(含 @media / @supports 等分组规则),收集在交互伪类下声明
64
+ * cursor:pointer 的选择器,并去掉伪类得到"主体选择器"。
65
+ */
66
+ function collectInteractivePointerSelectors(rules: CSSRuleList, out: Set<string>): void {
67
+ for (const rule of Array.from(rules)) {
68
+ const styleRule = rule as CSSStyleRule
69
+ const grouping = rule as CSSGroupingRule
70
+ // @media / @supports 等分组规则:自身无 selectorText,递归其子规则
71
+ if (!styleRule.selectorText && grouping.cssRules) {
72
+ collectInteractivePointerSelectors(grouping.cssRules, out)
73
+ continue
74
+ }
75
+ const sel = styleRule.selectorText
76
+ if (!sel || !styleRule.style || styleRule.style.cursor !== 'pointer') continue
77
+ if (!/:(hover|focus|focus-visible|focus-within|active)\b/i.test(sel)) continue
78
+ for (const part of sel.split(',')) {
79
+ const base = part.replace(INTERACTION_PSEUDO_RE, '').trim()
80
+ // 去掉伪类后可能为空(如 `:hover`)或过宽(`*`),跳过以免误伤全页
81
+ if (base && base !== '*') out.add(base)
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 收集页面中"仅在 :hover/:focus/:active 等交互态下声明 cursor:pointer"的元素集合。
88
+ *
89
+ * 背景(华为云控制台实测):大量可点击卡片(如 `.container-wrapper .shadow:hover{cursor:pointer}`)
90
+ * 只在鼠标悬停时才显示手势,静止态 `getComputedStyle(el).cursor` 读到的是 `auto`,
91
+ * 因此仅凭 computed cursor 的判定(含业界 getComputedStyle 方案)会漏判这类元素。
92
+ * 这里通过扫描样式表把这类"交互态手势"补齐,作为可点击性的兜底信号。
93
+ *
94
+ * 每次构建树只调用一次(O(规则数) 扫描 + 一次合并查询),避免逐元素扫描样式表。
95
+ */
96
+ export function collectHoverPointerElements(root: Element): Set<Element> {
97
+ const set = new Set<Element>()
98
+ const selectorSet = new Set<string>()
99
+ try {
100
+ for (const sheet of Array.from(document.styleSheets)) {
101
+ let rules: CSSRuleList | null = null
102
+ try {
103
+ rules = sheet.cssRules
104
+ } catch {
105
+ // 跨域样式表无法访问 cssRules,跳过
106
+ continue
107
+ }
108
+ if (rules) collectInteractivePointerSelectors(rules, selectorSet)
109
+ }
110
+ } catch {
111
+ return set
112
+ }
113
+ if (selectorSet.size === 0) return set
114
+
115
+ const selectors = Array.from(selectorSet)
116
+ // 优先合并为单条选择器一次性查询;若含非法片段导致整体失败,退化为逐条查询保证鲁棒
117
+ try {
118
+ for (const el of deepQuerySelectorAll(selectors.join(', '), root)) set.add(el)
119
+ return set
120
+ } catch {
121
+ for (const sel of selectors) {
122
+ try {
123
+ for (const el of deepQuerySelectorAll(sel, root)) set.add(el)
124
+ } catch {
125
+ // 忽略非法选择器
126
+ }
127
+ }
128
+ }
129
+ return set
130
+ }
131
+
132
+ /** 判断元素是否应被跳过(不可见、非内容或在黑名单中) */
133
+ export function isHidden(el: Element): boolean {
134
+ if (isNonContentElement(el)) return true
135
+ if (el.getAttribute('aria-hidden') === 'true') return true
136
+ if ((el as HTMLElement).hidden) return true
137
+ try {
138
+ const style = window.getComputedStyle(el as HTMLElement)
139
+ if (style.display === 'none' || style.visibility === 'hidden') return true
140
+ // opacity:0 是常见的视觉隐藏方式,元素对用户不可见
141
+ if (style.opacity === '0') return true
142
+ // 宽或高为 0 且有 overflow 裁剪时,子内容实际不可见(如折叠的侧边栏 width:0 !important)
143
+ // 若 overflow 为 visible,子内容仍然溢出可见,不能过滤
144
+ const isClipX = style.overflowX !== 'visible'
145
+ const isClipY = style.overflowY !== 'visible'
146
+ if (parseFloat(style.width) === 0 && isClipX) return true
147
+ if (parseFloat(style.height) === 0 && isClipY) return true
148
+ } catch {
149
+ // 忽略跨域 iframe 等无法访问 style 的场景
150
+ }
151
+ return false
152
+ }
153
+
154
+
155
+ /**
156
+ * 收集元素自身或子孙节点的 title,用作图标按钮等无文本节点的名字兜底。
157
+ * 云控制台常见模式:可点击容器无 aria-label,title 挂在内部 span 上。
158
+ */
159
+ export function collectTitleLabel(el: Element): string {
160
+ const selfTitle = el.getAttribute('title')?.trim()
161
+ if (selfTitle) return selfTitle
162
+
163
+ const titled = el.querySelector('[title]')
164
+ const childTitle = titled?.getAttribute('title')?.trim()
165
+ if (childTitle) return childTitle
166
+
167
+ return ''
168
+ }
169
+
170
+ /**
171
+ * 收集子孙节点的文本内容,用作无障碍名字的兜底。
172
+ * 当普通计算无法提取文本时,遍历后代并拼接可见文本。
173
+ * @param config 已规整的无障碍配置;传入后角色判断会尊重页面自定义 roles 规则
174
+ */
175
+ export function collectDescendantText(el: Element, config?: ResolvedA11yConfig): string {
176
+ let text = ''
177
+ const walk = (node: Node) => {
178
+ if (node.nodeType === Node.TEXT_NODE) {
179
+ text += (node.textContent ?? '') + ' '
180
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
181
+ const element = node as Element
182
+ // 根节点若是 style/script 则整段丢弃;子孙遇到非内容标签则跳过子树
183
+ if (element !== el && isNonContentElement(element)) return
184
+ if (isHidden(element) && element !== el) return
185
+ // 根为非内容元素时不收集(防御)
186
+ if (element === el && isNonContentElement(element)) return
187
+
188
+ // 如果遇到嵌套的列表项或其他交互/语义节点,停止向下遍历该子树,避免兜底文本重复吸收
189
+ if (element !== el) {
190
+ const tag = element.tagName.toLowerCase()
191
+ const role = resolveA11yRole(element, config)
192
+ const isInteractiveTag = ['button', 'a', 'input', 'select', 'textarea', 'li', 'option'].includes(tag)
193
+ const isInteractiveRole = ['button', 'link', 'checkbox', 'radio', 'textbox', 'listitem', 'option', 'combobox', 'listbox'].includes(role)
194
+ const isTrulyInteractive = isTabbable(element as HTMLElement)
195
+ const isOwnPointer = hasOwnPointerCursor(element)
196
+ const isMeaningfullyInteractive = isTrulyInteractive && !(role === 'generic' && !isOwnPointer)
197
+
198
+ if (isMeaningfullyInteractive || isInteractiveTag || isInteractiveRole) {
199
+ return
200
+ }
201
+ }
202
+
203
+ for (const child of Array.from(element.childNodes)) {
204
+ walk(child)
205
+ }
206
+ }
207
+ }
208
+ walk(el)
209
+ return text.trim().replace(/\s+/g, ' ')
210
+ }
211
+
212
+ /**
213
+ * 获取元素在 composed tree(组合树)中的有效子元素
214
+ *
215
+ * Shadow DOM 场景下,简单拼接 el.children + el.shadowRoot.children 不符合浏览器
216
+ * 实际渲染和无障碍树使用的组合树语义:slotted 节点应出现在 <slot> 的位置而非
217
+ * host 下,<slot> 本身不应作为噪音节点出现,未被任何 slot 接收的 light children
218
+ * 在组合树中不可见。
219
+ *
220
+ * 因此:有 shadowRoot 时遍历 shadow tree,遇到 <slot> 用 assignedElements 替换;
221
+ * 无 shadowRoot 时直接遍历 light DOM children。
222
+ */
223
+ export function getComposedChildren(el: Element): Element[] {
224
+ // <slot> 可嵌套在 shadow tree 的任意层级(如 <div><slot/></div>),递归时
225
+ // wrapper 节点无 shadowRoot 走 light 分支,因此两个分支都需解析 <slot>。
226
+ // light DOM 中不会出现 <slot>,检查无副作用。
227
+ const source = el.shadowRoot ? el.shadowRoot.children : el.children
228
+ const result: Element[] = []
229
+ for (const node of Array.from(source)) {
230
+ if (node instanceof HTMLSlotElement) {
231
+ const assigned = node.assignedElements({ flatten: true })
232
+ // 有分配节点时用 slotted 内容;否则用 slot 的 fallback content
233
+ result.push(...(assigned.length > 0 ? assigned : Array.from(node.children)))
234
+ } else {
235
+ result.push(node)
236
+ }
237
+ }
238
+ return result
239
+ }