@opentiny/next-sdk 0.4.2 → 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.
Files changed (88) hide show
  1. package/dist/{SimulatorMask-BHVXyogh-CARX3Rff.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
  2. package/dist/{index-R_HIbfUX.js → index-CHMa2zG4.js} +1065 -1042
  3. package/dist/index.js +1 -1
  4. package/dist/page-tools/a11y/config.d.ts +6 -3
  5. package/dist/page-tools/a11y/constants.d.ts +20 -0
  6. package/dist/page-tools/a11y/utils.d.ts +14 -0
  7. package/dist/page-tools/constants.d.ts +4 -2
  8. package/dist/page-tools/context.d.ts +2 -0
  9. package/dist/page-tools/handlers/hover.d.ts +9 -0
  10. package/dist/page-tools/schema.d.ts +3 -3
  11. package/dist/page-tools/utils/dom.d.ts +8 -2
  12. package/dist/runtime.js +19 -172
  13. package/package.json +6 -1
  14. package/WebMcpClient.ts +0 -505
  15. package/WebMcpServer.ts +0 -447
  16. package/agent/AgentModelProvider.ts +0 -932
  17. package/agent/type.ts +0 -91
  18. package/agent/utils/generateReActPrompt.ts +0 -71
  19. package/agent/utils/getAISDKTools.ts +0 -34
  20. package/agent/utils/getBuiltinMcpTools.ts +0 -86
  21. package/agent/utils/parseReActAction.ts +0 -34
  22. package/core.ts +0 -26
  23. package/index.ts +0 -91
  24. package/page-tools/a11y/build.ts +0 -74
  25. package/page-tools/a11y/config.ts +0 -465
  26. package/page-tools/a11y/constants.ts +0 -131
  27. package/page-tools/a11y/search.ts +0 -127
  28. package/page-tools/a11y/types.ts +0 -105
  29. package/page-tools/a11y/utils.ts +0 -239
  30. package/page-tools/a11y/vnode.ts +0 -439
  31. package/page-tools/a11y-tree.ts +0 -14
  32. package/page-tools/bridge.ts +0 -722
  33. package/page-tools/configs/console-cloud.ts +0 -172
  34. package/page-tools/constants.ts +0 -12
  35. package/page-tools/context.ts +0 -50
  36. package/page-tools/effects.ts +0 -345
  37. package/page-tools/handlers/browserState.ts +0 -12
  38. package/page-tools/handlers/click.ts +0 -30
  39. package/page-tools/handlers/executeJavascript.ts +0 -22
  40. package/page-tools/handlers/fill.ts +0 -65
  41. package/page-tools/handlers/scroll.ts +0 -66
  42. package/page-tools/handlers/searchTree.ts +0 -27
  43. package/page-tools/handlers/select.ts +0 -39
  44. package/page-tools/initialize-builtin-WebMCP.ts +0 -20
  45. package/page-tools/page-agent-highlight/index.ts +0 -245
  46. package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
  47. package/page-tools/page-agent-mask/SimulatorMask.ts +0 -299
  48. package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
  49. package/page-tools/page-agent-mask/cursor-border.svg +0 -3
  50. package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
  51. package/page-tools/page-agent-mask/cursor.module.css +0 -70
  52. package/page-tools/page-agent-mask/hauwei.svg +0 -25
  53. package/page-tools/page-agent-prompt.md +0 -139
  54. package/page-tools/page-agent-tool-event.ts +0 -113
  55. package/page-tools/page-agent-tool.ts +0 -207
  56. package/page-tools/page-state-cache.ts +0 -78
  57. package/page-tools/schema.ts +0 -52
  58. package/page-tools/tool-config.ts +0 -100
  59. package/page-tools/utils/dom.ts +0 -158
  60. package/page-tools/utils/scroll.ts +0 -58
  61. package/remoter/QrCode.ts +0 -54
  62. package/remoter/createRemoter.ts +0 -957
  63. package/remoter/svgs/chat.svg +0 -43
  64. package/remoter/svgs/icon-copy.svg +0 -25
  65. package/remoter/svgs/link.svg +0 -28
  66. package/remoter/svgs/logo.svg +0 -45
  67. package/remoter/svgs/qrcode.svg +0 -35
  68. package/remoter/svgs/scan.svg +0 -21
  69. package/remoter/tooltips.ts +0 -260
  70. package/runtime.html +0 -39
  71. package/runtime.ts +0 -44
  72. package/script/utils.ts +0 -26
  73. package/skills/index.ts +0 -287
  74. package/test/page-tools/a11y/build.test.ts +0 -638
  75. package/test/page-tools/a11y/config.test.ts +0 -370
  76. package/test/page-tools/configs/console-cloud.test.ts +0 -168
  77. package/test/page-tools/page-agent-highlight.test.ts +0 -110
  78. package/test/page-tools/page-agent-tool-dispatch.test.ts +0 -208
  79. package/test/page-tools/page-agent-tool.test.ts +0 -102
  80. package/test/page-tools/tool-config.test.ts +0 -112
  81. package/test/page-tools/utils/dom.test.ts +0 -122
  82. package/tsconfig.json +0 -16
  83. package/utils/builtinProxy.ts +0 -144
  84. package/utils/env.ts +0 -13
  85. package/utils/uuid.ts +0 -10
  86. package/vite.config.runtime.ts +0 -22
  87. package/vite.config.ts +0 -63
  88. package/vitest.config.ts +0 -10
@@ -1,439 +0,0 @@
1
- /**
2
- * a11y/vnode.ts
3
- *
4
- * 处理无障碍树节点 (VNode) 的构建、剪枝与序列化逻辑。
5
- */
6
-
7
- import { computeAccessibleName } from 'dom-accessibility-api'
8
- import { isTabbable } from 'tabbable'
9
- import type { VNode, RefMap, A11yTreeShapeOptions } from './types'
10
- import type { ResolvedA11yConfig } from './config'
11
- import { resolveA11yInfo } from './config'
12
- import {
13
- isHidden,
14
- isNonContentElement,
15
- collectDescendantText,
16
- collectTitleLabel,
17
- getComposedChildren,
18
- hasOwnPointerCursor,
19
- } from './utils'
20
-
21
- /**
22
- * 语义上即为可操作控件的 ARIA 角色。
23
- * jsdom 下部分带 tabindex 的自定义控件 isTabbable 可能为 false,
24
- * 若不强制分配 ref,Static-Lift 会把它们误判为静态分支并吸收进父 name。
25
- */
26
- const INTERACTIVE_ROLES = new Set([
27
- 'button',
28
- 'link',
29
- 'textbox',
30
- 'searchbox',
31
- 'combobox',
32
- 'listbox',
33
- 'checkbox',
34
- 'radio',
35
- 'switch',
36
- 'slider',
37
- 'spinbutton',
38
- 'menuitem',
39
- 'menuitemcheckbox',
40
- 'menuitemradio',
41
- 'tab',
42
- 'treeitem',
43
- ])
44
-
45
- /**
46
- * 递归将 DOM 元素转换为 VNode 中间表示
47
- * @param el 当前 DOM 元素
48
- * @param refCounter 引用计数器(使用对象引用避免全局可变状态)
49
- * @param refMap ref 索引 → 元素映射
50
- * @param blacklistSet 已解析的黑名单元素集合
51
- * @param whitelistSet 已解析的白名单元素集合
52
- * @param config 已与默认值合并的统一无障碍配置
53
- * @param hoverPointerSet 只在 :hover/:focus/:active 下声明 cursor:pointer 的元素集合(静止态读不到手势,靠样式表扫描兜底)
54
- */
55
- export function buildVNode(
56
- el: Element,
57
- refCounter: { value: number },
58
- refMap: RefMap,
59
- blacklistSet: Set<Element>,
60
- whitelistSet: Set<Element>,
61
- config: ResolvedA11yConfig,
62
- hoverPointerSet: Set<Element> = new Set(),
63
- ): VNode | null {
64
- // style/script 等非内容节点:不进树(避免 @font-face base64 等被当成 name)
65
- if (isNonContentElement(el) || isHidden(el) || blacklistSet.has(el)) return null
66
-
67
- const { role, tokens } = resolveA11yInfo(el, config)
68
-
69
- let name = computeAccessibleName(el as HTMLElement)
70
- const isTrulyInteractive = isTabbable(el as HTMLElement)
71
- // computed cursor 为 pointer(含继承)——仅用于「无文本时是否值得兜底收集名字」等宽松场景
72
- const isVisuallyClickable = tokens.includes('cursor=pointer')
73
- // 真正的"可点击边界元素"判定(用于是否分配 ref):
74
- // 1) 元素自身在静止态声明了 cursor:pointer(父级不是 pointer),排除 CSS 继承传染;
75
- // 2) 或元素命中了 :hover/:focus/:active 下声明 cursor:pointer 的规则(hoverPointerSet)——
76
- // 大量卡片只在 hover 时才显示手势,静止态 getComputedStyle 读到 auto,需靠样式表扫描兜底。
77
- const hasClickableCursor = hasOwnPointerCursor(el) || hoverPointerSet.has(el)
78
- // 白名单:仅由显式 whitelist 配置驱动,与 exposedAttributes(属性 token 输出)解耦,
79
- // 避免把 data-qa-id 等追踪属性误当成「强制可交互」信号污染无障碍树。
80
- const isWhitelisted = whitelistSet.has(el)
81
- // <label for="..."> 原生可点击:浏览器将点击转发到关联的表单控件(checkbox/radio 等)
82
- // Angular/React 自定义组件常隐藏原生 input,仅暴露 label 文本和自定义 skin
83
- const isLabelFor = el.tagName.toLowerCase() === 'label' && el.hasAttribute('for')
84
-
85
- // 兜底方案:如果 AccName 计算结果为空,但该节点具有明显的交互性或属于结构性列表项,
86
- // 我们从其子树收集纯文本作为其 fallback name,以防 AI 丢失可读上下文。
87
- // 注意:不要对 <select> / combobox / listbox 等下拉组件进行文本兜底,以防它们吸收所有子选项文本导致极其嘈杂。
88
- if (!name.trim()) {
89
- const tag = el.tagName.toLowerCase()
90
- const isDropdown = tag === 'select' || role === 'combobox' || role === 'listbox'
91
- if (!isDropdown) {
92
- const isInteractiveTag = ['button', 'a', 'input', 'textarea', 'li', 'label'].includes(tag)
93
- if (isTrulyInteractive || isWhitelisted || isVisuallyClickable || hasClickableCursor || isInteractiveTag || role === 'listitem' || role === 'option') {
94
- name = collectDescendantText(el, config)
95
- }
96
- }
97
- }
98
-
99
- // 图标按钮常见:可点击容器本身无文本,title/aria 挂在子节点上(如控制台服务列表按钮)
100
- if (!name.trim() && (isTrulyInteractive || isWhitelisted || isVisuallyClickable || hasClickableCursor || role === 'button')) {
101
- name = collectTitleLabel(el)
102
- }
103
-
104
- // 结构性交互角色(tab/menuitem 等)的标签文字常落在可聚焦子节点内,
105
- // collectDescendantText 会因“遇交互子节点即停”而拿不到 name;
106
- // 此处用「跳过 style/script」的文本收集,禁止 textContent 把内联 CSS/base64 吸进来
107
- if (
108
- !name.trim() &&
109
- ['tab', 'menuitem', 'option', 'treeitem', 'button'].includes(role)
110
- ) {
111
- name = collectVisiblePlainText(el)
112
- }
113
-
114
- // 最终兜底:非交互元素(如错误提示 ti-error-msg、状态信息)的纯文本捕获。
115
- // 仅当元素没有有价值的子元素时触发,避免与子节点文本重复:
116
- // - 叶子 span(只有文本节点)→ 捕获文本作为 name
117
- // - 容器元素(有 element 子节点)→ 不触发,由子节点各自捕获
118
- if (!name.trim()) {
119
- const hasElementChildren = getComposedChildren(el).length > 0
120
- if (!hasElementChildren) {
121
- const text = collectDescendantText(el, config)
122
- if (text) {
123
- name = text
124
- }
125
- }
126
- }
127
-
128
- // cursor:pointer 是业界公认的"视觉可点击"通用信号(browser-use 等 DOM 提取器均以此为核心兜底判据)。
129
- // 但 CSS 的 cursor 会向子孙继承,若直接以 computed cursor 判定,可点击容器(<a>、卡片等)会把
130
- // ref "传染"给所有子孙,导致高亮泛滥。因此改用 hasClickableCursor(自身声明手势 + hover 态手势)来
131
- // 定位真正的可点击边界元素:这样既能覆盖 <div class="card-wrapper" (click)=...> 这类无语义、无
132
- // tabindex 的自定义可点击卡片(包括只在 hover 时显示手势的卡片),又能自然排除仅靠继承拿到 pointer
133
- // 的内部子孙,无需再依赖祖先传播标记。
134
- //
135
- // 例外:<a>/<button>/<input> 等语义性交互标签始终强制分配 ref,因为它们在 HTML 语义上
136
- // 就是独立的操作单元,即使外层容器同样可点击也应各自暴露。
137
- const tagName = el.tagName.toLowerCase()
138
- const isSemanticInteractiveTag =
139
- (tagName === 'a' && el.hasAttribute('href')) ||
140
- (['button', 'input', 'select', 'textarea'].includes(tagName) && !el.hasAttribute('disabled'))
141
-
142
- // 虽然有些元素有 tabindex="0" (isTrulyInteractive),但如果它是 generic 且没有可点击手势,
143
- // 往往是开发者加的结构化 focus 容器(如 tp-card),而非真正的可交互按钮,我们在此过滤掉它们。
144
- const isMeaningfullyInteractive = isTrulyInteractive && !(role === 'generic' && !hasClickableCursor)
145
-
146
- const isDisabled = tokens.includes('disabled')
147
-
148
- const interactive =
149
- (!isDisabled && isMeaningfullyInteractive) ||
150
- isWhitelisted ||
151
- isLabelFor ||
152
- isSemanticInteractiveTag ||
153
- (!isDisabled && INTERACTIVE_ROLES.has(role)) ||
154
- // hasClickableCursor 已排除 CSS 继承(hasOwnPointerCursor)并覆盖 hover 态手势,
155
- // 无需再要求 role≠generic 或 name≠''——否则无文本的图标按钮(tp-icon.common-icon 等)
156
- // 虽是真正的可点击边界,仍会因 generic+空名被漏判。
157
- (!isDisabled && hasClickableCursor)
158
-
159
- let ref: number | undefined
160
- if (interactive) {
161
- ref = refCounter.value
162
- refMap.set(ref, el as HTMLElement)
163
- refCounter.value++
164
- }
165
-
166
- const children: VNode[] = []
167
- for (const child of getComposedChildren(el)) {
168
- const childVNode = buildVNode(
169
- child,
170
- refCounter,
171
- refMap,
172
- blacklistSet,
173
- whitelistSet,
174
- config,
175
- hoverPointerSet,
176
- )
177
- if (childVNode) children.push(childVNode)
178
- }
179
-
180
- const vnode: VNode = { role, name, tokens, ref, el: el as HTMLElement, children }
181
-
182
- // 有可交互子孙时:清空 AccName 内容汇总,仅保留自身声明名。
183
- // 静态子树文案的上提(Static-Lift)改由 serializeVNode 负责,避免与交互文案混进父 name。
184
- if (hasInteractiveDescendant(vnode)) {
185
- vnode.name = getOwnDeclaredName(el as HTMLElement)
186
- }
187
-
188
- return vnode
189
- }
190
-
191
- /**
192
- * 仅取元素自身声明的可访问名,不含内容汇总(name from contents)。
193
- */
194
- function getOwnDeclaredName(el: HTMLElement): string {
195
- if (el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby')) {
196
- return computeAccessibleName(el).trim()
197
- }
198
- return ''
199
- }
200
-
201
- /** 子节点自身无 ref,且子树无可交互子孙 → 纯静态分支(可上提文案) */
202
- function isStaticBranch(vnode: VNode): boolean {
203
- return vnode.ref === undefined && !hasInteractiveDescendant(vnode)
204
- }
205
-
206
- /**
207
- * 收集元素可见纯文本,跳过 style/script 等非内容子树。
208
- * 禁止使用 el.textContent:它会包含内联 @font-face base64,导致 token 爆炸。
209
- */
210
- function collectVisiblePlainText(el: Element): string {
211
- return collectDescendantText(el).replace(/\s+/g, ' ').trim()
212
- }
213
-
214
- /**
215
- * 收集纯静态分支的展示文案(保字用)。
216
- * 优先 name,其次递归子节点,最后可见纯文本兜底(不含 style/script)。
217
- */
218
- function getStaticDisplayText(vnode: VNode): string {
219
- const childJoined =
220
- vnode.children.length > 0
221
- ? vnode.children
222
- .map(getStaticDisplayText)
223
- .filter(Boolean)
224
- .join(' ')
225
- .replace(/\s+/g, ' ')
226
- .trim()
227
- : ''
228
- const plain = vnode.children.length === 0 ? collectVisiblePlainText(vnode.el) : ''
229
- const named = vnode.name.trim()
230
-
231
- const parts: string[] = []
232
- if (named && !isNoiseAccessibleName(named)) parts.push(named)
233
- if (childJoined && !isNoiseAccessibleName(childJoined)) {
234
- const normNamed = named.replace(/\s+/g, ' ').trim()
235
- const normChild = childJoined.replace(/\s+/g, ' ').trim()
236
- // 声明名与子孙文案不同则合并保留,避免仅有 aria-label 时丢掉静态说明
237
- if (!normNamed || (normChild !== normNamed && !normNamed.includes(normChild))) {
238
- parts.push(childJoined)
239
- }
240
- }
241
- if (parts.length === 0 && plain && !isNoiseAccessibleName(plain)) parts.push(plain)
242
- return parts.join(' ').replace(/\s+/g, ' ').trim()
243
- }
244
-
245
- /**
246
- * 名称若像 CSS/字体资源噪声(常见于误吸收的 <style> 内容),视为无效,避免输出到 YAML。
247
- */
248
- function isNoiseAccessibleName(name: string): boolean {
249
- const n = name.trim()
250
- if (!n) return false
251
- if (/@font-face\b/i.test(n)) return true
252
- if (/data:(?:font|application|image)\//i.test(n)) return true
253
- if (/base64,[A-Za-z0-9+/=]{80,}/.test(n)) return true
254
- return false
255
- }
256
-
257
- /**
258
- * 静态子节点可否省略:仅当其文案已由父 outputName 完整承载(去重且不丢字)。
259
- * - 父使用自身声明名:仅当子文案与声明名完全相同才可省略
260
- * - 父使用上提拼接名:子文案作为片段已包含在父名中才可省略
261
- */
262
- function canOmitStaticChild(
263
- child: VNode,
264
- parentOutputName: string,
265
- parentUsesOwnDeclaredName: boolean,
266
- ): boolean {
267
- const text = getStaticDisplayText(child)
268
- if (!text || !parentOutputName.trim()) return false
269
- const parentNorm = parentOutputName.replace(/\s+/g, ' ').trim()
270
- const textNorm = text.replace(/\s+/g, ' ').trim()
271
- if (parentUsesOwnDeclaredName) {
272
- return textNorm === parentNorm
273
- }
274
- return parentNorm.includes(textNorm)
275
- }
276
-
277
- /**
278
- * 判断 VNode 子树是否包含任何有价值的节点(有 ref 或有 accessible name)
279
- * 用于过滤空容器子树,避免输出无内容的嵌套层级
280
- */
281
- export function hasValue(vnode: VNode): boolean {
282
- if (vnode.ref !== undefined || vnode.name.trim() !== '') return true
283
- return vnode.children.some(hasValue)
284
- }
285
-
286
- /**
287
- * 检查节点子树中是否存在任何有 ref 的可交互子节点。
288
- * 用于判断父节点是否可以安全省略其子节点输出。
289
- */
290
- function hasInteractiveDescendant(vnode: VNode): boolean {
291
- return vnode.children.some(c => c.ref !== undefined || hasInteractiveDescendant(c))
292
- }
293
-
294
- /**
295
- * 找到子树中恰好唯一一个有 ref 的节点,若有多个则返回 null。
296
- * 用于判断是否可将该子节点与父节点合并输出。
297
- */
298
- function findSingleRefDescendant(vnode: VNode): VNode | null {
299
- const refs: VNode[] = []
300
- const collect = (v: VNode) => {
301
- if (v.ref !== undefined) refs.push(v)
302
- if (refs.length > 1) return // 超过一个就提前退出
303
- v.children.forEach(collect)
304
- }
305
- vnode.children.forEach(collect)
306
- return refs.length === 1 ? refs[0] : null
307
- }
308
-
309
- /**
310
- * 判断节点是否需要透明穿透(跳过本节点但保留子节点)
311
- *
312
- * 统一规则:无 ref(非交互)且无 accessible name → 穿透
313
- * 这样可以同时:
314
- * - 去掉 generic/list/listitem 等纯布局噪音(无 name 时穿透)
315
- * - 保留有 name 的 listitem(有 name 时保留,兼顾内容理解场景)
316
- */
317
- export function shouldPassThrough(vnode: VNode, opts: A11yTreeShapeOptions): boolean {
318
- if (!opts.pruneUnnamed) return false
319
- // preserveRoles 中的角色强制保留
320
- if (opts.preserveRoles.includes(vnode.role)) return false
321
- // 有 ref(交互节点)→ 永远保留
322
- if (vnode.ref !== undefined) return false
323
- // 有 accessible name → 保留(兼顾内容理解场景)
324
- if (vnode.name.trim() !== '') return false
325
- // 无 ref 且无 name → 透明穿透
326
- return true
327
- }
328
-
329
- /**
330
- * 将节点 name 转义为 YAML 引号内文本;空名返回空串(调用方决定是否拼引号)。
331
- */
332
- function formatNameAttr(name: string): string {
333
- const safe = name.replace(/[\r\n]+/g, ' ').replace(/"/g, '\\"').trim()
334
- return safe ? ` "${safe}"` : ''
335
- }
336
-
337
- /**
338
- * 将 VNode 序列化为 YAML 行数组
339
- * 穿透节点时,子节点在当前 depth 平铺输出(不增加缩进层级)
340
- *
341
- * 文本折叠规则(Static-Lift + Interactive-Keep,保字优先):
342
- * 1. 子树全静态 → 省略子节点,文字保留在父节点
343
- * 2. 混合子树 → 静态文案上提到父 name(成功才省略静态子节点);交互分支递归保留;
344
- * 禁止把交互文案并入父 name;上提失败则静态子节点独立输出,绝不丢字
345
- */
346
- export function serializeVNode(
347
- vnode: VNode,
348
- depth: number,
349
- opts: A11yTreeShapeOptions,
350
- ): string[] {
351
- const hasInteractive = hasInteractiveDescendant(vnode)
352
- const forceKeepStructure = opts.preserveRoles.includes(vnode.role)
353
- const indent = ' '.repeat(depth)
354
- const refStr = vnode.ref !== undefined ? ` #${vnode.ref}` : ''
355
- const safeTokens = vnode.tokens.map(t => t.replace(/[\r\n]+/g, ' ').replace(/"/g, '\\"'))
356
- const tokenStr = safeTokens.length > 0 ? ` [${safeTokens.join(' ')}]` : ''
357
-
358
- // 规则 1:子树无可交互节点,且无需强制保留结构 → 折叠为一行(文字不丢)
359
- if (!hasInteractive && !forceKeepStructure) {
360
- if (shouldPassThrough(vnode, opts)) {
361
- return vnode.children
362
- .filter(c => hasValue(c))
363
- .flatMap(c => serializeVNode(c, depth, opts))
364
- }
365
- let displayName = vnode.name.trim()
366
- if (!displayName && vnode.children.length > 0) {
367
- displayName = collectVisiblePlainText(vnode.el)
368
- }
369
- if (isNoiseAccessibleName(displayName)) displayName = ''
370
- return [`${indent}- ${vnode.role}${refStr}${tokenStr}${formatNameAttr(displayName)}`]
371
- }
372
-
373
- // preserveRoles 且无交互:保留结构,父级去掉 content name,子节点照常输出
374
- if (!hasInteractive && forceKeepStructure) {
375
- const outputName = getOwnDeclaredName(vnode.el)
376
- const line = `${indent}- ${vnode.role}${refStr}${tokenStr}${formatNameAttr(outputName)}`
377
- const childLines = vnode.children.flatMap(c => serializeVNode(c, depth + 1, opts))
378
- return [line, ...childLines]
379
- }
380
-
381
- // ── 混合子树:Static-Lift + Interactive-Keep ──
382
- const ownDeclaredName = getOwnDeclaredName(vnode.el)
383
- const parentUsesOwnDeclaredName = ownDeclaredName !== ''
384
- const staticChildren = vnode.children.filter(isStaticBranch)
385
- const liftedStaticName = staticChildren
386
- .map(getStaticDisplayText)
387
- .filter(Boolean)
388
- .join(' ')
389
- .replace(/\s+/g, ' ')
390
- .trim()
391
-
392
- // 父 name:声明名优先;否则上提静态子树文案(绝不吸收交互文案)
393
- let outputName = parentUsesOwnDeclaredName ? ownDeclaredName : liftedStaticName
394
- if (isNoiseAccessibleName(outputName)) outputName = ''
395
- // vnode.name(声明名)若被污染同样丢弃
396
- if (!parentUsesOwnDeclaredName && isNoiseAccessibleName(vnode.name)) {
397
- outputName = liftedStaticName && !isNoiseAccessibleName(liftedStaticName) ? liftedStaticName : ''
398
- }
399
-
400
- const willEmitParent =
401
- vnode.ref !== undefined ||
402
- outputName !== '' ||
403
- forceKeepStructure ||
404
- !opts.pruneUnnamed
405
-
406
- // 父节点不会输出时必须穿透,且不得省略静态子节点(保字)
407
- if (!willEmitParent) {
408
- return vnode.children
409
- .filter(c => hasValue(c))
410
- .flatMap(c => serializeVNode(c, depth, opts))
411
- }
412
-
413
- if (vnode.ref !== undefined) {
414
- // 子树中唯一一个 ref 节点且是无子 ref 的 generic 时,将其 name 提升到父节点,省略子节点输出。
415
- const singleChild = findSingleRefDescendant(vnode)
416
- if (
417
- singleChild &&
418
- singleChild.role === 'generic' &&
419
- !hasInteractiveDescendant(singleChild) &&
420
- staticChildren.length === 0
421
- ) {
422
- const mergedName = outputName.trim() || singleChild.name.trim()
423
- // 合并输出时使用子节点 ref,避免 refMap 中的 #N 在 YAML 里丢失
424
- const mergedRefStr =
425
- singleChild.ref !== undefined ? ` #${singleChild.ref}` : refStr
426
- return [`${indent}- ${vnode.role}${mergedRefStr}${tokenStr}${formatNameAttr(mergedName)}`]
427
- }
428
- }
429
-
430
- // 仅省略「文案已由父 outputName 承载」的静态子节点;其余(含全部交互分支)按原序输出
431
- const childrenToEmit = vnode.children.filter(c => {
432
- if (!isStaticBranch(c)) return true
433
- return !canOmitStaticChild(c, outputName, parentUsesOwnDeclaredName)
434
- })
435
-
436
- const line = `${indent}- ${vnode.role}${refStr}${tokenStr}${formatNameAttr(outputName)}`
437
- const childLines = childrenToEmit.flatMap(c => serializeVNode(c, depth + 1, opts))
438
- return [line, ...childLines]
439
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * a11y-tree.ts
3
- *
4
- * 此文件为 A11y Tree 模块的汇总出口,
5
- * 方便外部直接从 './a11y-tree' 引入相关类型及方法,而无需调整现有的引用路径。
6
- */
7
-
8
- export * from './a11y/types'
9
- export * from './a11y/constants'
10
- export * from './a11y/config'
11
- export * from './a11y/utils'
12
- export * from './a11y/vnode'
13
- export * from './a11y/build'
14
- export * from './a11y/search'