@opentiny/next-sdk 0.4.1 → 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 (64) hide show
  1. package/dist/{SimulatorMask-BHVXyogh-iNUb4dyF.js → SimulatorMask-BHVXyogh-CARX3Rff.js} +1 -1
  2. package/dist/core.js +1 -1
  3. package/dist/{index-CXK8CcKq.js → index-R_HIbfUX.js} +1698 -1170
  4. package/dist/index.d.ts +11 -0
  5. package/dist/index.js +69 -50
  6. package/dist/{initialize-builtin-WebMCP-rALfqS-o.js → initialize-builtin-WebMCP-JaoKwVlm.js} +355 -346
  7. package/dist/page-tools/a11y/build.d.ts +3 -4
  8. package/dist/page-tools/a11y/config.d.ts +96 -0
  9. package/dist/page-tools/a11y/constants.d.ts +2 -0
  10. package/dist/page-tools/a11y/search.d.ts +1 -1
  11. package/dist/page-tools/a11y/types.d.ts +13 -22
  12. package/dist/page-tools/a11y/utils.d.ts +32 -11
  13. package/dist/page-tools/a11y/vnode.d.ts +14 -11
  14. package/dist/page-tools/a11y-tree.d.ts +1 -0
  15. package/dist/page-tools/configs/console-cloud.d.ts +6 -0
  16. package/dist/page-tools/constants.d.ts +5 -17
  17. package/dist/page-tools/context.d.ts +12 -0
  18. package/dist/page-tools/handlers/searchTree.d.ts +2 -1
  19. package/dist/page-tools/page-agent-highlight/index.d.ts +12 -0
  20. package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +2 -0
  21. package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
  22. package/dist/page-tools/schema.d.ts +1 -0
  23. package/dist/page-tools/tool-config.d.ts +50 -0
  24. package/dist/runtime.d.ts +4 -1
  25. package/dist/runtime.js +37 -49
  26. package/dist/vitest.config.d.ts +2 -0
  27. package/index.ts +35 -0
  28. package/package.json +21 -16
  29. package/page-tools/a11y/build.ts +38 -34
  30. package/page-tools/a11y/config.ts +465 -0
  31. package/page-tools/a11y/constants.ts +21 -0
  32. package/page-tools/a11y/search.ts +1 -3
  33. package/page-tools/a11y/types.ts +14 -17
  34. package/page-tools/a11y/utils.ts +129 -156
  35. package/page-tools/a11y/vnode.ts +277 -67
  36. package/page-tools/a11y-tree.ts +1 -0
  37. package/page-tools/bridge.ts +22 -2
  38. package/page-tools/configs/console-cloud.ts +172 -0
  39. package/page-tools/constants.ts +5 -65
  40. package/page-tools/context.ts +38 -1
  41. package/page-tools/handlers/click.ts +6 -2
  42. package/page-tools/handlers/fill.ts +10 -5
  43. package/page-tools/handlers/searchTree.ts +7 -10
  44. package/page-tools/handlers/select.ts +10 -5
  45. package/page-tools/page-agent-highlight/index.ts +66 -19
  46. package/page-tools/page-agent-mask/SimulatorMask.ts +42 -22
  47. package/page-tools/page-agent-mask/hauwei.svg +25 -0
  48. package/page-tools/page-agent-prompt.md +1 -1
  49. package/page-tools/page-agent-tool-event.ts +113 -0
  50. package/page-tools/page-agent-tool.ts +85 -53
  51. package/page-tools/schema.ts +1 -0
  52. package/page-tools/tool-config.ts +100 -0
  53. package/page-tools/utils/dom.ts +6 -3
  54. package/runtime.ts +37 -6
  55. package/test/page-tools/a11y/build.test.ts +638 -0
  56. package/test/page-tools/a11y/config.test.ts +370 -0
  57. package/test/page-tools/configs/console-cloud.test.ts +168 -0
  58. package/test/page-tools/page-agent-highlight.test.ts +110 -0
  59. package/test/page-tools/page-agent-tool-dispatch.test.ts +208 -0
  60. package/test/page-tools/page-agent-tool.test.ts +102 -0
  61. package/test/page-tools/tool-config.test.ts +112 -0
  62. package/test/page-tools/utils/dom.test.ts +122 -0
  63. package/vite.config.runtime.ts +1 -1
  64. package/vitest.config.ts +10 -0
@@ -1,72 +1,12 @@
1
- /** 校验错误默认选择器:ARIA 标准 + 主流 UI 框架 */
2
- export const DEFAULT_ERROR_SELECTORS: string[] = [
3
- // W3C ARIA 标准(最可靠,框架无关)
4
- '[role="alert"]',
5
- '[aria-invalid="true"]',
6
- // Tiny3 / Lego(华为云)
7
- '.ti3-unifyvalid-error',
8
- '.ti3-error',
9
- '.ti-error',
10
- '.lego-text-error',
11
- '.lego-error',
12
- // Element UI / Element Plus
13
- '.el-form-item__error',
14
- // Ant Design
15
- '.ant-form-item-explain-error',
16
- // Bootstrap
17
- '.is-invalid',
18
- '.invalid-feedback',
19
- // Angular
20
- '.ng-invalid',
21
- // 通用命名约定
22
- '.error-msg',
23
- '.error-message',
24
- '.error-text',
25
- '.field-error',
26
- '.form-error',
27
- '.is-error',
28
- '.has-error',
29
- '.validate-error',
30
- '.valid-error'
31
- ]
1
+ /** 校验错误默认选择器:ARIA 标准 + 主流 UI 框架(唯一来源为 a11y/constants.ts,此处重新导出保持旧路径可用) */
2
+ export { DEFAULT_ERROR_SELECTORS, DEFAULT_DIALOG_SELECTORS } from './a11y/constants'
32
3
 
33
- /** 模态弹窗默认选择器:ARIA 标准 + 主流 UI 框架 */
34
- export const DEFAULT_DIALOG_SELECTORS: string[] = [
35
- // W3C ARIA 标准
36
- '[role="dialog"]',
37
- '[role="alertdialog"]',
38
- // Tiny3 / Lego(华为云)
39
- '[class*="ti3-modal"]',
40
- '[class*="ti3-message-box"]',
41
- // Element UI / Element Plus
42
- '[class*="el-dialog"]',
43
- '[class*="el-message-box"]',
44
- // Ant Design
45
- '[class*="ant-modal"]',
46
- // Bootstrap
47
- '[class*="modal-content"]',
48
- // Vuetify
49
- '[class*="v-dialog"]',
50
- // Naive UI
51
- '[class*="n-modal"]'
52
- ]
53
-
54
- export interface PageAgentToolOptions {
55
- /** 允许在无障碍树节点中额外暴露的 DOM 属性白名单 */
56
- exposedAttributes?: string[]
57
- /** 是否启用元素高亮 */
58
- enableHighlight?: boolean
59
- }
4
+ /** registerPageAgentTool(options) 的入参类型,定义见 tool-config.ts(与运行期 setPageAgentToolConfig 的补丁类型一致) */
5
+ export type { PageAgentToolOptions } from './tool-config'
60
6
 
61
7
  declare global {
62
8
  interface Window {
63
- __webmcpcli_interactiveWhitelist?: Element[]
64
- __webmcpcli_interactiveBlacklist?: Element[]
65
- __webmcpcli_exposedAttributes?: string[]
9
+ /** 指定网站可覆盖该函数,在每次 getBrowserState 之前调用,常用于配合 setPageAgentToolConfig 动态调整当前页面的配置 */
66
10
  __webmcpcli_beforeGetBrowserState?: (() => void) | null
67
- /** 校验错误元素 CSS 选择器列表(覆盖默认,用于检测页面可见的校验错误) */
68
- __webmcpcli_errorSelectors?: string[]
69
- /** 模态弹窗元素 CSS 选择器列表(覆盖默认,用于检测阻塞交互的弹窗) */
70
- __webmcpcli_dialogSelectors?: string[]
71
11
  }
72
12
  }
@@ -2,12 +2,49 @@ import type { PageController } from '@page-agent/page-controller'
2
2
  import type { PageStateCache } from './page-state-cache'
3
3
  import type { RefMap } from './a11y-tree'
4
4
 
5
+ export type PageAgentToolContentResult = {
6
+ content: Array<{ type: 'text'; text: string }>
7
+ }
8
+
9
+ export type PageAgentToolErrorResult = PageAgentToolContentResult & {
10
+ isError: true
11
+ error: string
12
+ }
13
+
14
+ export async function createActionErrorResult(
15
+ message: string,
16
+ buildBrowserStateResponse: ActionContext['buildBrowserStateResponse']
17
+ ): Promise<PageAgentToolErrorResult> {
18
+ try {
19
+ const browserState = await buildBrowserStateResponse('full')
20
+ return {
21
+ isError: true,
22
+ error: message,
23
+ content: [{ type: 'text', text: `${message}\n\n${browserState.content[0].text}` }]
24
+ }
25
+ } catch (error) {
26
+ return {
27
+ isError: true,
28
+ error: message,
29
+ content: [
30
+ {
31
+ type: 'text',
32
+ text: `${message}\n\n获取最新浏览器状态失败: ${error instanceof Error ? error.message : String(error)}`
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ }
38
+
5
39
  export interface ActionContext {
6
40
  pageController: PageController
7
41
  stateCache: PageStateCache
8
42
  getRefMap: () => RefMap
9
43
  setRefMap: (map: RefMap) => void
10
- buildBrowserStateResponse: (mode?: 'full' | 'diff' | 'both') => Promise<{ content: Array<{ type: 'text'; text: string }> }>
44
+ buildBrowserStateResponse: (
45
+ mode?: 'full' | 'diff' | 'both'
46
+ ) => Promise<{ content: Array<{ type: 'text'; text: string }> }>
11
47
  refreshOnStaleRef: (action: string, index: number) => Promise<{ content: Array<{ type: 'text'; text: string }> }>
12
48
  errContent: (msg: string) => Promise<{ content: Array<{ type: 'text'; text: string }> }>
49
+ actionError: (msg: string) => Promise<PageAgentToolErrorResult>
13
50
  }
@@ -4,9 +4,13 @@ import type { ActionContext } from '../context'
4
4
 
5
5
  export async function handleClick(args: any, ctx: ActionContext) {
6
6
  const mode = args.responseMode ?? 'diff'
7
- if (args.index === undefined) return ctx.errContent('点击结果: 缺少元素索引')
7
+ if (args.index === undefined) return ctx.actionError('点击结果: 缺少元素索引')
8
8
  const el = ctx.getRefMap().get(args.index)
9
- if (!el) return ctx.refreshOnStaleRef('点击', args.index)
9
+ if (!el) {
10
+ return ctx.actionError(
11
+ `点击失败: ref 索引 ${args.index} 已失效(页面可能已刷新),已自动重新加载页面状态,请使用新的 ref 索引重试。`
12
+ )
13
+ }
10
14
 
11
15
  // 若 ref 指向 shadow host,解析其内部真正的可点击元素
12
16
  let targetEl = el as HTMLElement
@@ -4,15 +4,20 @@ import type { ActionContext } from '../context'
4
4
 
5
5
  export async function handleFill(args: any, ctx: ActionContext) {
6
6
  const mode = args.responseMode ?? 'diff'
7
- if (args.index === undefined || typeof args.text !== 'string') return ctx.errContent('填写结果: 缺少元素索引或文本内容')
7
+ if (args.index === undefined || typeof args.text !== 'string')
8
+ return ctx.actionError('填写结果: 缺少元素索引或文本内容')
8
9
  const el = ctx.getRefMap().get(args.index)
9
- if (!el) return ctx.refreshOnStaleRef('填写', args.index)
10
+ if (!el) {
11
+ return ctx.actionError(
12
+ `填写失败: ref 索引 ${args.index} 已失效(页面可能已刷新),已自动重新加载页面状态,请使用新的 ref 索引重试。`
13
+ )
14
+ }
10
15
 
11
16
  let targetEl = el
12
17
  if (!(targetEl instanceof HTMLInputElement) && !(targetEl instanceof HTMLTextAreaElement)) {
13
18
  // querySelector 不穿透 shadow boundary,对 shadow host 补查其 shadowRoot
14
- const innerInput = el.querySelector('input, textarea')
15
- ?? (el.shadowRoot?.querySelector('input, textarea') as HTMLElement | null)
19
+ const innerInput =
20
+ el.querySelector('input, textarea') ?? (el.shadowRoot?.querySelector('input, textarea') as HTMLElement | null)
16
21
  if (innerInput) {
17
22
  targetEl = innerInput as HTMLElement
18
23
  }
@@ -46,7 +51,7 @@ export async function handleFill(args: any, ctx: ActionContext) {
46
51
  }
47
52
  }
48
53
  if (!fillSuccess) {
49
- return ctx.errContent(`填写结果: 无法填写元素 ${args.index},元素不是 input、textarea 或 contenteditable`)
54
+ return ctx.actionError(`填写结果: 无法填写元素 ${args.index},元素不是 input、textarea 或 contenteditable`)
50
55
  }
51
56
  // shadow DOM 内元素:inputTextElement 派发的合成事件 composed:false,
52
57
  // 补发 composed:true 事件确保事件委托框架(如 React)能收到
@@ -1,23 +1,20 @@
1
1
  import { searchA11yTree } from '../a11y-tree'
2
2
  import { detectPageDialog, detectValidationErrors } from '../utils/dom'
3
- import { DEFAULT_ERROR_SELECTORS } from '../constants'
3
+ import { getPageAgentToolConfig } from '../tool-config'
4
4
  import type { ActionContext } from '../context'
5
+ import type { PageAgentToolInput } from '../schema'
5
6
 
6
- export async function handleSearchTree(args: any, ctx: ActionContext) {
7
+ export async function handleSearchTree(args: PageAgentToolInput, ctx: ActionContext) {
7
8
  if (!args.query) return ctx.errContent('搜索失败: 缺少 query 参数')
8
- const blacklist = (window.__webmcpcli_interactiveBlacklist ?? []) as Element[]
9
- const whitelist = (window.__webmcpcli_interactiveWhitelist ?? []) as Element[]
10
- const exposedAttributes = (window.__webmcpcli_exposedAttributes ?? []) as string[]
11
- const result = searchA11yTree(args.query, document.body, blacklist, whitelist, {
9
+ const result = searchA11yTree(args.query, document.body, {
10
+ ...getPageAgentToolConfig().a11yConfig,
12
11
  contextLines: args.contextLines,
13
12
  maxMatches: args.maxMatches,
14
- exposedAttributes,
15
- errorSelectors: (window.__webmcpcli_errorSelectors ?? DEFAULT_ERROR_SELECTORS).join(', '),
16
13
  })
17
-
14
+
18
15
  ctx.setRefMap(result.refMap)
19
16
  ctx.stateCache.update(window.location.href, result.yaml)
20
-
17
+
21
18
  await ctx.pageController.hideMask()
22
19
  // 检测页面弹窗/遮罩层,帮助 AI 发现可能遮挡目标的确认弹窗
23
20
  const dialogAlert = detectPageDialog()
@@ -4,23 +4,28 @@ import type { ActionContext } from '../context'
4
4
 
5
5
  export async function handleSelect(args: any, ctx: ActionContext) {
6
6
  const mode = args.responseMode ?? 'diff'
7
- if (args.index === undefined || typeof args.text !== 'string') return ctx.errContent('选择结果: 缺少元素索引或文本内容')
7
+ if (args.index === undefined || typeof args.text !== 'string')
8
+ return ctx.actionError('选择结果: 缺少元素索引或文本内容')
8
9
  let el = ctx.getRefMap().get(args.index) as HTMLSelectElement | HTMLElement | undefined
9
- if (!el) return ctx.refreshOnStaleRef('选择', args.index)
10
+ if (!el) {
11
+ return ctx.actionError(
12
+ `选择失败: ref 索引 ${args.index} 已失效(页面可能已刷新),已自动重新加载页面状态,请使用新的 ref 索引重试。`
13
+ )
14
+ }
10
15
  // 若 ref 指向 shadow host,解析其内部真正的 <select>(与 fill 一致)
11
16
  let targetSelect: HTMLSelectElement | null = null
12
17
  if (el instanceof HTMLSelectElement) {
13
18
  targetSelect = el
14
19
  } else {
15
- const innerSelect = el.querySelector('select')
16
- ?? (el.shadowRoot?.querySelector('select') as HTMLSelectElement | null)
20
+ const innerSelect =
21
+ el.querySelector('select') ?? (el.shadowRoot?.querySelector('select') as HTMLSelectElement | null)
17
22
  if (innerSelect) {
18
23
  targetSelect = innerSelect
19
24
  }
20
25
  }
21
26
 
22
27
  if (!targetSelect) {
23
- return ctx.errContent(`选择结果: 无法选择元素 ${args.index},未找到对应的 <select> 元素`)
28
+ return ctx.actionError(`选择结果: 无法选择元素 ${args.index},未找到对应的 <select> 元素`)
24
29
  }
25
30
 
26
31
  await selectOptionElement(targetSelect, args.text)
@@ -50,14 +50,62 @@ const injectStyles = `
50
50
  }
51
51
  `
52
52
 
53
- function isElementViewport(element: HTMLElement) {
54
- const rect = element.getBoundingClientRect()
55
- return (
56
- rect.top >= 0 &&
57
- rect.left >= 0 &&
58
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
59
- rect.right <= (window.innerWidth || document.documentElement.clientWidth)
60
- )
53
+ /** 图标热区回退时父节点宽高上限,避免用到整行容器 */
54
+ const MAX_ICON_FALLBACK_SIZE = 48
55
+
56
+ export type HighlightRect = { top: number; left: number; width: number; height: number }
57
+
58
+ /**
59
+ * 解析高亮矩形。
60
+ * Tiny3 `ti-icon` / `tp-icon` 字形常画在 ::before 上,宿主 getBoundingClientRect 宽或高为 0,
61
+ * 旧逻辑直接跳过导致帮助中心关闭/全屏等图标有 ref 却不高亮。
62
+ */
63
+ export function resolveHighlightRect(el: HTMLElement): HighlightRect | null {
64
+ const rect = el.getBoundingClientRect()
65
+ if (rect.width > 0 && rect.height > 0) {
66
+ return { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
67
+ }
68
+
69
+ // 优先用紧凑父节点(帮助中心 header 图标外包 28×28 的可点区域)
70
+ let parent: HTMLElement | null = el.parentElement
71
+ for (let depth = 0; depth < 3 && parent; depth++) {
72
+ const pr = parent.getBoundingClientRect()
73
+ if (
74
+ pr.width > 0 &&
75
+ pr.height > 0 &&
76
+ pr.width <= MAX_ICON_FALLBACK_SIZE &&
77
+ pr.height <= MAX_ICON_FALLBACK_SIZE
78
+ ) {
79
+ return { top: pr.top, left: pr.left, width: pr.width, height: pr.height }
80
+ }
81
+ parent = parent.parentElement
82
+ }
83
+
84
+ // 单边有尺寸时,用 font-size 补齐另一边
85
+ if (rect.width > 0 || rect.height > 0) {
86
+ try {
87
+ const fs = parseFloat(getComputedStyle(el).fontSize) || 16
88
+ const width = rect.width > 0 ? rect.width : fs
89
+ const height = rect.height > 0 ? rect.height : fs
90
+ return {
91
+ top: rect.height > 0 ? rect.top : rect.top - (height - rect.height) / 2,
92
+ left: rect.width > 0 ? rect.left : rect.left - (width - rect.width) / 2,
93
+ width,
94
+ height,
95
+ }
96
+ } catch {
97
+ /* ignore */
98
+ }
99
+ }
100
+
101
+ return null
102
+ }
103
+
104
+ /** 与视口有交集即可(侧栏轻微负 left、贴边控件不应被整框剔除) */
105
+ function isRectInViewport(rect: HighlightRect): boolean {
106
+ const vh = window.innerHeight || document.documentElement.clientHeight
107
+ const vw = window.innerWidth || document.documentElement.clientWidth
108
+ return rect.top < vh && rect.top + rect.height > 0 && rect.left < vw && rect.left + rect.width > 0
61
109
  }
62
110
 
63
111
  const doHighlight = (refMap: RefMap, parentIframe: HTMLIFrameElement | null = null) => {
@@ -87,23 +135,23 @@ const doHighlight = (refMap: RefMap, parentIframe: HTMLIFrameElement | null = nu
87
135
  iframeOffset.y = iframeRect.top
88
136
  }
89
137
  const fragment = document.createDocumentFragment()
90
- const overlays = []
91
138
 
92
139
  // 3、 遍历 refMap
93
140
  for (const [index, el] of refMap.entries()) {
94
- if (!isElementViewport(el)) continue
141
+ const rect = resolveHighlightRect(el)
142
+ if (!rect) continue
143
+
144
+ const top = rect.top + iframeOffset.y
145
+ const left = rect.left + iframeOffset.x
146
+ const viewportRect = { top, left, width: rect.width, height: rect.height }
147
+ if (!isRectInViewport(viewportRect)) continue
95
148
 
96
149
  const color = colors[index % colors.length]
97
150
  const textColor = color + opacity
98
151
 
99
- const rect = el.getBoundingClientRect()
100
- if (rect.width === 0 || rect.height === 0) continue
101
-
102
152
  const overlay = document.createElement('div')
103
153
  overlay.style.border = `2px solid ${textColor}`
104
-
105
- const top = rect.top + iframeOffset.y
106
- const left = rect.left + iframeOffset.x
154
+ overlay.dataset.refIndex = String(index)
107
155
 
108
156
  overlay.style.top = `${top}px`
109
157
  overlay.style.left = `${left}px`
@@ -113,13 +161,12 @@ const doHighlight = (refMap: RefMap, parentIframe: HTMLIFrameElement | null = nu
113
161
  // 文字
114
162
  const text = document.createElement('span')
115
163
  text.style.backgroundColor = textColor
116
- text.style.height = `${rect.height - 4}px`
117
- text.style.lineHeight = `${rect.height - 4}px`
164
+ text.style.height = `${Math.max(rect.height - 4, 0)}px`
165
+ text.style.lineHeight = `${Math.max(rect.height - 4, 0)}px`
118
166
  text.textContent = index.toString()
119
167
  overlay.appendChild(text)
120
168
 
121
169
  fragment.appendChild(overlay)
122
- overlays.push({ element: overlay, initialRect: rect })
123
170
  }
124
171
 
125
172
  container.appendChild(fragment)
@@ -11,31 +11,15 @@ const injectStyles = `
11
11
  z-index: 10000;
12
12
  }
13
13
 
14
- .webmcp-page-agent-cursorBorder {
15
- position: absolute;
16
- width: 100%;
17
- height: 100%;
18
- background: linear-gradient(45deg, rgb(57, 182, 255), rgb(189, 69, 251));
19
- mask-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%20fill='none'%3e%3cg%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='none'%20stroke='%23000000'%20stroke-width='6'%20stroke-miterlimit='10'%20style='stroke:%20light-dark(rgb(0,%200,%200),%20rgb(255,%20255,%20255));'/%3e%3c/g%3e%3c/svg%3e");
20
- mask-size: 100% 100%;
21
- mask-repeat: no-repeat;
22
-
23
- transform-origin: center;
24
- transform: rotate(-90deg) scale(0.8);
25
- margin-left: -10px;
26
- margin-top: -18px;
27
- }
28
-
29
14
  .webmcp-page-agent-cursorFilling {
30
15
  position: absolute;
31
16
  width: 100%;
32
17
  height: 100%;
33
- background: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3e%3cdefs%3e%3c/defs%3e%3cg%20xmlns='http://www.w3.org/2000/svg'%20style='filter:%20drop-shadow(light-dark(rgba(0,%200,%200,%200.4),%20rgba(237,%20237,%20237,%200.4))%203px%204px%204px);'%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='%23ffffff'%20stroke='none'%20style='fill:%20%23ffffff;'/%3e%3c/g%3e%3c/svg%3e");
34
- background-size: 100% 100%;
18
+ background: url("data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%2079.0834%2079.4553%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20%20width%3D%2779.083374%27%20height%3D%2779.455322%27%20fill%3D%27none%27%20%3E--%3Cdefs%3E%3Cfilter%20id%3D%27filter_0%27%20width%3D%2779.083374%27%20height%3D%2779.455322%27%20x%3D%270.000000%27%20y%3D%270.000000%27%20filterUnits%3D%27userSpaceOnUse%27%20%20color-interpolation-filters%3D%27sRGB%27%3E%3CfeFlood%20flood-opacity%3D%270%27%20result%3D%27BackgroundImageFix%27%20%2F%3E%3CfeOffset%20dx%3D%276.000000%27%20dy%3D%276.000000%27%20in%3D%27SourceAlpha%27%20%2F%3E%3CfeGaussianBlur%20stdDeviation%3D%276.66666651%27%20%2F%3E%3CfeColorMatrix%20type%3D%27matrix%27%20values%3D%270%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200.18%200%20%27%20%2F%3E%3CfeBlend%20result%3D%27effect_dropShadow_1%27%20in2%3D%27BackgroundImageFix%27%20mode%3D%27normal%27%20%2F%3E%3CfeBlend%20result%3D%27shape%27%20in%3D%27SourceGraphic%27%20in2%3D%27effect_dropShadow_1%27%20mode%3D%27normal%27%20%2F%3E%3C%2Ffilter%3E%3ClinearGradient%20id%3D%27paint_linear_0%27%20x1%3D%2718.4743214%27%20x2%3D%2718.4213886%27%20y1%3D%27-1.86264515e-09%27%20y2%3D%2734.9221306%27%20gradientUnits%3D%27userSpaceOnUse%27%3E%3Cstop%20stop-color%3D%27rgb%28255%2C181%2C96%29%27%20offset%3D%270%27%20stop-opacity%3D%271%27%20%2F%3E%3Cstop%20stop-color%3D%27rgb%28255%2C120%2C114%29%27%20offset%3D%270.5%27%20stop-opacity%3D%271%27%20%2F%3E%3Cstop%20stop-color%3D%27rgb%28241%2C109%2C237%29%27%20offset%3D%271%27%20stop-opacity%3D%271%27%20%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20filter%3D%27url%28%23filter_0%29%27%3E%3Cpath%20d%3D%27M20.2632%203.57771L34.6257%2032.3027C35.4444%2033.9402%2033.8152%2035.7258%2032.1097%2035.0603L19.2014%2030.0229C18.7339%2029.8404%2018.2148%2029.8404%2017.7472%2030.0229L4.83891%2035.0603C3.13343%2035.7258%201.50423%2033.9402%202.32297%2032.3027L16.6855%203.57771C17.4225%202.10361%2019.5261%202.10361%2020.2632%203.57771Z%27%20fill%3D%27url%28%23paint_linear_0%29%27%20fill-rule%3D%27evenodd%27%20transform%3D%27matrix%280.719908%2C-0.69407%2C0.69407%2C0.719908%2C2.00256%2C28.0095%29%27%20%2F%3E%3Cpath%20d%3D%27M37.2169%2032.6848C37.1578%2032.189%2037.0096%2031.7039%2036.7723%2031.2294L22.4098%202.5044C22.1973%202.07946%2021.9328%201.70554%2021.6163%201.38261C21.3524%201.11342%2021.0524%200.879671%2020.7162%200.681366C20.3925%200.490407%2020.0558%200.34434%2019.7061%200.243163C19.312%200.129145%2018.9014%200.0721366%2018.4743%200.0721366C18.0472%200.0721366%2017.6367%200.129145%2017.2426%200.243163C16.8929%200.34434%2016.5562%200.490407%2016.2324%200.681366C15.8962%200.879671%2015.5962%201.11342%2015.3323%201.38261C15.0158%201.70553%2014.7513%202.07946%2014.5388%202.5044L0.176342%2031.2294C-0.0609121%2031.7039%20-0.209128%2032.189%20-0.268305%2032.6848C-0.316251%2033.0865%20-0.305747%2033.4951%20-0.236792%2033.9108C-0.171059%2034.307%20-0.0570567%2034.6809%200.105216%2035.0326C0.292784%2035.4391%200.544845%2035.8158%200.861397%2036.1627C1.17794%2036.5097%201.53001%2036.7951%201.91761%2037.0191C2.25297%2037.2128%202.61492%2037.3606%203.00348%2037.4623C3.41106%2037.5689%203.81704%2037.6168%204.22142%2037.6058C4.72053%2037.5922%205.21719%2037.4889%205.71141%2037.2961L18.4743%2032.3154L31.2372%2037.2961C31.7315%2037.4889%2032.2281%2037.5922%2032.7272%2037.6058C33.1316%2037.6168%2033.5376%2037.5689%2033.9452%2037.4623C34.3337%2037.3606%2034.6957%2037.2128%2035.0311%2037.0191C35.4187%2036.7951%2035.7707%2036.5097%2036.0873%2036.1627C36.4038%2035.8158%2036.6558%2035.4391%2036.8434%2035.0327C37.0057%2034.681%2037.1197%2034.307%2037.1854%2033.9108C37.2544%2033.4951%2037.2649%2033.0865%2037.2169%2032.6848ZM34.6257%2032.3027L20.2632%203.57771C19.5261%202.10361%2017.4225%202.10361%2016.6855%203.57771L2.32297%2032.3027C1.50423%2033.9402%203.13343%2035.7258%204.83891%2035.0603L17.7472%2030.0229C18.2148%2029.8404%2018.7339%2029.8404%2019.2014%2030.0229L32.1097%2035.0603C33.8152%2035.7258%2035.4444%2033.9402%2034.6257%2032.3027Z%27%20fill%3D%27rgb%28255%2C255%2C255%29%27%20fill-rule%3D%27evenodd%27%20transform%3D%27matrix%280.719908%2C-0.69407%2C0.69407%2C0.719908%2C2.00256%2C28.0095%29%27%20%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
19
+ background-size: 100% 100%;
35
20
  background-repeat: no-repeat;
36
21
 
37
22
  transform-origin: center;
38
- transform: rotate(-90deg) scale(0.8);
39
23
  margin-left: -10px;
40
24
  margin-top: -18px;
41
25
  }
@@ -117,7 +101,10 @@ export class SimulatorMask extends EventTarget {
117
101
  try {
118
102
  const motion = new Motion({
119
103
  mode: isPageDark() ? 'dark' : 'light',
120
- styles: { position: 'absolute', inset: '0' }
104
+ styles: { position: 'absolute', inset: '0' },
105
+ glowWidth: 80,
106
+ borderWidth: 8,
107
+ colors: ['rgb(121, 216, 247)', 'rgb(171, 219, 110)', 'rgb(252, 188, 114)', 'rgb(235, 117, 231)']
121
108
  })
122
109
  this.motion = motion
123
110
  this.wrapper.appendChild(motion.element)
@@ -186,9 +173,9 @@ export class SimulatorMask extends EventTarget {
186
173
  this.#cursor.appendChild(fillingLayer)
187
174
 
188
175
  // Create border layer
189
- const borderLayer = document.createElement('div')
190
- borderLayer.className = 'webmcp-page-agent-cursorBorder'
191
- this.#cursor.appendChild(borderLayer)
176
+ // const borderLayer = document.createElement('div')
177
+ // borderLayer.className = 'webmcp-page-agent-cursorBorder'
178
+ // this.#cursor.appendChild(borderLayer)
192
179
 
193
180
  this.wrapper.appendChild(this.#cursor)
194
181
  }
@@ -276,4 +263,37 @@ export class SimulatorMask extends EventTarget {
276
263
  this.wrapper.remove()
277
264
  this.dispatchEvent(new Event('dispose'))
278
265
  }
266
+ // 根据目标元素大小,在 wrapper 添加绝对定位边框:2px 宽、与目标间距 2px,对角渐变 (0,0 → 1,1)
267
+ borderElement(targetElement: HTMLElement) {
268
+ if (!targetElement) return
269
+
270
+ this.wrapper.querySelector('.webmcp-page-agent-border')?.remove()
271
+
272
+ const rect = targetElement.getBoundingClientRect()
273
+ const gap = 2
274
+ const borderWidth = 2
275
+ const offset = gap + borderWidth
276
+
277
+ const el = document.createElement('div')
278
+ el.className = 'webmcp-page-agent-border'
279
+ el.style.position = 'absolute'
280
+ el.style.left = `${rect.left - offset}px`
281
+ el.style.top = `${rect.top - offset}px`
282
+ el.style.width = `${rect.width + offset * 2}px`
283
+ el.style.height = `${rect.height + offset * 2}px`
284
+ el.style.boxSizing = 'border-box'
285
+ el.style.pointerEvents = 'none'
286
+ el.style.zIndex = '10000'
287
+ el.style.border = `${borderWidth}px solid transparent`
288
+ // 对角渐变:左上 (0,0) → 右下 (1,1)
289
+ el.style.borderImageSource = 'linear-gradient(to bottom right, #79D8F7, #ABDB6E, #FCBC72, #FA8682, #EB75E7)'
290
+ el.style.borderImageSlice = '1'
291
+ el.style.borderImageWidth = `${borderWidth}px`
292
+
293
+ this.wrapper.appendChild(el)
294
+ }
295
+
296
+ removeBorderElement() {
297
+ this.wrapper.querySelector('.webmcp-page-agent-border')?.remove()
298
+ }
279
299
  }
@@ -0,0 +1,25 @@
1
+ <svg viewBox="0 0 79.0834 79.4553" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79.083374" height="79.455322" fill="none" customFrame="#000000">
2
+ --
3
+ <defs>
4
+ <g id="pixso_custom_effect_0">
5
+ <effect x="6.000000" y="6.000000" visibility="visible" fill="rgb(0,0,0)" fill-opacity="0.180000007" effectType="dropShadow" stdDeviation="20" radius="0" />
6
+ </g>
7
+ <filter id="filter_0" width="79.083374" height="79.455322" x="0.000000" y="0.000000" filterUnits="userSpaceOnUse" customEffect="url(#pixso_custom_effect_0)" color-interpolation-filters="sRGB">
8
+ <feFlood flood-opacity="0" result="BackgroundImageFix" />
9
+ <feOffset dx="6.000000" dy="6.000000" in="SourceAlpha" />
10
+ <feGaussianBlur stdDeviation="6.66666651" />
11
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.18 0 " />
12
+ <feBlend result="effect_dropShadow_1" in2="BackgroundImageFix" mode="normal" />
13
+ <feBlend result="shape" in="SourceGraphic" in2="effect_dropShadow_1" mode="normal" />
14
+ </filter>
15
+ <linearGradient id="paint_linear_0" x1="18.4743214" x2="18.4213886" y1="-1.86264515e-09" y2="34.9221306" gradientUnits="userSpaceOnUse">
16
+ <stop stop-color="rgb(255,181,96)" offset="0" stop-opacity="1" />
17
+ <stop stop-color="rgb(255,120,114)" offset="0.5" stop-opacity="1" />
18
+ <stop stop-color="rgb(241,109,237)" offset="1" stop-opacity="1" />
19
+ </linearGradient>
20
+ </defs>
21
+ <g filter="url(#filter_0)">
22
+ <path id="多边形 11" d="M20.2632 3.57771L34.6257 32.3027C35.4444 33.9402 33.8152 35.7258 32.1097 35.0603L19.2014 30.0229C18.7339 29.8404 18.2148 29.8404 17.7472 30.0229L4.83891 35.0603C3.13343 35.7258 1.50423 33.9402 2.32297 32.3027L16.6855 3.57771C17.4225 2.10361 19.5261 2.10361 20.2632 3.57771Z" fill="url(#paint_linear_0)" fill-rule="evenodd" transform="matrix(0.719908,-0.69407,0.69407,0.719908,2.00256,28.0095)" />
23
+ <path id="多边形 11" d="M37.2169 32.6848C37.1578 32.189 37.0096 31.7039 36.7723 31.2294L22.4098 2.5044C22.1973 2.07946 21.9328 1.70554 21.6163 1.38261C21.3524 1.11342 21.0524 0.879671 20.7162 0.681366C20.3925 0.490407 20.0558 0.34434 19.7061 0.243163C19.312 0.129145 18.9014 0.0721366 18.4743 0.0721366C18.0472 0.0721366 17.6367 0.129145 17.2426 0.243163C16.8929 0.34434 16.5562 0.490407 16.2324 0.681366C15.8962 0.879671 15.5962 1.11342 15.3323 1.38261C15.0158 1.70553 14.7513 2.07946 14.5388 2.5044L0.176342 31.2294C-0.0609121 31.7039 -0.209128 32.189 -0.268305 32.6848C-0.316251 33.0865 -0.305747 33.4951 -0.236792 33.9108C-0.171059 34.307 -0.0570567 34.6809 0.105216 35.0326C0.292784 35.4391 0.544845 35.8158 0.861397 36.1627C1.17794 36.5097 1.53001 36.7951 1.91761 37.0191C2.25297 37.2128 2.61492 37.3606 3.00348 37.4623C3.41106 37.5689 3.81704 37.6168 4.22142 37.6058C4.72053 37.5922 5.21719 37.4889 5.71141 37.2961L18.4743 32.3154L31.2372 37.2961C31.7315 37.4889 32.2281 37.5922 32.7272 37.6058C33.1316 37.6168 33.5376 37.5689 33.9452 37.4623C34.3337 37.3606 34.6957 37.2128 35.0311 37.0191C35.4187 36.7951 35.7707 36.5097 36.0873 36.1627C36.4038 35.8158 36.6558 35.4391 36.8434 35.0327C37.0057 34.681 37.1197 34.307 37.1854 33.9108C37.2544 33.4951 37.2649 33.0865 37.2169 32.6848ZM34.6257 32.3027L20.2632 3.57771C19.5261 2.10361 17.4225 2.10361 16.6855 3.57771L2.32297 32.3027C1.50423 33.9402 3.13343 35.7258 4.83891 35.0603L17.7472 30.0229C18.2148 29.8404 18.7339 29.8404 19.2014 30.0229L32.1097 35.0603C33.8152 35.7258 35.4444 33.9402 34.6257 32.3027Z" fill="rgb(255,255,255)" fill-rule="evenodd" transform="matrix(0.719908,-0.69407,0.69407,0.719908,2.00256,28.0095)" />
24
+ </g>
25
+ </svg>
@@ -43,7 +43,7 @@
43
43
  - `- role #N [token1] [token2] "accessible name"`
44
44
  - `role`:ARIA 语义角色(如 button/link/radio/heading/listitem/generic 等)
45
45
  - `#N`:可交互元素的唯一操作索引,**只有带 `#N` 的节点才能被操作**,操作时将 N 作为 `index` 参数传入
46
- - `[token]`:可选 token,包括状态标记(如 `[checked]` `[selected]` `[disabled]` `[cursor=pointer]`)和定制属性(如 `[cf-uba="..."]`)
46
+ - `[token]`:可选 token,包括状态标记(如 `[checked]` `[selected]` `[pressed]` `[current]` `[expanded]` `[disabled]` `[readonly]` `[required]` `[invalid]` `[busy]` `[error]` `[warning]` `[cursor=pointer]`)和定制属性(如 `[cf-uba="..."]`)
47
47
  - `"accessible name"`:元素的语义化名称,**用双引号包裹**(通过 aria-label/aria-labelledby/innerText 等计算得出);无名称的节点此字段省略
48
48
  - 缩进表示父子关系
49
49
 
@@ -0,0 +1,113 @@
1
+ import { inputSchema, type PageAgentToolInput, type PageAgentToolRawInput } from './schema'
2
+ import { PageController } from '@page-agent/page-controller'
3
+
4
+ export const PAGE_AGENT_TOOL_CALL_EVENT = 'page-agent-tool-call'
5
+ export const PAGE_AGENT_TOOL_RESULT_EVENT = 'page-agent-tool-result'
6
+ export const PAGE_AGENT_CHAT_END_EVENT = 'page-agent-chat-end'
7
+
8
+ export type PageAgentToolCallEventDetail = {
9
+ data?: PageAgentToolRawInput
10
+ requestId: string
11
+ }
12
+
13
+ export type PageAgentToolResultEventDetail = {
14
+ data: {
15
+ success: boolean
16
+ result?: unknown
17
+ error?: string
18
+ }
19
+ requestId: string
20
+ }
21
+
22
+ type ExecutePageAgentTool = (data: PageAgentToolInput) => Promise<unknown>
23
+
24
+ function isPageAgentToolErrorResult(result: unknown): result is { isError: true; error: string } {
25
+ return (
26
+ typeof result === 'object' &&
27
+ result !== null &&
28
+ 'isError' in result &&
29
+ result.isError === true &&
30
+ 'error' in result &&
31
+ typeof result.error === 'string'
32
+ )
33
+ }
34
+
35
+ declare global {
36
+ interface Window {
37
+ __nextSdkPageAgentToolEventCleanup?: () => void
38
+ }
39
+ }
40
+
41
+ function dispatchPageAgentToolResult(detail: PageAgentToolResultEventDetail) {
42
+ window.dispatchEvent(new CustomEvent(PAGE_AGENT_TOOL_RESULT_EVENT, { detail }))
43
+ }
44
+
45
+ export function setupPageAgentToolEventBridge(
46
+ executePageAgentTool: ExecutePageAgentTool,
47
+ pageController: PageController
48
+ ) {
49
+ window.__nextSdkPageAgentToolEventCleanup?.()
50
+
51
+ const handleToolCall = async (event: Event) => {
52
+ const detail = (event as CustomEvent<PageAgentToolCallEventDetail>).detail
53
+ const requestId = detail?.requestId
54
+
55
+ if (!requestId) {
56
+ dispatchPageAgentToolResult({
57
+ requestId: '',
58
+ data: {
59
+ success: false,
60
+ error: '缺少 requestId'
61
+ }
62
+ })
63
+ return
64
+ }
65
+
66
+ try {
67
+ const data = inputSchema.parse(detail.data)
68
+ const result = await executePageAgentTool(data)
69
+ if (isPageAgentToolErrorResult(result)) {
70
+ dispatchPageAgentToolResult({
71
+ requestId,
72
+ data: {
73
+ success: false,
74
+ error: result.error,
75
+ result
76
+ }
77
+ })
78
+ return
79
+ }
80
+ dispatchPageAgentToolResult({
81
+ requestId,
82
+ data: {
83
+ success: true,
84
+ result
85
+ }
86
+ })
87
+ } catch (error) {
88
+ dispatchPageAgentToolResult({
89
+ requestId,
90
+ data: {
91
+ success: false,
92
+ error: error instanceof Error ? error.message : String(error)
93
+ }
94
+ })
95
+ }
96
+ }
97
+
98
+ // 聊天结束事件,用于在聊天结束时移除遮罩
99
+ const handleChatEnd = async () => {
100
+ try {
101
+ await pageController.hideMask()
102
+ } catch (error) {}
103
+ }
104
+
105
+ window.addEventListener(PAGE_AGENT_TOOL_CALL_EVENT, handleToolCall)
106
+ window.addEventListener(PAGE_AGENT_CHAT_END_EVENT, handleChatEnd)
107
+
108
+ window.__nextSdkPageAgentToolEventCleanup = () => {
109
+ window.removeEventListener(PAGE_AGENT_TOOL_CALL_EVENT, handleToolCall)
110
+ window.removeEventListener(PAGE_AGENT_CHAT_END_EVENT, handleChatEnd)
111
+ window.__nextSdkPageAgentToolEventCleanup = undefined
112
+ }
113
+ }