@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.
Files changed (90) hide show
  1. package/dist/{SimulatorMask-BHVXyogh-iNUb4dyF.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
  2. package/dist/core.js +1 -1
  3. package/dist/{index-CXK8CcKq.js → index-CHMa2zG4.js} +1985 -1434
  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 +99 -0
  9. package/dist/page-tools/a11y/constants.d.ts +22 -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 +46 -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 +7 -17
  17. package/dist/page-tools/context.d.ts +14 -0
  18. package/dist/page-tools/handlers/hover.d.ts +9 -0
  19. package/dist/page-tools/handlers/searchTree.d.ts +2 -1
  20. package/dist/page-tools/page-agent-highlight/index.d.ts +12 -0
  21. package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +2 -0
  22. package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
  23. package/dist/page-tools/schema.d.ts +4 -3
  24. package/dist/page-tools/tool-config.d.ts +50 -0
  25. package/dist/page-tools/utils/dom.d.ts +8 -2
  26. package/dist/runtime.d.ts +4 -1
  27. package/dist/runtime.js +22 -187
  28. package/dist/vitest.config.d.ts +2 -0
  29. package/package.json +26 -16
  30. package/WebMcpClient.ts +0 -505
  31. package/WebMcpServer.ts +0 -447
  32. package/agent/AgentModelProvider.ts +0 -932
  33. package/agent/type.ts +0 -91
  34. package/agent/utils/generateReActPrompt.ts +0 -71
  35. package/agent/utils/getAISDKTools.ts +0 -34
  36. package/agent/utils/getBuiltinMcpTools.ts +0 -86
  37. package/agent/utils/parseReActAction.ts +0 -34
  38. package/core.ts +0 -26
  39. package/index.ts +0 -56
  40. package/page-tools/a11y/build.ts +0 -70
  41. package/page-tools/a11y/constants.ts +0 -110
  42. package/page-tools/a11y/search.ts +0 -129
  43. package/page-tools/a11y/types.ts +0 -108
  44. package/page-tools/a11y/utils.ts +0 -266
  45. package/page-tools/a11y/vnode.ts +0 -229
  46. package/page-tools/a11y-tree.ts +0 -13
  47. package/page-tools/bridge.ts +0 -702
  48. package/page-tools/constants.ts +0 -72
  49. package/page-tools/context.ts +0 -13
  50. package/page-tools/effects.ts +0 -345
  51. package/page-tools/handlers/browserState.ts +0 -12
  52. package/page-tools/handlers/click.ts +0 -26
  53. package/page-tools/handlers/executeJavascript.ts +0 -22
  54. package/page-tools/handlers/fill.ts +0 -60
  55. package/page-tools/handlers/scroll.ts +0 -66
  56. package/page-tools/handlers/searchTree.ts +0 -30
  57. package/page-tools/handlers/select.ts +0 -34
  58. package/page-tools/initialize-builtin-WebMCP.ts +0 -20
  59. package/page-tools/page-agent-highlight/index.ts +0 -198
  60. package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
  61. package/page-tools/page-agent-mask/SimulatorMask.ts +0 -279
  62. package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
  63. package/page-tools/page-agent-mask/cursor-border.svg +0 -3
  64. package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
  65. package/page-tools/page-agent-mask/cursor.module.css +0 -70
  66. package/page-tools/page-agent-prompt.md +0 -139
  67. package/page-tools/page-agent-tool.ts +0 -175
  68. package/page-tools/page-state-cache.ts +0 -78
  69. package/page-tools/schema.ts +0 -51
  70. package/page-tools/utils/dom.ts +0 -155
  71. package/page-tools/utils/scroll.ts +0 -58
  72. package/remoter/QrCode.ts +0 -54
  73. package/remoter/createRemoter.ts +0 -957
  74. package/remoter/svgs/chat.svg +0 -43
  75. package/remoter/svgs/icon-copy.svg +0 -25
  76. package/remoter/svgs/link.svg +0 -28
  77. package/remoter/svgs/logo.svg +0 -45
  78. package/remoter/svgs/qrcode.svg +0 -35
  79. package/remoter/svgs/scan.svg +0 -21
  80. package/remoter/tooltips.ts +0 -260
  81. package/runtime.html +0 -39
  82. package/runtime.ts +0 -13
  83. package/script/utils.ts +0 -26
  84. package/skills/index.ts +0 -287
  85. package/tsconfig.json +0 -16
  86. package/utils/builtinProxy.ts +0 -144
  87. package/utils/env.ts +0 -13
  88. package/utils/uuid.ts +0 -10
  89. package/vite.config.runtime.ts +0 -22
  90. package/vite.config.ts +0 -63
@@ -1,702 +0,0 @@
1
- /**
2
- * page-tools/bridge - Web MCP 页面工具桥接模块(框架无关)
3
- *
4
- * 解决 Web-MCP 工具动态加载问题:工具定义(mcp-servers/)不直接写业务逻辑,
5
- * 而是通过 window.postMessage 将调用转发给目标页面,页面处理后返回结果。
6
- *
7
- * 核心 API:
8
- * - setNavigator(fn) 在应用入口注册导航函数
9
- * - withPageTools(server)
10
- * 包装 WebMcpServer,让 registerTool 第三个参数
11
- * 同时支持原始回调函数和路由配置对象(RouteConfig),
12
- * 并提供 server.unregisterTool / 工具状态查询能力
13
- * - registerPageTool() 在目标页面激活工具处理器,返回 cleanup 函数
14
- */
15
-
16
- import type { ZodRawShape } from 'zod'
17
- import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'
18
- import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
19
- import type { WebMcpServer } from '../WebMcpServer'
20
- import { randomUUID } from '../utils/uuid'
21
- import type { ToolInvokeEffectConfig } from './effects'
22
- import { hideToolInvokeEffect, resolveRuntimeEffectConfig, showToolInvokeEffect } from './effects'
23
- import { isBrowser } from '../utils/env'
24
-
25
- // 消息类型常量,使用命名空间前缀避免冲突
26
- const MSG_TOOL_CALL = 'next-sdk:tool-call'
27
- const MSG_TOOL_RESPONSE = 'next-sdk:tool-response'
28
- /** 有新工具注册进来(同时表示路由跳转成功,通知 remoter 刷新工具列表)*/
29
- export const MSG_TOOL_REGISTERED = 'next-sdk:tool-registered'
30
- /** 有工具取消注册(通知 remoter 刷新工具列表)*/
31
- export const MSG_TOOL_UNREGISTERED = 'next-sdk:tool-unregistered'
32
- /** iframe 内 Remoter 就绪后向父窗口发送,父窗口回传 route-state-initial */
33
- export const MSG_REMOTER_READY = 'next-sdk:remoter-ready'
34
-
35
- // 已激活页面注册表:路由路径 → 当前页面已挂载的工具名集合
36
- const activePages = new Map<string, Set<string>>()
37
-
38
- // 路由路径规范化:去除尾部斜杠,空路径兜底为 '/'
39
- const normalizeRoute = (value: string) => value.replace(/\/+$/, '') || '/'
40
-
41
- type BroadcastTarget = { win: Window; origin: string }
42
-
43
- // 跨窗口广播目标:同窗口默认 [window],iframe 场景下会加入 remoter 的 contentWindow
44
- const broadcastTargets = new Set<BroadcastTarget>()
45
-
46
- function initBroadcastTargets() {
47
- if (isBrowser()) {
48
- broadcastTargets.add({ win: window, origin: window.location.origin || '*' })
49
- }
50
- }
51
-
52
- initBroadcastTargets()
53
-
54
- /** 向所有广播目标发送工具变更消息(同窗口 + iframe 均能收到) */
55
- function broadcastToolChange(type: typeof MSG_TOOL_REGISTERED | typeof MSG_TOOL_UNREGISTERED) {
56
- if (!isBrowser()) return
57
- const msg = { type }
58
- broadcastTargets.forEach(({ win, origin }) => {
59
- try {
60
- win.postMessage(msg, origin)
61
- } catch {
62
- // 跨域 iframe 可能抛错,忽略
63
- }
64
- })
65
- }
66
-
67
- /** 监听 iframe 内 Remoter 的 remoter-ready,并加入广播目标 */
68
- function setupIframeRemoterBridge() {
69
- if (!isBrowser()) return
70
- window.addEventListener('message', (event: MessageEvent) => {
71
- if (event.data?.type !== MSG_REMOTER_READY || !event.source) return
72
- // 仅接受与当前页面同源的 remoter,避免潜在的 XSS 风险
73
- if (event.origin !== window.location.origin) return
74
- const target = event.source as Window
75
- broadcastTargets.add({ win: target, origin: event.origin || '*' })
76
- })
77
- }
78
-
79
- setupIframeRemoterBridge()
80
-
81
- /** 通过 MCP server 发送工具列表已更新消息 */
82
- function notifyServerToolListChanged(server: unknown) {
83
- if (!server) return
84
- const maybeServer = server as { sendToolListChanged?: () => void; server?: { sendToolListChanged?: () => void } }
85
- try {
86
- if (typeof maybeServer.sendToolListChanged === 'function') {
87
- maybeServer.sendToolListChanged()
88
- } else if (maybeServer.server && typeof maybeServer.server.sendToolListChanged === 'function') {
89
- maybeServer.server.sendToolListChanged()
90
- }
91
- } catch {
92
- // ignore
93
- }
94
- }
95
-
96
- function isToolReadyOnRoute(route: string, toolName: string): boolean {
97
- const toolNames = activePages.get(route)
98
- return !!toolNames && toolNames.has(toolName)
99
- }
100
-
101
- // 应用注册的导航函数,由 setNavigator 设置
102
- let _navigator: ((route: string) => void | boolean | Promise<void | boolean>) | null = null
103
-
104
- /**
105
- * 注册应用的导航函数,通常在应用入口(如 main.ts)调用一次。
106
- * @param fn 导航函数,接收路由路径并执行跳转(如 router.push)。
107
- * 若返回 true,视为目标页面已就绪(如同一路由忽略握手等待),将立即执行后续工具调用。
108
- */
109
- export function setNavigator(fn: (route: string) => void | boolean | Promise<void | boolean>) {
110
- _navigator = fn
111
- }
112
-
113
- /**
114
- * 当前 pathname 是否已匹配目标路由。
115
- */
116
- function isCurrentPathMatched(path: string): boolean {
117
- if (!isBrowser()) return false
118
- const target = normalizeRoute(path)
119
- const current = normalizeRoute(window.location.pathname)
120
- return current === target
121
- }
122
-
123
- function waitForNavigationReady(timeoutMs: number): Promise<void> {
124
- if (!isBrowser()) {
125
- return Promise.resolve()
126
- }
127
-
128
- return new Promise<void>((resolve) => {
129
- let done = false
130
-
131
- const cleanup = () => {
132
- if (done) return
133
- done = true
134
- window.removeEventListener('message', handleMessage)
135
- resolve()
136
- }
137
-
138
- const handleMessage = (event: MessageEvent) => {
139
- if (event.source !== window) return
140
- // 有新工具注册进来即表示目标页面已就绪
141
- if (event.data?.type === MSG_TOOL_REGISTERED) {
142
- cleanup()
143
- }
144
- }
145
-
146
- window.addEventListener('message', handleMessage)
147
- setTimeout(cleanup, timeoutMs)
148
- })
149
- }
150
-
151
- /**
152
- * registerTool 第三个参数的路由配置对象类型。
153
- * 当传入此类型时,工具调用会自动跳转到 route 对应的页面并通过消息通信执行。
154
- */
155
- export type RouteConfig = {
156
- /** 目标路由路径,如 '/comprehensive' */
157
- route: string
158
- /** 等待页面响应的超时时间(ms),默认 30000 */
159
- timeout?: number
160
- /**
161
- * 是否在调用该工具时启用页面级调用提示效果。
162
- *
163
- * - false / 未配置:不启用任何额外效果(保持现有行为)
164
- * - true:使用默认提示文案(优先取工具标题,其次为工具名)
165
- * - 对象:可自定义提示文案
166
- */
167
- invokeEffect?: boolean | ToolInvokeEffectConfig
168
- }
169
-
170
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
171
- export type WithPageToolsOptions = {}
172
-
173
- // 对外暴露调用提示配置类型,便于业务方在 RouteConfig 外单独复用
174
- export type { ToolInvokeEffectConfig }
175
-
176
- /**
177
- * PageAwareServer 的 registerTool 配置对象类型,与 WebMcpServer.registerTool 保持一致。
178
- */
179
- type RegisterToolConfig<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape> = {
180
- title?: string
181
- description?: string
182
- inputSchema?: InputArgs
183
- outputSchema?: OutputArgs
184
- annotations?: ToolAnnotations
185
- }
186
-
187
- /**
188
- * 包装 WebMcpServer 后的类型:registerTool 第三个参数额外支持 RouteConfig。
189
- * 泛型签名与 WebMcpServer.registerTool 对齐,保持完整的类型推导能力。
190
- * 原有的回调函数写法完全兼容,无需改动。
191
- */
192
- export type PageAwareServer = Omit<WebMcpServer, 'registerTool'> & {
193
- registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
194
- name: string,
195
- config: RegisterToolConfig<InputArgs, OutputArgs>,
196
- // handler 不引入 ToolCallback<InputArgs>:该类型含 MCP SDK 深层泛型,
197
- // 叠加 ZodRawShape 推断链后会触发"类型实例化过深"。
198
- // 实际类型安全由 Proxy 内部透传给 WebMcpServer.registerTool 保证。
199
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
- handlerOrRoute: ((...args: any[]) => any) | RouteConfig
201
- ): RegisteredTool
202
- unregisterTool(name: string): boolean
203
- }
204
-
205
- export type PageToolDefinition<
206
- InputArgs extends ZodRawShape = ZodRawShape,
207
- OutputArgs extends ZodRawShape = ZodRawShape
208
- > = {
209
- /** 工具名称 */
210
- name: string
211
- /** 工具声明配置(title/description/schema/annotations) */
212
- config: RegisterToolConfig<InputArgs, OutputArgs>
213
- /** 工具绑定路由 */
214
- route: string
215
- /** 页面响应超时(ms) */
216
- timeout?: number
217
- /** 页面调用特效 */
218
- invokeEffect?: boolean | ToolInvokeEffectConfig
219
- /** 工具执行回调(可选 context,便于页面注入运行时依赖) */
220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
- handler: (input: any, context?: unknown) => any | Promise<any>
222
- }
223
-
224
- /**
225
- * 注册一个通用的页面跳转工具(navigate_to_page),供大模型在需要时主动跳转到指定路由。
226
- *
227
- * 要求:
228
- * - 业务侧在应用入口通过 setNavigator 注册导航函数(如 router.push 或 navigateByUrl)
229
- * - 前端页面在目标路由下调用 registerPageTool,确保 page-ready 能正确广播
230
- *
231
- * 工具行为:
232
- * - 输入 path(如 "/orders"、"/price-protection"),调用 setNavigator 注册的函数执行跳转
233
- * - 等待目标页面完成挂载并广播 page-ready(或在超时时间到达时兜底返回)
234
- * - 返回简单的文本说明,提示跳转结果
235
- */
236
- export type NavigateToolOptions = {
237
- /** 工具名称,默认 'navigate_to_page' */
238
- name?: string
239
- /** 工具标题,默认 '页面跳转' */
240
- title?: string
241
- /** 工具描述 */
242
- description?: string
243
- /** 等待 page-ready 的超时时间(ms),默认 1500 */
244
- timeoutMs?: number
245
- }
246
-
247
- export function registerNavigateTool(server: WebMcpServer, options?: NavigateToolOptions): RegisteredTool {
248
- const name = options?.name ?? 'navigate_to_page'
249
- const title = options?.title ?? '页面跳转'
250
- const description =
251
- options?.description ??
252
- '当需要的工具在当前页面不可用时,使用此工具跳转到特定页面。例如:要查询订单时跳转到 "/orders",要创建价保时跳转到 "/price-protection"。'
253
- const timeoutMs = options?.timeoutMs ?? 5000
254
-
255
- const inputSchema = {
256
- type: 'object',
257
- properties: {
258
- path: {
259
- type: 'string',
260
- description: '目标页面的路由地址,例如 "/orders"、"/inventory"、"/price-protection" 等。'
261
- }
262
- },
263
- required: ['path']
264
- }
265
-
266
- const handler: ({ path }: { path: string }) => Promise<{ content: Array<{ type: 'text'; text: string }> }> = async ({
267
- path
268
- }: {
269
- path: string
270
- }) => {
271
- if (!isBrowser()) {
272
- return {
273
- content: [{ type: 'text', text: '当前环境不支持页面跳转(window 不存在)。' }]
274
- }
275
- }
276
-
277
- if (!_navigator) {
278
- return {
279
- content: [
280
- {
281
- type: 'text',
282
- text: '页面跳转失败:尚未在应用入口调用 setNavigator 注册导航函数,无法执行路由跳转。'
283
- }
284
- ]
285
- }
286
- }
287
-
288
- try {
289
- // 若当前已在目标路由上,直接返回成功,避免不必要的跳转。
290
- if (isCurrentPathMatched(path)) {
291
- return {
292
- content: [{ type: 'text', text: `当前已在页面:${path}。请继续你的下一步操作。` }]
293
- }
294
- }
295
-
296
- // 先注册握手监听再触发导航,避免极快导航下事件先于监听器触发而漏收。
297
- const readyPromise = waitForNavigationReady(timeoutMs)
298
- const isReady = await _navigator(path)
299
-
300
- // 若导航函数返回 true(如同一路由),视为页面已就绪,跳过握手等待
301
- if (isReady !== true) {
302
- await readyPromise
303
- await new Promise((resolve) => setTimeout(resolve, 500))
304
- }
305
-
306
- return {
307
- content: [{ type: 'text', text: `已成功跳转至页面:${path}。请继续你的下一步操作。` }]
308
- }
309
- } catch (err) {
310
- return {
311
- content: [
312
- {
313
- type: 'text',
314
- text: `页面跳转失败:${err instanceof Error ? err.message : String(err)}。`
315
- }
316
- ]
317
- }
318
- }
319
- }
320
-
321
- if (server && typeof (server as any).registerTool === 'function') {
322
- // 优先检查是否为被 SDK 拦截过的原生/Polyfill modelContext
323
- // 我们在 setupModelContextBridge 中显式注入了该标识位
324
- if ((server as any).__isNextSdkBridgeSetup) {
325
- return (server as any).registerTool({
326
- name,
327
- title,
328
- description,
329
- inputSchema,
330
- execute: handler
331
- })
332
- }
333
-
334
- // 否则默认为 SDK WebMcpServer 格式 (三个参数)
335
- return (server as any).registerTool(
336
- name,
337
- {
338
- title,
339
- description,
340
- inputSchema
341
- },
342
- handler
343
- )
344
- }
345
-
346
- throw new Error('Failed to register navigate tool: invalid server instance.')
347
- }
348
-
349
- /**
350
- * 内部:根据 name/route/timeout 生成转发给页面的 handler 函数。
351
- * 调用流程:
352
- * 1. 若目标路由已激活 → 直接 postMessage 发送工具调用
353
- * 2. 若未激活 → 调用导航函数跳转,等待 page-ready 信号后再发送
354
- * 3. 页面处理后回传结果,Promise resolve
355
- */
356
- function buildPageHandler(
357
- name: string,
358
- route: string,
359
- timeout = 30000,
360
- effectConfig?: ReturnType<typeof resolveRuntimeEffectConfig>
361
- ) {
362
- return (input: any): Promise<any> => {
363
- const callId = randomUUID()
364
-
365
- return new Promise<any>((resolve, reject) => {
366
- let timer: ReturnType<typeof setTimeout>
367
- // readyHandler 需在 cleanup 中一并移除,避免导航失败时泄漏监听器
368
- let readyHandler: ((event: MessageEvent) => void) | undefined
369
-
370
- const cleanup = () => {
371
- clearTimeout(timer)
372
- window.removeEventListener('message', responseHandler)
373
- if (readyHandler) {
374
- window.removeEventListener('message', readyHandler)
375
- }
376
- // 工具调用完成(成功 / 失败 / 超时 / 导航异常)后,无论结果如何都需要关闭调用提示效果
377
- if (effectConfig) {
378
- hideToolInvokeEffect()
379
- }
380
- }
381
-
382
- // 超时兜底,防止页面永远不响应
383
- timer = setTimeout(() => {
384
- cleanup()
385
- reject(new Error(`工具 [${name}] 调用超时 (${timeout}ms),请检查目标页面是否正确调用了 registerPageTool`))
386
- }, timeout)
387
-
388
- // 通过 callId 精确匹配响应,避免并发调用互相串扰
389
- const responseHandler = (event: MessageEvent) => {
390
- if (event.source === window && event.data?.type === MSG_TOOL_RESPONSE && event.data.callId === callId) {
391
- cleanup()
392
- event.data.error ? reject(new Error(event.data.error)) : resolve(event.data.result)
393
- }
394
- }
395
- window.addEventListener('message', responseHandler)
396
-
397
- const sendCall = () => {
398
- window.postMessage({ type: MSG_TOOL_CALL, callId, toolName: name, route, input }, window.location.origin || '*')
399
- }
400
-
401
- // 单次发送守卫:readyHandler 与导航后 activePages 补充检查均可触发 sendCall,
402
- // 用此 flag 确保同一次工具调用只发送一条消息,防止工具被重复执行。
403
- let callSent = false
404
- const sendCallOnce = () => {
405
- if (callSent) return
406
- callSent = true
407
- sendCall()
408
- }
409
-
410
- // 将异步导航逻辑提取为独立 run 函数并用 void 调用,
411
- // 避免在 Promise executor 中直接使用 async(Biome noAsyncPromiseExecutor 规则)。
412
- // 导航失败时显式 reject,防止外层 Promise 永远挂起。
413
- const run = async () => {
414
- try {
415
- // 一旦真正发起工具调用(无论页面是否已激活),优先开启页面调用提示效果
416
- if (effectConfig) {
417
- showToolInvokeEffect(effectConfig)
418
- }
419
-
420
- if (isToolReadyOnRoute(route, name)) {
421
- // 页面已激活,直接发送
422
- sendCallOnce()
423
- return
424
- }
425
-
426
- // ⚠️ 必须先注册 readyHandler 再触发导航:
427
- // 若先导航再注册,极快的导航(同步或微任务)可能导致
428
- // 目标页面已广播 page-ready 而监听器尚未挂载,从而错过信号。
429
- readyHandler = (event: MessageEvent) => {
430
- if (event.source !== window || event.data?.type !== MSG_TOOL_REGISTERED) return
431
- window.removeEventListener('message', readyHandler!)
432
- sendCallOnce()
433
- }
434
- window.addEventListener('message', readyHandler)
435
-
436
- if (_navigator) {
437
- const isReady = await _navigator(route)
438
- // 若导航函数返回 true(如同一路由),视为页面已就绪,直接发送调用并跳过握手同步
439
- if (isReady === true) {
440
- if (readyHandler) {
441
- window.removeEventListener('message', readyHandler)
442
- }
443
- sendCallOnce()
444
- return
445
- }
446
- }
447
- // sendCallOnce 保证即使两条路径都触发,消息也只发送一次。
448
- if (isToolReadyOnRoute(route, name)) {
449
- window.removeEventListener('message', readyHandler)
450
- sendCallOnce()
451
- }
452
- } catch (err) {
453
- // 导航本身抛出异常时,确保 Promise 被 reject 而非永远挂起
454
- cleanup()
455
- reject(err instanceof Error ? err : new Error(String(err)))
456
- }
457
- }
458
- void run()
459
- })
460
- }
461
- }
462
-
463
- /**
464
- * 包装 WebMcpServer,使 registerTool 第三个参数支持 RouteConfig。
465
- *
466
- * - 第三个参数为**回调函数**:与原始 registerTool 完全一致,直接透传
467
- * - 第三个参数为 **RouteConfig 对象**:自动生成转发 handler,工具调用时
468
- * 先导航到目标路由,再通过 postMessage 与页面通信
469
- */
470
- export function withPageTools(server: WebMcpServer): PageAwareServer {
471
- const proxyRegisteredTools = new Map<string, RegisteredTool>()
472
-
473
- const unregisterByName = (target: WebMcpServer, name: string, silent = false): boolean => {
474
- const existing = proxyRegisteredTools.get(name)
475
- const hadTrackedState = !!existing
476
-
477
- proxyRegisteredTools.delete(name)
478
-
479
- if (existing) {
480
- try {
481
- existing.remove()
482
- } catch {
483
- // ignore
484
- }
485
- }
486
-
487
- if (!silent && hadTrackedState) {
488
- notifyServerToolListChanged(target)
489
- broadcastToolChange(MSG_TOOL_UNREGISTERED)
490
- }
491
- return !!existing
492
- }
493
-
494
- return new Proxy(server, {
495
- get(target, prop, receiver) {
496
- if (prop === 'unregisterTool') {
497
- return (name: string) => unregisterByName(target, name, false)
498
- }
499
- if (prop === 'registerTool') {
500
- return (name: string, config: any, handlerOrRoute: ((...args: any[]) => any) | RouteConfig) => {
501
- // 同名工具热更新:先移除旧工具,再注册新工具,保证 remoter 始终拿到最新定义
502
- unregisterByName(target, name, true)
503
-
504
- // 第三个参数是函数 → 直接透传,行为与原始 registerTool 完全相同
505
- // 通过 (target as any) 避免 WebMcpServer.registerTool 深层泛型触发"类型实例化过深"
506
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
507
- const rawRegister = (target as any).registerTool.bind(target)
508
- if (typeof handlerOrRoute === 'function') {
509
- const registeredTool = rawRegister(name, config, handlerOrRoute)
510
- proxyRegisteredTools.set(name, registeredTool)
511
- notifyServerToolListChanged(target)
512
- broadcastToolChange(MSG_TOOL_REGISTERED)
513
- return registeredTool
514
- }
515
- // 第三个参数是路由配置对象 → 自动生成转发 handler,并记录 tool → route 映射
516
- const { route, timeout, invokeEffect } = handlerOrRoute
517
- const normalizedRoute = normalizeRoute(route)
518
- const effectConfig = resolveRuntimeEffectConfig(name, config?.title, invokeEffect)
519
- const pageHandler = buildPageHandler(name, normalizedRoute, timeout, effectConfig)
520
- const registeredTool = rawRegister(name, config, pageHandler)
521
- proxyRegisteredTools.set(name, registeredTool)
522
- notifyServerToolListChanged(target)
523
- broadcastToolChange(MSG_TOOL_REGISTERED)
524
- return registeredTool
525
- }
526
- }
527
- return Reflect.get(target, prop, receiver)
528
- }
529
- }) as unknown as PageAwareServer
530
- }
531
-
532
- /**
533
- * 在目标页面激活工具处理器(框架无关的纯 JS 函数)。
534
- *
535
- * 调用后立即:
536
- * - 将路由注册到 activePages(标记页面已激活)
537
- * - 添加 message 监听,处理来自 buildPageHandler 的工具调用
538
- * - 广播 page-ready 信号,通知正在等待导航完成的工具
539
- *
540
- * 返回 cleanup 函数,页面销毁时调用。
541
- */
542
- type PageToolHandlers = {
543
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
544
- [toolName: string]: (input: any) => Promise<any> | any
545
- }
546
-
547
- export type RegisterPageToolByHandlersOptions = {
548
- /**
549
- * 目标路由路径,与 RouteConfig.route 保持一致。
550
- * 省略时自动读取 window.location.pathname。
551
- * 当页面路由与 pathname 不一致时(如 hash 路由、子路径前缀等),需手动传入。
552
- */
553
- route?: string
554
- /**
555
- * 工具名 → 处理函数的映射表。
556
- *
557
- * 此处 handler 的 input 参数类型保留 any:
558
- * 若改为 unknown,TypeScript 函数参数逆变规则会导致用户的具名解构写法
559
- *(如 `async ({ productId }: { productId: string }) => ...`)无法通过类型检查,
560
- * 破坏现有调用方代码的开发体验。运行时输入由 MCP inputSchema 保证类型安全。
561
- */
562
- handlers: PageToolHandlers
563
- }
564
-
565
- export function registerPageTool(options: RegisterPageToolByHandlersOptions): () => void {
566
- const { route, handlers } = options
567
- const resultRoute = normalizeRoute(route ?? window.location.pathname)
568
- const toolNames = Object.keys(handlers)
569
-
570
- const handleMessage = async (event: MessageEvent) => {
571
- // 对消息携带的 route 同样规范化,避免因尾部斜杠等差异导致匹配失败
572
- if (event.source !== window || event.data?.type !== MSG_TOOL_CALL || !(event.data.toolName in handlers)) {
573
- return
574
- }
575
- const { callId, toolName, input } = event.data
576
- try {
577
- const handler = handlers[toolName]
578
- if (!handler) throw new Error(`Tool "${toolName}" handler not found.`)
579
- const result = await handler(input)
580
- window.postMessage({ type: MSG_TOOL_RESPONSE, callId, result }, window.location.origin || '*')
581
- } catch (err) {
582
- window.postMessage(
583
- {
584
- type: MSG_TOOL_RESPONSE,
585
- callId,
586
- error: err instanceof Error ? err.message : String(err)
587
- },
588
- window.location.origin || '*'
589
- )
590
- }
591
- }
592
-
593
- // 注册页面为已激活状态并广播工具注册信号(同窗口 + iframe Remoter 均能收到)
594
- activePages.set(resultRoute, new Set(toolNames))
595
- window.addEventListener('message', handleMessage)
596
- broadcastToolChange(MSG_TOOL_REGISTERED)
597
-
598
- // 返回 cleanup,由各框架在页面销毁时调用
599
- return () => {
600
- activePages.delete(resultRoute)
601
- window.removeEventListener('message', handleMessage)
602
- broadcastToolChange(MSG_TOOL_UNREGISTERED)
603
- }
604
- }
605
-
606
- /**
607
- * 建立浏览器原生或 polyfill 与 next-sdk 页面工具桥接的联系。
608
- * 若用户或第三方库直接操作 document.modelContext 进行工具注册,
609
- * 我们通过此函数进行拦截劫持,并同步到 next-sdk 的握手线路 (MSG_TOOL_REGISTERED / MSG_TOOL_UNREGISTERED),
610
- * 从而保证无论浏览器是否原生支持,均能正常完成 WebMCP 握手交互。
611
- */
612
- // 已注册工具列表(由 setupModelContextBridge 维护)
613
- // 供 content.ts 通过 window.__nextSdkRegisteredTools() 跨沙箱查询完整工具定义
614
- const _registeredTools = new Map<string, any>()
615
-
616
- export function setupModelContextBridge() {
617
- if (typeof document === 'undefined') return
618
- const doc = document as any
619
- const nativeCtx = doc.modelContext
620
-
621
- // 如果不存在或者已经被拦截过,则不处理
622
- if (!nativeCtx || nativeCtx.__isNextSdkBridgeSetup) return
623
-
624
- // 挂载全局查询函数,供 content script(隔离沙箱)访问
625
- // 注意:必须强制覆盖,不检查是否已存在。
626
- // init-webmcp.js 的 fallback polyfill 会提前注册一个基于内联 Map 的版本,
627
- // 而该版本无法感知 bridge 拦截后的注册行为,必须用 bridge 版覆盖。
628
- if (isBrowser()) {
629
- ;(window as any).__nextSdkRegisteredTools = () => Array.from(_registeredTools.values())
630
- }
631
-
632
- const originalRegisterTool = nativeCtx.registerTool?.bind(nativeCtx)
633
- const originalUnregisterTool = nativeCtx.unregisterTool?.bind(nativeCtx)
634
-
635
- if (typeof originalRegisterTool === 'function') {
636
- nativeCtx.registerTool = (config: any, options?: any) => {
637
- const name = config.name
638
- const toolConfig = { ...config } // 拷贝一份,避免修改用户原始对象
639
-
640
- // 1. 监听 AbortSignal 以同步卸载状态
641
- if (options?.signal) {
642
- options.signal.addEventListener('abort', () => {
643
- _registeredTools.delete(name)
644
- broadcastToolChange(MSG_TOOL_UNREGISTERED)
645
- })
646
- }
647
-
648
- // 2. 识别路由配置核心件:原生格式下仅从 config.routeConfig 对象识别
649
- const routeConfig: RouteConfig | null =
650
- toolConfig.routeConfig && typeof toolConfig.routeConfig === 'object' && 'route' in toolConfig.routeConfig
651
- ? (toolConfig.routeConfig as RouteConfig)
652
- : null
653
-
654
- // 3. 如果存在路由配置,且当前不在该目标页面,则包装为自动跳转的 handler
655
- if (routeConfig) {
656
- const normalizedRoute = normalizeRoute(routeConfig.route)
657
- const effectConfig = resolveRuntimeEffectConfig(name, toolConfig.title, routeConfig.invokeEffect)
658
- const pageHandler = buildPageHandler(name, normalizedRoute, routeConfig.timeout, effectConfig)
659
-
660
- // 注入跳转处理器
661
- toolConfig.execute = pageHandler
662
- // 剥离 SDK 扩展配置,确保符合原生 ToolRegistrationParams 结构
663
- delete toolConfig.routeConfig
664
- }
665
-
666
- // 4. 维护工具定义,供 content.ts 握手查询
667
- _registeredTools.set(name, {
668
- name,
669
- title: config.title,
670
- description: config.description,
671
- inputSchema: config.inputSchema
672
- })
673
-
674
- // 5. 执行底层的原生注册逻辑并广播同步
675
- try {
676
- originalRegisterTool(toolConfig, options)
677
- broadcastToolChange(MSG_TOOL_REGISTERED)
678
- } catch (err) {
679
- // 如果注册失败,撤销记录
680
- _registeredTools.delete(name)
681
- // 忽略重复注册错误
682
- }
683
- }
684
- }
685
-
686
- // 无论原生是否提供 unregisterTool,都在 bridge 侧保证此方法存在,从而向后兼容老旧的卸载写法
687
- nativeCtx.unregisterTool = (name: string) => {
688
- try {
689
- if (typeof originalUnregisterTool === 'function') {
690
- originalUnregisterTool(name)
691
- }
692
- } catch (err) {
693
- // 忽略原生注销错误
694
- } finally {
695
- // 从工具名集合中移除
696
- _registeredTools.delete(name)
697
- broadcastToolChange(MSG_TOOL_UNREGISTERED)
698
- }
699
- }
700
-
701
- nativeCtx.__isNextSdkBridgeSetup = true
702
- }