@_daniel_jiang/ai-host 0.1.0
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/README.md +78 -0
- package/dist/ai-host.js +262 -0
- package/dist/ai-host.mjs +1004 -0
- package/index.d.ts +14 -0
- package/package.json +32 -0
- package/src/iframe.ts +751 -0
- package/src/index.ts +410 -0
- package/src/messages.ts +102 -0
- package/src/store.ts +327 -0
- package/src/types.ts +159 -0
package/src/iframe.ts
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { AGENT_HOST_MSG, type ChildToParentMessage, type ParentToChildMessage, type WidgetPayload, cloneForPostMessage, isAgentHostMessage } from './messages'
|
|
2
|
+
import { getActionAnchorElement, getActionAnchorRect, type PrimaryPageHint } from './store'
|
|
3
|
+
|
|
4
|
+
const HUB_LAUNCHER_ID = 'agent-host-launcher'
|
|
5
|
+
const HUB_PANEL_OVERLAY_ID = 'agent-host-hub-panel-overlay'
|
|
6
|
+
const HUB_IFRAME_ID = 'agent-host-hub-iframe'
|
|
7
|
+
const WIDGET_OVERLAY_ID = 'agent-host-widget-overlay'
|
|
8
|
+
const WIDGET_IFRAME_ID = 'agent-host-widget-iframe'
|
|
9
|
+
|
|
10
|
+
const HOST_SDK_STYLE_ID = 'agent-host-ai-host-styles'
|
|
11
|
+
|
|
12
|
+
type IframeSurface = 'hub' | 'widget'
|
|
13
|
+
|
|
14
|
+
/** 保留部署子路径(iframe src),仅去掉末尾 `/` */
|
|
15
|
+
function normalizeBase(base: string): string {
|
|
16
|
+
return base.replace(/\/$/, '')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* postMessage 的 targetOrigin / event.origin 只有「协议+主机+端口」,不含路径。
|
|
21
|
+
* agent_web_origin 常为 `https://host/agent-platform`,必须先取 URL.origin 再比对。
|
|
22
|
+
*/
|
|
23
|
+
function toBrowserOrigin(value: string): string {
|
|
24
|
+
try {
|
|
25
|
+
return new URL(value).origin
|
|
26
|
+
} catch {
|
|
27
|
+
return value.replace(/\/$/, '')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** localhost / 127.0.0.1 视为等价,避免 dev 环境 origin 校验失败 */
|
|
32
|
+
function originsMatch(expected: string, actual: string): boolean {
|
|
33
|
+
const a = toBrowserOrigin(expected)
|
|
34
|
+
const b = toBrowserOrigin(actual)
|
|
35
|
+
if (a === b) return true
|
|
36
|
+
try {
|
|
37
|
+
const ua = new URL(a)
|
|
38
|
+
const ub = new URL(b)
|
|
39
|
+
const loopback = new Set(['localhost', '127.0.0.1', '[::1]'])
|
|
40
|
+
return (
|
|
41
|
+
ua.protocol === ub.protocol &&
|
|
42
|
+
ua.port === ub.port &&
|
|
43
|
+
loopback.has(ua.hostname) &&
|
|
44
|
+
loopback.has(ub.hostname)
|
|
45
|
+
)
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureHostSdkStyles() {
|
|
52
|
+
if (typeof document === 'undefined' || document.getElementById(HOST_SDK_STYLE_ID)) return
|
|
53
|
+
const style = document.createElement('style')
|
|
54
|
+
style.id = HOST_SDK_STYLE_ID
|
|
55
|
+
style.textContent = `
|
|
56
|
+
#${HUB_LAUNCHER_ID} {
|
|
57
|
+
position: fixed;
|
|
58
|
+
right: 24px;
|
|
59
|
+
bottom: 24px;
|
|
60
|
+
z-index: 2147482999;
|
|
61
|
+
width: 56px;
|
|
62
|
+
height: 56px;
|
|
63
|
+
border: none;
|
|
64
|
+
border-radius: 50%;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
color: #fff;
|
|
70
|
+
font-size: 22px;
|
|
71
|
+
line-height: 1;
|
|
72
|
+
background: linear-gradient(145deg, #12a892 0%, #0d8f7b 55%, #0a7a6a 100%);
|
|
73
|
+
box-shadow: 0 4px 18px rgba(13, 143, 123, 0.42);
|
|
74
|
+
transition: transform 0.15s ease, opacity 0.2s ease, visibility 0.2s ease;
|
|
75
|
+
}
|
|
76
|
+
#${HUB_LAUNCHER_ID}:hover { transform: scale(1.05); }
|
|
77
|
+
#${HUB_LAUNCHER_ID}.is-hidden {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
visibility: hidden;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
transform: scale(0.85);
|
|
82
|
+
}
|
|
83
|
+
#${HUB_LAUNCHER_ID}.is-pulse {
|
|
84
|
+
animation: agent-host-fab-pulse 2s ease-in-out infinite;
|
|
85
|
+
}
|
|
86
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-icon {
|
|
87
|
+
display: inline-block;
|
|
88
|
+
line-height: 1;
|
|
89
|
+
transform-origin: center center;
|
|
90
|
+
}
|
|
91
|
+
#${HUB_LAUNCHER_ID}.is-breathe .agent-host-launcher-icon {
|
|
92
|
+
animation: agent-host-clover-breathe 1.8s ease-in-out infinite;
|
|
93
|
+
}
|
|
94
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-bell {
|
|
95
|
+
display: none;
|
|
96
|
+
width: 22px;
|
|
97
|
+
height: 22px;
|
|
98
|
+
line-height: 0;
|
|
99
|
+
transform-origin: top center;
|
|
100
|
+
}
|
|
101
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-bell.is-visible {
|
|
102
|
+
display: inline-block;
|
|
103
|
+
animation: agent-host-bell-shake 0.85s ease-in-out infinite;
|
|
104
|
+
}
|
|
105
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-bell svg {
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
display: block;
|
|
109
|
+
}
|
|
110
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-badge {
|
|
111
|
+
position: absolute;
|
|
112
|
+
top: -4px;
|
|
113
|
+
right: -4px;
|
|
114
|
+
box-sizing: border-box;
|
|
115
|
+
min-width: 20px;
|
|
116
|
+
height: 20px;
|
|
117
|
+
padding: 0 5px;
|
|
118
|
+
border-radius: 999px;
|
|
119
|
+
font-size: 11px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
line-height: 1;
|
|
122
|
+
display: none;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
border: 2px solid #fff;
|
|
126
|
+
background: #f59e0b;
|
|
127
|
+
color: #fff;
|
|
128
|
+
}
|
|
129
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-badge.is-visible { display: flex; }
|
|
130
|
+
#${HUB_LAUNCHER_ID} .agent-host-launcher-badge.is-waiting { background: #ef4444; }
|
|
131
|
+
#agent-host-launcher-hint {
|
|
132
|
+
position: fixed;
|
|
133
|
+
right: 24px;
|
|
134
|
+
bottom: 88px;
|
|
135
|
+
z-index: 2147482998;
|
|
136
|
+
max-width: min(800px, calc(100vw - 48px));
|
|
137
|
+
padding: 10px 14px;
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
line-height: 1.45;
|
|
141
|
+
color: #333;
|
|
142
|
+
background: #fff;
|
|
143
|
+
border: 1px solid rgba(13, 143, 123, 0.22);
|
|
144
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
145
|
+
display: none;
|
|
146
|
+
pointer-events: auto;
|
|
147
|
+
}
|
|
148
|
+
#agent-host-launcher-hint.is-visible { display: block; }
|
|
149
|
+
#agent-host-launcher-hint.is-anchored {
|
|
150
|
+
right: auto;
|
|
151
|
+
bottom: auto;
|
|
152
|
+
}
|
|
153
|
+
#agent-host-launcher-hint .agent-host-hint-inner {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 10px;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
}
|
|
159
|
+
#agent-host-launcher-hint .agent-host-hint-icon {
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
width: 20px;
|
|
165
|
+
height: 20px;
|
|
166
|
+
font-size: 16px;
|
|
167
|
+
line-height: 1;
|
|
168
|
+
color: #0d8f7b;
|
|
169
|
+
transform-origin: center center;
|
|
170
|
+
animation: agent-host-hint-clover 4.5s ease-in-out infinite;
|
|
171
|
+
}
|
|
172
|
+
#agent-host-launcher-hint .agent-host-hint-text {
|
|
173
|
+
flex: 1;
|
|
174
|
+
min-width: 140px;
|
|
175
|
+
color: #333;
|
|
176
|
+
}
|
|
177
|
+
#agent-host-launcher-hint .agent-host-hint-actions {
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: 4px;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
}
|
|
183
|
+
#agent-host-launcher-hint .agent-host-hint-use {
|
|
184
|
+
border: 1px solid #0d8f7b;
|
|
185
|
+
border-radius: 6px;
|
|
186
|
+
padding: 0 12px;
|
|
187
|
+
height: 28px;
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
color: #0d8f7b;
|
|
191
|
+
background: transparent;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
#agent-host-launcher-hint .agent-host-hint-use:hover {
|
|
195
|
+
color: #fff;
|
|
196
|
+
background: #0d8f7b;
|
|
197
|
+
}
|
|
198
|
+
#agent-host-launcher-hint .agent-host-hint-dismiss {
|
|
199
|
+
border: none;
|
|
200
|
+
border-radius: 6px;
|
|
201
|
+
padding: 0 8px;
|
|
202
|
+
height: 28px;
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
color: #999;
|
|
205
|
+
background: transparent;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
}
|
|
208
|
+
#agent-host-launcher-hint .agent-host-hint-dismiss:hover { color: #666; }
|
|
209
|
+
#${HUB_PANEL_OVERLAY_ID} {
|
|
210
|
+
position: fixed;
|
|
211
|
+
inset: 0;
|
|
212
|
+
z-index: 2147483000;
|
|
213
|
+
pointer-events: none;
|
|
214
|
+
}
|
|
215
|
+
#${HUB_PANEL_OVERLAY_ID}.is-open {
|
|
216
|
+
pointer-events: auto;
|
|
217
|
+
}
|
|
218
|
+
#${HUB_PANEL_OVERLAY_ID} .agent-host-hub-backdrop {
|
|
219
|
+
position: absolute;
|
|
220
|
+
inset: 0;
|
|
221
|
+
background: rgba(15, 23, 42, 0.42);
|
|
222
|
+
opacity: 0;
|
|
223
|
+
transition: opacity 0.35s ease;
|
|
224
|
+
}
|
|
225
|
+
#${HUB_PANEL_OVERLAY_ID}.is-open .agent-host-hub-backdrop {
|
|
226
|
+
opacity: 1;
|
|
227
|
+
}
|
|
228
|
+
/* FAB 位圆形 → 居中工作室模态 */
|
|
229
|
+
#${HUB_PANEL_OVERLAY_ID} .agent-host-hub-panel-shell {
|
|
230
|
+
position: absolute;
|
|
231
|
+
right: 24px;
|
|
232
|
+
bottom: 24px;
|
|
233
|
+
width: 56px;
|
|
234
|
+
height: 56px;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
background: #fff;
|
|
237
|
+
box-shadow: 0 4px 18px rgba(13, 143, 123, 0.35);
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
opacity: 0;
|
|
240
|
+
transform: scale(0.92);
|
|
241
|
+
transition:
|
|
242
|
+
width 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
|
243
|
+
height 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
|
244
|
+
right 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
|
245
|
+
bottom 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
|
246
|
+
border-radius 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
|
247
|
+
box-shadow 0.42s ease,
|
|
248
|
+
opacity 0.2s ease,
|
|
249
|
+
transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
|
250
|
+
}
|
|
251
|
+
#${HUB_PANEL_OVERLAY_ID}.is-open .agent-host-hub-panel-shell {
|
|
252
|
+
right: max(24px, calc((100vw - min(1120px, calc(100vw - 48px))) / 2));
|
|
253
|
+
bottom: max(24px, calc((100vh - min(780px, calc(100vh - 48px))) / 2));
|
|
254
|
+
width: min(1120px, calc(100vw - 48px));
|
|
255
|
+
height: min(780px, calc(100vh - 48px));
|
|
256
|
+
border-radius: 16px;
|
|
257
|
+
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
|
|
258
|
+
opacity: 1;
|
|
259
|
+
transform: scale(1);
|
|
260
|
+
}
|
|
261
|
+
@media (max-width: 720px) {
|
|
262
|
+
#${HUB_PANEL_OVERLAY_ID}.is-open .agent-host-hub-panel-shell {
|
|
263
|
+
right: 12px;
|
|
264
|
+
bottom: 12px;
|
|
265
|
+
width: calc(100vw - 24px);
|
|
266
|
+
height: calc(100vh - 24px);
|
|
267
|
+
border-radius: 12px;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
#${HUB_PANEL_OVERLAY_ID} iframe {
|
|
271
|
+
width: 100%;
|
|
272
|
+
height: 100%;
|
|
273
|
+
border: none;
|
|
274
|
+
background: #fff;
|
|
275
|
+
display: block;
|
|
276
|
+
opacity: 0;
|
|
277
|
+
transition: opacity 0.2s ease 0.18s;
|
|
278
|
+
}
|
|
279
|
+
#${HUB_PANEL_OVERLAY_ID}.is-open iframe {
|
|
280
|
+
opacity: 1;
|
|
281
|
+
}
|
|
282
|
+
@keyframes agent-host-fab-pulse {
|
|
283
|
+
0%, 100% { box-shadow: 0 4px 18px rgba(13, 143, 123, 0.42); }
|
|
284
|
+
50% { box-shadow: 0 4px 26px rgba(13, 143, 123, 0.62); }
|
|
285
|
+
}
|
|
286
|
+
@keyframes agent-host-clover-breathe {
|
|
287
|
+
0%, 100% { transform: scale(1); }
|
|
288
|
+
50% { transform: scale(1.28); }
|
|
289
|
+
}
|
|
290
|
+
@keyframes agent-host-hint-clover {
|
|
291
|
+
0% { transform: scale(0.88) rotate(0deg); }
|
|
292
|
+
16.67% { transform: scale(1.12) rotate(0deg); }
|
|
293
|
+
33.33% { transform: scale(0.88) rotate(0deg); }
|
|
294
|
+
50% { transform: scale(1.12) rotate(0deg); }
|
|
295
|
+
66.67% { transform: scale(1) rotate(0deg); }
|
|
296
|
+
100% { transform: scale(1) rotate(360deg); }
|
|
297
|
+
}
|
|
298
|
+
@keyframes agent-host-bell-shake {
|
|
299
|
+
0%, 100% { transform: rotate(0deg); }
|
|
300
|
+
12% { transform: rotate(14deg); }
|
|
301
|
+
24% { transform: rotate(-12deg); }
|
|
302
|
+
36% { transform: rotate(10deg); }
|
|
303
|
+
48% { transform: rotate(-8deg); }
|
|
304
|
+
60% { transform: rotate(5deg); }
|
|
305
|
+
72% { transform: rotate(-3deg); }
|
|
306
|
+
84% { transform: rotate(1deg); }
|
|
307
|
+
}
|
|
308
|
+
`
|
|
309
|
+
document.head.appendChild(style)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export class IframeBridge {
|
|
313
|
+
private agentOrigin = ''
|
|
314
|
+
private hubLauncher: HTMLButtonElement | null = null
|
|
315
|
+
private hubLauncherBadge: HTMLSpanElement | null = null
|
|
316
|
+
private hubLauncherHint: HTMLDivElement | null = null
|
|
317
|
+
private hubPanelOverlay: HTMLDivElement | null = null
|
|
318
|
+
private hubIframe: HTMLIFrameElement | null = null
|
|
319
|
+
private widgetIframe: HTMLIFrameElement | null = null
|
|
320
|
+
private hubReady = false
|
|
321
|
+
private widgetReady = false
|
|
322
|
+
private hubPanelOpen = false
|
|
323
|
+
private lastRunning = 0
|
|
324
|
+
private lastWaiting = 0
|
|
325
|
+
private launcherHintAction: PrimaryPageHint | null = null
|
|
326
|
+
private hintRepositionBound = false
|
|
327
|
+
private hintScrollParents: EventTarget[] = []
|
|
328
|
+
private pendingHubMessages: ParentToChildMessage[] = []
|
|
329
|
+
private pendingWidgetMessages: ParentToChildMessage[] = []
|
|
330
|
+
private onChildMessage?: (msg: ChildToParentMessage) => void
|
|
331
|
+
private onHintUse?: (pageId: string, actionId: string) => void
|
|
332
|
+
private onHintDismiss?: () => void
|
|
333
|
+
|
|
334
|
+
constructor(
|
|
335
|
+
agentOrigin: string,
|
|
336
|
+
onChildMessage: (msg: ChildToParentMessage) => void,
|
|
337
|
+
handlers?: {
|
|
338
|
+
onHintUse?: (pageId: string, actionId: string) => void
|
|
339
|
+
onHintDismiss?: () => void
|
|
340
|
+
},
|
|
341
|
+
) {
|
|
342
|
+
this.agentOrigin = normalizeBase(agentOrigin)
|
|
343
|
+
this.onChildMessage = onChildMessage
|
|
344
|
+
this.onHintUse = handlers?.onHintUse
|
|
345
|
+
this.onHintDismiss = handlers?.onHintDismiss
|
|
346
|
+
window.addEventListener('message', this.handleMessage)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
destroy() {
|
|
350
|
+
window.removeEventListener('message', this.handleMessage)
|
|
351
|
+
this.unbindHintReposition()
|
|
352
|
+
this.removeHub()
|
|
353
|
+
this.closeWidget()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Launcher(宿主 DOM)+ 居中工作室模态(显隐用 CSS,不改 iframe 尺寸) */
|
|
357
|
+
mountHub() {
|
|
358
|
+
if (this.hubPanelOverlay || typeof document === 'undefined') return
|
|
359
|
+
ensureHostSdkStyles()
|
|
360
|
+
this.mountLauncher()
|
|
361
|
+
this.mountHubPanel()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
openHubPanel() {
|
|
365
|
+
if (!this.hubPanelOverlay) return
|
|
366
|
+
this.hubPanelOpen = true
|
|
367
|
+
this.hubPanelOverlay.classList.add('is-open')
|
|
368
|
+
this.hubLauncher?.classList.add('is-hidden')
|
|
369
|
+
this.hubLauncherHint?.classList.remove('is-visible')
|
|
370
|
+
this.post('hub', { channel: AGENT_HOST_MSG, type: 'open-hub-panel' })
|
|
371
|
+
this.applyLauncherVisual()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
closeHubPanel() {
|
|
375
|
+
if (!this.hubPanelOverlay) return
|
|
376
|
+
this.hubPanelOpen = false
|
|
377
|
+
this.hubPanelOverlay.classList.remove('is-open')
|
|
378
|
+
this.hubLauncher?.classList.remove('is-hidden')
|
|
379
|
+
this.post('hub', { channel: AGENT_HOST_MSG, type: 'close-hub-panel' })
|
|
380
|
+
this.updateLauncherHint(this.launcherHintAction)
|
|
381
|
+
this.applyLauncherVisual()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @deprecated 工作室模态固定宽度,忽略半屏展开 */
|
|
385
|
+
setHubPanelExpanded(_expanded: boolean) {
|
|
386
|
+
/* no-op: studio modal has fixed size */
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
updateLauncherHint(action: PrimaryPageHint | null) {
|
|
390
|
+
this.launcherHintAction = action
|
|
391
|
+
const el = this.hubLauncherHint
|
|
392
|
+
if (!el) return
|
|
393
|
+
if (this.hubPanelOpen) {
|
|
394
|
+
el.classList.remove('is-visible', 'is-anchored')
|
|
395
|
+
this.unbindHintReposition()
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
if (!action || !action.hint.trim()) {
|
|
399
|
+
el.innerHTML = ''
|
|
400
|
+
el.classList.remove('is-visible', 'is-anchored')
|
|
401
|
+
this.unbindHintReposition()
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
el.innerHTML = `
|
|
405
|
+
<div class="agent-host-hint-inner">
|
|
406
|
+
<span class="agent-host-hint-icon" aria-hidden="true">✦</span>
|
|
407
|
+
<span class="agent-host-hint-text"></span>
|
|
408
|
+
<div class="agent-host-hint-actions">
|
|
409
|
+
<button type="button" class="agent-host-hint-use">立即开始</button>
|
|
410
|
+
<button type="button" class="agent-host-hint-dismiss">稍后</button>
|
|
411
|
+
</div>
|
|
412
|
+
</div>`
|
|
413
|
+
const textEl = el.querySelector('.agent-host-hint-text')
|
|
414
|
+
if (textEl) textEl.textContent = action.hint
|
|
415
|
+
const useBtn = el.querySelector('.agent-host-hint-use')
|
|
416
|
+
const dismissBtn = el.querySelector('.agent-host-hint-dismiss')
|
|
417
|
+
const preventFocusSteal = (e: Event) => e.preventDefault()
|
|
418
|
+
useBtn?.addEventListener('mousedown', preventFocusSteal)
|
|
419
|
+
dismissBtn?.addEventListener('mousedown', preventFocusSteal)
|
|
420
|
+
useBtn?.addEventListener('click', (e) => {
|
|
421
|
+
e.stopPropagation()
|
|
422
|
+
this.onHintUse?.(action.pageId, action.actionId)
|
|
423
|
+
})
|
|
424
|
+
dismissBtn?.addEventListener('click', (e) => {
|
|
425
|
+
e.stopPropagation()
|
|
426
|
+
this.onHintDismiss?.()
|
|
427
|
+
})
|
|
428
|
+
el.classList.add('is-visible')
|
|
429
|
+
this.layoutHint(el, action)
|
|
430
|
+
this.bindHintReposition()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private layoutHint(el: HTMLDivElement, action: PrimaryPageHint) {
|
|
434
|
+
if (action.placement !== 'anchor') {
|
|
435
|
+
el.classList.remove('is-anchored')
|
|
436
|
+
el.style.top = ''
|
|
437
|
+
el.style.left = ''
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
const rect = getActionAnchorRect(action.pageId, action.actionId)
|
|
441
|
+
if (!rect) {
|
|
442
|
+
el.classList.remove('is-anchored')
|
|
443
|
+
el.style.top = ''
|
|
444
|
+
el.style.left = ''
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
el.classList.add('is-anchored')
|
|
448
|
+
const gap = 8
|
|
449
|
+
const hintW = el.offsetWidth || 320
|
|
450
|
+
const hintH = el.offsetHeight || 48
|
|
451
|
+
let top = rect.top
|
|
452
|
+
let left = rect.right + gap
|
|
453
|
+
if (left + hintW > window.innerWidth - 16) {
|
|
454
|
+
left = Math.max(16, rect.left)
|
|
455
|
+
top = rect.bottom + gap
|
|
456
|
+
}
|
|
457
|
+
if (top + hintH > window.innerHeight - 16) {
|
|
458
|
+
top = Math.max(16, rect.top - hintH - gap)
|
|
459
|
+
}
|
|
460
|
+
if (top < 16) top = 16
|
|
461
|
+
if (left < 16) left = 16
|
|
462
|
+
el.style.top = `${Math.round(top)}px`
|
|
463
|
+
el.style.left = `${Math.round(left)}px`
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private onHintReposition = () => {
|
|
467
|
+
const action = this.launcherHintAction
|
|
468
|
+
const el = this.hubLauncherHint
|
|
469
|
+
if (!action || !el || !el.classList.contains('is-visible')) return
|
|
470
|
+
this.layoutHint(el, action)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private collectScrollParents(node: HTMLElement | null): HTMLElement[] {
|
|
474
|
+
const out: HTMLElement[] = []
|
|
475
|
+
let p = node?.parentElement ?? null
|
|
476
|
+
while (p) {
|
|
477
|
+
const style = window.getComputedStyle(p)
|
|
478
|
+
const oy = style.overflowY
|
|
479
|
+
const ox = style.overflowX
|
|
480
|
+
if (/(auto|scroll|overlay)/.test(`${ox} ${oy}`)) out.push(p)
|
|
481
|
+
p = p.parentElement
|
|
482
|
+
}
|
|
483
|
+
return out
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private bindHintReposition() {
|
|
487
|
+
if (typeof window === 'undefined') return
|
|
488
|
+
if (!this.hintRepositionBound) {
|
|
489
|
+
window.addEventListener('scroll', this.onHintReposition, true)
|
|
490
|
+
window.addEventListener('resize', this.onHintReposition)
|
|
491
|
+
window.visualViewport?.addEventListener('resize', this.onHintReposition)
|
|
492
|
+
window.visualViewport?.addEventListener('scroll', this.onHintReposition)
|
|
493
|
+
this.hintRepositionBound = true
|
|
494
|
+
}
|
|
495
|
+
const action = this.launcherHintAction
|
|
496
|
+
for (const node of this.hintScrollParents) {
|
|
497
|
+
node.removeEventListener('scroll', this.onHintReposition)
|
|
498
|
+
}
|
|
499
|
+
this.hintScrollParents = []
|
|
500
|
+
if (!action || action.placement !== 'anchor') return
|
|
501
|
+
const anchor = getActionAnchorElement(action.pageId, action.actionId)
|
|
502
|
+
this.hintScrollParents = this.collectScrollParents(anchor)
|
|
503
|
+
for (const node of this.hintScrollParents) {
|
|
504
|
+
node.addEventListener('scroll', this.onHintReposition, { passive: true })
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private unbindHintReposition() {
|
|
509
|
+
if (typeof window === 'undefined') return
|
|
510
|
+
if (this.hintRepositionBound) {
|
|
511
|
+
window.removeEventListener('scroll', this.onHintReposition, true)
|
|
512
|
+
window.removeEventListener('resize', this.onHintReposition)
|
|
513
|
+
window.visualViewport?.removeEventListener('resize', this.onHintReposition)
|
|
514
|
+
window.visualViewport?.removeEventListener('scroll', this.onHintReposition)
|
|
515
|
+
this.hintRepositionBound = false
|
|
516
|
+
}
|
|
517
|
+
for (const node of this.hintScrollParents) {
|
|
518
|
+
node.removeEventListener('scroll', this.onHintReposition)
|
|
519
|
+
}
|
|
520
|
+
this.hintScrollParents = []
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private mountLauncher() {
|
|
524
|
+
if (this.hubLauncher || document.getElementById(HUB_LAUNCHER_ID)) return
|
|
525
|
+
const btn = document.createElement('button')
|
|
526
|
+
btn.id = HUB_LAUNCHER_ID
|
|
527
|
+
btn.type = 'button'
|
|
528
|
+
btn.title = '景行 AI'
|
|
529
|
+
btn.setAttribute('aria-label', '景行 AI')
|
|
530
|
+
btn.innerHTML =
|
|
531
|
+
'<span class="agent-host-launcher-icon" aria-hidden="true">✦</span>' +
|
|
532
|
+
'<span class="agent-host-launcher-bell" aria-hidden="true">' +
|
|
533
|
+
'<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
|
534
|
+
'<path d="M12 3a5 5 0 0 0-5 5v1.5c0 1.3-.4 2.6-1.2 3.7L4.5 15.5c-.4.5-.1 1.3.5 1.3h14c.6 0 .9-.8.5-1.3l-1.3-2.3A6.6 6.6 0 0 1 17 9.5V8a5 5 0 0 0-5-5Z" fill="#f59e0b"/>' +
|
|
535
|
+
'<path d="M10 18.5a2 2 0 0 0 4 0" stroke="#f59e0b" stroke-width="1.8" stroke-linecap="round"/>' +
|
|
536
|
+
'</svg></span>' +
|
|
537
|
+
'<span class="agent-host-launcher-badge"></span>'
|
|
538
|
+
btn.addEventListener('click', () => this.openHubPanel())
|
|
539
|
+
document.body.appendChild(btn)
|
|
540
|
+
this.hubLauncher = btn
|
|
541
|
+
this.hubLauncherBadge = btn.querySelector('.agent-host-launcher-badge')
|
|
542
|
+
|
|
543
|
+
if (!document.getElementById('agent-host-launcher-hint')) {
|
|
544
|
+
const hint = document.createElement('div')
|
|
545
|
+
hint.id = 'agent-host-launcher-hint'
|
|
546
|
+
document.body.appendChild(hint)
|
|
547
|
+
this.hubLauncherHint = hint
|
|
548
|
+
} else {
|
|
549
|
+
this.hubLauncherHint = document.getElementById('agent-host-launcher-hint') as HTMLDivElement
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private mountHubPanel() {
|
|
554
|
+
const overlay = document.createElement('div')
|
|
555
|
+
overlay.id = HUB_PANEL_OVERLAY_ID
|
|
556
|
+
|
|
557
|
+
const backdrop = document.createElement('div')
|
|
558
|
+
backdrop.className = 'agent-host-hub-backdrop'
|
|
559
|
+
backdrop.addEventListener('click', () => this.closeHubPanel())
|
|
560
|
+
|
|
561
|
+
const shell = document.createElement('div')
|
|
562
|
+
shell.className = 'agent-host-hub-panel-shell'
|
|
563
|
+
|
|
564
|
+
const iframe = document.createElement('iframe')
|
|
565
|
+
iframe.id = HUB_IFRAME_ID
|
|
566
|
+
iframe.title = '景行 AI'
|
|
567
|
+
iframe.src = `${this.agentOrigin}/host/hub?embed=1&panel=1&layout=studio`
|
|
568
|
+
iframe.allow = 'clipboard-read; clipboard-write'
|
|
569
|
+
|
|
570
|
+
shell.appendChild(iframe)
|
|
571
|
+
overlay.appendChild(backdrop)
|
|
572
|
+
overlay.appendChild(shell)
|
|
573
|
+
document.body.appendChild(overlay)
|
|
574
|
+
|
|
575
|
+
this.hubPanelOverlay = overlay
|
|
576
|
+
this.hubIframe = iframe
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
updateLauncherStats(running: number, waiting: number) {
|
|
580
|
+
this.lastRunning = running
|
|
581
|
+
this.lastWaiting = waiting
|
|
582
|
+
this.applyLauncherVisual()
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private applyLauncherVisual() {
|
|
586
|
+
const btn = this.hubLauncher
|
|
587
|
+
const badge = this.hubLauncherBadge
|
|
588
|
+
if (!btn || !badge) return
|
|
589
|
+
const icon = btn.querySelector('.agent-host-launcher-icon') as HTMLElement | null
|
|
590
|
+
const bell = btn.querySelector('.agent-host-launcher-bell') as HTMLElement | null
|
|
591
|
+
const running = this.lastRunning
|
|
592
|
+
const waiting = this.lastWaiting
|
|
593
|
+
// 抽屉关闭且有待确认:中间换成橙色抖动铃铛(底色不变)
|
|
594
|
+
const hitlAlert = waiting > 0 && !this.hubPanelOpen
|
|
595
|
+
|
|
596
|
+
btn.classList.toggle('is-pulse', running > 0)
|
|
597
|
+
// 有进行中/待确认任务且显示四叶草时:图标变大呼吸
|
|
598
|
+
btn.classList.toggle('is-breathe', (running > 0 || waiting > 0) && !hitlAlert)
|
|
599
|
+
if (hitlAlert) {
|
|
600
|
+
btn.title = '有待确认事项,点击打开助手'
|
|
601
|
+
btn.setAttribute('aria-label', '有待确认事项,点击打开助手')
|
|
602
|
+
} else {
|
|
603
|
+
btn.title = '景行 AI'
|
|
604
|
+
btn.setAttribute('aria-label', '景行 AI')
|
|
605
|
+
}
|
|
606
|
+
if (icon) icon.style.display = hitlAlert ? 'none' : ''
|
|
607
|
+
if (bell) bell.classList.toggle('is-visible', hitlAlert)
|
|
608
|
+
|
|
609
|
+
if (running > 0) {
|
|
610
|
+
badge.textContent = String(running)
|
|
611
|
+
badge.classList.add('is-visible')
|
|
612
|
+
badge.classList.remove('is-waiting')
|
|
613
|
+
} else if (waiting > 0) {
|
|
614
|
+
badge.textContent = '!'
|
|
615
|
+
badge.classList.add('is-visible', 'is-waiting')
|
|
616
|
+
} else {
|
|
617
|
+
badge.textContent = ''
|
|
618
|
+
badge.classList.remove('is-visible', 'is-waiting')
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
removeHub() {
|
|
623
|
+
document.getElementById(HUB_LAUNCHER_ID)?.remove()
|
|
624
|
+
document.getElementById('agent-host-launcher-hint')?.remove()
|
|
625
|
+
document.getElementById(HUB_PANEL_OVERLAY_ID)?.remove()
|
|
626
|
+
this.hubLauncher = null
|
|
627
|
+
this.hubLauncherBadge = null
|
|
628
|
+
this.hubLauncherHint = null
|
|
629
|
+
this.hubPanelOverlay = null
|
|
630
|
+
this.hubIframe = null
|
|
631
|
+
this.hubReady = false
|
|
632
|
+
this.hubPanelOpen = false
|
|
633
|
+
this.pendingHubMessages = []
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
openWidget(payload: WidgetPayload, token: string, refreshToken?: string) {
|
|
637
|
+
this.closeWidget()
|
|
638
|
+
const overlay = document.createElement('div')
|
|
639
|
+
overlay.id = WIDGET_OVERLAY_ID
|
|
640
|
+
overlay.style.cssText =
|
|
641
|
+
'position:fixed;inset:0;z-index:2147483646;background:rgba(15,23,42,0.45);display:flex;align-items:center;justify-content:center;padding:16px;'
|
|
642
|
+
overlay.addEventListener('click', (e) => {
|
|
643
|
+
if (e.target === overlay) this.closeWidget()
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
const iframe = document.createElement('iframe')
|
|
647
|
+
iframe.id = WIDGET_IFRAME_ID
|
|
648
|
+
iframe.title = payload.title || payload.workflow
|
|
649
|
+
const q = new URLSearchParams({
|
|
650
|
+
embed: '1',
|
|
651
|
+
workflow: payload.workflow,
|
|
652
|
+
host: payload.host || '',
|
|
653
|
+
title: payload.title || '',
|
|
654
|
+
})
|
|
655
|
+
iframe.src = `${this.agentOrigin}/host/widget?${q.toString()}`
|
|
656
|
+
iframe.style.cssText =
|
|
657
|
+
'width:min(560px,100%);height:80vh;border:none;border-radius:12px;background:#fff;box-shadow:0 20px 60px rgba(0,0,0,0.25);display:block;'
|
|
658
|
+
overlay.appendChild(iframe)
|
|
659
|
+
document.body.appendChild(overlay)
|
|
660
|
+
this.widgetIframe = iframe
|
|
661
|
+
this.widgetReady = false
|
|
662
|
+
|
|
663
|
+
const initMsg: ParentToChildMessage = {
|
|
664
|
+
channel: AGENT_HOST_MSG,
|
|
665
|
+
type: 'init',
|
|
666
|
+
token,
|
|
667
|
+
refreshToken,
|
|
668
|
+
}
|
|
669
|
+
const safePayload: WidgetPayload = {
|
|
670
|
+
workflow: payload.workflow,
|
|
671
|
+
title: payload.title,
|
|
672
|
+
host: payload.host,
|
|
673
|
+
prefilled: payload.prefilled ? cloneForPostMessage(payload.prefilled) : undefined,
|
|
674
|
+
hostContext: payload.hostContext ? cloneForPostMessage(payload.hostContext) : undefined,
|
|
675
|
+
sessionId: payload.sessionId,
|
|
676
|
+
preferSandboxFiles: payload.preferSandboxFiles,
|
|
677
|
+
}
|
|
678
|
+
const openMsg: ParentToChildMessage = {
|
|
679
|
+
channel: AGENT_HOST_MSG,
|
|
680
|
+
type: 'open-widget',
|
|
681
|
+
payload: safePayload,
|
|
682
|
+
}
|
|
683
|
+
this.pendingWidgetMessages = [initMsg, openMsg]
|
|
684
|
+
// 须等 iframe 内 postMessage('ready') 后再 flush,否则 init/open-widget 会丢失
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
closeWidget() {
|
|
688
|
+
document.getElementById(WIDGET_OVERLAY_ID)?.remove()
|
|
689
|
+
this.widgetIframe = null
|
|
690
|
+
this.widgetReady = false
|
|
691
|
+
this.pendingWidgetMessages = []
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
sendToHub(msg: ParentToChildMessage) {
|
|
695
|
+
this.post('hub', msg)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private post(surface: IframeSurface, msg: ParentToChildMessage) {
|
|
699
|
+
const ready = surface === 'hub' ? this.hubReady : this.widgetReady
|
|
700
|
+
const queue = surface === 'hub' ? this.pendingHubMessages : this.pendingWidgetMessages
|
|
701
|
+
if (!ready) {
|
|
702
|
+
queue.push(msg)
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
const iframe = surface === 'hub' ? this.hubIframe : this.widgetIframe
|
|
706
|
+
iframe?.contentWindow?.postMessage(msg, toBrowserOrigin(this.agentOrigin))
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private flush(surface: IframeSurface) {
|
|
710
|
+
const queue = surface === 'hub' ? this.pendingHubMessages : this.pendingWidgetMessages
|
|
711
|
+
const iframe = surface === 'hub' ? this.hubIframe : this.widgetIframe
|
|
712
|
+
if (!iframe?.contentWindow) return
|
|
713
|
+
const targetOrigin = toBrowserOrigin(this.agentOrigin)
|
|
714
|
+
while (queue.length) {
|
|
715
|
+
const msg = queue.shift()
|
|
716
|
+
if (msg) iframe.contentWindow.postMessage(msg, targetOrigin)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private handleMessage = (event: MessageEvent) => {
|
|
721
|
+
if (!originsMatch(this.agentOrigin, event.origin)) return
|
|
722
|
+
if (!isAgentHostMessage(event.data)) return
|
|
723
|
+
const msg = event.data as ChildToParentMessage
|
|
724
|
+
if (msg.type === 'ready') {
|
|
725
|
+
if (msg.surface === 'hub') {
|
|
726
|
+
this.hubReady = true
|
|
727
|
+
this.flush('hub')
|
|
728
|
+
} else {
|
|
729
|
+
this.widgetReady = true
|
|
730
|
+
this.flush('widget')
|
|
731
|
+
}
|
|
732
|
+
// 继续交给上层:Hub ready 时需重发 init + 同步 register-page
|
|
733
|
+
this.onChildMessage?.(msg)
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
if (msg.type === 'hub-panel-close') {
|
|
737
|
+
this.closeHubPanel()
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
if (msg.type === 'hub-panel-width') {
|
|
741
|
+
this.setHubPanelExpanded(Boolean(msg.expanded))
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
if (msg.type === 'hub-stats') {
|
|
745
|
+
this.updateLauncherStats(msg.running, msg.waiting)
|
|
746
|
+
this.onChildMessage?.(msg)
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
this.onChildMessage?.(msg)
|
|
750
|
+
}
|
|
751
|
+
}
|