@opentiny/next-sdk 0.2.6 → 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/agent/utils/generateReActPrompt.ts +36 -20
- package/dist/agent/AgentModelProvider.d.ts +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.dev.js +1138 -116
- package/dist/index.es.js +15866 -15040
- package/dist/index.js +2574 -2150
- package/dist/index.umd.dev.js +1138 -116
- package/dist/index.umd.js +254 -84
- package/dist/page-tools/bridge.d.ts +101 -0
- package/dist/page-tools/effects.d.ts +36 -0
- package/dist/remoter/createRemoter.d.ts +7 -5
- package/dist/skills/index.d.ts +7 -1
- package/dist/webagent.dev.js +586 -99
- package/dist/webagent.es.dev.js +586 -99
- package/dist/webagent.es.js +10799 -10378
- package/dist/webagent.js +76 -74
- package/index.ts +3 -0
- package/package.json +1 -1
- package/page-tools/bridge.ts +382 -0
- package/page-tools/effects.ts +343 -0
- package/remoter/createRemoter.ts +37 -16
- package/skills/index.ts +146 -19
|
@@ -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
|
@@ -39,8 +39,8 @@ export interface FloatingBlockOptions {
|
|
|
39
39
|
|
|
40
40
|
/** 遥控端页面地址,默认为: https://ai.opentiny.design/next-remoter */
|
|
41
41
|
qrCodeUrl?: string
|
|
42
|
-
/** 被遥控页面的 sessionId
|
|
43
|
-
sessionId
|
|
42
|
+
/** 被遥控页面的 sessionId;无 sessionId 时仅显示「打开对话框」菜单,不显示二维码、识别码、遥控器链接 */
|
|
43
|
+
sessionId?: string
|
|
44
44
|
/** 菜单项配置 */
|
|
45
45
|
menuItems?: MenuItemConfig[]
|
|
46
46
|
/** 遥控端页面地址,默认为: https://chat.opentiny.design */
|
|
@@ -52,11 +52,13 @@ export interface FloatingBlockOptions {
|
|
|
52
52
|
// 动作类型
|
|
53
53
|
export type ActionType = 'qr-code' | 'ai-chat' | 'remote-control' | 'remote-url'
|
|
54
54
|
|
|
55
|
+
/** 有 sessionId 时的完整菜单;无 sessionId 时仅返回 ai-chat(不依赖会话的菜单项) */
|
|
55
56
|
const getDefaultMenuItems = (options: FloatingBlockOptions): MenuItemConfig[] => {
|
|
56
|
-
|
|
57
|
+
const hasSession = !!options.sessionId
|
|
58
|
+
const baseItems: MenuItemConfig[] = [
|
|
57
59
|
{
|
|
58
60
|
action: 'qr-code',
|
|
59
|
-
show:
|
|
61
|
+
show: hasSession,
|
|
60
62
|
text: '扫码登录',
|
|
61
63
|
desc: '使用手机遥控页面',
|
|
62
64
|
icon: qrCode
|
|
@@ -70,7 +72,7 @@ const getDefaultMenuItems = (options: FloatingBlockOptions): MenuItemConfig[] =>
|
|
|
70
72
|
},
|
|
71
73
|
{
|
|
72
74
|
action: 'remote-url',
|
|
73
|
-
show:
|
|
75
|
+
show: hasSession,
|
|
74
76
|
text: `遥控器链接`,
|
|
75
77
|
desc: `${options.remoteUrl}`,
|
|
76
78
|
active: true,
|
|
@@ -80,14 +82,15 @@ const getDefaultMenuItems = (options: FloatingBlockOptions): MenuItemConfig[] =>
|
|
|
80
82
|
},
|
|
81
83
|
{
|
|
82
84
|
action: 'remote-control',
|
|
83
|
-
show:
|
|
85
|
+
show: hasSession,
|
|
84
86
|
text: `识别码`,
|
|
85
|
-
desc: `${options.sessionId
|
|
87
|
+
desc: hasSession ? `${options.sessionId!.slice(-6)}` : '',
|
|
86
88
|
know: true,
|
|
87
89
|
showCopyIcon: true,
|
|
88
90
|
icon: scan
|
|
89
91
|
}
|
|
90
92
|
]
|
|
93
|
+
return baseItems
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
class FloatingBlock {
|
|
@@ -105,10 +108,6 @@ class FloatingBlock {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
constructor(options: FloatingBlockOptions) {
|
|
108
|
-
if (!options.sessionId) {
|
|
109
|
-
throw new Error('sessionId is required')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
111
|
this.options = {
|
|
113
112
|
...options,
|
|
114
113
|
qrCodeUrl: options.qrCodeUrl || DEFAULT_QR_CODE_URL,
|
|
@@ -146,11 +145,16 @@ class FloatingBlock {
|
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
148
|
+
* 合并菜单项配置。
|
|
149
|
+
* - 有 sessionId:使用默认菜单 + 用户配置(可定制每一项的 show/text/icon 等)
|
|
150
|
+
* - 无 sessionId:不渲染任何下拉菜单,仅保留点击浮标打开对话框的能力
|
|
152
151
|
*/
|
|
153
152
|
private mergeMenuItems(userMenuItems?: MenuItemConfig[]): MenuItemConfig[] {
|
|
153
|
+
// 无 sessionId:完全关闭下拉菜单(包括 ai-chat 项),只保留点击浮标触发 onShowAIChat
|
|
154
|
+
if (!this.options.sessionId) {
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
|
|
154
158
|
if (!userMenuItems) {
|
|
155
159
|
return getDefaultMenuItems(this.options)
|
|
156
160
|
}
|
|
@@ -161,7 +165,6 @@ class FloatingBlock {
|
|
|
161
165
|
return {
|
|
162
166
|
...defaultItem,
|
|
163
167
|
...userItem,
|
|
164
|
-
// 确保show属性存在,默认为true
|
|
165
168
|
show: userItem.show !== undefined ? userItem.show : defaultItem.show
|
|
166
169
|
}
|
|
167
170
|
}
|
|
@@ -354,10 +357,12 @@ class FloatingBlock {
|
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
private copyRemoteControl(): void {
|
|
360
|
+
if (!this.options.sessionId) return
|
|
357
361
|
this.copyToClipboard(this.options.sessionId.slice(-6))
|
|
358
362
|
}
|
|
359
363
|
|
|
360
364
|
private copyRemoteURL(): void {
|
|
365
|
+
if (!this.options.sessionId) return
|
|
361
366
|
this.copyToClipboard(this.options.remoteUrl + this.sessionPrefix + this.options.sessionId)
|
|
362
367
|
}
|
|
363
368
|
|
|
@@ -417,8 +422,9 @@ class FloatingBlock {
|
|
|
417
422
|
}, 1500)
|
|
418
423
|
}
|
|
419
424
|
|
|
420
|
-
//
|
|
425
|
+
// 创建二维码弹窗(无 sessionId 时不展示)
|
|
421
426
|
private async showQRCode(): Promise<void> {
|
|
427
|
+
if (!this.options.sessionId) return
|
|
422
428
|
const qrCode = new QrCode((this.options.qrCodeUrl || '') + this.sessionPrefix + this.options.sessionId, {})
|
|
423
429
|
const base64 = await qrCode.toDataURL()
|
|
424
430
|
const modal = this.createModal(
|
|
@@ -906,6 +912,21 @@ class FloatingBlock {
|
|
|
906
912
|
this.dropdownMenu.parentNode.removeChild(this.dropdownMenu)
|
|
907
913
|
}
|
|
908
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
|
+
}
|
|
909
930
|
}
|
|
910
931
|
|
|
911
932
|
// 导出组件
|
package/skills/index.ts
CHANGED
|
@@ -42,21 +42,42 @@ export function parseSkillFrontMatter(content: string): { name: string; descript
|
|
|
42
42
|
return name && description ? { name, description } : null
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 将 Vite import.meta.glob 得到的多种 key 格式统一为「相对 skills 根目录」的路径(如 ./calculator/SKILL.md),
|
|
47
|
+
* 以便 getSkillMdContent / getMainSkillPathByName 等能正确按 path 查找。
|
|
48
|
+
* 兼容任意引入位置:./skills/xxx、../skills/xxx、src/skills/xxx 等,取最后一个 skills/ 后的部分并加上 ./
|
|
49
|
+
*/
|
|
50
|
+
function normalizeSkillModuleKeys(modules: Record<string, string>): Record<string, string> {
|
|
51
|
+
const result: Record<string, string> = {}
|
|
52
|
+
for (const [key, content] of Object.entries(modules)) {
|
|
53
|
+
const normalizedKey = key.replace(/\\/g, '/')
|
|
54
|
+
const skillsIndex = normalizedKey.lastIndexOf('skills/')
|
|
55
|
+
const relativePath = skillsIndex >= 0 ? normalizedKey.slice(skillsIndex + 7) : normalizedKey
|
|
56
|
+
const standardPath = relativePath.startsWith('./') ? relativePath : `./${relativePath}`
|
|
57
|
+
result[standardPath] = content
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
/**
|
|
46
63
|
* 获取所有「主 SKILL.md」的路径(一级子目录下的 SKILL.md)
|
|
64
|
+
* - 对传入的 modules 先做 normalize,兼容任意 import.meta.glob 写法
|
|
47
65
|
*/
|
|
48
66
|
export function getMainSkillPaths(modules: Record<string, string>): string[] {
|
|
49
|
-
|
|
67
|
+
const normalized = normalizeSkillModuleKeys(modules)
|
|
68
|
+
return Object.keys(normalized).filter((path) => MAIN_SKILL_PATH_REG.test(path))
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
53
72
|
* 获取所有技能的概况列表(name、description、path),用于 systemPrompt 或列表展示
|
|
73
|
+
* - 内部统一对 modules 做 normalize,避免调用方关心路径细节
|
|
54
74
|
*/
|
|
55
75
|
export function getSkillOverviews(modules: Record<string, string>): SkillMeta[] {
|
|
56
|
-
const
|
|
76
|
+
const normalized = normalizeSkillModuleKeys(modules)
|
|
77
|
+
const mainPaths = Object.keys(normalized).filter((path) => MAIN_SKILL_PATH_REG.test(path))
|
|
57
78
|
const list: SkillMeta[] = []
|
|
58
79
|
for (const path of mainPaths) {
|
|
59
|
-
const content =
|
|
80
|
+
const content = normalized[path]
|
|
60
81
|
if (!content) continue
|
|
61
82
|
const parsed = parseSkillFrontMatter(content)
|
|
62
83
|
if (!parsed) continue
|
|
@@ -81,23 +102,57 @@ export function formatSkillsForSystemPrompt(skills: SkillMeta[]): string {
|
|
|
81
102
|
|
|
82
103
|
/**
|
|
83
104
|
* 获取所有已加载的技能文件路径(含主 SKILL.md 与 reference 下的 .md/.json/.xml 等)
|
|
105
|
+
* - 对 modules 做 normalize 后再返回 key 列表
|
|
84
106
|
*/
|
|
85
107
|
export function getSkillMdPaths(modules: Record<string, string>): string[] {
|
|
86
|
-
|
|
108
|
+
const normalized = normalizeSkillModuleKeys(modules)
|
|
109
|
+
return Object.keys(normalized)
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
/**
|
|
90
113
|
* 根据相对路径获取某个技能文档的原始内容(支持 .md、.json、.xml 等文本格式)
|
|
114
|
+
* - 自动对 modules 做 normalize,再按 path 查找
|
|
91
115
|
*/
|
|
92
116
|
export function getSkillMdContent(modules: Record<string, string>, path: string): string | undefined {
|
|
93
|
-
|
|
117
|
+
const normalized = normalizeSkillModuleKeys(modules)
|
|
118
|
+
|
|
119
|
+
// 1. 尝试原有的严格匹配
|
|
120
|
+
const exactMatch = normalized[path]
|
|
121
|
+
if (exactMatch) return exactMatch
|
|
122
|
+
|
|
123
|
+
// 2. 降级匹配:如果严格匹配完整路径未找到
|
|
124
|
+
// 则尝试寻找后缀能够匹配上的真实文件路径。
|
|
125
|
+
// 去除开头的 '.' 或 './' 以精确匹配结尾部分的路径。
|
|
126
|
+
const suffix = path.replace(/^\.?\//, '/')
|
|
127
|
+
const matchingKey = Object.keys(normalized).find((key) => key.endsWith(suffix))
|
|
128
|
+
return matchingKey ? normalized[matchingKey] : undefined
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
/**
|
|
97
|
-
* 根据技能 name 查找其主 SKILL.md
|
|
132
|
+
* 根据技能 name 查找其主 SKILL.md 的路径
|
|
133
|
+
* 支持匹配目录名(如 ecommerce)或 SKILL.md 内 frontmatter 定义的 name
|
|
134
|
+
* - 依赖 getMainSkillPaths,内部已做 normalize
|
|
98
135
|
*/
|
|
99
136
|
export function getMainSkillPathByName(modules: Record<string, string>, name: string): string | undefined {
|
|
100
|
-
|
|
137
|
+
const normalizedModules = normalizeSkillModuleKeys(modules)
|
|
138
|
+
const paths = getMainSkillPaths(normalizedModules)
|
|
139
|
+
|
|
140
|
+
// 1. 先尝试按目录名精确匹配 (兼容老逻辑)
|
|
141
|
+
const dirMatch = paths.find((p) => p.startsWith(`./${name}/SKILL.md`))
|
|
142
|
+
if (dirMatch) return dirMatch
|
|
143
|
+
|
|
144
|
+
// 2. 如果按目录名找不到,则解析内容按 frontmatter 的 name 匹配
|
|
145
|
+
for (const p of paths) {
|
|
146
|
+
const content = normalizedModules[p]
|
|
147
|
+
if (content) {
|
|
148
|
+
const parsed = parseSkillFrontMatter(content)
|
|
149
|
+
if (parsed && parsed.name === name) {
|
|
150
|
+
return p
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return undefined
|
|
101
156
|
}
|
|
102
157
|
|
|
103
158
|
// ============ 内置工具:供 remoter 注入,替代业界 skill 中「读取文档」的操作 ============
|
|
@@ -106,32 +161,104 @@ export function getMainSkillPathByName(modules: Record<string, string>, name: st
|
|
|
106
161
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
162
|
export type SkillToolsSet = Record<string, any>
|
|
108
163
|
|
|
164
|
+
// 提升为模块级常量:避免 tool() 推断 PARAMETERS 泛型时递归展开 Zod 链导致"类型实例化过深"
|
|
165
|
+
const SKILL_INPUT_SCHEMA = z.object({
|
|
166
|
+
skillName: z
|
|
167
|
+
.string()
|
|
168
|
+
.optional()
|
|
169
|
+
.describe(
|
|
170
|
+
'进入某个技能的主入口名称。优先匹配技能的目录名(如 ecommerce),或者技能的中文名称(如"客户价保单创建及审核")。'
|
|
171
|
+
),
|
|
172
|
+
path: z
|
|
173
|
+
.string()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe('你想查阅的文档的路径。如 ./calculator/SKILL.md 或从其他文档里看到的相对路径 ./reference/inventory.md。'),
|
|
176
|
+
currentPath: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe(
|
|
180
|
+
'你当前正在阅读的文档路径(如果有)。比如你刚刚读取了 ./ecommerce/SKILL.md,请把这个路径原样传回来,这样系统才能根据你的相对路径准确找到下一份文件。'
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
109
184
|
/**
|
|
110
185
|
* 根据 skillMdModules 创建供 AI 调用的工具集
|
|
111
186
|
* - get_skill_content: 按技能名或路径获取完整文档内容,便于大模型自动识别并加载技能
|
|
112
187
|
* remoter 可将返回的 tools 合并进 extraTools 注入 agent
|
|
113
188
|
*/
|
|
114
189
|
export function createSkillTools(modules: Record<string, string>): SkillToolsSet {
|
|
190
|
+
const normalizedModules = normalizeSkillModuleKeys(modules)
|
|
191
|
+
|
|
192
|
+
// @ts-ignore ai package 的 tool() 函数类型推断存在"类型实例化过深"的已知限制,无法正确推断包含复杂 Zod 链的 schema
|
|
115
193
|
const getSkillContent = tool({
|
|
116
194
|
description:
|
|
117
|
-
'
|
|
118
|
-
inputSchema:
|
|
119
|
-
|
|
120
|
-
path:
|
|
121
|
-
}),
|
|
122
|
-
execute: (args: { skillName?: string; path?: string }) => {
|
|
123
|
-
const { skillName, path: pathArg } = args
|
|
195
|
+
'根据技能名称或文档路径获取该技能的完整文档内容。如果你想根据相对路径查阅文件,请务必同时提供你当前所在的文件路径 currentPath。',
|
|
196
|
+
inputSchema: SKILL_INPUT_SCHEMA,
|
|
197
|
+
execute: (args: { skillName?: string; path?: string; currentPath?: string }): Record<string, unknown> => {
|
|
198
|
+
const { skillName, path: pathArg, currentPath: currentPathArg } = args
|
|
124
199
|
let content: string | undefined
|
|
200
|
+
let resolvedPath = ''
|
|
201
|
+
|
|
125
202
|
if (pathArg) {
|
|
126
|
-
|
|
203
|
+
// 使用明确提供的当前阅读上下文作为基准路径(默认在根目录)
|
|
204
|
+
let basePathContext = '.'
|
|
205
|
+
if (currentPathArg) {
|
|
206
|
+
// 提取出当前文档所在的目录
|
|
207
|
+
// 比如 ./ecommerce/SKILL.md -> ./ecommerce
|
|
208
|
+
const lastSlashIndex = currentPathArg.lastIndexOf('/')
|
|
209
|
+
if (lastSlashIndex >= 0) {
|
|
210
|
+
basePathContext = currentPathArg.slice(0, lastSlashIndex)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 尝试 1:按照大模型当前提供的上下文进行标准相对路径解析
|
|
215
|
+
const dummyBase = `http://localhost/${basePathContext}/`
|
|
216
|
+
const url = new URL(pathArg, dummyBase)
|
|
217
|
+
resolvedPath = '.' + url.pathname
|
|
218
|
+
content = getSkillMdContent(normalizedModules, resolvedPath)
|
|
219
|
+
|
|
220
|
+
// 尝试 2:如果大模型忘了传正确的 currentPath,或者是强行传错,做个智能根目录回退
|
|
221
|
+
if (content === undefined && (pathArg.startsWith('./') || pathArg.startsWith('../')) && currentPathArg) {
|
|
222
|
+
const baseParts = currentPathArg.split('/')
|
|
223
|
+
if (baseParts.length >= 2) {
|
|
224
|
+
const skillRoot = baseParts[1]
|
|
225
|
+
const fallbackDummyBase = `http://localhost/${skillRoot}/`
|
|
226
|
+
const fallbackUrl = new URL(pathArg, fallbackDummyBase)
|
|
227
|
+
const fallbackPath = '.' + fallbackUrl.pathname
|
|
228
|
+
content = getSkillMdContent(normalizedModules, fallbackPath)
|
|
229
|
+
if (content) {
|
|
230
|
+
resolvedPath = fallbackPath
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 尝试 3:后缀自动降级匹配修正
|
|
236
|
+
if (content && !normalizedModules[resolvedPath]) {
|
|
237
|
+
const suffix = resolvedPath.replace(/^\.?\//, '/')
|
|
238
|
+
const matchingKey = Object.keys(normalizedModules).find((key) => key.endsWith(suffix))
|
|
239
|
+
if (matchingKey) {
|
|
240
|
+
resolvedPath = matchingKey
|
|
241
|
+
}
|
|
242
|
+
}
|
|
127
243
|
} else if (skillName) {
|
|
128
|
-
const mainPath = getMainSkillPathByName(
|
|
129
|
-
|
|
244
|
+
const mainPath = getMainSkillPathByName(normalizedModules, skillName)
|
|
245
|
+
if (mainPath) {
|
|
246
|
+
resolvedPath = mainPath
|
|
247
|
+
content = getSkillMdContent(normalizedModules, mainPath)
|
|
248
|
+
}
|
|
130
249
|
}
|
|
250
|
+
|
|
131
251
|
if (content === undefined) {
|
|
132
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
error: '未找到对应技能文档',
|
|
254
|
+
skillName,
|
|
255
|
+
path: pathArg,
|
|
256
|
+
providedCurrentPath: currentPathArg,
|
|
257
|
+
attemptedPath: resolvedPath
|
|
258
|
+
}
|
|
133
259
|
}
|
|
134
|
-
|
|
260
|
+
|
|
261
|
+
return { content, path: resolvedPath }
|
|
135
262
|
}
|
|
136
263
|
})
|
|
137
264
|
|