@opentiny/next-sdk 0.4.1 → 0.4.3
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.
- package/dist/{SimulatorMask-BHVXyogh-iNUb4dyF.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
- package/dist/core.js +1 -1
- package/dist/{index-CXK8CcKq.js → index-CHMa2zG4.js} +1985 -1434
- package/dist/index.d.ts +11 -0
- package/dist/index.js +69 -50
- package/dist/{initialize-builtin-WebMCP-rALfqS-o.js → initialize-builtin-WebMCP-JaoKwVlm.js} +355 -346
- package/dist/page-tools/a11y/build.d.ts +3 -4
- package/dist/page-tools/a11y/config.d.ts +99 -0
- package/dist/page-tools/a11y/constants.d.ts +22 -0
- package/dist/page-tools/a11y/search.d.ts +1 -1
- package/dist/page-tools/a11y/types.d.ts +13 -22
- package/dist/page-tools/a11y/utils.d.ts +46 -11
- package/dist/page-tools/a11y/vnode.d.ts +14 -11
- package/dist/page-tools/a11y-tree.d.ts +1 -0
- package/dist/page-tools/configs/console-cloud.d.ts +6 -0
- package/dist/page-tools/constants.d.ts +7 -17
- package/dist/page-tools/context.d.ts +14 -0
- package/dist/page-tools/handlers/hover.d.ts +9 -0
- package/dist/page-tools/handlers/searchTree.d.ts +2 -1
- package/dist/page-tools/page-agent-highlight/index.d.ts +12 -0
- package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +2 -0
- package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
- package/dist/page-tools/schema.d.ts +4 -3
- package/dist/page-tools/tool-config.d.ts +50 -0
- package/dist/page-tools/utils/dom.d.ts +8 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +22 -187
- package/dist/vitest.config.d.ts +2 -0
- package/package.json +26 -16
- package/WebMcpClient.ts +0 -505
- package/WebMcpServer.ts +0 -447
- package/agent/AgentModelProvider.ts +0 -932
- package/agent/type.ts +0 -91
- package/agent/utils/generateReActPrompt.ts +0 -71
- package/agent/utils/getAISDKTools.ts +0 -34
- package/agent/utils/getBuiltinMcpTools.ts +0 -86
- package/agent/utils/parseReActAction.ts +0 -34
- package/core.ts +0 -26
- package/index.ts +0 -56
- package/page-tools/a11y/build.ts +0 -70
- package/page-tools/a11y/constants.ts +0 -110
- package/page-tools/a11y/search.ts +0 -129
- package/page-tools/a11y/types.ts +0 -108
- package/page-tools/a11y/utils.ts +0 -266
- package/page-tools/a11y/vnode.ts +0 -229
- package/page-tools/a11y-tree.ts +0 -13
- package/page-tools/bridge.ts +0 -702
- package/page-tools/constants.ts +0 -72
- package/page-tools/context.ts +0 -13
- package/page-tools/effects.ts +0 -345
- package/page-tools/handlers/browserState.ts +0 -12
- package/page-tools/handlers/click.ts +0 -26
- package/page-tools/handlers/executeJavascript.ts +0 -22
- package/page-tools/handlers/fill.ts +0 -60
- package/page-tools/handlers/scroll.ts +0 -66
- package/page-tools/handlers/searchTree.ts +0 -30
- package/page-tools/handlers/select.ts +0 -34
- package/page-tools/initialize-builtin-WebMCP.ts +0 -20
- package/page-tools/page-agent-highlight/index.ts +0 -198
- package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
- package/page-tools/page-agent-mask/SimulatorMask.ts +0 -279
- package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
- package/page-tools/page-agent-mask/cursor-border.svg +0 -3
- package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
- package/page-tools/page-agent-mask/cursor.module.css +0 -70
- package/page-tools/page-agent-prompt.md +0 -139
- package/page-tools/page-agent-tool.ts +0 -175
- package/page-tools/page-state-cache.ts +0 -78
- package/page-tools/schema.ts +0 -51
- package/page-tools/utils/dom.ts +0 -155
- package/page-tools/utils/scroll.ts +0 -58
- package/remoter/QrCode.ts +0 -54
- package/remoter/createRemoter.ts +0 -957
- package/remoter/svgs/chat.svg +0 -43
- package/remoter/svgs/icon-copy.svg +0 -25
- package/remoter/svgs/link.svg +0 -28
- package/remoter/svgs/logo.svg +0 -45
- package/remoter/svgs/qrcode.svg +0 -35
- package/remoter/svgs/scan.svg +0 -21
- package/remoter/tooltips.ts +0 -260
- package/runtime.html +0 -39
- package/runtime.ts +0 -13
- package/script/utils.ts +0 -26
- package/skills/index.ts +0 -287
- package/tsconfig.json +0 -16
- package/utils/builtinProxy.ts +0 -144
- package/utils/env.ts +0 -13
- package/utils/uuid.ts +0 -10
- package/vite.config.runtime.ts +0 -22
- package/vite.config.ts +0 -63
|
@@ -1,129 +0,0 @@
|
|
|
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
|
-
blacklist: Element[] = [],
|
|
28
|
-
whitelist: Element[] = [],
|
|
29
|
-
options?: SearchA11yTreeOptions,
|
|
30
|
-
): SearchA11yTreeResult {
|
|
31
|
-
const {
|
|
32
|
-
contextLines = 2,
|
|
33
|
-
caseInsensitive = true,
|
|
34
|
-
maxMatches = 20,
|
|
35
|
-
...treeOptions
|
|
36
|
-
} = options ?? {}
|
|
37
|
-
|
|
38
|
-
const safeContextLines = Math.max(0, contextLines)
|
|
39
|
-
const safeMaxMatches = Math.max(1, maxMatches)
|
|
40
|
-
|
|
41
|
-
// 复用 buildA11yTree 生成完整树,直接取 lines 数组(不重复构建 DOM 遍历)
|
|
42
|
-
const { lines, refMap, yaml } = buildA11yTree(root, blacklist, whitelist, treeOptions)
|
|
43
|
-
|
|
44
|
-
const needle = caseInsensitive ? query.toLowerCase() : query
|
|
45
|
-
const totalLines = lines.length
|
|
46
|
-
|
|
47
|
-
const isRefQuery = /^#\d+$/.test(query)
|
|
48
|
-
const refRegex = isRefQuery ? new RegExp(`\\s${query}(?:\\s|[\\[]|$)`) : null
|
|
49
|
-
|
|
50
|
-
// 找出所有命中行的下标(0-based)
|
|
51
|
-
const hitIndices: number[] = []
|
|
52
|
-
for (let i = 0; i < lines.length; i++) {
|
|
53
|
-
let matched = false
|
|
54
|
-
if (refRegex) {
|
|
55
|
-
matched = refRegex.test(lines[i])
|
|
56
|
-
} else {
|
|
57
|
-
const haystack = caseInsensitive ? lines[i].toLowerCase() : lines[i]
|
|
58
|
-
matched = haystack.includes(needle)
|
|
59
|
-
}
|
|
60
|
-
if (matched) {
|
|
61
|
-
hitIndices.push(i)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 合并重叠的上下文区间,避免重复输出行
|
|
66
|
-
const mergedRanges: Array<{ start: number; end: number; hits: number[] }> = []
|
|
67
|
-
let isTruncated = false
|
|
68
|
-
for (const idx of hitIndices) {
|
|
69
|
-
const start = Math.max(0, idx - safeContextLines)
|
|
70
|
-
const end = Math.min(totalLines - 1, idx + safeContextLines)
|
|
71
|
-
const last = mergedRanges[mergedRanges.length - 1]
|
|
72
|
-
if (last && start <= last.end + 1) {
|
|
73
|
-
// 区间重叠或紧邻,合并
|
|
74
|
-
last.end = Math.max(last.end, end)
|
|
75
|
-
last.hits.push(idx)
|
|
76
|
-
} else {
|
|
77
|
-
if (mergedRanges.length >= safeMaxMatches) {
|
|
78
|
-
isTruncated = true
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
mergedRanges.push({ start, end, hits: [idx] })
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 构建结构化结果
|
|
86
|
-
const matches: A11ySearchMatch[] = mergedRanges.map((range) => ({
|
|
87
|
-
lineNumber: range.hits[0] + 1,
|
|
88
|
-
line: lines[range.hits[0]],
|
|
89
|
-
context: Array.from({ length: range.end - range.start + 1 }, (_, k) => ({
|
|
90
|
-
lineNumber: range.start + k + 1,
|
|
91
|
-
line: lines[range.start + k],
|
|
92
|
-
})),
|
|
93
|
-
}))
|
|
94
|
-
|
|
95
|
-
// 格式化为可读文本(模仿 grep -n -C 风格,便于 LLM 直接理解)
|
|
96
|
-
const textParts: string[] = [
|
|
97
|
-
`无障碍树搜索结果 — 关键词: "${query}" | 总行数: ${totalLines} | 命中: ${hitIndices.length} 行 | 返回分组: ${matches.length}`,
|
|
98
|
-
'',
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
if (matches.length === 0) {
|
|
102
|
-
textParts.push('(未找到匹配项)')
|
|
103
|
-
} else {
|
|
104
|
-
matches.forEach((match, m) => {
|
|
105
|
-
const range = mergedRanges[m]
|
|
106
|
-
textParts.push(`── 分组 ${m + 1}(第 ${range.start + 1}–${range.end + 1} 行)──`)
|
|
107
|
-
match.context.forEach(({ lineNumber, line }) => {
|
|
108
|
-
const isHit = range.hits.includes(lineNumber - 1)
|
|
109
|
-
const ln = String(lineNumber).padStart(4)
|
|
110
|
-
// 命中行用 >>> 标记,上下文行用普通行号前缀
|
|
111
|
-
textParts.push(isHit ? `>>>${ln} | ${line}` : ` ${ln} | ${line}`)
|
|
112
|
-
})
|
|
113
|
-
textParts.push('')
|
|
114
|
-
})
|
|
115
|
-
if (isTruncated) {
|
|
116
|
-
textParts.push(`⚠️ 命中过多,已截断至前 ${safeMaxMatches} 个分组,建议缩小搜索范围`)
|
|
117
|
-
}
|
|
118
|
-
textParts.push(`提示:如需操作命中元素,使用其 #N 索引;如需查看完整树,请使用 browserState。`)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
text: textParts.join('\n'),
|
|
123
|
-
matches,
|
|
124
|
-
totalLines,
|
|
125
|
-
matchCount: hitIndices.length,
|
|
126
|
-
refMap,
|
|
127
|
-
yaml,
|
|
128
|
-
}
|
|
129
|
-
}
|
package/page-tools/a11y/types.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* a11y/types.ts
|
|
3
|
-
*
|
|
4
|
-
* 定义无障碍树模块(A11y Tree)的核心接口和类型。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** ref 索引 → HTMLElement 映射,供 click/fill/select 操作使用 */
|
|
8
|
-
export type RefMap = Map<number, HTMLElement>
|
|
9
|
-
|
|
10
|
-
/** 内部中间态节点,与 DOM 解耦,便于剪枝和序列化 */
|
|
11
|
-
export interface VNode {
|
|
12
|
-
/** 节点的 ARIA 角色 (如 'button', 'link', 'textbox') */
|
|
13
|
-
role: string
|
|
14
|
-
/** W3C AccName 算法计算出的语义化名称 */
|
|
15
|
-
name: string
|
|
16
|
-
/** 元素相关的状态和属性标记 (如 'checked', 'disabled', 'value="xxx"') */
|
|
17
|
-
tokens: string[]
|
|
18
|
-
/** 只有交互节点才有 ref (内部索引,用于标识可在其上执行交互动作的元素) */
|
|
19
|
-
ref?: number
|
|
20
|
-
/** 关联的 DOM 元素实例 */
|
|
21
|
-
el: HTMLElement
|
|
22
|
-
/** 包含的子节点列表 */
|
|
23
|
-
children: VNode[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** 构建无障碍树时的配置选项 */
|
|
27
|
-
export interface A11yTreeOptions {
|
|
28
|
-
/**
|
|
29
|
-
* 是否启用剪枝:无 ref 且无 accessible name 的节点透明穿透
|
|
30
|
-
* 默认 true(推荐)
|
|
31
|
-
*/
|
|
32
|
-
pruneUnnamed?: boolean
|
|
33
|
-
/**
|
|
34
|
-
* 强制保留的角色列表,即使无 name 也不穿透(优先级最高)
|
|
35
|
-
* 例如:['table', 'row'] 用于保留表格结构
|
|
36
|
-
*/
|
|
37
|
-
preserveRoles?: string[]
|
|
38
|
-
/**
|
|
39
|
-
* 允许在无障碍树节点中作为 token 额外输出的 DOM 属性白名单
|
|
40
|
-
* 包含这些属性的节点会被自动视为需要暴露/交互的节点(分配 ref 并保留),
|
|
41
|
-
* 且属性及其值会显示在节点的 token 列表中,如 [cf-uba="cloudShell"]
|
|
42
|
-
*/
|
|
43
|
-
exposedAttributes?: string[]
|
|
44
|
-
/**
|
|
45
|
-
* 校验错误元素 CSS 选择器(逗号分隔或选择器数组),用于在 token 中标记 [error]
|
|
46
|
-
* 默认覆盖 ARIA 标准 + 主流 UI 框架
|
|
47
|
-
*/
|
|
48
|
-
errorSelectors?: string | string[]
|
|
49
|
-
/**
|
|
50
|
-
* 校验警告元素 CSS 选择器(逗号分隔或选择器数组)
|
|
51
|
-
*/
|
|
52
|
-
warningSelectors?: string | string[]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 构建无障碍树的返回结果 */
|
|
56
|
-
export interface A11yTreeResult {
|
|
57
|
-
/** 语义化 YAML 文本(供 AI 阅读和 Diff 计算) */
|
|
58
|
-
yaml: string
|
|
59
|
-
/** ref 索引 → HTMLElement 映射(供后续操作使用) */
|
|
60
|
-
refMap: RefMap
|
|
61
|
-
/** 可交互元素总数 */
|
|
62
|
-
interactiveCount: number
|
|
63
|
-
/** 原始行数组(不含 yaml 代码块包裹),供搜索使用 */
|
|
64
|
-
lines: string[]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 关键词搜索选项 */
|
|
68
|
-
export interface SearchA11yTreeOptions extends A11yTreeOptions {
|
|
69
|
-
/**
|
|
70
|
-
* 每个匹配行前后保留的上下文行数(类似 grep -C N)
|
|
71
|
-
* 默认 2
|
|
72
|
-
*/
|
|
73
|
-
contextLines?: number
|
|
74
|
-
/**
|
|
75
|
-
* 是否大小写不敏感,默认 true
|
|
76
|
-
*/
|
|
77
|
-
caseInsensitive?: boolean
|
|
78
|
-
/**
|
|
79
|
-
* 最大返回匹配分组数(防止命中过多撑爆上下文),默认 20
|
|
80
|
-
*/
|
|
81
|
-
maxMatches?: number
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 单个搜索匹配的分组结构 */
|
|
85
|
-
export interface A11ySearchMatch {
|
|
86
|
-
/** 主命中行行号(1-based) */
|
|
87
|
-
lineNumber: number
|
|
88
|
-
/** 主命中行内容 */
|
|
89
|
-
line: string
|
|
90
|
-
/** 含上下文的行列表(带行号) */
|
|
91
|
-
context: Array<{ lineNumber: number; line: string }>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** searchA11yTree 函数的返回结果 */
|
|
95
|
-
export interface SearchA11yTreeResult {
|
|
96
|
-
/** 格式化后可直接发给 LLM 的文本 */
|
|
97
|
-
text: string
|
|
98
|
-
/** 结构化匹配列表 */
|
|
99
|
-
matches: A11ySearchMatch[]
|
|
100
|
-
/** 无障碍树总行数 */
|
|
101
|
-
totalLines: number
|
|
102
|
-
/** 原始命中行数(去重前) */
|
|
103
|
-
matchCount: number
|
|
104
|
-
/** 返回搜索时的 ref 映射 */
|
|
105
|
-
refMap: RefMap
|
|
106
|
-
/** 返回搜索时的 yaml 状态,用于更新缓存 */
|
|
107
|
-
yaml: string
|
|
108
|
-
}
|
package/page-tools/a11y/utils.ts
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* a11y/utils.ts
|
|
3
|
-
*
|
|
4
|
-
* 存放生成无障碍树时的通用工具函数:节点解析、状态获取、角色推断及纯文本兜底等。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { isFocusable, isTabbable } from 'tabbable'
|
|
8
|
-
import { TAG_ROLE_MAP, INPUT_TYPE_ROLE, DEFAULT_ERROR_SELECTORS, DEFAULT_WARNING_SELECTORS } from './constants'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 获取元素的 ARIA 角色
|
|
12
|
-
* 优先级:显式 role 属性 > 标签隐式角色 > 'generic'
|
|
13
|
-
*/
|
|
14
|
-
export function inferRole(el: Element): string {
|
|
15
|
-
const explicit = el.getAttribute('role')
|
|
16
|
-
if (explicit && explicit !== 'presentation' && explicit !== 'none') {
|
|
17
|
-
return explicit
|
|
18
|
-
}
|
|
19
|
-
const tag = el.tagName.toLowerCase()
|
|
20
|
-
if (tag === 'input') {
|
|
21
|
-
const inputType = (el as HTMLInputElement).type?.toLowerCase() ?? 'text'
|
|
22
|
-
return INPUT_TYPE_ROLE[inputType] ?? 'textbox'
|
|
23
|
-
}
|
|
24
|
-
return TAG_ROLE_MAP[tag] ?? 'generic'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 收集节点的 ARIA 状态 token
|
|
29
|
-
* 格式:[checked] [selected] [disabled] [hasPopup] [cursor=pointer] [value="..."]
|
|
30
|
-
*/
|
|
31
|
-
export function getStateTokens(
|
|
32
|
-
el: Element,
|
|
33
|
-
exposedAttributes?: string[],
|
|
34
|
-
errorSelectors?: string | string[],
|
|
35
|
-
warningSelectors?: string | string[],
|
|
36
|
-
): string[] {
|
|
37
|
-
const tokens: string[] = []
|
|
38
|
-
const aria = (k: string) => el.getAttribute(k)
|
|
39
|
-
|
|
40
|
-
// 选中状态:优先 aria-checked,其次原生 input.checked
|
|
41
|
-
// 对 <label for="X"> 检测关联 checkbox/radio 的选中状态,帮助 AI 识别复选框当前状态
|
|
42
|
-
const ariaChecked = aria('aria-checked')
|
|
43
|
-
if (ariaChecked === 'true') {
|
|
44
|
-
tokens.push('checked')
|
|
45
|
-
} else if (ariaChecked === 'false') {
|
|
46
|
-
tokens.push('unchecked')
|
|
47
|
-
} else {
|
|
48
|
-
const elTag = el.tagName.toLowerCase()
|
|
49
|
-
let nativeChecked: boolean | undefined
|
|
50
|
-
if (elTag === 'input' && ((el as HTMLInputElement).type === 'checkbox' || (el as HTMLInputElement).type === 'radio')) {
|
|
51
|
-
nativeChecked = (el as HTMLInputElement).checked
|
|
52
|
-
} else if (elTag === 'label' && el.hasAttribute('for')) {
|
|
53
|
-
const target = document.getElementById(el.getAttribute('for')!)
|
|
54
|
-
if (target instanceof HTMLInputElement && (target.type === 'checkbox' || target.type === 'radio')) {
|
|
55
|
-
nativeChecked = target.checked
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (nativeChecked !== undefined) {
|
|
59
|
-
tokens.push(nativeChecked ? 'checked' : 'unchecked')
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (aria('aria-selected') === 'true') tokens.push('selected')
|
|
64
|
-
|
|
65
|
-
const disabled = aria('aria-disabled') === 'true' || (el as HTMLInputElement).disabled
|
|
66
|
-
if (disabled) tokens.push('disabled')
|
|
67
|
-
|
|
68
|
-
const hasPopup = aria('aria-haspopup')
|
|
69
|
-
if (hasPopup && hasPopup !== 'false') tokens.push('hasPopup')
|
|
70
|
-
|
|
71
|
-
if (aria('aria-expanded') === 'true') tokens.push('expanded')
|
|
72
|
-
|
|
73
|
-
// heading level(h1-h6)
|
|
74
|
-
const headingMatch = el.tagName.match(/^H([1-6])$/)
|
|
75
|
-
if (headingMatch) tokens.push(`level=${headingMatch[1]}`)
|
|
76
|
-
const ariaLevel = aria('aria-level')
|
|
77
|
-
if (ariaLevel && !headingMatch) tokens.push(`level=${ariaLevel}`)
|
|
78
|
-
|
|
79
|
-
// cursor=pointer 表示"视觉上可点击"
|
|
80
|
-
try {
|
|
81
|
-
const style = window.getComputedStyle(el as HTMLElement)
|
|
82
|
-
if (style.cursor === 'pointer') tokens.push('cursor=pointer')
|
|
83
|
-
} catch {
|
|
84
|
-
// 某些元素 getComputedStyle 可能抛异常,忽略
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 记录输入元素的值,以便在 fill/输入后在 A11y 树中显示并产生 Diff
|
|
88
|
-
const tag = el.tagName.toLowerCase()
|
|
89
|
-
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
90
|
-
const val = (el as HTMLInputElement).value
|
|
91
|
-
if (val !== undefined && val !== '') {
|
|
92
|
-
tokens.push(`value="${val}"`)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const valuenow = el.getAttribute('aria-valuenow')
|
|
96
|
-
if (valuenow) {
|
|
97
|
-
tokens.push(`valuenow="${valuenow}"`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// link 元素:检测 target=_blank,提示 Agent 该链接会在新标签页打开
|
|
101
|
-
if (el.tagName.toLowerCase() === 'a' && el.getAttribute('target') === '_blank') {
|
|
102
|
-
tokens.push('opens-new-tab')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 检测 CSS 激活/选中状态类名(用于未使用标准 ARIA 的 Tab/选项组件,如华为云镜像选择)
|
|
106
|
-
// 若元素携带常见激活类名,输出 [active] token,帮助 Agent 判断当前选中项
|
|
107
|
-
const cls = typeof el.className === 'string' ? el.className : ''
|
|
108
|
-
const ACTIVE_CLASS_PATTERNS = [
|
|
109
|
-
'is-active', 'isActive',
|
|
110
|
-
'is-selected', 'isSelected',
|
|
111
|
-
'is-current', 'isCurrent',
|
|
112
|
-
'active-item', 'activeItem',
|
|
113
|
-
'tab-active', 'tabActive',
|
|
114
|
-
// 仅当作为独立 class 词或有连字符前缀时匹配 "active",避免误匹配 "interactive" 等
|
|
115
|
-
/\bactive\b/,
|
|
116
|
-
/\bselected\b/,
|
|
117
|
-
/\bcurrent\b/,
|
|
118
|
-
]
|
|
119
|
-
const hasActiveClass = ACTIVE_CLASS_PATTERNS.some(p =>
|
|
120
|
-
typeof p === 'string' ? cls.split(/\s+/).includes(p) : p.test(cls)
|
|
121
|
-
)
|
|
122
|
-
// 只对有 cursor=pointer 或明确角色的元素输出 active token,避免太多噪音
|
|
123
|
-
const roleForActive = el.getAttribute('role') || el.tagName.toLowerCase()
|
|
124
|
-
const roles = roleForActive.split(/\s+/)
|
|
125
|
-
const isTabLike = roles.some(r => ['button', 'option', 'a', 'li', 'generic'].includes(r) || r.startsWith('tab'))
|
|
126
|
-
if (hasActiveClass && isTabLike && !tokens.includes('checked') && !tokens.includes('selected')) {
|
|
127
|
-
tokens.push('active')
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 检测校验错误/警告状态(ARIA 标准 + 主流 UI 框架,可配置)
|
|
131
|
-
// 输出 [error] / [warning] token,让 AI 能区分校验错误与普通说明文字
|
|
132
|
-
// 使用 closest() 向上查找,确保嵌套在错误容器内的子元素也能获得 error 语义
|
|
133
|
-
const errorSelector = Array.isArray(errorSelectors)
|
|
134
|
-
? errorSelectors.join(', ')
|
|
135
|
-
: (errorSelectors || DEFAULT_ERROR_SELECTORS.join(', '))
|
|
136
|
-
const warningSelector = Array.isArray(warningSelectors)
|
|
137
|
-
? warningSelectors.join(', ')
|
|
138
|
-
: (warningSelectors || DEFAULT_WARNING_SELECTORS.join(', '))
|
|
139
|
-
let errorAncestor = null
|
|
140
|
-
if (errorSelector) {
|
|
141
|
-
try {
|
|
142
|
-
errorAncestor = el.closest(errorSelector)
|
|
143
|
-
} catch {
|
|
144
|
-
// ignore
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (errorAncestor) {
|
|
149
|
-
tokens.push('error')
|
|
150
|
-
} else {
|
|
151
|
-
let warningAncestor = null
|
|
152
|
-
if (warningSelector) {
|
|
153
|
-
try {
|
|
154
|
-
warningAncestor = el.closest(warningSelector)
|
|
155
|
-
} catch {
|
|
156
|
-
// ignore
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (warningAncestor) {
|
|
160
|
-
tokens.push('warning')
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 额外暴露的自定义属性白名单
|
|
165
|
-
if (exposedAttributes) {
|
|
166
|
-
for (const attr of exposedAttributes) {
|
|
167
|
-
const val = el.getAttribute(attr)
|
|
168
|
-
if (val !== null) {
|
|
169
|
-
tokens.push(`${attr}="${val}"`)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return tokens
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 判断元素是否应被跳过(不可见或在黑名单中) */
|
|
178
|
-
export function isHidden(el: Element): boolean {
|
|
179
|
-
if (el.getAttribute('aria-hidden') === 'true') return true
|
|
180
|
-
if ((el as HTMLElement).hidden) return true
|
|
181
|
-
try {
|
|
182
|
-
const style = window.getComputedStyle(el as HTMLElement)
|
|
183
|
-
if (style.display === 'none' || style.visibility === 'hidden') return true
|
|
184
|
-
// opacity:0 是常见的视觉隐藏方式,元素对用户不可见
|
|
185
|
-
if (style.opacity === '0') return true
|
|
186
|
-
// 宽或高为 0 且有 overflow 裁剪时,子内容实际不可见(如折叠的侧边栏 width:0 !important)
|
|
187
|
-
// 若 overflow 为 visible,子内容仍然溢出可见,不能过滤
|
|
188
|
-
const isClipX = style.overflowX !== 'visible'
|
|
189
|
-
const isClipY = style.overflowY !== 'visible'
|
|
190
|
-
if (parseFloat(style.width) === 0 && isClipX) return true
|
|
191
|
-
if (parseFloat(style.height) === 0 && isClipY) return true
|
|
192
|
-
} catch {
|
|
193
|
-
// 忽略跨域 iframe 等无法访问 style 的场景
|
|
194
|
-
}
|
|
195
|
-
return false
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* 收集子孙节点的文本内容,用作无障碍名字的兜底。
|
|
201
|
-
* 当普通计算无法提取文本时,遍历后代并拼接可见文本。
|
|
202
|
-
*/
|
|
203
|
-
export function collectDescendantText(el: Element): string {
|
|
204
|
-
let text = ''
|
|
205
|
-
const walk = (node: Node) => {
|
|
206
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
207
|
-
text += (node.textContent ?? '') + ' '
|
|
208
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
209
|
-
const element = node as Element
|
|
210
|
-
if (isHidden(element)) return
|
|
211
|
-
|
|
212
|
-
// 如果遇到嵌套的列表项或其他交互/语义节点,停止向下遍历该子树,避免兜底文本重复吸收
|
|
213
|
-
if (element !== el) {
|
|
214
|
-
const tag = element.tagName.toLowerCase()
|
|
215
|
-
const role = inferRole(element)
|
|
216
|
-
const isInteractiveTag = ['button', 'a', 'input', 'select', 'textarea', 'li', 'option'].includes(tag)
|
|
217
|
-
const isInteractiveRole = ['button', 'link', 'checkbox', 'radio', 'textbox', 'listitem', 'option', 'combobox', 'listbox'].includes(role)
|
|
218
|
-
const isTrulyInteractive = isTabbable(element as HTMLElement)
|
|
219
|
-
let isCursorPointer = false
|
|
220
|
-
try {
|
|
221
|
-
isCursorPointer = window.getComputedStyle(element as HTMLElement).cursor === 'pointer'
|
|
222
|
-
} catch { /* ignore */ }
|
|
223
|
-
const isMeaningfullyInteractive = isTrulyInteractive && !(role === 'generic' && !isCursorPointer)
|
|
224
|
-
|
|
225
|
-
if (isMeaningfullyInteractive || isInteractiveTag || isInteractiveRole) {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (const child of Array.from(element.childNodes)) {
|
|
231
|
-
walk(child)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
walk(el)
|
|
236
|
-
return text.trim().replace(/\s+/g, ' ')
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 获取元素在 composed tree(组合树)中的有效子元素
|
|
241
|
-
*
|
|
242
|
-
* Shadow DOM 场景下,简单拼接 el.children + el.shadowRoot.children 不符合浏览器
|
|
243
|
-
* 实际渲染和无障碍树使用的组合树语义:slotted 节点应出现在 <slot> 的位置而非
|
|
244
|
-
* host 下,<slot> 本身不应作为噪音节点出现,未被任何 slot 接收的 light children
|
|
245
|
-
* 在组合树中不可见。
|
|
246
|
-
*
|
|
247
|
-
* 因此:有 shadowRoot 时遍历 shadow tree,遇到 <slot> 用 assignedElements 替换;
|
|
248
|
-
* 无 shadowRoot 时直接遍历 light DOM children。
|
|
249
|
-
*/
|
|
250
|
-
export function getComposedChildren(el: Element): Element[] {
|
|
251
|
-
// <slot> 可嵌套在 shadow tree 的任意层级(如 <div><slot/></div>),递归时
|
|
252
|
-
// wrapper 节点无 shadowRoot 走 light 分支,因此两个分支都需解析 <slot>。
|
|
253
|
-
// light DOM 中不会出现 <slot>,检查无副作用。
|
|
254
|
-
const source = el.shadowRoot ? el.shadowRoot.children : el.children
|
|
255
|
-
const result: Element[] = []
|
|
256
|
-
for (const node of Array.from(source)) {
|
|
257
|
-
if (node instanceof HTMLSlotElement) {
|
|
258
|
-
const assigned = node.assignedElements({ flatten: true })
|
|
259
|
-
// 有分配节点时用 slotted 内容;否则用 slot 的 fallback content
|
|
260
|
-
result.push(...(assigned.length > 0 ? assigned : Array.from(node.children)))
|
|
261
|
-
} else {
|
|
262
|
-
result.push(node)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return result
|
|
266
|
-
}
|