@opentiny/next-sdk 0.2.7 → 0.2.8
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 +22 -10
- package/dist/agent/AgentModelProvider.d.ts +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.dev.js +389 -20
- package/dist/index.es.js +11135 -10850
- package/dist/index.js +1998 -1713
- package/dist/index.umd.dev.js +389 -20
- package/dist/index.umd.js +217 -49
- package/dist/{page-tool-bridge.d.ts → page-tools/bridge.d.ts} +11 -42
- package/dist/page-tools/effects.d.ts +36 -0
- package/dist/remoter/createRemoter.d.ts +2 -0
- package/dist/skills/index.d.ts +2 -1
- package/dist/webagent.dev.js +31 -7
- package/dist/webagent.es.dev.js +31 -7
- package/dist/webagent.es.js +33 -17
- package/dist/webagent.js +3 -3
- package/index.ts +1 -1
- package/package.json +1 -1
- package/{page-tool-bridge.ts → page-tools/bridge.ts} +36 -95
- package/page-tools/effects.ts +343 -0
- package/remoter/createRemoter.ts +25 -11
- package/skills/index.ts +109 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* page-
|
|
2
|
+
* page-tools/bridge - Web MCP 页面工具桥接模块(框架无关)
|
|
3
3
|
*
|
|
4
4
|
* 解决 Web-MCP 工具动态加载问题:工具定义(mcp-servers/)不直接写业务逻辑,
|
|
5
5
|
* 而是通过 window.postMessage 将调用转发给目标页面,页面处理后返回结果。
|
|
@@ -10,61 +10,15 @@
|
|
|
10
10
|
* 包装 WebMcpServer,让 registerTool 第三个参数
|
|
11
11
|
* 同时支持原始回调函数和路由配置对象(RouteConfig)
|
|
12
12
|
* - registerPageTool() 在目标页面激活工具处理器,返回 cleanup 函数
|
|
13
|
-
*
|
|
14
|
-
* 使用方式:
|
|
15
|
-
* // mcp-servers/index.ts
|
|
16
|
-
* const server = withPageTools(new WebMcpServer())
|
|
17
|
-
*
|
|
18
|
-
* // mcp-servers/product-guide/tools.ts
|
|
19
|
-
* server.registerTool('product-guide', { title, description, inputSchema },
|
|
20
|
-
* { route: '/comprehensive', timeout: 15000 } // ← 路由配置:route 必填,timeout 可选(ms,默认 30000)
|
|
21
|
-
* )
|
|
22
|
-
* // 或仍然使用普通回调(完全兼容)
|
|
23
|
-
* server.registerTool('simple-tool', { ... }, async (input) => { ... })
|
|
24
|
-
*
|
|
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 }) })
|
|
31
|
-
* onUnmounted(() => cleanup())
|
|
32
|
-
*
|
|
33
|
-
* // 目标页面(React)
|
|
34
|
-
* useEffect(() => registerPageTool({ handlers }), [])
|
|
35
|
-
*
|
|
36
|
-
* // 目标页面(Angular)
|
|
37
|
-
* export class MyComponent implements OnInit, OnDestroy {
|
|
38
|
-
* private cleanupPageTool!: () => void
|
|
39
|
-
* ngOnInit() { this.cleanupPageTool = registerPageTool({ handlers }) }
|
|
40
|
-
* ngOnDestroy() { this.cleanupPageTool() }
|
|
41
|
-
* }
|
|
42
|
-
*
|
|
43
|
-
* setNavigator 在不同框架中的注册方式:
|
|
44
|
-
* // Vue(main.ts)
|
|
45
|
-
* const router = createRouter(...)
|
|
46
|
-
* app.use(router)
|
|
47
|
-
* setNavigator((route) => router.push(route))
|
|
48
|
-
*
|
|
49
|
-
* // React(App.tsx,使用 react-router-dom)
|
|
50
|
-
* function AppNavigator() {
|
|
51
|
-
* const navigate = useNavigate()
|
|
52
|
-
* useEffect(() => { setNavigator((route) => navigate(route)) }, [navigate])
|
|
53
|
-
* return null
|
|
54
|
-
* }
|
|
55
|
-
*
|
|
56
|
-
* // Angular(AppComponent,使用 @angular/router)
|
|
57
|
-
* export class AppComponent {
|
|
58
|
-
* constructor(private router: Router) {
|
|
59
|
-
* setNavigator((route) => this.router.navigateByUrl(route))
|
|
60
|
-
* }
|
|
61
|
-
* }
|
|
62
13
|
*/
|
|
14
|
+
|
|
63
15
|
import type { ZodRawShape } from 'zod'
|
|
64
16
|
import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
65
17
|
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
|
|
66
|
-
import type { WebMcpServer } from '
|
|
67
|
-
import { randomUUID } from '
|
|
18
|
+
import type { WebMcpServer } from '../WebMcpServer'
|
|
19
|
+
import { randomUUID } from '../utils/uuid'
|
|
20
|
+
import type { ToolInvokeEffectConfig } from './effects'
|
|
21
|
+
import { hideToolInvokeEffect, resolveRuntimeEffectConfig, showToolInvokeEffect } from './effects'
|
|
68
22
|
|
|
69
23
|
// 消息类型常量,使用命名空间前缀避免冲突
|
|
70
24
|
const MSG_TOOL_CALL = 'next-sdk:tool-call'
|
|
@@ -168,8 +122,19 @@ export type RouteConfig = {
|
|
|
168
122
|
route: string
|
|
169
123
|
/** 等待页面响应的超时时间(ms),默认 30000 */
|
|
170
124
|
timeout?: number
|
|
125
|
+
/**
|
|
126
|
+
* 是否在调用该工具时启用页面级调用提示效果。
|
|
127
|
+
*
|
|
128
|
+
* - false / 未配置:不启用任何额外效果(保持现有行为)
|
|
129
|
+
* - true:使用默认提示文案(优先取工具标题,其次为工具名)
|
|
130
|
+
* - 对象:可自定义提示文案
|
|
131
|
+
*/
|
|
132
|
+
invokeEffect?: boolean | ToolInvokeEffectConfig
|
|
171
133
|
}
|
|
172
134
|
|
|
135
|
+
// 对外暴露调用提示配置类型,便于业务方在 RouteConfig 外单独复用
|
|
136
|
+
export type { ToolInvokeEffectConfig }
|
|
137
|
+
|
|
173
138
|
/**
|
|
174
139
|
* PageAwareServer 的 registerTool 配置对象类型,与 WebMcpServer.registerTool 保持一致。
|
|
175
140
|
*/
|
|
@@ -205,7 +170,12 @@ export type PageAwareServer = Omit<WebMcpServer, 'registerTool'> & {
|
|
|
205
170
|
* 2. 若未激活 → 调用导航函数跳转,等待 page-ready 信号后再发送
|
|
206
171
|
* 3. 页面处理后回传结果,Promise resolve
|
|
207
172
|
*/
|
|
208
|
-
function buildPageHandler(
|
|
173
|
+
function buildPageHandler(
|
|
174
|
+
name: string,
|
|
175
|
+
route: string,
|
|
176
|
+
timeout = 30000,
|
|
177
|
+
effectConfig?: ReturnType<typeof resolveRuntimeEffectConfig>
|
|
178
|
+
) {
|
|
209
179
|
return (input: any): Promise<any> => {
|
|
210
180
|
const callId = randomUUID()
|
|
211
181
|
|
|
@@ -220,6 +190,10 @@ function buildPageHandler(name: string, route: string, timeout = 30000) {
|
|
|
220
190
|
if (readyHandler) {
|
|
221
191
|
window.removeEventListener('message', readyHandler)
|
|
222
192
|
}
|
|
193
|
+
// 工具调用完成(成功 / 失败 / 超时 / 导航异常)后,无论结果如何都需要关闭调用提示效果
|
|
194
|
+
if (effectConfig) {
|
|
195
|
+
hideToolInvokeEffect()
|
|
196
|
+
}
|
|
223
197
|
}
|
|
224
198
|
|
|
225
199
|
// 超时兜底,防止页面永远不响应
|
|
@@ -255,6 +229,11 @@ function buildPageHandler(name: string, route: string, timeout = 30000) {
|
|
|
255
229
|
// 导航失败时显式 reject,防止外层 Promise 永远挂起。
|
|
256
230
|
const run = async () => {
|
|
257
231
|
try {
|
|
232
|
+
// 一旦真正发起工具调用(无论页面是否已激活),优先开启页面调用提示效果
|
|
233
|
+
if (effectConfig) {
|
|
234
|
+
showToolInvokeEffect(effectConfig)
|
|
235
|
+
}
|
|
236
|
+
|
|
258
237
|
if (activePages.get(route)) {
|
|
259
238
|
// 页面已激活,直接发送
|
|
260
239
|
sendCallOnce()
|
|
@@ -302,15 +281,6 @@ function buildPageHandler(name: string, route: string, timeout = 30000) {
|
|
|
302
281
|
* - 第三个参数为**回调函数**:与原始 registerTool 完全一致,直接透传
|
|
303
282
|
* - 第三个参数为 **RouteConfig 对象**:自动生成转发 handler,工具调用时
|
|
304
283
|
* 先导航到目标路由,再通过 postMessage 与页面通信
|
|
305
|
-
*
|
|
306
|
-
* @example
|
|
307
|
-
* const server = withPageTools(new WebMcpServer())
|
|
308
|
-
*
|
|
309
|
-
* // 路由模式:第三个参数传路由配置(route 必填,timeout 可选,单位 ms,默认 30000)
|
|
310
|
-
* server.registerTool('product-guide', { title, inputSchema }, { route: '/comprehensive', timeout: 15000 })
|
|
311
|
-
*
|
|
312
|
-
* // 普通模式:第三个参数传回调(兼容原有写法)
|
|
313
|
-
* server.registerTool('simple-tool', { title }, async (input) => ({ content: [...] }))
|
|
314
284
|
*/
|
|
315
285
|
export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
316
286
|
return new Proxy(server, {
|
|
@@ -325,9 +295,10 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
325
295
|
return rawRegister(name, config, handlerOrRoute)
|
|
326
296
|
}
|
|
327
297
|
// 第三个参数是路由配置对象 → 自动生成转发 handler,并记录 tool → route 映射
|
|
328
|
-
const { route, timeout } = handlerOrRoute
|
|
298
|
+
const { route, timeout, invokeEffect } = handlerOrRoute
|
|
329
299
|
toolRouteMap.set(name, route)
|
|
330
|
-
|
|
300
|
+
const effectConfig = resolveRuntimeEffectConfig(name, config?.title, invokeEffect)
|
|
301
|
+
return rawRegister(name, config, buildPageHandler(name, route, timeout, effectConfig))
|
|
331
302
|
}
|
|
332
303
|
}
|
|
333
304
|
return Reflect.get(target, prop, receiver)
|
|
@@ -344,37 +315,6 @@ export function withPageTools(server: WebMcpServer): PageAwareServer {
|
|
|
344
315
|
* - 广播 page-ready 信号,通知正在等待导航完成的工具
|
|
345
316
|
*
|
|
346
317
|
* 返回 cleanup 函数,页面销毁时调用。
|
|
347
|
-
*
|
|
348
|
-
* @example
|
|
349
|
-
* // Vue(Composition API)— 省略 route,默认读 window.location.pathname
|
|
350
|
-
* let cleanup: () => void
|
|
351
|
-
* onMounted(() => { cleanup = registerPageTool({ handlers: { ... } }) })
|
|
352
|
-
* onUnmounted(() => cleanup())
|
|
353
|
-
*
|
|
354
|
-
* // Vue — 当页面路由与 pathname 不一致时,手动指定 route
|
|
355
|
-
* onMounted(() => { cleanup = registerPageTool({ route: '/comprehensive', handlers: { ... } }) })
|
|
356
|
-
* onUnmounted(() => cleanup())
|
|
357
|
-
*
|
|
358
|
-
* // React(Hooks)
|
|
359
|
-
* useEffect(() => registerPageTool({ handlers: { ... } }), [])
|
|
360
|
-
* // useEffect 直接返回 cleanup 函数,React 会在组件卸载时自动调用
|
|
361
|
-
*
|
|
362
|
-
* // Angular(实现 OnInit / OnDestroy 接口)
|
|
363
|
-
* export class PriceProtectionComponent implements OnInit, OnDestroy {
|
|
364
|
-
* private cleanupPageTool!: () => void
|
|
365
|
-
*
|
|
366
|
-
* ngOnInit(): void {
|
|
367
|
-
* this.cleanupPageTool = registerPageTool({
|
|
368
|
-
* handlers: {
|
|
369
|
-
* 'price-protection-query': async ({ status }) => { ... },
|
|
370
|
-
* }
|
|
371
|
-
* })
|
|
372
|
-
* }
|
|
373
|
-
*
|
|
374
|
-
* ngOnDestroy(): void {
|
|
375
|
-
* this.cleanupPageTool()
|
|
376
|
-
* }
|
|
377
|
-
* }
|
|
378
318
|
*/
|
|
379
319
|
export function registerPageTool(options: {
|
|
380
320
|
/**
|
|
@@ -439,3 +379,4 @@ export function registerPageTool(options: {
|
|
|
439
379
|
broadcastRouteChange(MSG_PAGE_LEAVE, route)
|
|
440
380
|
}
|
|
441
381
|
}
|
|
382
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page-tools/effects - 页面工具调用提示效果模块(框架无关)
|
|
3
|
+
*
|
|
4
|
+
* 作用:
|
|
5
|
+
* - 在调用通过 withPageTools 注册的页面工具时,显示页面级的调用状态提示
|
|
6
|
+
* - 以左下角小 tip 形式展示“当前正在调用的工具”文案,尽量不打扰用户操作
|
|
7
|
+
* - 纯 DOM + CSS 实现,不依赖 Vue/React 等框架
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ToolInvokeEffectConfig = {
|
|
11
|
+
/**
|
|
12
|
+
* 自定义提示文案,默认使用“工具标题 || 工具名称”
|
|
13
|
+
* 例如:"正在为你整理订单数据"
|
|
14
|
+
*/
|
|
15
|
+
label?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RuntimeEffectConfig = {
|
|
19
|
+
label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let overlayElement: HTMLDivElement | null = null
|
|
23
|
+
let labelElement: HTMLDivElement | null = null
|
|
24
|
+
let styleElement: HTMLStyleElement | null = null
|
|
25
|
+
let activeCount = 0
|
|
26
|
+
|
|
27
|
+
const BODY_GLOW_CLASS = 'next-sdk-tool-body-glow'
|
|
28
|
+
|
|
29
|
+
function ensureDomReady() {
|
|
30
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureStyleElement() {
|
|
34
|
+
if (!ensureDomReady()) return
|
|
35
|
+
if (styleElement) return
|
|
36
|
+
|
|
37
|
+
const style = document.createElement('style')
|
|
38
|
+
style.textContent = `
|
|
39
|
+
.${BODY_GLOW_CLASS} {
|
|
40
|
+
position: relative;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.next-sdk-tool-overlay {
|
|
44
|
+
position: fixed;
|
|
45
|
+
inset: 0;
|
|
46
|
+
z-index: 999999;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: flex-end;
|
|
50
|
+
justify-content: flex-start;
|
|
51
|
+
padding: 0 0 18px 18px;
|
|
52
|
+
background: transparent;
|
|
53
|
+
animation: next-sdk-overlay-fade-in 260ms ease-out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.next-sdk-tool-overlay--exit {
|
|
57
|
+
animation: next-sdk-overlay-fade-out 220ms ease-in forwards;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.next-sdk-tool-overlay__glow-ring {
|
|
61
|
+
display: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.next-sdk-tool-overlay__panel {
|
|
65
|
+
position: relative;
|
|
66
|
+
min-width: min(320px, 78vw);
|
|
67
|
+
max-width: min(420px, 82vw);
|
|
68
|
+
padding: 10px 14px;
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
background:
|
|
71
|
+
linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(17, 24, 39, 0.9)),
|
|
72
|
+
radial-gradient(circle at top left, rgba(96, 165, 250, 0.25), transparent 55%),
|
|
73
|
+
radial-gradient(circle at bottom right, rgba(45, 212, 191, 0.22), transparent 60%);
|
|
74
|
+
box-shadow:
|
|
75
|
+
0 12px 28px rgba(15, 23, 42, 0.78),
|
|
76
|
+
0 0 0 1px rgba(148, 163, 184, 0.26);
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
transform-origin: center;
|
|
82
|
+
animation: next-sdk-panel-pop-in 260ms cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
|
83
|
+
color: #e5e7eb;
|
|
84
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.next-sdk-tool-overlay__indicator {
|
|
88
|
+
width: 26px;
|
|
89
|
+
height: 26px;
|
|
90
|
+
border-radius: 999px;
|
|
91
|
+
background: radial-gradient(circle at 30% 10%, #f9fafb, #93c5fd);
|
|
92
|
+
box-shadow:
|
|
93
|
+
0 0 0 1px rgba(191, 219, 254, 0.6),
|
|
94
|
+
0 8px 18px rgba(37, 99, 235, 0.8),
|
|
95
|
+
0 0 28px rgba(56, 189, 248, 0.9);
|
|
96
|
+
position: relative;
|
|
97
|
+
flex-shrink: 0;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.next-sdk-tool-overlay__indicator-orbit {
|
|
105
|
+
position: absolute;
|
|
106
|
+
inset: 2px;
|
|
107
|
+
border-radius: inherit;
|
|
108
|
+
border: 1px solid rgba(248, 250, 252, 0.6);
|
|
109
|
+
box-sizing: border-box;
|
|
110
|
+
opacity: 0.9;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.next-sdk-tool-overlay__indicator-orbit::before {
|
|
114
|
+
content: '';
|
|
115
|
+
position: absolute;
|
|
116
|
+
width: 6px;
|
|
117
|
+
height: 6px;
|
|
118
|
+
border-radius: 999px;
|
|
119
|
+
background: #f9fafb;
|
|
120
|
+
box-shadow:
|
|
121
|
+
0 0 12px rgba(248, 250, 252, 0.9),
|
|
122
|
+
0 0 24px rgba(250, 249, 246, 0.9);
|
|
123
|
+
top: 0;
|
|
124
|
+
left: 50%;
|
|
125
|
+
transform: translate(-50%, -50%);
|
|
126
|
+
transform-origin: 50% 18px;
|
|
127
|
+
animation: next-sdk-indicator-orbit 1.4s linear infinite;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.next-sdk-tool-overlay__indicator-core {
|
|
131
|
+
width: 14px;
|
|
132
|
+
height: 14px;
|
|
133
|
+
border-radius: inherit;
|
|
134
|
+
background: radial-gradient(circle at 30% 20%, #f9fafb, #bfdbfe);
|
|
135
|
+
box-shadow:
|
|
136
|
+
0 0 12px rgba(248, 250, 252, 0.9),
|
|
137
|
+
0 0 32px rgba(191, 219, 254, 0.8);
|
|
138
|
+
opacity: 0.96;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.next-sdk-tool-overlay__content {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
gap: 1px;
|
|
145
|
+
min-width: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.next-sdk-tool-overlay__title {
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
letter-spacing: 0.08em;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
color: rgba(156, 163, 175, 0.96);
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.next-sdk-tool-overlay__title-dot {
|
|
159
|
+
width: 6px;
|
|
160
|
+
height: 6px;
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
background: #22c55e;
|
|
163
|
+
box-shadow:
|
|
164
|
+
0 0 8px rgba(34, 197, 94, 0.9),
|
|
165
|
+
0 0 14px rgba(22, 163, 74, 0.9);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.next-sdk-tool-overlay__label {
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
color: #e5e7eb;
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
text-overflow: ellipsis;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@keyframes next-sdk-overlay-fade-in {
|
|
178
|
+
from { opacity: 0; }
|
|
179
|
+
to { opacity: 1; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyframes next-sdk-overlay-fade-out {
|
|
183
|
+
from { opacity: 1; }
|
|
184
|
+
to { opacity: 0; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes next-sdk-indicator-orbit {
|
|
188
|
+
from {
|
|
189
|
+
transform: translate(-50%, -50%) rotate(0deg) translateY(0);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
transform: translate(-50%, -50%) rotate(360deg) translateY(0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes next-sdk-panel-pop-in {
|
|
197
|
+
0% {
|
|
198
|
+
opacity: 0;
|
|
199
|
+
transform: scale(0.92) translateY(10px);
|
|
200
|
+
}
|
|
201
|
+
100% {
|
|
202
|
+
opacity: 1;
|
|
203
|
+
transform: scale(1) translateY(0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`
|
|
207
|
+
|
|
208
|
+
document.head.appendChild(style)
|
|
209
|
+
styleElement = style
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureOverlayElement() {
|
|
213
|
+
if (!ensureDomReady()) return
|
|
214
|
+
if (overlayElement) return
|
|
215
|
+
|
|
216
|
+
ensureStyleElement()
|
|
217
|
+
|
|
218
|
+
const overlay = document.createElement('div')
|
|
219
|
+
overlay.className = 'next-sdk-tool-overlay'
|
|
220
|
+
|
|
221
|
+
const glowRing = document.createElement('div')
|
|
222
|
+
glowRing.className = 'next-sdk-tool-overlay__glow-ring'
|
|
223
|
+
|
|
224
|
+
const panel = document.createElement('div')
|
|
225
|
+
panel.className = 'next-sdk-tool-overlay__panel'
|
|
226
|
+
|
|
227
|
+
const indicator = document.createElement('div')
|
|
228
|
+
indicator.className = 'next-sdk-tool-overlay__indicator'
|
|
229
|
+
|
|
230
|
+
const indicatorOrbit = document.createElement('div')
|
|
231
|
+
indicatorOrbit.className = 'next-sdk-tool-overlay__indicator-orbit'
|
|
232
|
+
|
|
233
|
+
const indicatorCore = document.createElement('div')
|
|
234
|
+
indicatorCore.className = 'next-sdk-tool-overlay__indicator-core'
|
|
235
|
+
|
|
236
|
+
const content = document.createElement('div')
|
|
237
|
+
content.className = 'next-sdk-tool-overlay__content'
|
|
238
|
+
|
|
239
|
+
const titleRow = document.createElement('div')
|
|
240
|
+
titleRow.className = 'next-sdk-tool-overlay__title'
|
|
241
|
+
titleRow.textContent = 'AI 正在调用页面工具'
|
|
242
|
+
|
|
243
|
+
const titleDot = document.createElement('span')
|
|
244
|
+
titleDot.className = 'next-sdk-tool-overlay__title-dot'
|
|
245
|
+
|
|
246
|
+
const label = document.createElement('div')
|
|
247
|
+
label.className = 'next-sdk-tool-overlay__label'
|
|
248
|
+
|
|
249
|
+
titleRow.prepend(titleDot)
|
|
250
|
+
content.appendChild(titleRow)
|
|
251
|
+
content.appendChild(label)
|
|
252
|
+
|
|
253
|
+
indicator.appendChild(indicatorOrbit)
|
|
254
|
+
indicator.appendChild(indicatorCore)
|
|
255
|
+
panel.appendChild(indicator)
|
|
256
|
+
panel.appendChild(content)
|
|
257
|
+
overlay.appendChild(glowRing)
|
|
258
|
+
overlay.appendChild(panel)
|
|
259
|
+
|
|
260
|
+
document.body.appendChild(overlay)
|
|
261
|
+
|
|
262
|
+
overlayElement = overlay
|
|
263
|
+
labelElement = label
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function updateOverlay(config: RuntimeEffectConfig) {
|
|
267
|
+
if (!ensureDomReady()) return
|
|
268
|
+
ensureOverlayElement()
|
|
269
|
+
if (!overlayElement || !labelElement) return
|
|
270
|
+
overlayElement.classList.remove('next-sdk-tool-overlay--exit')
|
|
271
|
+
labelElement.textContent = config.label
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function removeOverlayWithAnimation() {
|
|
275
|
+
if (!overlayElement) return
|
|
276
|
+
overlayElement.classList.add('next-sdk-tool-overlay--exit')
|
|
277
|
+
const localOverlay = overlayElement
|
|
278
|
+
let handled = false
|
|
279
|
+
let timerId: ReturnType<typeof setTimeout> | undefined
|
|
280
|
+
const handle = () => {
|
|
281
|
+
if (handled) return
|
|
282
|
+
handled = true
|
|
283
|
+
if (timerId !== undefined) {
|
|
284
|
+
clearTimeout(timerId)
|
|
285
|
+
timerId = undefined
|
|
286
|
+
}
|
|
287
|
+
if (localOverlay.parentNode) {
|
|
288
|
+
localOverlay.parentNode.removeChild(localOverlay)
|
|
289
|
+
}
|
|
290
|
+
if (overlayElement === localOverlay) {
|
|
291
|
+
overlayElement = null
|
|
292
|
+
labelElement = null
|
|
293
|
+
}
|
|
294
|
+
localOverlay.removeEventListener('animationend', handle)
|
|
295
|
+
}
|
|
296
|
+
localOverlay.addEventListener('animationend', handle)
|
|
297
|
+
// 兜底:若 animationend 未触发(如 prefers-reduced-motion 或样式被覆盖),确保清理
|
|
298
|
+
timerId = setTimeout(handle, 500)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 在页面上显示工具调用提示效果。
|
|
303
|
+
* - 若当前已有其他工具在执行,仅更新文案,不重复创建 DOM
|
|
304
|
+
* - 会增加 activeCount 计数,需配对调用 hideToolInvokeEffect
|
|
305
|
+
*/
|
|
306
|
+
export function showToolInvokeEffect(config: RuntimeEffectConfig) {
|
|
307
|
+
if (!ensureDomReady()) return
|
|
308
|
+
activeCount += 1
|
|
309
|
+
updateOverlay(config)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 隐藏工具调用提示效果。
|
|
314
|
+
* - 使用引用计数:只有当所有调用结束后才真正移除 Overlay
|
|
315
|
+
*/
|
|
316
|
+
export function hideToolInvokeEffect() {
|
|
317
|
+
if (!ensureDomReady() || activeCount <= 0) return
|
|
318
|
+
activeCount -= 1
|
|
319
|
+
if (activeCount === 0) {
|
|
320
|
+
removeOverlayWithAnimation()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 将 RouteConfig 中的 invokeEffect 配置编译成运行时效果配置。
|
|
326
|
+
* - boolean / undefined:关闭或开启默认文案
|
|
327
|
+
* - 对象:允许自定义 label
|
|
328
|
+
*/
|
|
329
|
+
export function resolveRuntimeEffectConfig(
|
|
330
|
+
toolName: string,
|
|
331
|
+
toolTitle: string | undefined,
|
|
332
|
+
value: boolean | ToolInvokeEffectConfig | undefined
|
|
333
|
+
): RuntimeEffectConfig | undefined {
|
|
334
|
+
if (!value) return undefined
|
|
335
|
+
const baseLabel = toolTitle || toolName
|
|
336
|
+
if (typeof value === 'boolean') {
|
|
337
|
+
return value ? { label: baseLabel } : undefined
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
label: value.label || baseLabel
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
package/remoter/createRemoter.ts
CHANGED
|
@@ -159,18 +159,17 @@ class FloatingBlock {
|
|
|
159
159
|
return getDefaultMenuItems(this.options)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
return getDefaultMenuItems(this.options)
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
show: userItem.show !== undefined ? userItem.show : defaultItem.show
|
|
170
|
-
}
|
|
162
|
+
return getDefaultMenuItems(this.options).map((defaultItem) => {
|
|
163
|
+
const userItem = userMenuItems.find((item) => item.action === defaultItem.action)
|
|
164
|
+
if (userItem) {
|
|
165
|
+
return {
|
|
166
|
+
...defaultItem,
|
|
167
|
+
...userItem,
|
|
168
|
+
show: userItem.show !== undefined ? userItem.show : defaultItem.show
|
|
171
169
|
}
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
}
|
|
171
|
+
return defaultItem
|
|
172
|
+
})
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
private init(): void {
|
|
@@ -913,6 +912,21 @@ class FloatingBlock {
|
|
|
913
912
|
this.dropdownMenu.parentNode.removeChild(this.dropdownMenu)
|
|
914
913
|
}
|
|
915
914
|
}
|
|
915
|
+
|
|
916
|
+
// 隐藏组件
|
|
917
|
+
public hide(): void {
|
|
918
|
+
if (this.floatingBlock) {
|
|
919
|
+
this.floatingBlock.style.display = 'none'
|
|
920
|
+
}
|
|
921
|
+
this.closeDropdown()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 显示组件
|
|
925
|
+
public show(): void {
|
|
926
|
+
if (this.floatingBlock) {
|
|
927
|
+
this.floatingBlock.style.display = 'flex'
|
|
928
|
+
}
|
|
929
|
+
}
|
|
916
930
|
}
|
|
917
931
|
|
|
918
932
|
// 导出组件
|