@opentiny/next-sdk 0.2.6-beta.0 → 0.2.6-beta.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/dist/index.es.dev.js +60 -6
- package/dist/index.es.js +3930 -3887
- package/dist/index.js +1500 -1457
- package/dist/index.umd.dev.js +60 -6
- package/dist/index.umd.js +39 -39
- package/dist/page-tool-bridge.d.ts +30 -3
- package/package.json +1 -1
- package/page-tool-bridge.ts +108 -16
|
@@ -3,6 +3,24 @@ import { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { WebMcpServer } from './WebMcpServer';
|
|
5
5
|
|
|
6
|
+
/** 页面卸载广播,供 routeBasedPageTools 模式监听 */
|
|
7
|
+
export declare const MSG_PAGE_LEAVE = "next-sdk:page-leave";
|
|
8
|
+
/** iframe 内 Remoter 就绪后向父窗口发送,父窗口回传 route-state-initial */
|
|
9
|
+
export declare const MSG_REMOTER_READY = "next-sdk:remoter-ready";
|
|
10
|
+
/** 父窗口向 iframe Remoter 回传的初始路由状态(toolRouteMap + activeRoutes) */
|
|
11
|
+
export declare const MSG_ROUTE_STATE_INITIAL = "next-sdk:route-state-initial";
|
|
12
|
+
/**
|
|
13
|
+
* 获取通过 withPageTools + RouteConfig 注册的全部工具路由映射。
|
|
14
|
+
* 返回的是内部 Map 的只读快照,可安全遍历。
|
|
15
|
+
* @returns toolName → route 的只读 Map
|
|
16
|
+
*/
|
|
17
|
+
export declare function getToolRouteMap(): ReadonlyMap<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* 获取当前已激活(已挂载)的路由集合。
|
|
20
|
+
* 即调用了 registerPageTool 且尚未执行 cleanup 的页面路由。
|
|
21
|
+
* @returns 当前激活路由的 Set 快照
|
|
22
|
+
*/
|
|
23
|
+
export declare function getActiveRoutes(): Set<string>;
|
|
6
24
|
/**
|
|
7
25
|
* 注册应用的导航函数,通常在应用入口(如 main.ts)调用一次。
|
|
8
26
|
* @param fn 导航函数,接收路由路径并执行跳转(如 router.push)
|
|
@@ -64,13 +82,17 @@ export declare function withPageTools(server: WebMcpServer): PageAwareServer;
|
|
|
64
82
|
* 返回 cleanup 函数,页面销毁时调用。
|
|
65
83
|
*
|
|
66
84
|
* @example
|
|
67
|
-
* // Vue(Composition API
|
|
85
|
+
* // Vue(Composition API)— 省略 route,默认读 window.location.pathname
|
|
68
86
|
* let cleanup: () => void
|
|
87
|
+
* onMounted(() => { cleanup = registerPageTool({ handlers: { ... } }) })
|
|
88
|
+
* onUnmounted(() => cleanup())
|
|
89
|
+
*
|
|
90
|
+
* // Vue — 当页面路由与 pathname 不一致时,手动指定 route
|
|
69
91
|
* onMounted(() => { cleanup = registerPageTool({ route: '/comprehensive', handlers: { ... } }) })
|
|
70
92
|
* onUnmounted(() => cleanup())
|
|
71
93
|
*
|
|
72
94
|
* // React(Hooks)
|
|
73
|
-
* useEffect(() => registerPageTool({
|
|
95
|
+
* useEffect(() => registerPageTool({ handlers: { ... } }), [])
|
|
74
96
|
* // useEffect 直接返回 cleanup 函数,React 会在组件卸载时自动调用
|
|
75
97
|
*
|
|
76
98
|
* // Angular(实现 OnInit / OnDestroy 接口)
|
|
@@ -79,7 +101,6 @@ export declare function withPageTools(server: WebMcpServer): PageAwareServer;
|
|
|
79
101
|
*
|
|
80
102
|
* ngOnInit(): void {
|
|
81
103
|
* this.cleanupPageTool = registerPageTool({
|
|
82
|
-
* route: '/price-protection',
|
|
83
104
|
* handlers: {
|
|
84
105
|
* 'price-protection-query': async ({ status }) => { ... },
|
|
85
106
|
* }
|
|
@@ -92,6 +113,12 @@ export declare function withPageTools(server: WebMcpServer): PageAwareServer;
|
|
|
92
113
|
* }
|
|
93
114
|
*/
|
|
94
115
|
export declare function registerPageTool(options: {
|
|
116
|
+
/**
|
|
117
|
+
* 目标路由路径,与 RouteConfig.route 保持一致。
|
|
118
|
+
* 省略时自动读取 window.location.pathname。
|
|
119
|
+
* 当页面路由与 pathname 不一致时(如 hash 路由、子路径前缀等),需手动传入。
|
|
120
|
+
*/
|
|
121
|
+
route?: string;
|
|
95
122
|
/**
|
|
96
123
|
* 工具名 → 处理函数的映射表。
|
|
97
124
|
*
|
package/package.json
CHANGED
package/page-tool-bridge.ts
CHANGED
|
@@ -22,17 +22,21 @@
|
|
|
22
22
|
* // 或仍然使用普通回调(完全兼容)
|
|
23
23
|
* server.registerTool('simple-tool', { ... }, async (input) => { ... })
|
|
24
24
|
*
|
|
25
|
-
* // 目标页面(Vue
|
|
26
|
-
* onMounted(() => { cleanup = registerPageTool({
|
|
25
|
+
* // 目标页面(Vue)— route 可省略,默认取 window.location.pathname
|
|
26
|
+
* onMounted(() => { cleanup = registerPageTool({ handlers }) })
|
|
27
|
+
* onUnmounted(() => cleanup())
|
|
28
|
+
*
|
|
29
|
+
* // 目标页面(Vue)— 当页面路由与 pathname 不一致时,手动指定 route
|
|
30
|
+
* onMounted(() => { cleanup = registerPageTool({ route: '/my-page', handlers }) })
|
|
27
31
|
* onUnmounted(() => cleanup())
|
|
28
32
|
*
|
|
29
33
|
* // 目标页面(React)
|
|
30
|
-
* useEffect(() => registerPageTool({
|
|
34
|
+
* useEffect(() => registerPageTool({ handlers }), [])
|
|
31
35
|
*
|
|
32
36
|
* // 目标页面(Angular)
|
|
33
37
|
* export class MyComponent implements OnInit, OnDestroy {
|
|
34
38
|
* private cleanupPageTool!: () => void
|
|
35
|
-
* ngOnInit() { this.cleanupPageTool = registerPageTool({
|
|
39
|
+
* ngOnInit() { this.cleanupPageTool = registerPageTool({ handlers }) }
|
|
36
40
|
* ngOnDestroy() { this.cleanupPageTool() }
|
|
37
41
|
* }
|
|
38
42
|
*
|
|
@@ -66,10 +70,81 @@ import { randomUUID } from './utils/uuid'
|
|
|
66
70
|
const MSG_TOOL_CALL = 'next-sdk:tool-call'
|
|
67
71
|
const MSG_TOOL_RESPONSE = 'next-sdk:tool-response'
|
|
68
72
|
const MSG_PAGE_READY = 'next-sdk:page-ready'
|
|
73
|
+
/** 页面卸载广播,供 routeBasedPageTools 模式监听 */
|
|
74
|
+
export const MSG_PAGE_LEAVE = 'next-sdk:page-leave'
|
|
75
|
+
/** iframe 内 Remoter 就绪后向父窗口发送,父窗口回传 route-state-initial */
|
|
76
|
+
export const MSG_REMOTER_READY = 'next-sdk:remoter-ready'
|
|
77
|
+
/** 父窗口向 iframe Remoter 回传的初始路由状态(toolRouteMap + activeRoutes) */
|
|
78
|
+
export const MSG_ROUTE_STATE_INITIAL = 'next-sdk:route-state-initial'
|
|
69
79
|
|
|
70
80
|
// 已激活页面注册表:路由路径 → 是否已挂载
|
|
71
81
|
const activePages = new Map<string, boolean>()
|
|
72
82
|
|
|
83
|
+
// 跨窗口广播目标:同窗口默认 [window],iframe 场景下会加入 remoter 的 contentWindow
|
|
84
|
+
const broadcastTargets = new Set<Window>()
|
|
85
|
+
|
|
86
|
+
function initBroadcastTargets() {
|
|
87
|
+
if (typeof window !== 'undefined') {
|
|
88
|
+
broadcastTargets.add(window)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
initBroadcastTargets()
|
|
92
|
+
|
|
93
|
+
/** 向所有广播目标发送路由变更消息(同窗口 + iframe 均能收到) */
|
|
94
|
+
function broadcastRouteChange(type: string, route: string) {
|
|
95
|
+
const msg = { type, route }
|
|
96
|
+
const origin = window.location.origin || '*'
|
|
97
|
+
broadcastTargets.forEach((target) => {
|
|
98
|
+
try {
|
|
99
|
+
target.postMessage(msg, origin)
|
|
100
|
+
} catch {
|
|
101
|
+
// 跨域 iframe 可能抛错,忽略
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 监听 iframe 内 Remoter 的 remoter-ready,回传初始路由状态并加入广播目标 */
|
|
107
|
+
function setupIframeRemoterBridge() {
|
|
108
|
+
if (typeof window === 'undefined') return
|
|
109
|
+
window.addEventListener('message', (event: MessageEvent) => {
|
|
110
|
+
if (event.data?.type !== MSG_REMOTER_READY || !event.source) return
|
|
111
|
+
const target = event.source as Window
|
|
112
|
+
broadcastTargets.add(target)
|
|
113
|
+
const payload = {
|
|
114
|
+
type: MSG_ROUTE_STATE_INITIAL,
|
|
115
|
+
toolRouteMap: Array.from(toolRouteMap.entries()),
|
|
116
|
+
activeRoutes: Array.from(activePages.keys())
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
target.postMessage(payload, window.location.origin || '*')
|
|
120
|
+
} catch {
|
|
121
|
+
// 忽略跨域错误
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
setupIframeRemoterBridge()
|
|
126
|
+
|
|
127
|
+
// withPageTools 注册的工具路由映射表:工具名 → 目标路由
|
|
128
|
+
const toolRouteMap = new Map<string, string>()
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 获取通过 withPageTools + RouteConfig 注册的全部工具路由映射。
|
|
132
|
+
* 返回的是内部 Map 的只读快照,可安全遍历。
|
|
133
|
+
* @returns toolName → route 的只读 Map
|
|
134
|
+
*/
|
|
135
|
+
export function getToolRouteMap(): ReadonlyMap<string, string> {
|
|
136
|
+
return toolRouteMap
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 获取当前已激活(已挂载)的路由集合。
|
|
141
|
+
* 即调用了 registerPageTool 且尚未执行 cleanup 的页面路由。
|
|
142
|
+
* @returns 当前激活路由的 Set 快照
|
|
143
|
+
*/
|
|
144
|
+
export function getActiveRoutes(): Set<string> {
|
|
145
|
+
return new Set(activePages.keys())
|
|
146
|
+
}
|
|
147
|
+
|
|
73
148
|
// 应用注册的导航函数,由 setNavigator 设置
|
|
74
149
|
let _navigator: ((route: string) => void | Promise<void>) | null = null
|
|
75
150
|
|
|
@@ -240,12 +315,16 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
240
315
|
if (prop === 'registerTool') {
|
|
241
316
|
return (name: string, config: any, handlerOrRoute: ((...args: any[]) => any) | RouteConfig) => {
|
|
242
317
|
// 第三个参数是函数 → 直接透传,行为与原始 registerTool 完全相同
|
|
318
|
+
// 通过 (target as any) 避免 WebMcpServer.registerTool 深层泛型触发"类型实例化过深"
|
|
319
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
320
|
+
const rawRegister = (target as any).registerTool.bind(target)
|
|
243
321
|
if (typeof handlerOrRoute === 'function') {
|
|
244
|
-
return
|
|
322
|
+
return rawRegister(name, config, handlerOrRoute)
|
|
245
323
|
}
|
|
246
|
-
// 第三个参数是路由配置对象 → 自动生成转发 handler
|
|
324
|
+
// 第三个参数是路由配置对象 → 自动生成转发 handler,并记录 tool → route 映射
|
|
247
325
|
const { route, timeout } = handlerOrRoute
|
|
248
|
-
|
|
326
|
+
toolRouteMap.set(name, route)
|
|
327
|
+
return rawRegister(name, config, buildPageHandler(name, route, timeout))
|
|
249
328
|
}
|
|
250
329
|
}
|
|
251
330
|
return Reflect.get(target, prop, receiver)
|
|
@@ -264,13 +343,17 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
264
343
|
* 返回 cleanup 函数,页面销毁时调用。
|
|
265
344
|
*
|
|
266
345
|
* @example
|
|
267
|
-
* // Vue(Composition API
|
|
346
|
+
* // Vue(Composition API)— 省略 route,默认读 window.location.pathname
|
|
268
347
|
* let cleanup: () => void
|
|
348
|
+
* onMounted(() => { cleanup = registerPageTool({ handlers: { ... } }) })
|
|
349
|
+
* onUnmounted(() => cleanup())
|
|
350
|
+
*
|
|
351
|
+
* // Vue — 当页面路由与 pathname 不一致时,手动指定 route
|
|
269
352
|
* onMounted(() => { cleanup = registerPageTool({ route: '/comprehensive', handlers: { ... } }) })
|
|
270
353
|
* onUnmounted(() => cleanup())
|
|
271
354
|
*
|
|
272
355
|
* // React(Hooks)
|
|
273
|
-
* useEffect(() => registerPageTool({
|
|
356
|
+
* useEffect(() => registerPageTool({ handlers: { ... } }), [])
|
|
274
357
|
* // useEffect 直接返回 cleanup 函数,React 会在组件卸载时自动调用
|
|
275
358
|
*
|
|
276
359
|
* // Angular(实现 OnInit / OnDestroy 接口)
|
|
@@ -279,7 +362,6 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
279
362
|
*
|
|
280
363
|
* ngOnInit(): void {
|
|
281
364
|
* this.cleanupPageTool = registerPageTool({
|
|
282
|
-
* route: '/price-protection',
|
|
283
365
|
* handlers: {
|
|
284
366
|
* 'price-protection-query': async ({ status }) => { ... },
|
|
285
367
|
* }
|
|
@@ -292,6 +374,12 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
292
374
|
* }
|
|
293
375
|
*/
|
|
294
376
|
export function registerPageTool(options: {
|
|
377
|
+
/**
|
|
378
|
+
* 目标路由路径,与 RouteConfig.route 保持一致。
|
|
379
|
+
* 省略时自动读取 window.location.pathname。
|
|
380
|
+
* 当页面路由与 pathname 不一致时(如 hash 路由、子路径前缀等),需手动传入。
|
|
381
|
+
*/
|
|
382
|
+
route?: string
|
|
295
383
|
/**
|
|
296
384
|
* 工具名 → 处理函数的映射表。
|
|
297
385
|
*
|
|
@@ -303,16 +391,19 @@ export function registerPageTool(options: {
|
|
|
303
391
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
392
|
handlers: Record<string, (input: any) => Promise<any>>
|
|
305
393
|
}): () => void {
|
|
306
|
-
const { handlers } = options
|
|
307
|
-
//
|
|
308
|
-
|
|
394
|
+
const { route: routeOption, handlers } = options
|
|
395
|
+
// 规范化路由:去除尾部斜杠,空路径兜底为 '/',确保与 buildPageHandler 侧一致
|
|
396
|
+
// 优先使用用户传入的 route,否则回退到 window.location.pathname
|
|
397
|
+
const normalizeRoute = (value: string) => value.replace(/\/+$/, '') || '/'
|
|
398
|
+
const route = normalizeRoute(routeOption ?? window.location.pathname)
|
|
309
399
|
|
|
310
400
|
const handleMessage = async (event: MessageEvent) => {
|
|
311
401
|
// 同时校验 route 字段,防止多页面注册同名工具时发生跨路由串扰
|
|
402
|
+
// 对消息携带的 route 同样规范化,避免因尾部斜杠等差异导致匹配失败
|
|
312
403
|
if (
|
|
313
404
|
event.source !== window ||
|
|
314
405
|
event.data?.type !== MSG_TOOL_CALL ||
|
|
315
|
-
event.data?.route !== route ||
|
|
406
|
+
normalizeRoute(String(event.data?.route ?? '')) !== route ||
|
|
316
407
|
!(event.data.toolName in handlers)
|
|
317
408
|
) {
|
|
318
409
|
return
|
|
@@ -333,14 +424,15 @@ export function registerPageTool(options: {
|
|
|
333
424
|
}
|
|
334
425
|
}
|
|
335
426
|
|
|
336
|
-
//
|
|
427
|
+
// 注册页面为已激活状态并广播就绪信号(同窗口 + iframe Remoter 均能收到)
|
|
337
428
|
activePages.set(route, true)
|
|
338
429
|
window.addEventListener('message', handleMessage)
|
|
339
|
-
|
|
430
|
+
broadcastRouteChange(MSG_PAGE_READY, route)
|
|
340
431
|
|
|
341
432
|
// 返回 cleanup,由各框架在页面销毁时调用
|
|
342
433
|
return () => {
|
|
343
434
|
activePages.delete(route)
|
|
344
435
|
window.removeEventListener('message', handleMessage)
|
|
436
|
+
broadcastRouteChange(MSG_PAGE_LEAVE, route)
|
|
345
437
|
}
|
|
346
438
|
}
|