@opentiny/next-sdk 0.2.8 → 0.2.10
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 +10 -3
- package/agent/type.ts +3 -3
- package/dist/agent/type.d.ts +3 -0
- package/dist/index.es.dev.js +123 -25
- package/dist/index.es.js +10161 -10085
- package/dist/index.js +897 -821
- package/dist/index.umd.dev.js +123 -25
- package/dist/index.umd.js +41 -41
- package/dist/page-tools/bridge.d.ts +23 -0
- package/dist/remoter/createRemoter.d.ts +3 -2
- package/dist/webagent.dev.js +15891 -15877
- package/dist/webagent.es.dev.js +15891 -15877
- package/dist/webagent.es.js +3154 -3142
- package/dist/webagent.js +35 -35
- package/package.json +1 -1
- package/page-tools/bridge.ts +134 -0
- package/remoter/createRemoter.ts +44 -23
package/package.json
CHANGED
package/page-tools/bridge.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ZodRawShape } from 'zod'
|
|
16
|
+
import { z } from 'zod'
|
|
16
17
|
import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
17
18
|
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
|
|
18
19
|
import type { WebMcpServer } from '../WebMcpServer'
|
|
@@ -34,6 +35,9 @@ export const MSG_ROUTE_STATE_INITIAL = 'next-sdk:route-state-initial'
|
|
|
34
35
|
// 已激活页面注册表:路由路径 → 是否已挂载
|
|
35
36
|
const activePages = new Map<string, boolean>()
|
|
36
37
|
|
|
38
|
+
// 路由路径规范化:去除尾部斜杠,空路径兜底为 '/'
|
|
39
|
+
const normalizeRoute = (value: string) => value.replace(/\/+$/, '') || '/'
|
|
40
|
+
|
|
37
41
|
type BroadcastTarget = { win: Window; origin: string }
|
|
38
42
|
|
|
39
43
|
// 跨窗口广播目标:同窗口默认 [window],iframe 场景下会加入 remoter 的 contentWindow
|
|
@@ -113,6 +117,42 @@ export function setNavigator(fn: (route: string) => void | Promise<void>) {
|
|
|
113
117
|
_navigator = fn
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* 等待指定路由页面完成挂载并广播 page-ready。
|
|
122
|
+
* - 仅在浏览器环境下生效(window 存在时)
|
|
123
|
+
* - 使用与 registerPageTool 相同的路由规范化规则
|
|
124
|
+
* - 内置兜底超时,防止 Promise 永远不 resolve
|
|
125
|
+
*/
|
|
126
|
+
function waitForPageReady(path: string, timeoutMs = 1500): Promise<void> {
|
|
127
|
+
if (typeof window === 'undefined') {
|
|
128
|
+
return Promise.resolve()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const target = normalizeRoute(path)
|
|
132
|
+
|
|
133
|
+
return new Promise<void>((resolve) => {
|
|
134
|
+
let done = false
|
|
135
|
+
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
if (done) return
|
|
138
|
+
done = true
|
|
139
|
+
window.removeEventListener('message', handleMessage)
|
|
140
|
+
resolve()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const handleMessage = (event: MessageEvent) => {
|
|
144
|
+
if (event.source !== window || event.data?.type !== MSG_PAGE_READY) return
|
|
145
|
+
const route = normalizeRoute(String(event.data.route ?? ''))
|
|
146
|
+
if (route === target) {
|
|
147
|
+
cleanup()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
window.addEventListener('message', handleMessage)
|
|
152
|
+
setTimeout(cleanup, timeoutMs)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
/**
|
|
117
157
|
* registerTool 第三个参数的路由配置对象类型。
|
|
118
158
|
* 当传入此类型时,工具调用会自动跳转到 route 对应的页面并通过消息通信执行。
|
|
@@ -163,6 +203,100 @@ export type PageAwareServer = Omit<WebMcpServer, 'registerTool'> & {
|
|
|
163
203
|
): RegisteredTool
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
/**
|
|
207
|
+
* 注册一个通用的页面跳转工具(navigate_to_page),供大模型在需要时主动跳转到指定路由。
|
|
208
|
+
*
|
|
209
|
+
* 要求:
|
|
210
|
+
* - 业务侧在应用入口通过 setNavigator 注册导航函数(如 router.push 或 navigateByUrl)
|
|
211
|
+
* - 前端页面在目标路由下调用 registerPageTool,确保 page-ready 能正确广播
|
|
212
|
+
*
|
|
213
|
+
* 工具行为:
|
|
214
|
+
* - 输入 path(如 "/orders"、"/price-protection"),调用 setNavigator 注册的函数执行跳转
|
|
215
|
+
* - 等待目标页面完成挂载并广播 page-ready(或在超时时间到达时兜底返回)
|
|
216
|
+
* - 返回简单的文本说明,提示跳转结果
|
|
217
|
+
*/
|
|
218
|
+
export type NavigateToolOptions = {
|
|
219
|
+
/** 工具名称,默认 'navigate_to_page' */
|
|
220
|
+
name?: string
|
|
221
|
+
/** 工具标题,默认 '页面跳转' */
|
|
222
|
+
title?: string
|
|
223
|
+
/** 工具描述 */
|
|
224
|
+
description?: string
|
|
225
|
+
/** 等待 page-ready 的超时时间(ms),默认 1500 */
|
|
226
|
+
timeoutMs?: number
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function registerNavigateTool(server: WebMcpServer, options?: NavigateToolOptions): RegisteredTool {
|
|
230
|
+
const name = options?.name ?? 'navigate_to_page'
|
|
231
|
+
const title = options?.title ?? '页面跳转'
|
|
232
|
+
const description =
|
|
233
|
+
options?.description ??
|
|
234
|
+
'当需要的工具在当前页面不可用时,使用此工具跳转到特定页面。例如:要查询订单时跳转到 "/orders",要创建价保时跳转到 "/price-protection"。'
|
|
235
|
+
const timeoutMs = options?.timeoutMs ?? 1500
|
|
236
|
+
|
|
237
|
+
return server.registerTool(
|
|
238
|
+
name,
|
|
239
|
+
{
|
|
240
|
+
title,
|
|
241
|
+
description,
|
|
242
|
+
inputSchema: {
|
|
243
|
+
path: z.string().describe('目标页面的路由地址,例如 "/orders"、"/inventory"、"/price-protection" 等。')
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async ({ path }: { path: string }) => {
|
|
247
|
+
if (typeof window === 'undefined') {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: 'text', text: '当前环境不支持页面跳转(window 不存在)。' }]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!_navigator) {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: '页面跳转失败:尚未在应用入口调用 setNavigator 注册导航函数,无法执行路由跳转。'
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const target = normalizeRoute(path)
|
|
266
|
+
// 若当前已在目标路由上,直接返回成功,避免不必要的跳转。
|
|
267
|
+
// 兼容子路径部署(如 base: '/ai-vue/'):pathname 可能为 /ai-vue/orders,而 path 为 /orders,需判断 pathname 是否“以目标路由结尾”
|
|
268
|
+
const current = normalizeRoute(window.location.pathname)
|
|
269
|
+
const isAlreadyOnTarget =
|
|
270
|
+
current === target ||
|
|
271
|
+
(current.endsWith(target) && (current.length === target.length || current[current.lastIndexOf(target) - 1] === '/'))
|
|
272
|
+
if (isAlreadyOnTarget) {
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: 'text', text: `当前已在页面:${path}。请继续你的下一步操作。` }]
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 先注册 page-ready 监听再触发导航,避免极快导航下事件先于监听器触发而漏收(与 buildPageHandler 中的顺序一致)
|
|
279
|
+
const readyPromise = waitForPageReady(path, timeoutMs)
|
|
280
|
+
await _navigator(path)
|
|
281
|
+
await readyPromise
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: 'text', text: `已成功跳转至页面:${path}。请继续你的下一步操作。` }]
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: 'text',
|
|
291
|
+
text: `页面跳转失败:${err instanceof Error ? err.message : String(err)}。`
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
166
300
|
/**
|
|
167
301
|
* 内部:根据 name/route/timeout 生成转发给页面的 handler 函数。
|
|
168
302
|
* 调用流程:
|
package/remoter/createRemoter.ts
CHANGED
|
@@ -146,30 +146,33 @@ class FloatingBlock {
|
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* 合并菜单项配置。
|
|
149
|
-
* -
|
|
150
|
-
* -
|
|
149
|
+
* - 用户明确传入 menuItems:直接使用用户配置,不受 sessionId 限制;未传 icon 时自动补充默认图标
|
|
150
|
+
* - 有 sessionId 且未传 menuItems:使用默认菜单
|
|
151
|
+
* - 无 sessionId 且未传 menuItems:不渲染任何下拉菜单,仅保留点击浮标打开对话框的能力
|
|
151
152
|
*/
|
|
152
153
|
private mergeMenuItems(userMenuItems?: MenuItemConfig[]): MenuItemConfig[] {
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// 各 action 对应的默认图标映射
|
|
155
|
+
const defaultIcons: Partial<Record<ActionType, string>> = {
|
|
156
|
+
'qr-code': qrCode,
|
|
157
|
+
'ai-chat': chat,
|
|
158
|
+
'remote-url': link,
|
|
159
|
+
'remote-control': scan
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
// 用户明确传入了 menuItems,直接使用,并补充缺失的图标
|
|
163
|
+
if (userMenuItems) {
|
|
164
|
+
return userMenuItems.map((item) => ({
|
|
165
|
+
...item,
|
|
166
|
+
icon: item.icon ?? defaultIcons[item.action]
|
|
167
|
+
}))
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
show: userItem.show !== undefined ? userItem.show : defaultItem.show
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return defaultItem
|
|
172
|
-
})
|
|
170
|
+
// 无 sessionId 且无用户菜单:完全关闭下拉菜单,只保留点击浮标触发 onShowAIChat
|
|
171
|
+
if (!this.options.sessionId) {
|
|
172
|
+
return []
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return getDefaultMenuItems(this.options)
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
private init(): void {
|
|
@@ -220,7 +223,7 @@ class FloatingBlock {
|
|
|
220
223
|
<div class="tiny-remoter-dropdown-item__content">
|
|
221
224
|
<div title="${item.tip}">${item.text}</div>
|
|
222
225
|
<div class="tiny-remoter-dropdown-item__desc-wrapper">
|
|
223
|
-
<div class="tiny-remoter-dropdown-item__desc ${item.active ? 'tiny-remoter-dropdown-item__desc--active' : ''} ${item.know ? 'tiny-remoter-dropdown-item__desc--know' : ''}">${item.desc}</div>
|
|
226
|
+
<div class="tiny-remoter-dropdown-item__desc ${item.active ? 'tiny-remoter-dropdown-item__desc--active' : ''} ${item.know ? 'tiny-remoter-dropdown-item__desc--know' : ''}">${item.desc ?? ''}</div>
|
|
224
227
|
<div>
|
|
225
228
|
${
|
|
226
229
|
item.showCopyIcon
|
|
@@ -357,13 +360,31 @@ class FloatingBlock {
|
|
|
357
360
|
}
|
|
358
361
|
|
|
359
362
|
private copyRemoteControl(): void {
|
|
360
|
-
|
|
361
|
-
this.
|
|
363
|
+
// 优先使用用户菜单项中的 desc/text(支持自定义识别码),回退到 sessionId 末 6 位
|
|
364
|
+
const menuItem = this.menuItems.find((item) => item.action === 'remote-control')
|
|
365
|
+
const codeToCopy =
|
|
366
|
+
menuItem?.desc || menuItem?.text || (this.options.sessionId ? this.options.sessionId.slice(-6) : '')
|
|
367
|
+
if (codeToCopy) {
|
|
368
|
+
this.copyToClipboard(codeToCopy)
|
|
369
|
+
}
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
private copyRemoteURL(): void {
|
|
365
|
-
|
|
366
|
-
|
|
373
|
+
const menuItem = this.menuItems.find((item) => item.action === 'remote-url')
|
|
374
|
+
|
|
375
|
+
// 构造带 sessionId 的完整遥控链接(默认行为)
|
|
376
|
+
const sessionUrl = this.options.sessionId
|
|
377
|
+
? this.options.remoteUrl + this.sessionPrefix + this.options.sessionId
|
|
378
|
+
: ''
|
|
379
|
+
|
|
380
|
+
// 仅当 desc 是用户真正自定义的值(不同于裸 remoteUrl)时才优先使用,
|
|
381
|
+
// 否则回退到带 sessionId 的完整链接,避免默认菜单场景复制裸域名
|
|
382
|
+
const customDesc = menuItem?.desc && menuItem.desc !== this.options.remoteUrl ? menuItem.desc : undefined
|
|
383
|
+
const urlToCopy = customDesc || sessionUrl || menuItem?.text || ''
|
|
384
|
+
|
|
385
|
+
if (urlToCopy) {
|
|
386
|
+
this.copyToClipboard(urlToCopy)
|
|
387
|
+
}
|
|
367
388
|
}
|
|
368
389
|
|
|
369
390
|
// 实现复制到剪贴板功能
|