@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.
@@ -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({ route: '/comprehensive', handlers: { ... } }), [])
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentiny/next-sdk",
3
- "version": "0.2.6-beta.0",
3
+ "version": "0.2.6-beta.2",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.opentiny.design/next-sdk/guide/",
6
6
  "repository": {
@@ -22,17 +22,21 @@
22
22
  * // 或仍然使用普通回调(完全兼容)
23
23
  * server.registerTool('simple-tool', { ... }, async (input) => { ... })
24
24
  *
25
- * // 目标页面(Vue
26
- * onMounted(() => { cleanup = registerPageTool({ route, handlers }) })
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({ route, handlers }), [])
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({ route, handlers }) }
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 target.registerTool(name, config, handlerOrRoute as any)
322
+ return rawRegister(name, config, handlerOrRoute)
245
323
  }
246
- // 第三个参数是路由配置对象 → 自动生成转发 handler
324
+ // 第三个参数是路由配置对象 → 自动生成转发 handler,并记录 tool → route 映射
247
325
  const { route, timeout } = handlerOrRoute
248
- return target.registerTool(name, config, buildPageHandler(name, route, timeout) as any)
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({ route: '/comprehensive', handlers: { ... } }), [])
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
- const route = window.location.pathname
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
- window.postMessage({ type: MSG_PAGE_READY, route }, window.location.origin || '*')
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
  }