@_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/src/index.ts ADDED
@@ -0,0 +1,410 @@
1
+ import { IframeBridge } from './iframe'
2
+ import {
3
+ buildActionLaunchPayload,
4
+ buildActionWidgetRequest,
5
+ clearHostPageStore,
6
+ dismissCurrentPageHint,
7
+ dispatchRegisteredPageRefresh,
8
+ getCurrentPageId,
9
+ getCurrentPagePrimaryAction,
10
+ getRegisteredPages,
11
+ isDirectHostAction,
12
+ registerHostPage,
13
+ serializePageActions,
14
+ setCurrentHostPage,
15
+ setHintUiListener,
16
+ replaceHostContext as applyReplaceHostContextStore,
17
+ getHostContext as readHostContextStore,
18
+ unregisterHostPage,
19
+ } from './store'
20
+ import type {
21
+ BootstrapResult,
22
+ HostInitOptions,
23
+ HostPageContext,
24
+ HostPrefilledContext,
25
+ OpenWorkflowOptions,
26
+ RegisterPageOptions,
27
+ } from './types'
28
+ import type { ChildToParentMessage } from './messages'
29
+
30
+ export type {
31
+ BootstrapResult,
32
+ HostActionKind,
33
+ HostAnchorRef,
34
+ HostHintPlacement,
35
+ HostInitOptions,
36
+ HostPageAction,
37
+ HostPageContext,
38
+ HostPrefilledContext,
39
+ HostPrefilledPatients,
40
+ HostRefreshPayload,
41
+ OpenWorkflowOptions,
42
+ RegisterPageOptions,
43
+ } from './types'
44
+ export { AGENT_HOST_MSG, isAgentHostMessage } from './messages'
45
+ export type { WidgetPayload, SerializedPageAction } from './messages'
46
+
47
+ let _init: HostInitOptions = {}
48
+ let _bridge: IframeBridge | null = null
49
+ let _cachedToken = ''
50
+ let _cachedRefreshToken = ''
51
+ let _initPromise: Promise<void> | null = null
52
+ let _hubRunningCount = 0
53
+
54
+ function resolveAgentOrigin(): string {
55
+ const origin = (_init.agentOrigin || '').replace(/\/$/, '')
56
+ if (!origin) {
57
+ throw new Error(
58
+ 'Agent 前端地址未配置:请在 init({ agentOrigin }) 传入,或由宿主 BFF bootstrap 返回 agent_web_origin',
59
+ )
60
+ }
61
+ return origin
62
+ }
63
+
64
+ function applyBootstrap(boot: BootstrapResult) {
65
+ if (boot.agent_web_origin) {
66
+ _init.agentOrigin = boot.agent_web_origin.replace(/\/$/, '')
67
+ }
68
+ if (boot.agent_token) {
69
+ _cachedToken = boot.agent_token
70
+ }
71
+ if (boot.refresh_token) {
72
+ _cachedRefreshToken = boot.refresh_token
73
+ }
74
+ _init.onBootstrap?.(boot)
75
+ }
76
+
77
+ async function ensureToken(): Promise<string> {
78
+ if (_init.getToken) {
79
+ _cachedToken = await _init.getToken()
80
+ return _cachedToken
81
+ }
82
+ if (_cachedToken) return _cachedToken
83
+ if (_init.bootstrap) {
84
+ const boot = await _init.bootstrap()
85
+ applyBootstrap(boot)
86
+ if (_cachedToken) return _cachedToken
87
+ }
88
+ throw new Error('未连接 Agent:请配置 bootstrap 或 getToken')
89
+ }
90
+
91
+ function refreshLauncherHint() {
92
+ if (!_init.agentOrigin) return
93
+ try {
94
+ ensureBridge().updateLauncherHint(getCurrentPagePrimaryAction())
95
+ } catch {
96
+ /* agentOrigin 尚未就绪 */
97
+ }
98
+ }
99
+
100
+ function ensureBridge(): IframeBridge {
101
+ const origin = resolveAgentOrigin()
102
+ if (!_bridge) {
103
+ setHintUiListener(refreshLauncherHint)
104
+ _bridge = new IframeBridge(origin, handleChildMessage, {
105
+ onHintUse: (pageId, actionId) => {
106
+ void launchPageAction(pageId, actionId)
107
+ },
108
+ onHintDismiss: () => {
109
+ dismissCurrentPageHint()
110
+ refreshLauncherHint()
111
+ },
112
+ })
113
+ if (_init.showHub !== false) {
114
+ _bridge.mountHub()
115
+ }
116
+ }
117
+ return _bridge
118
+ }
119
+
120
+ function syncHubIframeState() {
121
+ if (!_bridge || !_init.agentOrigin) return
122
+ for (const { pageId, actions } of getRegisteredPages()) {
123
+ _bridge.sendToHub({
124
+ channel: 'agent-host',
125
+ type: 'register-page',
126
+ pageId,
127
+ actions: serializePageActions(actions),
128
+ })
129
+ }
130
+ _bridge.sendToHub({
131
+ channel: 'agent-host',
132
+ type: 'set-page',
133
+ pageId: getCurrentPageId(),
134
+ })
135
+ refreshLauncherHint()
136
+ }
137
+
138
+ function handleChildMessage(msg: ChildToParentMessage) {
139
+ if (msg.type === 'ready' && msg.surface === 'hub') {
140
+ void ensureToken()
141
+ .then((token) => {
142
+ _bridge?.sendToHub({
143
+ channel: 'agent-host',
144
+ type: 'init',
145
+ token,
146
+ refreshToken: _cachedRefreshToken || undefined,
147
+ })
148
+ syncHubIframeState()
149
+ })
150
+ .catch(() => {})
151
+ }
152
+ if (msg.type === 'started') {
153
+ _bridge?.closeWidget()
154
+ _bridge?.openHubPanel()
155
+ _bridge?.sendToHub({
156
+ channel: 'agent-host',
157
+ type: 'task-started',
158
+ taskId: msg.taskId,
159
+ sessionId: msg.sessionId,
160
+ workflow: msg.workflow,
161
+ label: msg.label || msg.workflow,
162
+ formValues: msg.formValues,
163
+ prefilled: msg.prefilled,
164
+ hostContext: msg.hostContext,
165
+ hubContinue: msg.hubContinue,
166
+ })
167
+ }
168
+ if (msg.type === 'complete') {
169
+ // 成功回填;失败/取消也刷新,便于宿主关掉 OCR loading 等 UI
170
+ if (msg.status === 'success' || msg.status === 'failed' || msg.status === 'cancelled') {
171
+ const refreshKey = msg.hostContext && (msg.hostContext as { refresh?: unknown }).refresh
172
+ dispatchRegisteredPageRefresh(refreshKey, {
173
+ taskId: msg.taskId,
174
+ result: msg.result,
175
+ hostContext: msg.hostContext,
176
+ apply: msg.apply,
177
+ })
178
+ }
179
+ _init.onTaskComplete?.({
180
+ taskId: msg.taskId,
181
+ status: msg.status,
182
+ hostContext: msg.hostContext,
183
+ result: msg.result,
184
+ apply: msg.apply,
185
+ })
186
+ _bridge?.closeWidget()
187
+ }
188
+ if (msg.type === 'close') {
189
+ _bridge?.closeWidget()
190
+ }
191
+ if (msg.type === 'open-widget') {
192
+ void openWidgetIframe(msg.payload)
193
+ }
194
+ if (msg.type === 'open-action') {
195
+ void launchPageAction(msg.pageId, msg.actionId)
196
+ }
197
+ if (msg.type === 'hub-stats') {
198
+ _hubRunningCount = msg.running
199
+ _bridge?.updateLauncherStats(msg.running, msg.waiting)
200
+ refreshLauncherHint()
201
+ }
202
+ if (msg.type === 'request-host-context') {
203
+ void respondHostContextRequest()
204
+ }
205
+ }
206
+
207
+ async function respondHostContextRequest() {
208
+ if (!_bridge || !_init.agentOrigin) return
209
+ let raw: HostPageContext = {}
210
+ try {
211
+ if (_init.getHostContext) {
212
+ raw = (await _init.getHostContext()) || {}
213
+ } else {
214
+ raw = readHostContextStore()
215
+ }
216
+ } catch {
217
+ raw = readHostContextStore()
218
+ }
219
+ const context = applyReplaceHostContextStore(raw)
220
+ _bridge.sendToHub({
221
+ channel: 'agent-host',
222
+ type: 'set-host-context',
223
+ context,
224
+ })
225
+ }
226
+
227
+ async function launchPageAction(pageId: string, actionId: string) {
228
+ const packed = buildActionLaunchPayload(pageId, actionId)
229
+ if (!packed) return
230
+ if (isDirectHostAction(packed.action)) {
231
+ await startDirectRun(packed)
232
+ return
233
+ }
234
+ const req = buildActionWidgetRequest(pageId, actionId)
235
+ if (req) await openWidgetIframe(req)
236
+ }
237
+
238
+ async function startDirectRun(packed: NonNullable<ReturnType<typeof buildActionLaunchPayload>>) {
239
+ await ensureInitialized()
240
+ if (packed.action.workflow && !packed.action.skill && _init.prepareOpenWidget) {
241
+ await _init.prepareOpenWidget()
242
+ }
243
+ const token = await ensureToken()
244
+ const bridge = ensureBridge()
245
+ bridge.openHubPanel()
246
+ bridge.sendToHub({
247
+ channel: 'agent-host',
248
+ type: 'init',
249
+ token,
250
+ refreshToken: _cachedRefreshToken || undefined,
251
+ })
252
+ bridge.sendToHub({
253
+ channel: 'agent-host',
254
+ type: 'run-direct',
255
+ workflow: packed.action.workflow || packed.action.skill || '',
256
+ title: packed.action.label,
257
+ host: packed.action.host || _init.host,
258
+ skill: packed.action.skill,
259
+ noTools: packed.action.noTools === true,
260
+ prefilled: packed.prefilled,
261
+ hostContext: packed.hostContext,
262
+ })
263
+ }
264
+
265
+ async function openWidgetIframe(payload: OpenWorkflowOptions & { workflow: string }) {
266
+ await ensureInitialized()
267
+ if (_init.prepareOpenWidget) {
268
+ await _init.prepareOpenWidget()
269
+ }
270
+ const bridge = ensureBridge()
271
+ const token = await ensureToken()
272
+ bridge.openWidget(
273
+ {
274
+ workflow: payload.workflow,
275
+ title: payload.title || payload.label,
276
+ host: payload.host || _init.host,
277
+ prefilled: payload.prefilled,
278
+ hostContext: payload.hostContext,
279
+ sessionId: payload.sessionId,
280
+ preferSandboxFiles: payload.preferSandboxFiles,
281
+ },
282
+ token,
283
+ _cachedRefreshToken || undefined,
284
+ )
285
+ }
286
+
287
+ async function ensureInitialized() {
288
+ if (_initPromise) await _initPromise
289
+ }
290
+
291
+ async function runInit(opts: HostInitOptions) {
292
+ _init = { showHub: true, ...opts }
293
+ if (_init.bootstrap) {
294
+ const boot = await _init.bootstrap()
295
+ applyBootstrap(boot)
296
+ }
297
+ // 配置了 bootstrap 时必须有 agent_web_origin,禁止回退到宿主 origin(避免 Hub 指到 EDC 自己)
298
+ if (!_init.agentOrigin) {
299
+ return
300
+ }
301
+ if (_init.showHub !== false && typeof document !== 'undefined') {
302
+ try {
303
+ ensureBridge()
304
+ const token = await ensureToken()
305
+ _bridge?.sendToHub({
306
+ channel: 'agent-host',
307
+ type: 'init',
308
+ token,
309
+ refreshToken: _cachedRefreshToken || undefined,
310
+ })
311
+ } catch {
312
+ /* 未登录或 bootstrap 失败:不挂 Hub */
313
+ }
314
+ }
315
+ }
316
+
317
+ export const AgentHost = {
318
+ async init(opts: HostInitOptions): Promise<void> {
319
+ if (_initPromise) await _initPromise
320
+ _initPromise = runInit(opts).finally(() => {
321
+ _initPromise = null
322
+ })
323
+ await _initPromise
324
+ },
325
+
326
+ async getToken(): Promise<string> {
327
+ await ensureInitialized()
328
+ return ensureToken()
329
+ },
330
+
331
+ async registerPage(
332
+ pageId: string,
333
+ actions: Parameters<typeof registerHostPage>[1],
334
+ options: RegisterPageOptions = {},
335
+ ) {
336
+ await ensureInitialized()
337
+ registerHostPage(pageId, actions, options)
338
+ if (_init.agentOrigin) {
339
+ ensureBridge().sendToHub({
340
+ channel: 'agent-host',
341
+ type: 'register-page',
342
+ pageId,
343
+ actions: serializePageActions(actions),
344
+ })
345
+ }
346
+ refreshLauncherHint()
347
+ },
348
+
349
+ /** 最近一次按需回写的宿主上下文(系统 / 名称 / 描述 / 菜单) */
350
+ getHostContext() {
351
+ return readHostContextStore()
352
+ },
353
+
354
+ async unregisterPage(pageId: string) {
355
+ await ensureInitialized()
356
+ unregisterHostPage(pageId)
357
+ if (_init.agentOrigin) {
358
+ ensureBridge().sendToHub({ channel: 'agent-host', type: 'unregister-page', pageId })
359
+ ensureBridge().sendToHub({ channel: 'agent-host', type: 'set-page', pageId: null })
360
+ }
361
+ refreshLauncherHint()
362
+ },
363
+
364
+ async setCurrentPage(pageId: string | null) {
365
+ await ensureInitialized()
366
+ setCurrentHostPage(pageId)
367
+ if (_init.agentOrigin) {
368
+ ensureBridge().sendToHub({ channel: 'agent-host', type: 'set-page', pageId })
369
+ }
370
+ refreshLauncherHint()
371
+ },
372
+
373
+ async openAction(pageId: string, actionId: string) {
374
+ const packed = buildActionLaunchPayload(pageId, actionId)
375
+ if (!packed) throw new Error('当前操作不可用')
376
+ await launchPageAction(pageId, actionId)
377
+ },
378
+
379
+ async openWorkflow(workflowName: string, opts: OpenWorkflowOptions = {}) {
380
+ await openWidgetIframe({
381
+ workflow: workflowName,
382
+ ...opts,
383
+ host: opts.host || _init.host,
384
+ title: opts.title || opts.label || workflowName,
385
+ })
386
+ },
387
+
388
+ /**
389
+ * 兼容保留。场景能否启动由宿主 `action.enabled` 判断,SDK 不识别具体 workflow/skill slug。
390
+ */
391
+ canOpen(_workflowName?: string, _opts?: { prefilled?: HostPrefilledContext & Record<string, unknown> }): boolean {
392
+ return true
393
+ },
394
+
395
+ destroy() {
396
+ _bridge?.destroy()
397
+ _bridge = null
398
+ _cachedToken = ''
399
+ _cachedRefreshToken = ''
400
+ _init = {}
401
+ _initPromise = null
402
+ _hubRunningCount = 0
403
+ setHintUiListener(null)
404
+ clearHostPageStore()
405
+ },
406
+ }
407
+
408
+ if (typeof window !== 'undefined') {
409
+ ;(window as unknown as { AgentHost: typeof AgentHost }).AgentHost = AgentHost
410
+ }
@@ -0,0 +1,102 @@
1
+ /** postMessage 契约:宿主页面 ↔ Agent 嵌入 iframe */
2
+
3
+ export const AGENT_HOST_MSG = 'agent-host' as const
4
+
5
+ export type WidgetPayload = {
6
+ workflow: string
7
+ title?: string
8
+ host?: string
9
+ prefilled?: Record<string, unknown>
10
+ hostContext?: Record<string, unknown>
11
+ /** 复用已有会话沙盒(续聊工作流) */
12
+ sessionId?: string
13
+ /** 文件字段优先从会话沙盒选择,而非本地上传 */
14
+ preferSandboxFiles?: boolean
15
+ }
16
+
17
+ export type SerializedPageAction = {
18
+ id: string
19
+ workflow: string
20
+ label: string
21
+ host?: string
22
+ hint?: string
23
+ hostContext?: Record<string, unknown>
24
+ prefilled?: Record<string, unknown>
25
+ /** registerPage 时在宿主侧求值,供 iframe hint 展示/点击判断 */
26
+ enabled?: boolean
27
+ /** 缺省视为 widget;direct 由 Hub 无弹窗启动 */
28
+ kind?: 'widget' | 'direct'
29
+ skill?: string
30
+ noTools?: boolean
31
+ }
32
+
33
+ export type ParentToChildMessage =
34
+ | { channel: typeof AGENT_HOST_MSG; type: 'init'; token: string; refreshToken?: string }
35
+ | { channel: typeof AGENT_HOST_MSG; type: 'register-page'; pageId: string; actions: SerializedPageAction[] }
36
+ | { channel: typeof AGENT_HOST_MSG; type: 'unregister-page'; pageId: string }
37
+ | { channel: typeof AGENT_HOST_MSG; type: 'set-page'; pageId: string | null }
38
+ /** 宿主对 request-host-context 的回写(整份上下文) */
39
+ | { channel: typeof AGENT_HOST_MSG; type: 'set-host-context'; context: Record<string, unknown> }
40
+ | { channel: typeof AGENT_HOST_MSG; type: 'open-widget'; payload: WidgetPayload }
41
+ | { channel: typeof AGENT_HOST_MSG; type: 'open-hub-panel' }
42
+ | { channel: typeof AGENT_HOST_MSG; type: 'close-hub-panel' }
43
+ | {
44
+ channel: typeof AGENT_HOST_MSG
45
+ type: 'run-direct'
46
+ workflow: string
47
+ title?: string
48
+ host?: string
49
+ skill?: string
50
+ noTools?: boolean
51
+ prefilled?: Record<string, unknown>
52
+ hostContext?: Record<string, unknown>
53
+ }
54
+ | {
55
+ channel: typeof AGENT_HOST_MSG
56
+ type: 'task-started'
57
+ taskId: string
58
+ sessionId?: string
59
+ workflow: string
60
+ label?: string
61
+ formValues?: Record<string, unknown>
62
+ prefilled?: Record<string, unknown>
63
+ hostContext?: Record<string, unknown>
64
+ hubContinue?: boolean
65
+ }
66
+
67
+ export type ChildToParentMessage =
68
+ | { channel: typeof AGENT_HOST_MSG; type: 'ready'; surface: 'hub' | 'widget' }
69
+ | { channel: typeof AGENT_HOST_MSG; type: 'hub-panel-close' }
70
+ | { channel: typeof AGENT_HOST_MSG; type: 'hub-panel-width'; expanded: boolean }
71
+ | { channel: typeof AGENT_HOST_MSG; type: 'hub-stats'; running: number; waiting: number; done: number }
72
+ | { channel: typeof AGENT_HOST_MSG; type: 'started'; taskId: string; sessionId?: string; workflow: string; label?: string; formValues?: Record<string, unknown>; prefilled?: Record<string, unknown>; hostContext?: Record<string, unknown>; hubContinue?: boolean }
73
+ | {
74
+ channel: typeof AGENT_HOST_MSG
75
+ type: 'complete'
76
+ taskId: string
77
+ status: string
78
+ hostContext?: Record<string, unknown>
79
+ result?: string
80
+ apply?: Record<string, unknown>
81
+ }
82
+ | { channel: typeof AGENT_HOST_MSG; type: 'close' }
83
+ | { channel: typeof AGENT_HOST_MSG; type: 'open-widget'; payload: WidgetPayload }
84
+ | { channel: typeof AGENT_HOST_MSG; type: 'open-action'; pageId: string; actionId: string }
85
+ | { channel: typeof AGENT_HOST_MSG; type: 'widget-resize'; height: number }
86
+ /** Hub 按需向宿主索取当前上下文(配合 init.getHostContext) */
87
+ | { channel: typeof AGENT_HOST_MSG; type: 'request-host-context' }
88
+
89
+ export function isAgentHostMessage(data: unknown): data is ParentToChildMessage | ChildToParentMessage {
90
+ return (
91
+ typeof data === 'object' &&
92
+ data !== null &&
93
+ (data as { channel?: string }).channel === AGENT_HOST_MSG &&
94
+ typeof (data as { type?: string }).type === 'string'
95
+ )
96
+ }
97
+
98
+ /** postMessage 仅接受 plain object;剥离 Vue reactive / 函数等不可克隆值 */
99
+ export function cloneForPostMessage<T>(value: T): T {
100
+ if (value === undefined || value === null) return value
101
+ return JSON.parse(JSON.stringify(value)) as T
102
+ }