@_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/store.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HostAnchorRef,
|
|
3
|
+
HostHintPlacement,
|
|
4
|
+
HostPageAction,
|
|
5
|
+
HostPageContext,
|
|
6
|
+
HostRefreshPayload,
|
|
7
|
+
RegisterPageOptions,
|
|
8
|
+
} from './types'
|
|
9
|
+
import type { SerializedPageAction, WidgetPayload } from './messages'
|
|
10
|
+
import { cloneForPostMessage } from './messages'
|
|
11
|
+
|
|
12
|
+
function resolveMaybe<T>(v: T | (() => T)): T {
|
|
13
|
+
return typeof v === 'function' ? (v as () => T)() : v
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PrimaryPageHint = {
|
|
17
|
+
pageId: string
|
|
18
|
+
actionId: string
|
|
19
|
+
hint: string
|
|
20
|
+
placement: HostHintPlacement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PageRefreshBinding = {
|
|
24
|
+
refreshKey: string
|
|
25
|
+
onRefresh: (payload?: HostRefreshPayload) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pageActions: Record<string, HostPageAction[]> = {}
|
|
29
|
+
const pageRefresh: Record<string, PageRefreshBinding> = {}
|
|
30
|
+
/** 宿主会话级上下文(系统 / 名称 / 描述 / 菜单;由 init.getHostContext 按需提供) */
|
|
31
|
+
let hostContext: HostPageContext = {}
|
|
32
|
+
let currentPageId: string | null = null
|
|
33
|
+
let hintDismissed = false
|
|
34
|
+
/** 当前页、hintPlacement=anchor 且焦点在该 action 锚点内 */
|
|
35
|
+
let anchorFocusedActionId: string | null = null
|
|
36
|
+
let hintUiListener: (() => void) | null = null
|
|
37
|
+
let docFocusBound = false
|
|
38
|
+
|
|
39
|
+
export function setHintUiListener(fn: (() => void) | null) {
|
|
40
|
+
hintUiListener = fn
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function notifyHintUi() {
|
|
44
|
+
try {
|
|
45
|
+
hintUiListener?.()
|
|
46
|
+
} catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveAnchorElement(anchor: HostPageAction['anchor']): HTMLElement | null {
|
|
52
|
+
if (!anchor) return null
|
|
53
|
+
const raw = typeof anchor === 'function' ? anchor() : anchor
|
|
54
|
+
if (!raw) return null
|
|
55
|
+
let el: Element | null = null
|
|
56
|
+
if (raw instanceof Element) {
|
|
57
|
+
el = raw
|
|
58
|
+
} else if (typeof raw === 'object' && raw.$el instanceof Element) {
|
|
59
|
+
el = raw.$el
|
|
60
|
+
}
|
|
61
|
+
if (!el) return null
|
|
62
|
+
const inner = el.querySelector?.('textarea, input, [contenteditable="true"]')
|
|
63
|
+
return (inner instanceof HTMLElement ? inner : el) as HTMLElement
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isHintNode(node: Node | null): boolean {
|
|
67
|
+
if (!node || typeof (node as Element).closest !== 'function') return false
|
|
68
|
+
return Boolean((node as Element).closest('#agent-host-launcher-hint'))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function actionWantsAnchorHint(action: HostPageAction): boolean {
|
|
72
|
+
return Boolean(action.anchor) && action.hintPlacement === 'anchor'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function syncAnchorFocusFromTarget(target: EventTarget | null, related?: EventTarget | null) {
|
|
76
|
+
if (!currentPageId) {
|
|
77
|
+
if (anchorFocusedActionId) {
|
|
78
|
+
anchorFocusedActionId = null
|
|
79
|
+
notifyHintUi()
|
|
80
|
+
}
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (isHintNode(target as Node) || isHintNode(related as Node)) return
|
|
84
|
+
const actions = pageActions[currentPageId] || []
|
|
85
|
+
let next: string | null = null
|
|
86
|
+
if (target instanceof Node) {
|
|
87
|
+
for (const a of actions) {
|
|
88
|
+
if (!actionWantsAnchorHint(a)) continue
|
|
89
|
+
const el = resolveAnchorElement(a.anchor)
|
|
90
|
+
if (el && el.contains(target)) {
|
|
91
|
+
next = a.id
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (next === anchorFocusedActionId) return
|
|
97
|
+
anchorFocusedActionId = next
|
|
98
|
+
notifyHintUi()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function onDocFocusIn(ev: FocusEvent) {
|
|
102
|
+
syncAnchorFocusFromTarget(ev.target)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onDocFocusOut(ev: FocusEvent) {
|
|
106
|
+
if (isHintNode(ev.relatedTarget as Node)) return
|
|
107
|
+
const t = ev.target
|
|
108
|
+
if (!(t instanceof Node) || !currentPageId) return
|
|
109
|
+
const actions = pageActions[currentPageId] || []
|
|
110
|
+
const focused = actions.find((a) => a.id === anchorFocusedActionId)
|
|
111
|
+
if (!focused) return
|
|
112
|
+
const el = resolveAnchorElement(focused.anchor)
|
|
113
|
+
if (el && el.contains(t) && !el.contains(ev.relatedTarget as Node)) {
|
|
114
|
+
anchorFocusedActionId = null
|
|
115
|
+
notifyHintUi()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureDocFocusListeners() {
|
|
120
|
+
if (docFocusBound || typeof document === 'undefined') return
|
|
121
|
+
document.addEventListener('focusin', onDocFocusIn, true)
|
|
122
|
+
document.addEventListener('focusout', onDocFocusOut, true)
|
|
123
|
+
docFocusBound = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function inferRefreshKey(actions: HostPageAction[], explicit?: string): string | undefined {
|
|
127
|
+
const key = (explicit || '').trim()
|
|
128
|
+
if (key) return key
|
|
129
|
+
for (const a of actions) {
|
|
130
|
+
if (!a.hostContext) continue
|
|
131
|
+
const hc = resolveMaybe(a.hostContext)
|
|
132
|
+
const refresh = hc && typeof hc === 'object' ? String((hc as { refresh?: unknown }).refresh || '').trim() : ''
|
|
133
|
+
if (refresh) return refresh
|
|
134
|
+
}
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isDirectHostAction(action: Pick<HostPageAction, 'kind'> | Pick<SerializedPageAction, 'kind'> | null | undefined): boolean {
|
|
139
|
+
return (action?.kind || 'widget') === 'direct'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function registerHostPage(
|
|
143
|
+
pageId: string,
|
|
144
|
+
actions: HostPageAction[],
|
|
145
|
+
options: RegisterPageOptions = {},
|
|
146
|
+
) {
|
|
147
|
+
ensureDocFocusListeners()
|
|
148
|
+
const pageChanged = pageId !== currentPageId
|
|
149
|
+
if (pageChanged) {
|
|
150
|
+
hintDismissed = false
|
|
151
|
+
anchorFocusedActionId = null
|
|
152
|
+
}
|
|
153
|
+
pageActions[pageId] = actions
|
|
154
|
+
currentPageId = pageId
|
|
155
|
+
if (!pageChanged && actions.some((a) => !a.enabled || a.enabled())) {
|
|
156
|
+
hintDismissed = false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const refreshKey = inferRefreshKey(actions, options.refreshKey)
|
|
160
|
+
if (refreshKey && typeof options.onRefresh === 'function') {
|
|
161
|
+
pageRefresh[pageId] = { refreshKey, onRefresh: options.onRefresh }
|
|
162
|
+
} else {
|
|
163
|
+
delete pageRefresh[pageId]
|
|
164
|
+
}
|
|
165
|
+
notifyHintUi()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function unregisterHostPage(pageId: string) {
|
|
169
|
+
delete pageActions[pageId]
|
|
170
|
+
delete pageRefresh[pageId]
|
|
171
|
+
if (currentPageId === pageId) {
|
|
172
|
+
currentPageId = null
|
|
173
|
+
anchorFocusedActionId = null
|
|
174
|
+
}
|
|
175
|
+
notifyHintUi()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sanitizeHostContext(raw: HostPageContext): HostPageContext {
|
|
179
|
+
const next = cloneForPostMessage(raw || {})
|
|
180
|
+
for (const key of Object.keys(next)) {
|
|
181
|
+
const val = next[key]
|
|
182
|
+
if (val == null || (typeof val === 'string' && !val.trim())) {
|
|
183
|
+
delete next[key]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return next
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 整份写入宿主会话上下文(按需拉取后回写;离开专病时不应残留旧 name) */
|
|
190
|
+
export function replaceHostContext(partial: HostPageContext): HostPageContext {
|
|
191
|
+
hostContext = sanitizeHostContext(partial || {})
|
|
192
|
+
return { ...hostContext }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getHostContext(): HostPageContext {
|
|
196
|
+
return { ...hostContext }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function clearHostContext() {
|
|
200
|
+
hostContext = {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 任务完成:仅对仍注册且 refreshKey 匹配的页面触发刷新 */
|
|
204
|
+
export function dispatchRegisteredPageRefresh(refreshKey: unknown, payload?: HostRefreshPayload) {
|
|
205
|
+
const key = String(refreshKey || '').trim()
|
|
206
|
+
if (!key) return
|
|
207
|
+
for (const binding of Object.values(pageRefresh)) {
|
|
208
|
+
if (binding.refreshKey !== key) continue
|
|
209
|
+
try {
|
|
210
|
+
binding.onRefresh(payload)
|
|
211
|
+
} catch {
|
|
212
|
+
/* 业务刷新失败不阻断完成回调 */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function clearHostPageStore() {
|
|
218
|
+
for (const id of Object.keys(pageActions)) delete pageActions[id]
|
|
219
|
+
for (const id of Object.keys(pageRefresh)) delete pageRefresh[id]
|
|
220
|
+
hostContext = {}
|
|
221
|
+
currentPageId = null
|
|
222
|
+
hintDismissed = false
|
|
223
|
+
anchorFocusedActionId = null
|
|
224
|
+
notifyHintUi()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function setCurrentHostPage(pageId: string | null) {
|
|
228
|
+
if (pageId !== currentPageId) {
|
|
229
|
+
hintDismissed = false
|
|
230
|
+
anchorFocusedActionId = null
|
|
231
|
+
}
|
|
232
|
+
currentPageId = pageId
|
|
233
|
+
notifyHintUi()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function dismissCurrentPageHint() {
|
|
237
|
+
hintDismissed = true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function serializePageActions(actions: HostPageAction[]): SerializedPageAction[] {
|
|
241
|
+
return actions.map((a) => ({
|
|
242
|
+
id: a.id,
|
|
243
|
+
workflow: a.workflow || a.skill || '',
|
|
244
|
+
label: a.label,
|
|
245
|
+
host: a.host,
|
|
246
|
+
hint: a.hint ? resolveMaybe(a.hint) : undefined,
|
|
247
|
+
hostContext: a.hostContext ? cloneForPostMessage(resolveMaybe(a.hostContext)) : undefined,
|
|
248
|
+
prefilled: a.prefilled ? cloneForPostMessage(resolveMaybe(a.prefilled)) : undefined,
|
|
249
|
+
enabled: a.enabled ? a.enabled() : true,
|
|
250
|
+
kind: a.kind,
|
|
251
|
+
skill: a.skill,
|
|
252
|
+
noTools: a.noTools === true ? true : undefined,
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildActionLaunchPayload(pageId: string, actionId: string): {
|
|
257
|
+
action: HostPageAction
|
|
258
|
+
prefilled?: Record<string, unknown>
|
|
259
|
+
hostContext?: Record<string, unknown>
|
|
260
|
+
} | null {
|
|
261
|
+
const action = (pageActions[pageId] || []).find((a) => a.id === actionId)
|
|
262
|
+
if (!action) return null
|
|
263
|
+
if (action.enabled && !action.enabled()) return null
|
|
264
|
+
const prefilled = action.prefilled ? cloneForPostMessage(resolveMaybe(action.prefilled)) : undefined
|
|
265
|
+
return {
|
|
266
|
+
action,
|
|
267
|
+
prefilled,
|
|
268
|
+
hostContext: action.hostContext ? cloneForPostMessage(resolveMaybe(action.hostContext)) : undefined,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function buildActionWidgetRequest(pageId: string, actionId: string): WidgetPayload | null {
|
|
273
|
+
const packed = buildActionLaunchPayload(pageId, actionId)
|
|
274
|
+
if (!packed) return null
|
|
275
|
+
if (isDirectHostAction(packed.action)) return null
|
|
276
|
+
return {
|
|
277
|
+
workflow: packed.action.workflow || packed.action.skill || '',
|
|
278
|
+
title: packed.action.label,
|
|
279
|
+
host: packed.action.host,
|
|
280
|
+
prefilled: packed.prefilled,
|
|
281
|
+
hostContext: packed.hostContext,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getCurrentPageId() {
|
|
286
|
+
return currentPageId
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getRegisteredPages(): { pageId: string; actions: HostPageAction[] }[] {
|
|
290
|
+
return Object.entries(pageActions).map(([pageId, actions]) => ({ pageId, actions }))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getActionAnchorElement(pageId: string, actionId: string): HTMLElement | null {
|
|
294
|
+
const action = (pageActions[pageId] || []).find((a) => a.id === actionId)
|
|
295
|
+
if (!action) return null
|
|
296
|
+
return resolveAnchorElement(action.anchor)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function getActionAnchorRect(pageId: string, actionId: string): DOMRect | null {
|
|
300
|
+
const el = getActionAnchorElement(pageId, actionId)
|
|
301
|
+
if (!el) return null
|
|
302
|
+
const r = el.getBoundingClientRect()
|
|
303
|
+
if (!r.width && !r.height) return null
|
|
304
|
+
return r
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function getCurrentPagePrimaryAction(): PrimaryPageHint | null {
|
|
308
|
+
if (hintDismissed || !currentPageId) return null
|
|
309
|
+
const actions = pageActions[currentPageId] || []
|
|
310
|
+
for (const a of actions) {
|
|
311
|
+
if (a.enabled && !a.enabled()) continue
|
|
312
|
+
if (actionWantsAnchorHint(a) && anchorFocusedActionId !== a.id) continue
|
|
313
|
+
const hint = a.hint ? resolveMaybe(a.hint) : `可使用「${a.label}」`
|
|
314
|
+
const placement: HostHintPlacement = actionWantsAnchorHint(a) ? 'anchor' : 'launcher'
|
|
315
|
+
return { pageId: currentPageId, actionId: a.id, hint, placement }
|
|
316
|
+
}
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** @deprecated 使用 getCurrentPagePrimaryAction */
|
|
321
|
+
export function getCurrentPageHintText(): string | null {
|
|
322
|
+
return getCurrentPagePrimaryAction()?.hint ?? null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function resolveHostAnchorElement(ref: HostAnchorRef | (() => HostAnchorRef)): HTMLElement | null {
|
|
326
|
+
return resolveAnchorElement(ref)
|
|
327
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export type HostActionKind = 'widget' | 'direct'
|
|
2
|
+
|
|
3
|
+
export type HostHintPlacement = 'launcher' | 'anchor'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 横幅锚点。优先传真实 DOM(`HTMLElement`)。
|
|
7
|
+
* Vue 2 单根组件可传实例(用 `$el`)。Vue 3 多根 / React 组件实例没有稳定 `$el`,请 `querySelector` 或 ref 取到节点再传入。
|
|
8
|
+
*/
|
|
9
|
+
export type HostAnchorRef =
|
|
10
|
+
| Element
|
|
11
|
+
| { $el?: Element | null }
|
|
12
|
+
| null
|
|
13
|
+
| undefined
|
|
14
|
+
|
|
15
|
+
export type HostPageAction = {
|
|
16
|
+
id: string
|
|
17
|
+
/** Widget 工作流名;kind=direct 时可省略,此时用 skill */
|
|
18
|
+
workflow?: string
|
|
19
|
+
label: string
|
|
20
|
+
host?: string
|
|
21
|
+
/**
|
|
22
|
+
* widget(默认):打开 FormWidget。
|
|
23
|
+
* direct:不弹窗。有 skill 时 Hub 发 Chat;仅有 workflow 时静默 POST /host/scenarios(不弹 Widget)。
|
|
24
|
+
*/
|
|
25
|
+
kind?: HostActionKind
|
|
26
|
+
/** 内置 skill slug;kind=direct 时写入启动上下文 */
|
|
27
|
+
skill?: string
|
|
28
|
+
/**
|
|
29
|
+
* 可选覆盖。场景默认的 no_tools / skip_post_summary 在 Agent host_ui_registry。
|
|
30
|
+
* 不要写进 Skill,以免 Chat `@` 被连带改成无工具。
|
|
31
|
+
*/
|
|
32
|
+
noTools?: boolean
|
|
33
|
+
hostContext?: Record<string, unknown> | (() => Record<string, unknown>)
|
|
34
|
+
prefilled?: Record<string, unknown> | (() => Record<string, unknown>)
|
|
35
|
+
enabled?: () => boolean
|
|
36
|
+
hint?: string | (() => string)
|
|
37
|
+
/**
|
|
38
|
+
* launcher(默认):横幅贴在右下角 FAB 上方。
|
|
39
|
+
* anchor:横幅贴在 `anchor` 控件旁;仅当该控件处于焦点时显示。
|
|
40
|
+
*/
|
|
41
|
+
hintPlacement?: HostHintPlacement
|
|
42
|
+
/** 控件锚点;函数在 registerPage 时于宿主求值 */
|
|
43
|
+
anchor?: HostAnchorRef | (() => HostAnchorRef)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** EDC Host prefilled 公共字段(snake_case 为主,兼容宿主 camelCase) */
|
|
47
|
+
export type HostPrefilledContext = {
|
|
48
|
+
sub_project_id?: string
|
|
49
|
+
subProjectId?: string
|
|
50
|
+
project_id?: string
|
|
51
|
+
projectId?: string
|
|
52
|
+
sort_start?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** EDC AI 填表 prefilled */
|
|
56
|
+
export type HostPrefilledPatients = HostPrefilledContext & {
|
|
57
|
+
forms?: unknown[]
|
|
58
|
+
visits?: unknown[]
|
|
59
|
+
target?: {
|
|
60
|
+
form_name?: string
|
|
61
|
+
form_guid?: string
|
|
62
|
+
visit_name?: string
|
|
63
|
+
visit_guid?: string
|
|
64
|
+
period_name?: string
|
|
65
|
+
form_fields: { fields: unknown[]; formName?: string; formGuid?: string }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type OpenWorkflowOptions = {
|
|
70
|
+
host?: string
|
|
71
|
+
hostContext?: Record<string, unknown>
|
|
72
|
+
prefilled?: Record<string, unknown>
|
|
73
|
+
title?: string
|
|
74
|
+
label?: string
|
|
75
|
+
sessionId?: string
|
|
76
|
+
preferSandboxFiles?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type HostRefreshPayload = {
|
|
80
|
+
taskId?: string
|
|
81
|
+
result?: string
|
|
82
|
+
hostContext?: Record<string, unknown>
|
|
83
|
+
apply?: Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 宿主会话级上下文(抽屉提问注入;专病 / 我的项目 / 数据探索 / 数据分析共用 name + description) */
|
|
87
|
+
export type HostPageContext = {
|
|
88
|
+
/**
|
|
89
|
+
* 宿主系统标识,例如:
|
|
90
|
+
* - 专病数据库系统
|
|
91
|
+
* - 我的项目(EDC;属科研平台子系统)
|
|
92
|
+
* - 数据探索(属科研平台子系统)
|
|
93
|
+
* - 数据分析(在线分析;属科研平台子系统,路由 /analyzer)
|
|
94
|
+
* 科研平台本身包含「我的项目」「数据探索」「数据分析」等子系统,不要笼统写「科研平台」。
|
|
95
|
+
*/
|
|
96
|
+
system?: string
|
|
97
|
+
/** 专病名或 EDC 项目名 */
|
|
98
|
+
name?: string
|
|
99
|
+
/** 专病描述或项目说明 */
|
|
100
|
+
description?: string
|
|
101
|
+
/** 当前菜单或页面名(按需读取时从路由取) */
|
|
102
|
+
menu?: string
|
|
103
|
+
[key: string]: unknown
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* registerPage 可选:任务完成刷新。
|
|
108
|
+
* 页面 unregister 后自动失效,无需宿主自己管 window 事件。
|
|
109
|
+
*/
|
|
110
|
+
export type RegisterPageOptions = {
|
|
111
|
+
/** 与 hostContext.refresh 对齐,如 form_list / export_list */
|
|
112
|
+
refreshKey?: string
|
|
113
|
+
/** 当前页仍注册时,任务成功完成且 hostContext.refresh 匹配则调用 */
|
|
114
|
+
onRefresh?: (payload?: HostRefreshPayload) => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 宿主 BFF bootstrap 响应(boot-admin GET /agent/bootstrap) */
|
|
118
|
+
export type BootstrapResult = {
|
|
119
|
+
agent_token?: string
|
|
120
|
+
refresh_token?: string
|
|
121
|
+
/** Agent 前端 origin,由 BFF 下发,SDK 用于 iframe 地址 */
|
|
122
|
+
agent_web_origin?: string
|
|
123
|
+
integrations?: Record<string, unknown>
|
|
124
|
+
cached?: boolean
|
|
125
|
+
error?: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type HostInitOptions = {
|
|
129
|
+
/**
|
|
130
|
+
* Agent 前端 origin(可选)。
|
|
131
|
+
* 优先使用 bootstrap 返回的 `agent_web_origin`;未配置且 bootstrap 未返回时报错。
|
|
132
|
+
*/
|
|
133
|
+
agentOrigin?: string
|
|
134
|
+
host?: string
|
|
135
|
+
/** 宿主 BFF bootstrap;返回 token + agent_web_origin */
|
|
136
|
+
bootstrap?: () => Promise<BootstrapResult>
|
|
137
|
+
/**
|
|
138
|
+
* 打开 Widget 前钩子(如强制重新 bootstrap / establish)。
|
|
139
|
+
* Hub 可复用缓存 JWT;推沙盒等业务 API 需要 establish,应在此刷新。
|
|
140
|
+
*/
|
|
141
|
+
prepareOpenWidget?: () => Promise<void>
|
|
142
|
+
/** 获取 Agent JWT;未提供时由 bootstrap 缓存 token */
|
|
143
|
+
getToken?: () => Promise<string>
|
|
144
|
+
/**
|
|
145
|
+
* 按需读取宿主上下文(系统 / 名称 / 描述 / 菜单)。
|
|
146
|
+
* Hub 场景弹窗打开时会 `request-host-context`;优先走此回调,无需路由全局监测。
|
|
147
|
+
*/
|
|
148
|
+
getHostContext?: () => HostPageContext | Promise<HostPageContext>
|
|
149
|
+
showHub?: boolean
|
|
150
|
+
onBootstrap?: (result: BootstrapResult) => void
|
|
151
|
+
onTaskComplete?: (payload: {
|
|
152
|
+
taskId: string
|
|
153
|
+
status: string
|
|
154
|
+
hostContext?: Record<string, unknown>
|
|
155
|
+
result?: string
|
|
156
|
+
apply?: Record<string, unknown>
|
|
157
|
+
}) => void
|
|
158
|
+
onAuthRequired?: () => void
|
|
159
|
+
}
|