@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.
- package/agent/AgentModelProvider.ts +78 -81
- package/agent/type.ts +6 -9
- package/agent/utils/getAISDKTools.ts +0 -1
- package/agent/utils/getBuiltinMcpTools.ts +7 -7
- package/core.ts +0 -3
- package/dist/SimulatorMask-BHVXyogh-CARX3Rff.js +361 -0
- package/dist/agent/type.d.ts +4 -12
- package/dist/agent/utils/getBuiltinMcpTools.d.ts +3 -3
- package/dist/core.d.ts +0 -1
- package/dist/core.js +16 -17
- package/dist/index-R_HIbfUX.js +6604 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +76 -4969
- package/dist/{initialize-builtin-WebMCP-HgObT902.js → initialize-builtin-WebMCP-JaoKwVlm.js} +1156 -1037
- package/dist/page-tools/a11y/build.d.ts +10 -0
- package/dist/page-tools/a11y/config.d.ts +96 -0
- package/dist/page-tools/a11y/constants.d.ts +11 -0
- package/dist/page-tools/a11y/search.d.ts +17 -0
- package/dist/page-tools/a11y/types.d.ts +95 -0
- package/dist/page-tools/a11y/utils.d.ts +55 -0
- package/dist/page-tools/a11y/vnode.d.ts +40 -0
- package/dist/page-tools/a11y-tree.d.ts +9 -99
- package/dist/page-tools/configs/console-cloud.d.ts +6 -0
- package/dist/page-tools/constants.d.ts +10 -0
- package/dist/page-tools/context.d.ts +40 -0
- package/dist/page-tools/handlers/browserState.d.ts +8 -0
- package/dist/page-tools/handlers/click.d.ts +8 -0
- package/dist/page-tools/handlers/executeJavascript.d.ts +8 -0
- package/dist/page-tools/handlers/fill.d.ts +8 -0
- package/dist/page-tools/handlers/scroll.d.ts +8 -0
- package/dist/page-tools/handlers/searchTree.d.ts +9 -0
- package/dist/page-tools/handlers/select.d.ts +8 -0
- package/dist/page-tools/page-agent-highlight/index.d.ts +21 -0
- package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +16 -0
- package/dist/page-tools/page-agent-mask/checkDarkMode.d.ts +5 -0
- package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
- package/dist/page-tools/page-agent-tool.d.ts +3 -8
- package/dist/page-tools/schema.d.ts +44 -0
- package/dist/page-tools/tool-config.d.ts +50 -0
- package/dist/page-tools/utils/dom.d.ts +6 -0
- package/dist/page-tools/utils/scroll.d.ts +15 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +732 -0
- package/dist/utils/builtinProxy.d.ts +1 -1
- package/dist/vitest.config.d.ts +2 -0
- package/index.ts +35 -5
- package/package.json +23 -29
- package/page-tools/a11y/build.ts +74 -0
- package/page-tools/a11y/config.ts +465 -0
- package/page-tools/a11y/constants.ts +131 -0
- package/page-tools/a11y/search.ts +127 -0
- package/page-tools/a11y/types.ts +105 -0
- package/page-tools/a11y/utils.ts +239 -0
- package/page-tools/a11y/vnode.ts +439 -0
- package/page-tools/a11y-tree.ts +9 -527
- package/page-tools/bridge.ts +23 -3
- package/page-tools/configs/console-cloud.ts +172 -0
- package/page-tools/constants.ts +12 -0
- package/page-tools/context.ts +50 -0
- package/page-tools/handlers/browserState.ts +12 -0
- package/page-tools/handlers/click.ts +30 -0
- package/page-tools/handlers/executeJavascript.ts +22 -0
- package/page-tools/handlers/fill.ts +65 -0
- package/page-tools/handlers/scroll.ts +66 -0
- package/page-tools/handlers/searchTree.ts +27 -0
- package/page-tools/handlers/select.ts +39 -0
- package/page-tools/page-agent-highlight/index.ts +245 -0
- package/page-tools/page-agent-mask/SimulatorMask.module.css +14 -0
- package/page-tools/page-agent-mask/SimulatorMask.ts +299 -0
- package/page-tools/page-agent-mask/checkDarkMode.ts +181 -0
- package/page-tools/page-agent-mask/cursor-border.svg +3 -0
- package/page-tools/page-agent-mask/cursor-fill.svg +5 -0
- package/page-tools/page-agent-mask/cursor.module.css +70 -0
- package/page-tools/page-agent-mask/hauwei.svg +25 -0
- package/page-tools/page-agent-prompt.md +34 -18
- package/page-tools/page-agent-tool-event.ts +113 -0
- package/page-tools/page-agent-tool.ts +146 -162
- package/page-tools/schema.ts +52 -0
- package/page-tools/tool-config.ts +100 -0
- package/page-tools/utils/dom.ts +158 -0
- package/page-tools/utils/scroll.ts +58 -0
- package/runtime.ts +44 -0
- package/test/page-tools/a11y/build.test.ts +638 -0
- package/test/page-tools/a11y/config.test.ts +370 -0
- package/test/page-tools/configs/console-cloud.test.ts +168 -0
- package/test/page-tools/page-agent-highlight.test.ts +110 -0
- package/test/page-tools/page-agent-tool-dispatch.test.ts +208 -0
- package/test/page-tools/page-agent-tool.test.ts +102 -0
- package/test/page-tools/tool-config.test.ts +112 -0
- package/test/page-tools/utils/dom.test.ts +122 -0
- package/utils/builtinProxy.ts +45 -13
- package/vite.config.runtime.ts +22 -0
- package/vite.config.ts +52 -8
- package/vitest.config.ts +10 -0
- package/McpSdk.ts +0 -14
- package/WebAgent.ts +0 -5
- package/WebMcp.ts +0 -26
- package/Zod.ts +0 -1
- package/dist/McpSdk.d.ts +0 -14
- package/dist/SimulatorMask-BHVXyogh-BFEGpD5S.js +0 -1048
- package/dist/SimulatorMask-BHVXyogh-CCYbrb84.js +0 -801
- package/dist/WebAgent.d.ts +0 -5
- package/dist/WebMcp.d.ts +0 -23
- package/dist/Zod.d.ts +0 -1
- package/dist/index.es.dev.js +0 -59017
- package/dist/index.es.js +0 -46795
- package/dist/index.umd.dev.js +0 -60355
- package/dist/index.umd.js +0 -1248
- package/dist/mcpsdk@1.25.3.dev.js +0 -22780
- package/dist/mcpsdk@1.25.3.es.dev.js +0 -22778
- package/dist/mcpsdk@1.25.3.es.js +0 -16960
- package/dist/mcpsdk@1.25.3.js +0 -48
- package/dist/transport/ExtensionClientTransport.d.ts +0 -24
- package/dist/transport/ExtensionContentServerTransport.d.ts +0 -39
- package/dist/transport/ExtensionPageServerTransport.d.ts +0 -36
- package/dist/transport/messages.d.ts +0 -9
- package/dist/vite.config.mcpSdk.d.ts +0 -2
- package/dist/vite.config.webAgent.d.ts +0 -2
- package/dist/vite.config.webMcp.d.ts +0 -2
- package/dist/vite.config.webMcpFull.d.ts +0 -2
- package/dist/vite.config.zod.d.ts +0 -2
- package/dist/webagent.dev.js +0 -49360
- package/dist/webagent.es.dev.js +0 -49071
- package/dist/webagent.es.js +0 -39219
- package/dist/webagent.js +0 -642
- package/dist/webmcp-full.dev.js +0 -31336
- package/dist/webmcp-full.es.dev.js +0 -30283
- package/dist/webmcp-full.es.js +0 -22889
- package/dist/webmcp-full.js +0 -645
- package/dist/webmcp.dev.js +0 -9572
- package/dist/webmcp.es.dev.js +0 -8518
- package/dist/webmcp.es.js +0 -6727
- package/dist/webmcp.js +0 -602
- package/dist/zod@3.25.76.dev.js +0 -4037
- package/dist/zod@3.25.76.es.dev.js +0 -4033
- package/dist/zod@3.25.76.es.js +0 -2945
- package/dist/zod@3.25.76.js +0 -1
- package/transport/ExtensionClientTransport.ts +0 -100
- package/transport/ExtensionContentServerTransport.ts +0 -162
- package/transport/ExtensionPageServerTransport.ts +0 -149
- package/transport/messages.ts +0 -63
- package/vite-build-tsc.ts +0 -63
- package/vite-env.d.ts +0 -10
- package/vite.config.mcpSdk.ts +0 -28
- package/vite.config.webAgent.ts +0 -19
- package/vite.config.webMcp.ts +0 -40
- package/vite.config.webMcpFull.ts +0 -19
- package/vite.config.zod.ts +0 -23
- /package/dist/{vite-build-tsc.d.ts → vite.config.runtime.d.ts} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 保障 page-agent-tool 的 switch(action) 不会 fall-through:
|
|
5
|
+
* 每个 action 只能调用对应 handler 一次,且不得落入后续分支 / default。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { handlerMocks } = vi.hoisted(() => {
|
|
9
|
+
const marker = (name: string) => ({
|
|
10
|
+
content: [{ type: 'text' as const, text: `ok:${name}` }],
|
|
11
|
+
})
|
|
12
|
+
return {
|
|
13
|
+
handlerMocks: {
|
|
14
|
+
handleBrowserState: vi.fn(async () => marker('browserState')),
|
|
15
|
+
handleClick: vi.fn(async () => marker('click')),
|
|
16
|
+
handleFill: vi.fn(async () => marker('fill')),
|
|
17
|
+
handleSelect: vi.fn(async () => marker('select')),
|
|
18
|
+
handleScroll: vi.fn(async () => marker('scroll')),
|
|
19
|
+
handleExecuteJavascript: vi.fn(async () => marker('executeJavascript')),
|
|
20
|
+
handleSearchTree: vi.fn(async () => marker('searchTree')),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
vi.mock('../../page-tools/page-agent-mask/SimulatorMask', () => ({
|
|
26
|
+
SimulatorMask: class SimulatorMask {
|
|
27
|
+
show() {}
|
|
28
|
+
hide() {}
|
|
29
|
+
dispose() {}
|
|
30
|
+
borderElement() {}
|
|
31
|
+
removeBorderElement() {}
|
|
32
|
+
},
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
vi.mock('@page-agent/page-controller', () => ({
|
|
36
|
+
PageController: class PageController {
|
|
37
|
+
mask = {
|
|
38
|
+
borderElement() {},
|
|
39
|
+
removeBorderElement() {},
|
|
40
|
+
}
|
|
41
|
+
maskReady = Promise.resolve()
|
|
42
|
+
async showMask() {}
|
|
43
|
+
async hideMask() {}
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
vi.mock('../../page-tools/handlers/browserState', () => ({
|
|
48
|
+
handleBrowserState: handlerMocks.handleBrowserState,
|
|
49
|
+
}))
|
|
50
|
+
vi.mock('../../page-tools/handlers/click', () => ({
|
|
51
|
+
handleClick: handlerMocks.handleClick,
|
|
52
|
+
}))
|
|
53
|
+
vi.mock('../../page-tools/handlers/fill', () => ({
|
|
54
|
+
handleFill: handlerMocks.handleFill,
|
|
55
|
+
}))
|
|
56
|
+
vi.mock('../../page-tools/handlers/select', () => ({
|
|
57
|
+
handleSelect: handlerMocks.handleSelect,
|
|
58
|
+
}))
|
|
59
|
+
vi.mock('../../page-tools/handlers/scroll', () => ({
|
|
60
|
+
handleScroll: handlerMocks.handleScroll,
|
|
61
|
+
}))
|
|
62
|
+
vi.mock('../../page-tools/handlers/executeJavascript', () => ({
|
|
63
|
+
handleExecuteJavascript: handlerMocks.handleExecuteJavascript,
|
|
64
|
+
}))
|
|
65
|
+
vi.mock('../../page-tools/handlers/searchTree', () => ({
|
|
66
|
+
handleSearchTree: handlerMocks.handleSearchTree,
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
import { registerPageAgentTool } from '../../page-tools/page-agent-tool'
|
|
70
|
+
import type { PageAgentToolInput } from '../../page-tools/schema'
|
|
71
|
+
|
|
72
|
+
interface ToolTextContent {
|
|
73
|
+
type: string
|
|
74
|
+
text: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ToolExecuteResult {
|
|
78
|
+
content: ToolTextContent[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type PageAgentExecute = (args: PageAgentToolInput) => Promise<ToolExecuteResult>
|
|
82
|
+
|
|
83
|
+
interface RegisterablePageAgentTool {
|
|
84
|
+
name?: string
|
|
85
|
+
execute?: PageAgentExecute
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ModelContextWithRegisterTool {
|
|
89
|
+
registerTool: (
|
|
90
|
+
tool: RegisterablePageAgentTool,
|
|
91
|
+
options?: unknown
|
|
92
|
+
) => unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface DocumentWithModelContext {
|
|
96
|
+
modelContext?: ModelContextWithRegisterTool
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ACTIONS = [
|
|
100
|
+
'browserState',
|
|
101
|
+
'click',
|
|
102
|
+
'fill',
|
|
103
|
+
'select',
|
|
104
|
+
'scroll',
|
|
105
|
+
'executeJavascript',
|
|
106
|
+
'searchTree',
|
|
107
|
+
] as const
|
|
108
|
+
|
|
109
|
+
type ActionName = (typeof ACTIONS)[number]
|
|
110
|
+
|
|
111
|
+
const handlerByAction: Record<ActionName, keyof typeof handlerMocks> = {
|
|
112
|
+
browserState: 'handleBrowserState',
|
|
113
|
+
click: 'handleClick',
|
|
114
|
+
fill: 'handleFill',
|
|
115
|
+
select: 'handleSelect',
|
|
116
|
+
scroll: 'handleScroll',
|
|
117
|
+
executeJavascript: 'handleExecuteJavascript',
|
|
118
|
+
searchTree: 'handleSearchTree',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resetWindowGlobals() {
|
|
122
|
+
delete window.__webmcpcli_toolConfig
|
|
123
|
+
delete window.__webmcpcli_beforeGetBrowserState
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function captureExecute(): PageAgentExecute {
|
|
127
|
+
const mcp = (document as DocumentWithModelContext).modelContext
|
|
128
|
+
if (!mcp?.registerTool) {
|
|
129
|
+
throw new Error('document.modelContext.registerTool unavailable')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let execute: PageAgentExecute | null = null
|
|
133
|
+
const original = mcp.registerTool.bind(mcp)
|
|
134
|
+
mcp.registerTool = (tool: RegisterablePageAgentTool, options?: unknown) => {
|
|
135
|
+
if (tool?.name === 'page-agent-tool' && typeof tool.execute === 'function') {
|
|
136
|
+
execute = tool.execute.bind(tool)
|
|
137
|
+
}
|
|
138
|
+
return original(tool, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
registerPageAgentTool()
|
|
143
|
+
} finally {
|
|
144
|
+
mcp.registerTool = original
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!execute) {
|
|
148
|
+
throw new Error('failed to capture page-agent-tool execute')
|
|
149
|
+
}
|
|
150
|
+
return execute
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function argsFor(action: ActionName): PageAgentToolInput {
|
|
154
|
+
switch (action) {
|
|
155
|
+
case 'browserState':
|
|
156
|
+
return { action, responseMode: 'full', contextLines: 2, maxMatches: 20 }
|
|
157
|
+
case 'click':
|
|
158
|
+
return { action, index: 0, contextLines: 2, maxMatches: 20 }
|
|
159
|
+
case 'fill':
|
|
160
|
+
return { action, index: 0, text: 'hello', contextLines: 2, maxMatches: 20 }
|
|
161
|
+
case 'select':
|
|
162
|
+
return { action, index: 0, text: 'opt', contextLines: 2, maxMatches: 20 }
|
|
163
|
+
case 'scroll':
|
|
164
|
+
return { action, down: true, numPages: 1, contextLines: 2, maxMatches: 20 }
|
|
165
|
+
case 'executeJavascript':
|
|
166
|
+
return { action, script: '1+1', contextLines: 2, maxMatches: 20 }
|
|
167
|
+
case 'searchTree':
|
|
168
|
+
return { action, query: 'button', contextLines: 2, maxMatches: 20 }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('page-agent-tool action dispatch(防 switch fall-through)', () => {
|
|
173
|
+
let execute: PageAgentExecute
|
|
174
|
+
|
|
175
|
+
beforeAll(async () => {
|
|
176
|
+
resetWindowGlobals()
|
|
177
|
+
execute = captureExecute()
|
|
178
|
+
// 等待 pageController.maskReady 把 SimulatorMask 挂上
|
|
179
|
+
await Promise.resolve()
|
|
180
|
+
await Promise.resolve()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
for (const mock of Object.values(handlerMocks)) {
|
|
185
|
+
mock.mockClear()
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
afterAll(() => {
|
|
190
|
+
resetWindowGlobals()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it.each(ACTIONS)('action=%s 只调用对应 handler,且不落入 default', async (action) => {
|
|
194
|
+
const result = await execute(argsFor(action))
|
|
195
|
+
|
|
196
|
+
expect(result.content[0]?.text).toBe(`ok:${action}`)
|
|
197
|
+
expect(result.content[0]?.text).not.toMatch(/^未知操作:/)
|
|
198
|
+
|
|
199
|
+
const expectedKey = handlerByAction[action]
|
|
200
|
+
for (const [key, mock] of Object.entries(handlerMocks)) {
|
|
201
|
+
if (key === expectedKey) {
|
|
202
|
+
expect(mock, `${key} should be called once`).toHaveBeenCalledTimes(1)
|
|
203
|
+
} else {
|
|
204
|
+
expect(mock, `${key} must not be called for action=${action}`).not.toHaveBeenCalled()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// jsdom 无 WebGL2,避免 SimulatorMask → ai-motion 初始化刷 stderr
|
|
4
|
+
vi.mock('../../page-tools/page-agent-mask/SimulatorMask', () => ({
|
|
5
|
+
SimulatorMask: class SimulatorMask {
|
|
6
|
+
show() {}
|
|
7
|
+
hide() {}
|
|
8
|
+
dispose() {}
|
|
9
|
+
},
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
import { registerPageAgentTool } from '../../page-tools/page-agent-tool'
|
|
13
|
+
import { getPageAgentToolConfig, setPageAgentToolConfig } from '../../page-tools/tool-config'
|
|
14
|
+
|
|
15
|
+
function resetWindowGlobals() {
|
|
16
|
+
delete window.__webmcpcli_toolConfig
|
|
17
|
+
delete window.__webmcpcli_beforeGetBrowserState
|
|
18
|
+
// 注意:initializeBuiltinWebMCP() 内部用模块级单例 flag 控制只初始化一次 document.modelContext,
|
|
19
|
+
// 且没有对外暴露重置能力,因此这里不删除 document.modelContext,避免二次调用时因单例 flag
|
|
20
|
+
// 仍为 true 而不会重新创建 modelContext,导致 registerTool 拿到 undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetWindowGlobals()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
resetWindowGlobals()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('registerPageAgentTool - 统一无障碍配置接入', () => {
|
|
32
|
+
it('仅传 options.a11yConfig 时,与默认配置合并生效', () => {
|
|
33
|
+
registerPageAgentTool({
|
|
34
|
+
a11yConfig: { roles: [{ role: 'tab', selector: '.tab-item' }] },
|
|
35
|
+
})
|
|
36
|
+
const config = getPageAgentToolConfig().a11yConfig
|
|
37
|
+
expect(config.roles).toEqual([{ role: 'tab', selector: '.tab-item' }])
|
|
38
|
+
// 默认内置规则仍然存在(不会被覆盖丢失)
|
|
39
|
+
expect(config.states.error?.length).toBeGreaterThan(0)
|
|
40
|
+
expect(window.__webmcpcli_toolConfig?.a11yConfig).toBe(config)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('不传 a11yConfig 时,使用默认配置初始化', () => {
|
|
44
|
+
registerPageAgentTool()
|
|
45
|
+
const config = getPageAgentToolConfig().a11yConfig
|
|
46
|
+
expect(config.roles).toEqual([])
|
|
47
|
+
expect(config.states.error?.length).toBeGreaterThan(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('a11yConfig 中的 whitelist/exposedAttributes 与默认配置合并,互不覆盖', () => {
|
|
51
|
+
registerPageAgentTool({
|
|
52
|
+
a11yConfig: {
|
|
53
|
+
whitelist: ['.new-whitelist'],
|
|
54
|
+
exposedAttributes: ['data-config-attr'],
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
const config = getPageAgentToolConfig().a11yConfig
|
|
58
|
+
expect(config.whitelist).toContain('.new-whitelist')
|
|
59
|
+
expect(config.exposedAttributes).toContain('data-config-attr')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('重复调用 registerPageAgentTool 以 replace 模式重新初始化,不会无限累加旧配置', () => {
|
|
63
|
+
registerPageAgentTool({ a11yConfig: { roles: [{ role: 'tab', selector: '.v1' }] } })
|
|
64
|
+
registerPageAgentTool({ a11yConfig: { roles: [{ role: 'tab', selector: '.v2' }] } })
|
|
65
|
+
const config = getPageAgentToolConfig().a11yConfig
|
|
66
|
+
expect(config.roles).toEqual([{ role: 'tab', selector: '.v2' }])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('registerPageAgentTool - 顶层工具配置(PageAgentToolConfig)接入', () => {
|
|
71
|
+
it('不传 enableHighlight 时默认关闭元素高亮', () => {
|
|
72
|
+
registerPageAgentTool()
|
|
73
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('options.enableHighlight = true 时运行期配置同步生效', () => {
|
|
77
|
+
registerPageAgentTool({ enableHighlight: true })
|
|
78
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(true)
|
|
79
|
+
expect(window.__webmcpcli_toolConfig?.enableHighlight).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('注册后可通过 setPageAgentToolConfig 在运行期动态修改,无需重新 registerPageAgentTool', () => {
|
|
83
|
+
registerPageAgentTool({ enableHighlight: true })
|
|
84
|
+
setPageAgentToolConfig({ enableHighlight: false })
|
|
85
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('重复调用 registerPageAgentTool 以 replace 模式重新初始化顶层配置', () => {
|
|
89
|
+
registerPageAgentTool({ enableHighlight: false })
|
|
90
|
+
registerPageAgentTool({ enableHighlight: true })
|
|
91
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('a11yConfig 与顶层配置项互不影响,分别合并生效', () => {
|
|
95
|
+
registerPageAgentTool({
|
|
96
|
+
enableHighlight: false,
|
|
97
|
+
a11yConfig: { roles: [{ role: 'tab', selector: '.tab-item' }] },
|
|
98
|
+
})
|
|
99
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(false)
|
|
100
|
+
expect(getPageAgentToolConfig().a11yConfig.roles).toEqual([{ role: 'tab', selector: '.tab-item' }])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getPageAgentToolConfig,
|
|
4
|
+
setPageAgentToolConfig,
|
|
5
|
+
DEFAULT_PAGE_AGENT_TOOL_CONFIG
|
|
6
|
+
} from '../../page-tools/tool-config'
|
|
7
|
+
|
|
8
|
+
function resetWindowGlobals() {
|
|
9
|
+
delete window.__webmcpcli_toolConfig
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetWindowGlobals()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
resetWindowGlobals()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getPageAgentToolConfig / setPageAgentToolConfig', () => {
|
|
21
|
+
it('未初始化时返回默认配置', () => {
|
|
22
|
+
expect(getPageAgentToolConfig()).toEqual(DEFAULT_PAGE_AGENT_TOOL_CONFIG)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('merge 模式下只覆盖传入的字段,未传入字段保持原值', () => {
|
|
26
|
+
setPageAgentToolConfig({ enableHighlight: false })
|
|
27
|
+
const current = getPageAgentToolConfig()
|
|
28
|
+
expect(current.enableHighlight).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('merge 模式下只覆盖传入的字段,removeMaskAfterToolCall字段保持原值', () => {
|
|
32
|
+
setPageAgentToolConfig({ removeMaskAfterToolCall: false })
|
|
33
|
+
const current = getPageAgentToolConfig()
|
|
34
|
+
expect(current.removeMaskAfterToolCall).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('merge 模式下只覆盖传入的字段,removeMaskAfterToolCall字段保持原值', () => {
|
|
38
|
+
setPageAgentToolConfig({ removeMaskAfterToolCall: true })
|
|
39
|
+
const current = getPageAgentToolConfig()
|
|
40
|
+
expect(current.removeMaskAfterToolCall).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('函数式 patch:入参为当前生效配置,返回值与默认配置合并', () => {
|
|
44
|
+
setPageAgentToolConfig({ enableHighlight: false })
|
|
45
|
+
const next = setPageAgentToolConfig((current) => ({ enableHighlight: !current.enableHighlight }))
|
|
46
|
+
expect(next.enableHighlight).toBe(true)
|
|
47
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('replace 模式丢弃之前的运行期状态,只与默认值重新合并', () => {
|
|
51
|
+
setPageAgentToolConfig({ enableHighlight: true })
|
|
52
|
+
setPageAgentToolConfig({}, { mode: 'replace' })
|
|
53
|
+
expect(getPageAgentToolConfig().enableHighlight).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('setPageAgentToolConfig 后 getPageAgentToolConfig 读到最新值', () => {
|
|
57
|
+
const next = setPageAgentToolConfig({ enableHighlight: false })
|
|
58
|
+
expect(getPageAgentToolConfig()).toEqual(next)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('setPageAgentToolConfig - a11yConfig 子字段', () => {
|
|
63
|
+
it('未初始化时 a11yConfig 与默认无障碍配置等价', () => {
|
|
64
|
+
const config = getPageAgentToolConfig().a11yConfig
|
|
65
|
+
expect(config.roles).toEqual([])
|
|
66
|
+
expect(config.whitelist).toEqual([])
|
|
67
|
+
expect(config.blacklist).toEqual([])
|
|
68
|
+
expect(config.exposedAttributes).toEqual([])
|
|
69
|
+
expect(config.dialogSelectors).toEqual(DEFAULT_PAGE_AGENT_TOOL_CONFIG.a11yConfig.dialogSelectors)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('merge 模式下多次调用累加合并(拼接而非覆盖)', () => {
|
|
73
|
+
setPageAgentToolConfig({ a11yConfig: { roles: [{ role: 'tab', selector: '.tab-1' }] } })
|
|
74
|
+
setPageAgentToolConfig({ a11yConfig: { roles: [{ role: 'tabpanel', selector: '.panel-1' }] } })
|
|
75
|
+
const current = getPageAgentToolConfig().a11yConfig
|
|
76
|
+
expect(current.roles).toEqual([
|
|
77
|
+
{ role: 'tab', selector: '.tab-1' },
|
|
78
|
+
{ role: 'tabpanel', selector: '.panel-1' }
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('函数式 patch 可用于过滤/移除旧规则(真正移除,不会因再次合并而复活)', () => {
|
|
83
|
+
setPageAgentToolConfig({
|
|
84
|
+
a11yConfig: {
|
|
85
|
+
roles: [
|
|
86
|
+
{ role: 'tab', selector: '.tab-1' },
|
|
87
|
+
{ role: 'keep-me', selector: '.k' }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
setPageAgentToolConfig((current) => ({
|
|
92
|
+
a11yConfig: { roles: current.a11yConfig.roles.filter((r) => r.role !== 'tab') }
|
|
93
|
+
}))
|
|
94
|
+
const result = getPageAgentToolConfig().a11yConfig
|
|
95
|
+
expect(result.roles.some((r) => r.role === 'tab')).toBe(false)
|
|
96
|
+
expect(result.roles.some((r) => r.role === 'keep-me')).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('replace 模式丢弃之前的运行期状态,只与默认值重新合并', () => {
|
|
100
|
+
setPageAgentToolConfig({ a11yConfig: { roles: [{ role: 'tab', selector: '.old' }] } })
|
|
101
|
+
setPageAgentToolConfig({ a11yConfig: { roles: [{ role: 'v2', selector: '.new' }] } }, { mode: 'replace' })
|
|
102
|
+
const result = getPageAgentToolConfig().a11yConfig
|
|
103
|
+
expect(result.roles).toEqual([{ role: 'v2', selector: '.new' }])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('a11yConfig 与 enableHighlight 可在同一次 patch 中一起设置,互不影响', () => {
|
|
107
|
+
setPageAgentToolConfig({ enableHighlight: false, a11yConfig: { dialogSelectors: ['.my-modal'] } })
|
|
108
|
+
const current = getPageAgentToolConfig()
|
|
109
|
+
expect(current.enableHighlight).toBe(false)
|
|
110
|
+
expect(current.a11yConfig.dialogSelectors).toContain('.my-modal')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { detectPageDialog, detectValidationErrors, deepQuerySelectorAll } from '../../../page-tools/utils/dom'
|
|
3
|
+
import { setPageAgentToolConfig } from '../../../page-tools/tool-config'
|
|
4
|
+
|
|
5
|
+
function mockRect(el: Element, rect: Partial<DOMRect>) {
|
|
6
|
+
const full: DOMRect = {
|
|
7
|
+
x: 0,
|
|
8
|
+
y: 0,
|
|
9
|
+
width: 0,
|
|
10
|
+
height: 0,
|
|
11
|
+
top: 0,
|
|
12
|
+
left: 0,
|
|
13
|
+
right: 0,
|
|
14
|
+
bottom: 0,
|
|
15
|
+
toJSON: () => ({}),
|
|
16
|
+
...rect,
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(el, 'getBoundingClientRect', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: () => full,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resetWindowGlobals() {
|
|
25
|
+
delete window.__webmcpcli_toolConfig
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetWindowGlobals()
|
|
30
|
+
document.body.innerHTML = ''
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
resetWindowGlobals()
|
|
35
|
+
document.body.innerHTML = ''
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('detectValidationErrors', () => {
|
|
39
|
+
it('通过默认 states.error 选择器(ARIA 标准 + 主流框架)检出校验错误', () => {
|
|
40
|
+
const err = document.createElement('div')
|
|
41
|
+
err.className = 'el-form-item__error'
|
|
42
|
+
err.textContent = '用户名不能为空'
|
|
43
|
+
document.body.appendChild(err)
|
|
44
|
+
mockRect(err, { width: 100, height: 20 })
|
|
45
|
+
|
|
46
|
+
const result = detectValidationErrors()
|
|
47
|
+
expect(result).toContain('用户名不能为空')
|
|
48
|
+
expect(result).toContain('[校验提示]')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('通过 setPageAgentToolConfig 追加的自定义 states.error 选择器检出', () => {
|
|
52
|
+
setPageAgentToolConfig({ a11yConfig: { states: { error: { selector: '.my-custom-error' } } } })
|
|
53
|
+
const err = document.createElement('div')
|
|
54
|
+
err.className = 'my-custom-error'
|
|
55
|
+
err.textContent = '自定义错误文案'
|
|
56
|
+
document.body.appendChild(err)
|
|
57
|
+
mockRect(err, { width: 100, height: 20 })
|
|
58
|
+
|
|
59
|
+
expect(detectValidationErrors()).toContain('自定义错误文案')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('无校验错误时返回空字符串', () => {
|
|
63
|
+
expect(detectValidationErrors()).toBe('')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('detectPageDialog', () => {
|
|
68
|
+
it('通过默认 dialogSelectors(role=dialog)检出真正阻塞交互的模态弹窗', () => {
|
|
69
|
+
const dialog = document.createElement('div')
|
|
70
|
+
dialog.setAttribute('role', 'dialog')
|
|
71
|
+
dialog.textContent = '确认删除该记录吗?此操作不可撤销。'
|
|
72
|
+
dialog.style.position = 'fixed'
|
|
73
|
+
dialog.style.zIndex = '1000'
|
|
74
|
+
document.body.appendChild(dialog)
|
|
75
|
+
mockRect(dialog, { width: 400, height: 200, left: 300, right: 700, top: 250, bottom: 450 })
|
|
76
|
+
|
|
77
|
+
const result = detectPageDialog()
|
|
78
|
+
expect(result).toContain('[页面弹窗检测]')
|
|
79
|
+
expect(result).toContain('确认删除该记录吗')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('通过 setPageAgentToolConfig 追加的自定义 dialogSelectors 检出', () => {
|
|
83
|
+
setPageAgentToolConfig({ a11yConfig: { dialogSelectors: ['.my-modal'] } })
|
|
84
|
+
const dialog = document.createElement('div')
|
|
85
|
+
dialog.className = 'my-modal'
|
|
86
|
+
dialog.textContent = '自定义配置检测到的模态弹窗内容'
|
|
87
|
+
dialog.style.position = 'fixed'
|
|
88
|
+
dialog.style.zIndex = '1000'
|
|
89
|
+
document.body.appendChild(dialog)
|
|
90
|
+
mockRect(dialog, { width: 400, height: 200, left: 300, right: 700, top: 250, bottom: 450 })
|
|
91
|
+
|
|
92
|
+
expect(detectPageDialog()).toContain('自定义配置检测到的模态弹窗内容')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('非模态(非 fixed/absolute 或 z-index 过低)的弹窗不会被误报', () => {
|
|
96
|
+
const notModal = document.createElement('div')
|
|
97
|
+
notModal.setAttribute('role', 'dialog')
|
|
98
|
+
notModal.textContent = '这是一个静态定位的普通提示框内容'
|
|
99
|
+
document.body.appendChild(notModal)
|
|
100
|
+
mockRect(notModal, { width: 400, height: 200, left: 300, right: 700, top: 250, bottom: 450 })
|
|
101
|
+
|
|
102
|
+
expect(detectPageDialog()).toBe('')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('无弹窗时返回空字符串', () => {
|
|
106
|
+
expect(detectPageDialog()).toBe('')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('deepQuerySelectorAll', () => {
|
|
111
|
+
it('可以穿透 Shadow DOM 查询元素', () => {
|
|
112
|
+
const host = document.createElement('div')
|
|
113
|
+
document.body.appendChild(host)
|
|
114
|
+
const shadow = host.attachShadow({ mode: 'open' })
|
|
115
|
+
const btn = document.createElement('button')
|
|
116
|
+
btn.textContent = 'Shadow 内部按钮'
|
|
117
|
+
shadow.appendChild(btn)
|
|
118
|
+
|
|
119
|
+
const found = deepQuerySelectorAll('button', document.body)
|
|
120
|
+
expect(found).toContain(btn)
|
|
121
|
+
})
|
|
122
|
+
})
|
package/utils/builtinProxy.ts
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
2
2
|
|
|
3
|
+
/** 浏览器内置 WebMCP 原生上下文接口 */
|
|
4
|
+
interface NativeModelContext {
|
|
5
|
+
getTools(): Promise<NativeTool[]> | NativeTool[]
|
|
6
|
+
executeTool(tool: NativeTool, argsStr: string): Promise<unknown>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 原生工具描述 */
|
|
10
|
+
interface NativeTool {
|
|
11
|
+
name: string
|
|
12
|
+
description?: string
|
|
13
|
+
title?: string
|
|
14
|
+
inputSchema?: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** JSON-RPC 消息基础结构 */
|
|
18
|
+
interface JsonRpcMessage {
|
|
19
|
+
jsonrpc: string
|
|
20
|
+
id?: string | number
|
|
21
|
+
method?: string
|
|
22
|
+
params?: {
|
|
23
|
+
name?: string
|
|
24
|
+
arguments?: Record<string, unknown>
|
|
25
|
+
[key: string]: unknown
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
3
29
|
/**
|
|
4
30
|
* 建立原生的 JSON-RPC 拦截层:
|
|
5
|
-
* 将发向 remote 的 MCP 请求拦截,代理给所在浏览器的
|
|
31
|
+
* 将发向 remote 的 MCP 请求拦截,代理给所在浏览器的 document.modelContext 上执行。
|
|
6
32
|
*/
|
|
7
33
|
export const setupBuiltinProxy = (transport: Transport) => {
|
|
8
|
-
const getNativeCtx = () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
34
|
+
const getNativeCtx = (): NativeModelContext | null => {
|
|
35
|
+
let ctx: NativeModelContext | null = null
|
|
36
|
+
if (typeof document !== 'undefined') ctx = ctx || ((document as unknown as Record<string, unknown>).modelContext as NativeModelContext | null)
|
|
37
|
+
if (typeof navigator !== 'undefined') ctx = ctx || ((navigator as unknown as Record<string, unknown>).modelContext as NativeModelContext | null)
|
|
38
|
+
ctx = ctx || ((globalThis as unknown as Record<string, unknown>).modelContext as NativeModelContext | null)
|
|
39
|
+
return ctx || null
|
|
12
40
|
}
|
|
13
41
|
|
|
14
|
-
transport.onmessage = async (message:
|
|
42
|
+
transport.onmessage = async (message: JsonRpcMessage) => {
|
|
15
43
|
if (!message || typeof message !== 'object') return
|
|
16
44
|
const id = message.id
|
|
17
45
|
const method = message.method
|
|
@@ -36,8 +64,8 @@ export const setupBuiltinProxy = (transport: Transport) => {
|
|
|
36
64
|
await transport.send({ jsonrpc: '2.0', id, result: {} })
|
|
37
65
|
} else if (method === 'tools/list') {
|
|
38
66
|
const nativeCtx = getNativeCtx()
|
|
39
|
-
if (nativeCtx && nativeCtx.
|
|
40
|
-
const rawTools = await nativeCtx.
|
|
67
|
+
if (nativeCtx && nativeCtx.getTools) {
|
|
68
|
+
const rawTools = await nativeCtx.getTools()
|
|
41
69
|
const tools = rawTools.map((t: any) => {
|
|
42
70
|
let schemaObj: any = {}
|
|
43
71
|
if (typeof t.inputSchema === 'string') {
|
|
@@ -67,19 +95,22 @@ export const setupBuiltinProxy = (transport: Transport) => {
|
|
|
67
95
|
const nativeCtx = getNativeCtx()
|
|
68
96
|
if (nativeCtx && nativeCtx.executeTool) {
|
|
69
97
|
if (!message.params || typeof message.params !== 'object' || !message.params.name) {
|
|
70
|
-
const error:
|
|
98
|
+
const error: Error & { code?: number } = new Error('Invalid params: "name" is required and params must be an object')
|
|
71
99
|
error.code = -32602 // Invalid params
|
|
72
100
|
throw error
|
|
73
101
|
}
|
|
74
102
|
const { name, arguments: args } = message.params
|
|
75
|
-
const
|
|
103
|
+
const tools = await (nativeCtx.getTools ? nativeCtx.getTools() : [])
|
|
104
|
+
const toolObj = tools.find((t: any) => t.name === name)
|
|
105
|
+
if (!toolObj) throw new Error(`Tool ${name} not found`)
|
|
106
|
+
const result = await nativeCtx.executeTool(toolObj, JSON.stringify(args || {}))
|
|
76
107
|
const finalResult =
|
|
77
108
|
result && typeof result === 'object' && 'content' in result
|
|
78
109
|
? result
|
|
79
110
|
: { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) }] }
|
|
80
111
|
await transport.send({ jsonrpc: '2.0', id, result: finalResult })
|
|
81
112
|
} else {
|
|
82
|
-
const error:
|
|
113
|
+
const error: Error & { code?: number } = new Error('executeTool not implemented in Browser built-in WebMCP')
|
|
83
114
|
error.code = -32601 // Method not found
|
|
84
115
|
throw error
|
|
85
116
|
}
|
|
@@ -99,12 +130,13 @@ export const setupBuiltinProxy = (transport: Transport) => {
|
|
|
99
130
|
error: { code: -32601, message: `Method not found: ${method}` }
|
|
100
131
|
})
|
|
101
132
|
}
|
|
102
|
-
} catch (err:
|
|
133
|
+
} catch (err: unknown) {
|
|
103
134
|
if (id !== undefined) {
|
|
135
|
+
const errObj = err as { code?: number; message?: string }
|
|
104
136
|
await transport.send({
|
|
105
137
|
jsonrpc: '2.0',
|
|
106
138
|
id,
|
|
107
|
-
error: { code:
|
|
139
|
+
error: { code: errObj.code || -32000, message: errObj.message || String(err) }
|
|
108
140
|
})
|
|
109
141
|
}
|
|
110
142
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
|
|
3
|
+
export default defineConfig(() => {
|
|
4
|
+
return {
|
|
5
|
+
define: {
|
|
6
|
+
'process.env.NODE_ENV': JSON.stringify('production')
|
|
7
|
+
},
|
|
8
|
+
build: {
|
|
9
|
+
emptyOutDir: false,
|
|
10
|
+
lib: {
|
|
11
|
+
entry: 'runtime.ts',
|
|
12
|
+
name: 'WebMCPSDK',
|
|
13
|
+
formats: ['iife'] as any,
|
|
14
|
+
fileName: () => 'runtime.js'
|
|
15
|
+
},
|
|
16
|
+
rollupOptions: {
|
|
17
|
+
// 全量打包第三方依赖,便于浏览器中直接注入或运行
|
|
18
|
+
external: []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|