@sophonsai/agent-embed-sdk 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,321 @@
1
- # Temporary Holding Version
1
+ # @sophonsai/agent-embed-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## 应用接入
4
+
5
+ 传入服务地址 `endpoint` 和 Agent 的应用接入 `token`;可选 `baseUrl` 默认是 `/e/v1/embed`。
6
+ 最终 iframe 地址是 `endpoint + baseUrl + '/' + encodeURIComponent(token)`。
7
+
8
+ ```ts
9
+ const client = createAgentEmbed({ endpoint: 'https://agent.example.com', token: '应用接入Token' })
10
+ await client.mount(document.getElementById('agent-embed')!)
11
+ ```
12
+
13
+ 不再接收完整 `url`、旧 `initConfig`、Agent ID 或平台用户凭证。
14
+ `threadId`、外观配置、实例命令和事件回调继续支持。
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ # npm
20
+ npm install @sophonsai/agent-embed-sdk
21
+
22
+ # pnpm
23
+ pnpm add @sophonsai/agent-embed-sdk
24
+
25
+ # yarn
26
+ yarn add @sophonsai/agent-embed-sdk
27
+
28
+ # bun
29
+ bun add @sophonsai/agent-embed-sdk
30
+ ```
31
+
32
+ ## 快速开始
33
+
34
+ ```html
35
+ <div id="agent-embed" style="height: 640px"></div>
36
+ ```
37
+
38
+ ```ts
39
+ import { createAgentEmbed, type AgentAppearance, type AgentEmbedInstance } from '@sophonsai/agent-embed-sdk'
40
+
41
+ const appearance: AgentAppearance = {
42
+ theme: { preset: 'glacier-blue' },
43
+ fontSize: 16,
44
+ header: {
45
+ historyButton: true,
46
+ newConversationButton: true,
47
+ themeSettingButton: false,
48
+ },
49
+ }
50
+
51
+ const client = createAgentEmbed({
52
+ endpoint: 'https://agent.example.com',
53
+ token: '应用接入Token',
54
+ threadId: 'optional-conversation-id',
55
+ appearance,
56
+ onReady(instance) {
57
+ instance.focusInput()
58
+ },
59
+ onInitError({ code, message }) {
60
+ console.error('初始化失败', code, message)
61
+ },
62
+ onSettled(result) {
63
+ console.log('本轮会话结束', result)
64
+ },
65
+ })
66
+
67
+ const container = document.getElementById('agent-embed')!
68
+
69
+ await client.mount(container)
70
+ const instance: AgentEmbedInstance = await client.whenReady()
71
+
72
+ instance.sendMessage({ text: '你好,请介绍一下自己' })
73
+ ```
74
+
75
+ 页面退出或不再使用面板时应主动销毁:
76
+
77
+ ```ts
78
+ client.destroy()
79
+ ```
80
+
81
+ ## 初始化配置
82
+
83
+ | 配置 | 类型 | 必填 | 说明 |
84
+ | ------------------- | ----------------------- | ---- | ----------------------------------------------- |
85
+ | `endpoint` | `string` | 是 | Gateway / Extension 服务地址,包含 HTTP(S) 协议 |
86
+ | `token` | `string` | 是 | Agent 应用接入凭证 |
87
+ | `baseUrl` | `string` | 否 | 嵌入页面路径,默认 `/e/v1/embed` |
88
+ | `threadId` | `string` | 否 | 初始会话 ID |
89
+ | `runtime_variables` | `AgentRuntimeVariables` | 否 | 新会话绑定的运行时变量,值必须为 JSON |
90
+ | `appearance` | `AgentAppearance` | 否 | 主题、字号与 Header 按钮 |
91
+
92
+ `endpoint` 可包含反向代理路径前缀;`baseUrl` 必须是以 `/` 开头的路径。
93
+ 凭证只写入 iframe 路径,bridge 初始化消息不再传递凭证或 Agent ID。
94
+ 配置与回调在创建 client 时保存为快照;切换接入地址、token、会话或回调时,销毁并重新创建 client。
95
+ 外观可以使用 `updateTheme`、`updateFontSize`、`updateHeader` 更新。
96
+
97
+ ## 外观配置
98
+
99
+ ```ts
100
+ const appearance: AgentAppearance = {
101
+ theme: { preset: 'studio-white' },
102
+ fontSize: 16,
103
+ header: {
104
+ historyButton: true,
105
+ newConversationButton: true,
106
+ themeSettingButton: true,
107
+ },
108
+ }
109
+ ```
110
+
111
+ `header.themeSettingButton` 默认值为 `false`,只有显式设置为 `true` 时才显示主题设置按钮。
112
+
113
+ ### 可用主题
114
+
115
+ 可以使用 `AgentEmbedThemePreset` 枚举式常量设置主题:
116
+
117
+ ```ts
118
+ import { AgentEmbedThemePreset } from '@sophonsai/agent-embed-sdk'
119
+
120
+ client.updateTheme({ preset: AgentEmbedThemePreset.GlacierBlue })
121
+ ```
122
+
123
+ | 主题名 | preset | 类型 |
124
+ | ------ | ----------------- | -------- |
125
+ | 曜石紫 | `obsidian-violet` | 深色主题 |
126
+ | 石墨青 | `graphite-cyan` | 深色主题 |
127
+ | 午夜靛 | `midnight-indigo` | 深色主题 |
128
+ | 深海蓝 | `deep-ocean` | 深色主题 |
129
+ | 森野绿 | `forest-emerald` | 深色主题 |
130
+ | 影棚白 | `studio-white` | 浅色主题 |
131
+ | 珍珠紫 | `pearl-violet` | 浅色主题 |
132
+ | 冰川蓝 | `glacier-blue` | 浅色主题 |
133
+ | 暖杏色 | `warm-cream` | 浅色主题 |
134
+ | 薰衣草 | `lavender-mist` | 浅色主题 |
135
+
136
+ ### 动态更新
137
+
138
+ 创建 client 后,只有外观支持动态更新:
139
+
140
+ ```ts
141
+ client.updateTheme({ preset: 'warm-cream' })
142
+ client.updateFontSize(18)
143
+ client.updateHeader({
144
+ historyButton: false,
145
+ newConversationButton: true,
146
+ themeSettingButton: false,
147
+ })
148
+ ```
149
+
150
+ 传入 `undefined` 会撤销对应的受控配置,让面板恢复内部默认值:
151
+
152
+ ```ts
153
+ client.updateTheme(undefined)
154
+ client.updateFontSize(undefined)
155
+ client.updateHeader(undefined)
156
+ ```
157
+
158
+ 面板内部主动切换主题或字号时,会调用 `onThemeChange` 或 `onFontSizeChange`。如果宿主把这些字段作为受控状态,应在回调中同步外部状态,再调用相应的更新方法。
159
+
160
+ ## `mount` 与 `ready`
161
+
162
+ 这两个阶段含义不同:
163
+
164
+ ```text
165
+ 挂载面板
166
+ → mount 完成
167
+ → 鉴权并加载智能体
168
+ → 面板就绪
169
+ → whenReady 完成
170
+ ```
171
+
172
+ ### `mount(container)`
173
+
174
+ `mount()` 会把 SDK 管理的面板挂载到指定容器。`mount()` 完成只表示面板已经启动,不表示面板已经初始化完成。
175
+
176
+ 以下情况会让 `mount()` reject:
177
+
178
+ - client 已销毁;
179
+ - 同一个 client 被重复挂载;
180
+ - 面板无法挂载到指定容器;
181
+ - 面板启动失败;
182
+ - 在挂载完成前调用了 `destroy()`。
183
+
184
+ 当前没有内置 mount 超时。如果面板持续处于加载中且没有返回错误,Promise 会保持 pending,业务可按需在外层增加超时。
185
+
186
+ ### `whenReady()`
187
+
188
+ `whenReady()` 等待面板初始化完成,并返回可调用的 `AgentEmbedInstance`。它可以调用多次;ready 后再次调用会立即返回同一个 instance。
189
+
190
+ 在 ready 之前调用 instance 命令会抛出 `AgentEmbed is not ready.`。推荐只保存 `whenReady()` 或 `onReady` 返回的实例。
191
+
192
+ 初始化失败通过 `onInitError` 报告,同时 `whenReady()` reject。Vue 使用 `@init-error`。
193
+
194
+ ## 实例方法
195
+
196
+ `AgentEmbedInstance` 提供以下命令:
197
+
198
+ | 方法 | 说明 |
199
+ | ----------------------------------- | ------------------------------------------------------- |
200
+ | `sendMessage({ text })` | 立即提交一条消息;正在生成回复时会使用面板的 steer 行为 |
201
+ | `addInputQuery({ text, context? })` | 把文本和可选上下文写入输入框,但不立即提交 |
202
+ | `abortResponse()` | 终止当前回复 |
203
+ | `openHistoryPanel()` | 打开历史会话面板 |
204
+ | `openThemeSettingsPanel()` | 打开主题设置面板 |
205
+ | `newConversation()` | 新建会话 |
206
+ | `focusInput()` | 聚焦输入框 |
207
+ | `scrollToBottom()` | 滚动到消息底部 |
208
+
209
+ 示例:
210
+
211
+ ```ts
212
+ const agent = await client.whenReady()
213
+
214
+ agent.addInputQuery({
215
+ text: '请解释这段内容',
216
+ context: '用户在页面中选中的文本',
217
+ })
218
+ agent.focusInput()
219
+ ```
220
+
221
+ ## 生命周期回调
222
+
223
+ | 回调 | 参数 | 说明 |
224
+ | ------------------ | --------------------- | -------------------- |
225
+ | `onReady` | `AgentEmbedInstance` | 面板初始化完成 |
226
+ | `onThemeChange` | `AgentEmbedTheme` | 面板内部主题发生变化 |
227
+ | `onFontSizeChange` | `number` | 面板内部字号发生变化 |
228
+ | `onInitError` | `AgentEmbedInitError` | 初始化失败 |
229
+
230
+ ### `onReady(instance)`
231
+
232
+ 面板初始化完成后调用,参数与 `whenReady()` 返回值相同。
233
+
234
+ ### `onInitError(error)`
235
+
236
+ 初始化失败时调用一次,`error` 包含 `code` 和 `message`:
237
+
238
+ | code | 含义 |
239
+ | ------------------- | --------------------- |
240
+ | `invalid_token` | 凭证无效或已失效 |
241
+ | `forbidden` | 当前凭证无访问权限 |
242
+ | `agent_not_found` | 智能体不存在 |
243
+ | `request_failed` | 网络或服务请求失败 |
244
+ | `frame_load_failed` | iframe 加载或挂载失败 |
245
+
246
+ 失败后不会触发 `onReady`,当前与后续 `whenReady()` 都会 reject,拒绝值为包含相同 `code`、`message` 的 `Error`。握手前失败还会使 `mount()` reject;已完成的 `mount()` 不受影响。聊天运行失败仍使用 `onError`。
247
+
248
+ ## 会话事件
249
+
250
+ 可以在 `createAgentEmbed()` 中传入以下全部运行事件回调:
251
+
252
+ | 回调 | 参数 | 说明 |
253
+ | --------------------------- | --------------------------------------------------------------- | ------------------------------------------- |
254
+ | `onConcurrentSend` | 无 | 回复生成期间再次发送消息 |
255
+ | `onEvent` | `AgentServerEvent` | 接收原始服务端事件 |
256
+ | `onSessionContextReady` | `AgentSessionContext` | 会话上下文可用,包含 `threadId` 和 `turnId` |
257
+ | `onToolCallCreated` | `AgentToolCallCreatedEvent`, `AgentSessionEventContext` | 创建一个工具调用 |
258
+ | `onToolApprovalRequested` | `AgentToolApprovalRequestedEvent`, `AgentSessionEventContext` | 一组工具调用需要确认 |
259
+ | `onToolCallStarted` | `AgentToolCallStartedEvent`, `AgentSessionEventContext` | 工具调用开始 |
260
+ | `onToolCallProgressUpdated` | `AgentToolCallProgressUpdatedEvent`, `AgentSessionEventContext` | 工具调用进度更新 |
261
+ | `onToolCallBackgrounded` | `AgentToolCallBackgroundedEvent`, `AgentSessionEventContext` | 工具调用转为后台执行 |
262
+ | `onToolCallSucceeded` | `AgentToolCallSucceededEvent`, `AgentSessionEventContext` | 工具调用成功 |
263
+ | `onToolCallFailed` | `AgentToolCallFailedEvent`, `AgentSessionEventContext` | 工具调用失败 |
264
+ | `onToolCallCancelled` | `AgentToolCallCancelledEvent`, `AgentSessionEventContext` | 工具调用取消 |
265
+ | `onClientActionRequest` | `AgentClientActionRequestEvent`, `AgentSessionEventContext` | 智能体请求宿主执行客户端动作 |
266
+ | `onUserFeedbackConsumed` | `AgentUserFeedbackConsumedEvent`, `AgentSessionEventContext` | 用户反馈已被处理 |
267
+ | `onUserSteerConsumed` | `AgentUserSteerConsumedEvent`, `AgentSessionEventContext` | 用户 steer 指令已被处理 |
268
+ | `onUserSteerCancelled` | `AgentUserSteerCancelledEvent`, `AgentSessionRunEventContext` | 用户 steer 指令已取消 |
269
+ | `onUserInputSubmitted` | `AgentUserInputSubmittedEvent`, `AgentSessionEventContext` | 用户输入已提交 |
270
+ | `onSessionTurnStarted` | `AgentSessionTurnStartedEvent`, `AgentSessionEventContext` | Turn 开始执行 |
271
+ | `onSessionTurnSucceeded` | `AgentSessionTurnSucceededEvent`, `AgentSessionEventContext` | Turn 执行成功 |
272
+ | `onSessionTurnFailed` | `AgentSessionTurnFailedEvent`, `AgentSessionEventContext` | Turn 执行失败 |
273
+ | `onSessionTurnCancelled` | `AgentSessionTurnCancelledEvent`, `AgentSessionEventContext` | Turn 已取消 |
274
+ | `onSessionRunStarted` | `AgentSessionRunStartedEvent`, `AgentSessionRunEventContext` | Run 已开始 |
275
+ | `onSessionRunIdled` | `AgentSessionRunIdledEvent`, `AgentSessionRunEventContext` | Run 已空闲 |
276
+ | `onSessionRunEnded` | `AgentSessionRunEndedEvent`, `AgentSessionRunEventContext` | Run 已结束 |
277
+ | `onSystemMessageConsumed` | `AgentSystemMessageConsumedEvent`, `AgentSessionEventContext` | 系统消息已被处理 |
278
+ | `onSuccess` | `AgentSessionSuccess` | 本轮会话成功结束 |
279
+ | `onError` | `AgentSessionFailure` | 本轮会话失败 |
280
+ | `onCancelled` | `AgentSessionCancelled` | 本轮会话取消 |
281
+ | `onSettled` | `AgentSessionSettlement` | 本轮会话以任意状态结束 |
282
+
283
+ 所有事件、事件参数及上下文类型都从包中导出,可以直接用于 TypeScript 标注。
284
+
285
+ ```ts
286
+ const client = createAgentEmbed({
287
+ endpoint,
288
+ token,
289
+ onToolCallFailed(event, context) {
290
+ console.error(context.turnId, event.toolName, event.error)
291
+ },
292
+ onSettled(result) {
293
+ if (result.status === 'failure') console.error(result.error)
294
+ },
295
+ })
296
+ ```
297
+
298
+ 用户回调内部抛出的异常会被 SDK 隔离,不会影响其他回调或面板后续运行。
299
+
300
+ ## 销毁与清理
301
+
302
+ ```ts
303
+ client.destroy()
304
+ ```
305
+
306
+ `destroy()` 会:
307
+
308
+ - 移除面板;
309
+ - 注销浏览器事件监听器;
310
+ - 拒绝尚未完成的 mount/ready 等待;
311
+ - 使 client 和已经获得的 instance 失效。
312
+
313
+ 重复调用 `destroy()` 是安全的。销毁后调用外观更新或实例命令会抛错,销毁后的 client 也不能重新挂载。
314
+
315
+ ### 运行时变量
316
+
317
+ `runtime_variables` 是可选的 JSON 对象,名称须与智能体声明的运行时变量一致。仅创建新会话时发送,已有会话沿用已绑定的变量;同一面板新建会话时复用初始化值。参数在初始化时生成快照,改变值需要重建面板。数据通过 iframe 消息传递,不写入接入 URL。
318
+
319
+ ```ts
320
+ const panel = createAgentEmbed({ endpoint, token, runtime_variables: { customer_id: 'customer-1' } })
321
+ ```
@@ -0,0 +1,54 @@
1
+ import { AgentAppearance as AgentAppearance$1, AgentEmbedHeaderAppearance, AgentEmbedInitError, AgentEmbedInstance, AgentEmbedTheme as AgentEmbedTheme$1, AgentEvents, AgentRuntimeVariables } from "@sophonsai/agent-embed-core";
2
+ export * from "@sophonsai/agent-embed-core";
3
+ //#region src/theme-presets.d.ts
4
+ declare const AgentEmbedThemePreset: {
5
+ readonly ObsidianViolet: "obsidian-violet";
6
+ readonly GraphiteCyan: "graphite-cyan";
7
+ readonly MidnightIndigo: "midnight-indigo";
8
+ readonly DeepOcean: "deep-ocean";
9
+ readonly ForestEmerald: "forest-emerald";
10
+ readonly StudioWhite: "studio-white";
11
+ readonly PearlViolet: "pearl-violet";
12
+ readonly GlacierBlue: "glacier-blue";
13
+ readonly WarmCream: "warm-cream";
14
+ readonly LavenderMist: "lavender-mist";
15
+ };
16
+ type AgentEmbedThemePreset = (typeof AgentEmbedThemePreset)[keyof typeof AgentEmbedThemePreset];
17
+ //#endregion
18
+ //#region src/index.d.ts
19
+ interface AgentEmbedTheme extends Omit<AgentEmbedTheme$1, 'preset'> {
20
+ preset: AgentEmbedThemePreset;
21
+ }
22
+ interface AgentAppearance extends Omit<AgentAppearance$1, 'theme'> {
23
+ theme?: AgentEmbedTheme;
24
+ }
25
+ interface AgentEmbedOptions extends AgentEvents {
26
+ appearance?: AgentAppearance;
27
+ onReady?: (instance: AgentEmbedInstance) => void;
28
+ onThemeChange?: (theme: AgentEmbedTheme) => void;
29
+ onFontSizeChange?: (fontSize: number) => void;
30
+ onInitError?: (error: AgentEmbedInitError) => void;
31
+ }
32
+ interface CreateAgentEmbedOptions extends AgentEmbedOptions {
33
+ /** Gateway / Extension service address, including HTTP or HTTPS scheme. */
34
+ endpoint: string;
35
+ /** Shared Agent access token. */
36
+ token: string;
37
+ /** Embed page path. Defaults to /e/v1/embed. */
38
+ baseUrl?: string;
39
+ threadId?: string;
40
+ /** JSON values bound to each new conversation; snapshotted at client creation. */
41
+ runtime_variables?: AgentRuntimeVariables;
42
+ }
43
+ interface AgentEmbedClient {
44
+ mount(container: HTMLElement): Promise<void>;
45
+ whenReady(): Promise<AgentEmbedInstance>;
46
+ updateTheme(theme: AgentEmbedTheme | undefined): void;
47
+ updateFontSize(fontSize: number | undefined): void;
48
+ updateHeader(header: AgentEmbedHeaderAppearance | undefined): void;
49
+ destroy(): void;
50
+ }
51
+ declare const DEFAULT_AGENT_EMBED_BASE_URL = "/e/v1/embed";
52
+ declare const createAgentEmbed: (options: CreateAgentEmbedOptions) => AgentEmbedClient;
53
+ //#endregion
54
+ export { AgentAppearance, AgentEmbedClient, AgentEmbedTheme, AgentEmbedThemePreset, CreateAgentEmbedOptions, DEFAULT_AGENT_EMBED_BASE_URL, createAgentEmbed };
package/dist/index.mjs ADDED
@@ -0,0 +1,273 @@
1
+ import { AGENT_EMBED_BRIDGE, AGENT_EMBED_BRIDGE_VERSION, AGENT_EMBED_QUERY_PARAMS, AGENT_EVENT_NAMES, isAgentEmbedFrameMessage, isAgentRuntimeVariables } from "@sophonsai/agent-embed-core";
2
+ export * from "@sophonsai/agent-embed-core";
3
+ //#region src/theme-presets.ts
4
+ const AgentEmbedThemePreset = {
5
+ ObsidianViolet: "obsidian-violet",
6
+ GraphiteCyan: "graphite-cyan",
7
+ MidnightIndigo: "midnight-indigo",
8
+ DeepOcean: "deep-ocean",
9
+ ForestEmerald: "forest-emerald",
10
+ StudioWhite: "studio-white",
11
+ PearlViolet: "pearl-violet",
12
+ GlacierBlue: "glacier-blue",
13
+ WarmCream: "warm-cream",
14
+ LavenderMist: "lavender-mist"
15
+ };
16
+ //#endregion
17
+ //#region src/index.ts
18
+ const DEFAULT_AGENT_EMBED_BASE_URL = "/e/v1/embed";
19
+ const createInstanceId = () => `agent-${Date.now()}-${Math.random().toString(36).slice(2)}`;
20
+ const pickAgentEvents = (source) => Object.fromEntries(AGENT_EVENT_NAMES.map((name) => [name, source[name]]));
21
+ const cloneAppearance = (appearance) => {
22
+ const result = {};
23
+ if (appearance?.theme) result.theme = { ...appearance.theme };
24
+ if (appearance?.fontSize !== void 0) result.fontSize = appearance.fontSize;
25
+ if (appearance?.header) result.header = { ...appearance.header };
26
+ return result;
27
+ };
28
+ var AgentEmbedClientImpl = class {
29
+ constructor(options) {
30
+ this.instanceId = createInstanceId();
31
+ this.mounted = false;
32
+ this.initialized = false;
33
+ this.ready = false;
34
+ this.destroyed = false;
35
+ this.readyWaiters = /* @__PURE__ */ new Set();
36
+ this.handleFrameError = () => {
37
+ this.failInitialization({
38
+ code: "frame_load_failed",
39
+ message: "Failed to load AgentEmbed iframe."
40
+ });
41
+ };
42
+ this.handleMessage = (event) => {
43
+ if (event.source !== this.iframe.contentWindow || event.origin !== this.targetOrigin) return;
44
+ if (!isAgentEmbedFrameMessage(event.data)) return;
45
+ const message = event.data;
46
+ if (message.type === "hello") {
47
+ const firstInitialization = !this.initialized;
48
+ this.initialized = true;
49
+ this.postInit();
50
+ if (firstInitialization) {
51
+ this.settleMount?.();
52
+ this.clearMountSettlement();
53
+ }
54
+ return;
55
+ }
56
+ if (message.instanceId !== this.instanceId) return;
57
+ if (message.type === "ready") {
58
+ if (this.ready || this.destroyed || this.initError) return;
59
+ this.ready = true;
60
+ this.resolveReadyWaiters();
61
+ try {
62
+ this.onReady?.(this.instance);
63
+ } catch {}
64
+ return;
65
+ }
66
+ if (message.type !== "callback" || !message.callback) return;
67
+ if (message.callback === "onThemeChange") {
68
+ try {
69
+ this.onThemeChange?.(message.args?.[0]);
70
+ } catch {}
71
+ return;
72
+ }
73
+ if (message.callback === "onFontSizeChange") {
74
+ try {
75
+ this.onFontSizeChange?.(message.args?.[0]);
76
+ } catch {}
77
+ return;
78
+ }
79
+ if (message.callback === "onInitError") {
80
+ this.failInitialization(message.args?.[0]);
81
+ return;
82
+ }
83
+ const callback = this.events[message.callback];
84
+ try {
85
+ callback?.(...message.args ?? []);
86
+ } catch {}
87
+ };
88
+ if (typeof options.token !== "string" || !options.token.trim() || options.token === "." || options.token === "..") throw new Error("A valid Agent access token is required.");
89
+ const endpoint = new URL(options.endpoint);
90
+ if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") throw new Error("Panel endpoint must use HTTP or HTTPS.");
91
+ const baseUrl = options.baseUrl ?? "/e/v1/embed";
92
+ if (!baseUrl.startsWith("/") || /[?#\s]/.test(baseUrl)) throw new Error("Panel baseUrl must be a pathname starting with /.");
93
+ const basePath = baseUrl.replace(/^\/+|\/+$/g, "");
94
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, "")}/${basePath ? `${basePath}/` : ""}${encodeURIComponent(options.token)}`;
95
+ endpoint.search = "";
96
+ endpoint.hash = "";
97
+ this.url = endpoint.toString();
98
+ if (options.runtime_variables !== void 0 && !isAgentRuntimeVariables(options.runtime_variables)) throw new Error("runtime_variables must be a JSON object.");
99
+ this.runtimeVariables = options.runtime_variables === void 0 ? void 0 : JSON.parse(JSON.stringify(options.runtime_variables));
100
+ this.threadId = options.threadId;
101
+ this.targetOrigin = endpoint.origin;
102
+ this.appearance = cloneAppearance(options.appearance);
103
+ this.events = pickAgentEvents(options);
104
+ this.onReady = options.onReady;
105
+ this.onThemeChange = options.onThemeChange;
106
+ this.onFontSizeChange = options.onFontSizeChange;
107
+ this.onInitError = options.onInitError;
108
+ this.instance = {
109
+ sendMessage: (input) => this.command("sendMessage", [{ text: input.text }]),
110
+ addInputQuery: (input) => this.command("addInputQuery", [input.context === void 0 ? { text: input.text } : {
111
+ text: input.text,
112
+ context: input.context
113
+ }]),
114
+ abortResponse: () => this.command("abortResponse", []),
115
+ openHistoryPanel: () => this.command("openHistoryPanel", []),
116
+ openThemeSettingsPanel: () => this.command("openThemeSettingsPanel", []),
117
+ newConversation: () => this.command("newConversation", []),
118
+ focusInput: () => this.command("focusInput", []),
119
+ scrollToBottom: () => this.command("scrollToBottom", [])
120
+ };
121
+ this.iframe = document.createElement("iframe");
122
+ this.iframe.title = "Agent chat";
123
+ this.iframe.allow = "microphone";
124
+ this.iframe.style.cssText = "display:block;width:100%;height:100%;border:0;background:transparent;";
125
+ this.iframe.src = this.createSrc();
126
+ }
127
+ mount(container) {
128
+ if (this.destroyed) return Promise.reject(/* @__PURE__ */ new Error("AgentEmbed has been destroyed."));
129
+ if (this.mounted) return Promise.reject(/* @__PURE__ */ new Error("AgentEmbed has already been mounted."));
130
+ this.mounted = true;
131
+ window.addEventListener("message", this.handleMessage);
132
+ this.iframe.addEventListener("error", this.handleFrameError);
133
+ const promise = new Promise((resolve, reject) => {
134
+ this.settleMount = resolve;
135
+ this.rejectMount = reject;
136
+ });
137
+ try {
138
+ container.appendChild(this.iframe);
139
+ } catch (error) {
140
+ this.failInitialization({
141
+ code: "frame_load_failed",
142
+ message: error instanceof Error ? error.message : "Failed to mount AgentEmbed."
143
+ });
144
+ }
145
+ return promise;
146
+ }
147
+ whenReady() {
148
+ if (this.destroyed) return Promise.reject(/* @__PURE__ */ new Error("AgentEmbed has been destroyed."));
149
+ if (this.initError) return Promise.reject(this.initError);
150
+ if (this.ready) return Promise.resolve(this.instance);
151
+ return new Promise((resolve, reject) => {
152
+ this.readyWaiters.add({
153
+ resolve,
154
+ reject
155
+ });
156
+ });
157
+ }
158
+ updateTheme(theme) {
159
+ this.updateAppearance({ theme: theme ? { ...theme } : null });
160
+ }
161
+ updateFontSize(fontSize) {
162
+ this.updateAppearance({ fontSize: fontSize ?? null });
163
+ }
164
+ updateHeader(header) {
165
+ this.updateAppearance({ header: header ? { ...header } : null });
166
+ }
167
+ destroy() {
168
+ if (this.destroyed) return;
169
+ const wasInitialized = this.initialized;
170
+ this.destroyed = true;
171
+ this.initialized = false;
172
+ this.ready = false;
173
+ window.removeEventListener("message", this.handleMessage);
174
+ this.iframe.removeEventListener("error", this.handleFrameError);
175
+ this.iframe.remove();
176
+ const error = /* @__PURE__ */ new Error(wasInitialized ? "AgentEmbed was destroyed before becoming ready." : "AgentEmbed was destroyed before mounting completed.");
177
+ this.rejectMount?.(error);
178
+ this.rejectReadyWaiters(error);
179
+ this.clearMountSettlement();
180
+ }
181
+ createSrc() {
182
+ const url = new URL(this.url);
183
+ if (this.threadId) url.searchParams.set(AGENT_EMBED_QUERY_PARAMS.threadId, this.threadId);
184
+ if (this.appearance.theme) url.searchParams.set(AGENT_EMBED_QUERY_PARAMS.theme, this.appearance.theme.preset);
185
+ if (this.appearance.fontSize !== void 0) url.searchParams.set(AGENT_EMBED_QUERY_PARAMS.fontSize, String(this.appearance.fontSize));
186
+ const headerParams = [
187
+ [AGENT_EMBED_QUERY_PARAMS.headerHistoryButton, this.appearance.header?.historyButton],
188
+ [AGENT_EMBED_QUERY_PARAMS.headerNewConversationButton, this.appearance.header?.newConversationButton],
189
+ [AGENT_EMBED_QUERY_PARAMS.headerThemeSettingButton, this.appearance.header?.themeSettingButton]
190
+ ];
191
+ for (const [name, value] of headerParams) if (value !== void 0) url.searchParams.set(name, String(value));
192
+ return url.toString();
193
+ }
194
+ assertActive() {
195
+ if (this.destroyed) throw new Error("AgentEmbed has been destroyed.");
196
+ }
197
+ updateAppearance(patch) {
198
+ this.assertActive();
199
+ const appearance = cloneAppearance(this.appearance);
200
+ const messagePatch = {};
201
+ if (Object.prototype.hasOwnProperty.call(patch, "theme")) {
202
+ appearance.theme = patch.theme ? { ...patch.theme } : void 0;
203
+ messagePatch.theme = patch.theme ? { ...patch.theme } : null;
204
+ }
205
+ if (Object.prototype.hasOwnProperty.call(patch, "fontSize")) {
206
+ appearance.fontSize = patch.fontSize ?? void 0;
207
+ messagePatch.fontSize = patch.fontSize ?? null;
208
+ }
209
+ if (Object.prototype.hasOwnProperty.call(patch, "header")) {
210
+ appearance.header = patch.header ? { ...patch.header } : void 0;
211
+ messagePatch.header = patch.header ? { ...patch.header } : null;
212
+ }
213
+ this.appearance = appearance;
214
+ if (this.initialized) this.post({
215
+ type: "update-appearance",
216
+ appearance: messagePatch
217
+ });
218
+ }
219
+ command(action, args) {
220
+ this.assertActive();
221
+ if (!this.ready) throw new Error("AgentEmbed is not ready.");
222
+ this.post({
223
+ type: "command",
224
+ action,
225
+ args
226
+ });
227
+ }
228
+ postInit() {
229
+ this.post({
230
+ type: "init",
231
+ config: {
232
+ mode: "url",
233
+ threadId: this.threadId,
234
+ appearance: this.appearance,
235
+ ...this.runtimeVariables === void 0 ? {} : { runtime_variables: this.runtimeVariables }
236
+ }
237
+ });
238
+ }
239
+ post(payload) {
240
+ this.iframe.contentWindow?.postMessage({
241
+ bridge: AGENT_EMBED_BRIDGE,
242
+ version: AGENT_EMBED_BRIDGE_VERSION,
243
+ instanceId: this.instanceId,
244
+ ...payload
245
+ }, this.targetOrigin);
246
+ }
247
+ clearMountSettlement() {
248
+ this.settleMount = void 0;
249
+ this.rejectMount = void 0;
250
+ }
251
+ resolveReadyWaiters() {
252
+ for (const waiter of this.readyWaiters) waiter.resolve(this.instance);
253
+ this.readyWaiters.clear();
254
+ }
255
+ rejectReadyWaiters(error) {
256
+ for (const waiter of this.readyWaiters) waiter.reject(error);
257
+ this.readyWaiters.clear();
258
+ }
259
+ failInitialization(failure) {
260
+ if (this.destroyed || this.ready || this.initError) return;
261
+ const error = Object.assign(new Error(failure.message), failure);
262
+ this.initError = error;
263
+ this.rejectMount?.(error);
264
+ this.rejectReadyWaiters(error);
265
+ this.clearMountSettlement();
266
+ try {
267
+ this.onInitError?.(failure);
268
+ } catch {}
269
+ }
270
+ };
271
+ const createAgentEmbed = (options) => new AgentEmbedClientImpl(options);
272
+ //#endregion
273
+ export { AgentEmbedThemePreset, DEFAULT_AGENT_EMBED_BASE_URL, createAgentEmbed };
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@sophonsai/agent-embed-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Framework-independent browser SDK for integrating Sophon Agent Embed.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "sophonsai",
8
+ "agent",
9
+ "agent-embed",
10
+ "sdk"
11
+ ],
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org/",
14
+ "access": "public"
15
+ },
16
+ "types": "./dist/index.d.mts",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.mts",
21
+ "import": "./dist/index.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "dependencies": {
28
+ "@sophonsai/agent-embed-core": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "jsdom": "^27.4.0",
32
+ "tsdown": "^0.22.14"
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown src/index.ts --format esm --dts",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit",
37
+ "test": "vitest run --config vitest.config.ts"
38
+ }
6
39
  }