@lark-apaas/aily-web-sdk 0.0.2-alpha.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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,202 @@
1
+ # Aily Web SDK 本地测试指南
2
+
3
+ ## 使用 Storybook 测试
4
+
5
+ 本包使用 Storybook 作为测试和文档工具,可以方便地测试 `initAilyChat` API 的各种场景。
6
+
7
+ > **注意**:本地开发调试涉及 **两个项目**:
8
+ >
9
+ > | 项目 | 仓库 | 说明 |
10
+ > |------|------|------|
11
+ > | **aily-web-sdk**(本包) | `fullstack-plugin` | SDK 本体 + Storybook 测试页面 |
12
+ > | **cui-iframe**(iframe 应用) | `x-ai` | iframe 内加载的对话 UI,SDK 创建的 iframe 会指向该服务 |
13
+ >
14
+ > aily-web-sdk 负责创建 iframe 并通过 postMessage 通信,cui-iframe 负责实际的对话渲染。两者需要同时运行。
15
+
16
+ ### 启动步骤
17
+
18
+ **步骤 1: 配置飞书 BOE session cookie**
19
+
20
+ cui-iframe 的 devServer 会将 API 请求代理到 BOE 环境,但需要注入飞书 session cookie 来通过后端鉴权。
21
+
22
+ 1. 在浏览器中访问 https://ae.feishu-boe.cn 并登录
23
+ 2. 打开 DevTools → Application → Cookies → `ae.feishu-boe.cn`
24
+ 3. 复制 `session` 和 `sl_session` 两个 cookie 的值
25
+ 4. 在 x-ai 仓库的 `apps/cui-iframe/` 目录下创建 `.env.local` 文件(已在 .gitignore 中,不会被提交):
26
+
27
+ ```bash
28
+ SESSION_COOKIE=session=你的session值; sl_session=你的sl_session值
29
+ ```
30
+
31
+ > cookie 过期后需要重新登录 BOE 并更新 `.env.local`。
32
+
33
+ **步骤 2: 启动 cui-iframe 服务(x-ai 仓库)**
34
+
35
+ 在 x-ai 仓库中:
36
+
37
+ ```bash
38
+ cd apps/cui-iframe
39
+ npm run dev
40
+ ```
41
+ 服务运行在: `http://localhost:8080`
42
+
43
+ **步骤 3: 启动 Storybook(fullstack-plugin 仓库,新终端)**
44
+
45
+ 在 fullstack-plugin 仓库中:
46
+
47
+ ```bash
48
+ # 安装依赖(首次或依赖变更后,在仓库根目录执行)
49
+ corepack yarn install
50
+
51
+ # 进入包目录启动 storybook
52
+ cd packages/client/aily-web-sdk
53
+ corepack yarn storybook
54
+ ```
55
+
56
+ Storybook 将自动打开,在左侧导航选择 **AilyWebSDK / initAilyChat** 即可看到测试面板。
57
+
58
+ ## 参数填写指南
59
+
60
+ 在 Storybook 测试页面,你需要在「配置参数」区域填写以下信息:
61
+
62
+ ### 1. App Key(必填)
63
+
64
+ **是什么**:Aily 平台的应用标识符,每个接入的应用都有唯一的 appKey
65
+
66
+ **从哪里获取**:
67
+
68
+ 1. **如果你有 Aily 平台账号**:
69
+ - 访问 https://aily.feishu.cn/(或对应环境的控制台)
70
+ - 进入「应用管理」或「我的应用」
71
+ - 创建新应用或使用已有应用
72
+ - 在应用详情页找到 `appKey`(格式如 `aily_xxxxxxxx`)
73
+
74
+ 2. **BOE 环境测试**:
75
+ - 访问 BOE 环境控制台
76
+ - 使用测试账号创建应用
77
+ - 记录生成的 appKey
78
+
79
+ 3. **如果暂时没有**:
80
+ - 可以先填 `test-app` 测试 iframe 是否能正常加载(API 会报错,但能验证 iframe 架构)
81
+ - 联系 Aily 团队申请测试用 appKey
82
+
83
+ **示例**:
84
+ ```
85
+ aily_app_12345678
86
+ ```
87
+
88
+ ### 2. UUID(可选)
89
+
90
+ **是什么**:用户唯一标识符,用于标识当前会话的用户
91
+
92
+ **测试时填写**:
93
+ - 任意字符串即可,如 `test-user-001`
94
+ - 或使用 `test-${Date.now()}` 确保每次测试都是新用户
95
+
96
+ **真实场景**:从宿主应用获取当前用户的标识(如飞书 open_id)
97
+
98
+ ### 3. Base URL(服务地址)
99
+
100
+ **不同环境的地址**:
101
+
102
+ | 环境 | 地址 | 用途 |
103
+ |------|------|------|
104
+ | 本地代理 | `/oda` | **推荐用于本地开发**,走 cui-iframe devServer 代理到 BOE |
105
+ | BOE | `https://ae.feishu-boe.cn/oda` | 直连 BOE(跨域,需后端 CORS 支持) |
106
+ | 预发 | `https://ae.feishu-pre.cn/oda` | 预发验证 |
107
+ | 线上 | `https://ae.feishu.cn/oda` | 生产环境 |
108
+
109
+ **本地开发建议**:选择**本地代理 `/oda`**,请求会被 cui-iframe 的 devServer 代理到 BOE,避免跨域问题
110
+
111
+ ### 4. 高度
112
+
113
+ 调整 iframe 的显示高度,默认 600px,可根据需要调整
114
+
115
+ ## 测试流程
116
+
117
+ 1. **确保 cui-iframe 已启动**(x-ai 仓库,`http://localhost:8080`)
118
+ 2. **在 Storybook 中填写 App Key**(从 Aily 平台获取)
119
+ 3. **选择 Base URL**(建议选本地代理 `/oda`)
120
+ 4. **点击「初始化 Chat」** 按钮
121
+ 5. **查看右侧事件日志**:
122
+ - 看到 `✅ Chat ready` 表示成功
123
+ - 看到 `❌ Error` 根据提示排查问题
124
+ 6. **在「消息操作」区域测试发送消息**
125
+
126
+ ## 架构说明
127
+
128
+ ```
129
+ fullstack-plugin 仓库 x-ai 仓库
130
+ ┌──────────────────────────┐ ┌──────────────────────────┐
131
+ │ Storybook 测试页面 │ │ cui-iframe 应用 │
132
+ │ (aily-web-sdk) │ │ (静态导入 cui-sdk) │
133
+ │ │ │ http://localhost:8080 │
134
+ │ ┌────────────────────┐ │ └──────────────────────────┘
135
+ │ │ iframe │──┼── 加载 ──▶ cui-iframe 页面
136
+ │ │ │◀─┼── postMessage ──▶
137
+ │ └────────────────────┘ │
138
+ └──────────────────────────┘
139
+ ```
140
+
141
+ ## 运行测试
142
+
143
+ ```bash
144
+ yarn workspace @lark-apaas/aily-web-sdk test
145
+ ```
146
+
147
+ ## 添加新的测试 Story
148
+
149
+ 在 `stories/` 目录下创建新的 `.stories.tsx` 文件,使用标准 CSF 格式:
150
+
151
+ ```typescript
152
+ import React, { useRef, useEffect } from 'react';
153
+ import type { Meta, StoryObj } from '@storybook/react';
154
+ import { initAilyChat } from '../src';
155
+
156
+ const meta: Meta = {
157
+ title: 'AilyWebSDK/MyTest',
158
+ };
159
+
160
+ export default meta;
161
+
162
+ function MyTestDemo() {
163
+ const containerRef = useRef<HTMLDivElement>(null);
164
+
165
+ useEffect(() => {
166
+ if (containerRef.current) {
167
+ initAilyChat(containerRef.current, {
168
+ appKey: 'test',
169
+ common: { baseURL: '/oda' },
170
+ });
171
+ }
172
+ }, []);
173
+
174
+ return <div ref={containerRef} style={{ height: 500 }} />;
175
+ }
176
+
177
+ export const Default: StoryObj = {
178
+ render: () => <MyTestDemo />,
179
+ };
180
+ ```
181
+
182
+ ## 故障排除
183
+
184
+ ### "请先填写 App Key"
185
+ App Key 是必填项,请从 Aily 平台获取后填入
186
+
187
+ ### iframe 无法加载
188
+ - 确保 x-ai 仓库的 `apps/cui-iframe` 已启动(`http://localhost:8080`)
189
+ - 检查浏览器控制台是否有跨域错误
190
+
191
+ ### API 请求返回 400/401
192
+ - 确认 x-ai 仓库的 `apps/cui-iframe/.env.local` 是否已配置 `SESSION_COOKIE`
193
+ - cookie 可能已过期,重新登录 BOE 并更新 `.env.local`
194
+ - 确认 Base URL 选择的是 `/oda`(本地代理),而不是直连远端地址
195
+
196
+ ### "App not found" 或类似的 API 错误
197
+ - 确认 appKey 是否正确
198
+ - 确认选择的 Base URL 环境是否正确(线上 appKey 不能在 BOE 使用)
199
+
200
+ ### postMessage 通信失败
201
+ - 查看事件日志中的错误信息
202
+ - 检查浏览器 DevTools 的 Console 面板
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Aily-Web-SDK
2
+
3
+ Aily Web SDK 是飞书 Aily 团队推出的轻量级 SDK,通过 iframe + postMessage 方式让你快速接入 Aily AI 能力。
4
+
5
+ ## 特点
6
+
7
+ - **轻量级**: 纯 iframe 实现,无 CDN 资源加载
8
+ - **高隔离性**: 完全隔离的运行环境
9
+ - **简单易用**: 简洁的 API 设计
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ npm install @lark-apaas/aily-web-sdk
15
+ ```
16
+
17
+ ## 使用
18
+
19
+ ```typescript
20
+ import { initAilyChat } from '@lark-apaas/aily-web-sdk'
21
+
22
+ const container = document.querySelector('.container');
23
+ const chatPanel = await initAilyChat(container, {
24
+ appKey: 'your-app-key',
25
+ common: {},
26
+ events: {
27
+ onReady: () => console.log('ready'),
28
+ onError: (error) => console.error(error)
29
+ }
30
+ });
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### `initAilyChat(root, config)`
36
+
37
+ 初始化 Aily Chat 面板。
38
+
39
+ **参数:**
40
+
41
+ - `root` (`HTMLElement`) - iframe 挂载的容器元素
42
+ - `config` (`WebSDKConfig`) - 配置项
43
+ - `appKey` (`string`) - 应用 Key
44
+ - `uuid` (`string`, 可选) - 用户标识
45
+ - `device` (`'pc' | 'mobile'`, 可选) - 设备类型
46
+ - `common` (`WebSDKCommonConfig`) - 通用配置
47
+ - `baseURL` (`string`) - Aily 服务地址
48
+ - `headers` (`Record<string, string>`, 可选) - 自定义请求头
49
+ - `events` (`WebSDKEvents`, 可选) - 事件回调
50
+ - `onReady` - iframe 就绪回调
51
+ - `onError` - 错误回调
52
+ - `onMessage` - 消息回调
53
+
54
+ **返回:** `Promise<ChatPanel>`
55
+
56
+ ### `ChatPanel`
57
+
58
+ | 方法 | 说明 |
59
+ | --- | --- |
60
+ | `sendMessage(data)` | 发送消息 |
61
+ | `clear()` | 清空聊天记录 |
62
+ | `cancelMessage(messageItem)` | 取消消息 |
63
+ | `clearAndStop()` | 清空消息并停止当前响应 |
64
+ | `updateWelcomeMessage()` | 刷新欢迎消息 |
65
+ | `updateConfig(config)` | 更新面板配置 |
66
+ | `destroy()` | 销毁实例 |
67
+
68
+ ## License
69
+
70
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ IFRAME_BASE_URL: () => IFRAME_BASE_URL,
25
+ MESSAGE_TIMEOUT: () => MESSAGE_TIMEOUT,
26
+ getIframeBaseURL: () => getIframeBaseURL,
27
+ initAilyChat: () => initAilyChat
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/constants.ts
32
+ var MESSAGE_BIZ_ID = "cui-sdk-message";
33
+ var IFRAME_BASE_URL = {
34
+ production: "https://aily.feishu.cn/cui",
35
+ staging: "https://aily.feishu-pre.cn/cui",
36
+ boe: "https://aily.feishu-boe.cn/cui",
37
+ development: "http://localhost:8080/cui"
38
+ };
39
+ function getIframeBaseURL(env) {
40
+ if (env && IFRAME_BASE_URL[env]) {
41
+ return IFRAME_BASE_URL[env];
42
+ }
43
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
44
+ if (hostname.includes("aiforce-boe")) {
45
+ return IFRAME_BASE_URL.boe;
46
+ }
47
+ if (hostname.includes("aiforce-pre")) {
48
+ return IFRAME_BASE_URL.staging;
49
+ }
50
+ if (hostname.includes("localhost") || hostname.includes("127.0.0.1")) {
51
+ return IFRAME_BASE_URL.development;
52
+ }
53
+ return IFRAME_BASE_URL.production;
54
+ }
55
+ __name(getIframeBaseURL, "getIframeBaseURL");
56
+ var MESSAGE_TIMEOUT = 30 * 1e3;
57
+ var CHANNEL_TYPE = "MIAODA_CUI_SDK";
58
+ var ANONYMOUS_CHANNEL_TYPE = "MIAODA_ANONYMOUS_CUI_SDK";
59
+ var ALLOWED_ORIGINS = [
60
+ "https://aily.feishu.cn",
61
+ "https://aily.feishu-pre.cn",
62
+ "https://aily.feishu-boe.cn",
63
+ "http://localhost:3000",
64
+ "http://localhost:5173",
65
+ "http://localhost:8080"
66
+ ];
67
+
68
+ // src/device-detector.ts
69
+ var MOBILE_BREAKPOINT = 768;
70
+ var DeviceDetector = class {
71
+ static {
72
+ __name(this, "DeviceDetector");
73
+ }
74
+ mediaQuery;
75
+ currentDevice;
76
+ callback = null;
77
+ handleChange = null;
78
+ constructor() {
79
+ this.mediaQuery = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
80
+ this.currentDevice = window.innerWidth < MOBILE_BREAKPOINT ? "mobile" : "pc";
81
+ }
82
+ getDevice() {
83
+ return this.currentDevice;
84
+ }
85
+ observe(callback) {
86
+ this.callback = callback;
87
+ this.handleChange = () => {
88
+ const newDevice = window.innerWidth < MOBILE_BREAKPOINT ? "mobile" : "pc";
89
+ if (newDevice !== this.currentDevice) {
90
+ this.currentDevice = newDevice;
91
+ this.callback?.(newDevice);
92
+ }
93
+ };
94
+ this.mediaQuery.addEventListener("change", this.handleChange);
95
+ }
96
+ destroy() {
97
+ if (this.handleChange) {
98
+ this.mediaQuery.removeEventListener("change", this.handleChange);
99
+ this.handleChange = null;
100
+ }
101
+ this.callback = null;
102
+ }
103
+ };
104
+
105
+ // src/message-bridge.ts
106
+ var MessageBridge = class {
107
+ static {
108
+ __name(this, "MessageBridge");
109
+ }
110
+ iframe = null;
111
+ requestId = 0;
112
+ pendingRequests = /* @__PURE__ */ new Map();
113
+ eventHandlers = /* @__PURE__ */ new Map();
114
+ targetOrigin;
115
+ constructor(targetOrigin) {
116
+ this.targetOrigin = targetOrigin;
117
+ this.handleMessage = this.handleMessage.bind(this);
118
+ }
119
+ /**
120
+ * Attach to an iframe and start listening for messages
121
+ */
122
+ attach(iframe) {
123
+ this.iframe = iframe;
124
+ window.addEventListener("message", this.handleMessage);
125
+ }
126
+ /**
127
+ * Detach from iframe and clean up
128
+ */
129
+ detach() {
130
+ window.removeEventListener("message", this.handleMessage);
131
+ this.iframe = null;
132
+ this.pendingRequests.forEach(({ reject }) => {
133
+ reject(new Error("MessageBridge detached"));
134
+ });
135
+ this.pendingRequests.clear();
136
+ }
137
+ /**
138
+ * Send a message to the iframe and wait for response
139
+ */
140
+ send(type, data) {
141
+ return new Promise((resolve, reject) => {
142
+ if (!this.iframe?.contentWindow) {
143
+ reject(new Error("Iframe not attached"));
144
+ return;
145
+ }
146
+ this.requestId += 1;
147
+ const id = this.requestId;
148
+ const message = {
149
+ messageBizId: MESSAGE_BIZ_ID,
150
+ id,
151
+ type,
152
+ data,
153
+ timestamp: Date.now(),
154
+ isRequest: true
155
+ };
156
+ const timer = setTimeout(() => {
157
+ this.pendingRequests.delete(id);
158
+ reject(new Error(`Message timeout: ${type}`));
159
+ }, MESSAGE_TIMEOUT);
160
+ this.pendingRequests.set(id, {
161
+ resolve,
162
+ reject,
163
+ timer
164
+ });
165
+ this.iframe.contentWindow.postMessage(message, this.targetOrigin);
166
+ });
167
+ }
168
+ /**
169
+ * Register an event handler
170
+ */
171
+ onEvent(eventName, handler) {
172
+ const handlers = this.eventHandlers.get(eventName) || [];
173
+ handlers.push(handler);
174
+ this.eventHandlers.set(eventName, handlers);
175
+ }
176
+ /**
177
+ * Remove an event handler
178
+ */
179
+ offEvent(eventName, handler) {
180
+ const handlers = this.eventHandlers.get(eventName) || [];
181
+ const index = handlers.indexOf(handler);
182
+ if (index > -1) {
183
+ handlers.splice(index, 1);
184
+ this.eventHandlers.set(eventName, handlers);
185
+ }
186
+ }
187
+ handleMessage(event) {
188
+ if (!ALLOWED_ORIGINS.includes(event.origin)) {
189
+ return;
190
+ }
191
+ const { data } = event;
192
+ if (data?.messageBizId !== MESSAGE_BIZ_ID) {
193
+ return;
194
+ }
195
+ if (this.isResponseMessage(data)) {
196
+ const pending = this.pendingRequests.get(data.id);
197
+ if (pending) {
198
+ clearTimeout(pending.timer);
199
+ this.pendingRequests.delete(data.id);
200
+ if (data.error) {
201
+ pending.reject(new Error(data.error.message));
202
+ } else {
203
+ pending.resolve(data.data);
204
+ }
205
+ }
206
+ }
207
+ if (this.isEventMessage(data)) {
208
+ const handlers = this.eventHandlers.get(data.eventName) || [];
209
+ handlers.forEach((handler) => {
210
+ try {
211
+ handler(data.data);
212
+ } catch {
213
+ }
214
+ });
215
+ }
216
+ }
217
+ isResponseMessage(data) {
218
+ return "isResponse" in data && data.isResponse === true;
219
+ }
220
+ isEventMessage(data) {
221
+ return data.type === "event";
222
+ }
223
+ };
224
+
225
+ // src/iframe-manager.ts
226
+ var IframeManager = class {
227
+ static {
228
+ __name(this, "IframeManager");
229
+ }
230
+ iframe = null;
231
+ messageBridge;
232
+ config;
233
+ channelType;
234
+ root;
235
+ deviceDetector = null;
236
+ constructor(root, config) {
237
+ this.root = root;
238
+ this.config = config;
239
+ this.channelType = config.anonymous ? ANONYMOUS_CHANNEL_TYPE : CHANNEL_TYPE;
240
+ this.messageBridge = new MessageBridge(getIframeBaseURL());
241
+ }
242
+ /**
243
+ * Initialize the iframe and establish communication
244
+ * 配置通过 URL hash 编码传入 iframe,iframe 加载后自行初始化
245
+ */
246
+ async init() {
247
+ const initStart = performance.now();
248
+ if (!this.config.device) {
249
+ this.deviceDetector = new DeviceDetector();
250
+ this.config.device = this.deviceDetector.getDevice();
251
+ }
252
+ this.iframe = document.createElement("iframe");
253
+ this.iframe.src = this.buildIframeURL();
254
+ this.iframe.style.width = "100%";
255
+ this.iframe.style.height = "100%";
256
+ this.iframe.style.border = "none";
257
+ this.iframe.allow = "microphone; clipboard-write";
258
+ this.root.appendChild(this.iframe);
259
+ this.messageBridge.attach(this.iframe);
260
+ this.setupEventHandlers();
261
+ if (this.deviceDetector) {
262
+ this.deviceDetector.observe((newDevice) => {
263
+ this.messageBridge.send("updateConfig", {
264
+ device: newDevice
265
+ });
266
+ });
267
+ }
268
+ await this.waitForReady();
269
+ this.reportInitMetrics(initStart);
270
+ return this.createChatPanel();
271
+ }
272
+ /**
273
+ * Destroy the iframe and cleanup
274
+ */
275
+ async destroy() {
276
+ try {
277
+ await this.messageBridge.send("destroy");
278
+ } catch {
279
+ }
280
+ this.messageBridge.detach();
281
+ if (this.deviceDetector) {
282
+ this.deviceDetector.destroy();
283
+ this.deviceDetector = null;
284
+ }
285
+ if (this.iframe && this.iframe.parentNode) {
286
+ this.iframe.parentNode.removeChild(this.iframe);
287
+ }
288
+ this.iframe = null;
289
+ }
290
+ /**
291
+ * 构建 iframe URL,将配置编码到 hash 中
292
+ * 格式: {baseURL}#{params} 其中 config 为 base64(JSON.stringify(configWithoutEvents))
293
+ *
294
+ * 鉴权说明:SDK 不参与 token 获取/传递。iframe 自行处理飞书登录流程,
295
+ * 允许用户在 iframe 中独立登录。匿名渠道跳过登录检查。
296
+ */
297
+ buildIframeURL() {
298
+ const baseURL = getIframeBaseURL();
299
+ const { events, anonymous: _anonymous, ...configWithoutEvents } = this.config;
300
+ const iframeConfig = {
301
+ ...configWithoutEvents,
302
+ common: {
303
+ ...configWithoutEvents.common,
304
+ channelType: this.channelType,
305
+ resourceType: this.channelType
306
+ }
307
+ };
308
+ const configBase64 = btoa(JSON.stringify(iframeConfig));
309
+ const params = new URLSearchParams({
310
+ appKey: this.config.appKey,
311
+ config: encodeURIComponent(configBase64)
312
+ });
313
+ return `${baseURL}#${params.toString()}`;
314
+ }
315
+ /**
316
+ * 等待 iframe 自行初始化完成后发送 ready 事件
317
+ */
318
+ waitForReady() {
319
+ return new Promise((resolve, reject) => {
320
+ const timeout = setTimeout(() => {
321
+ reject(new Error("Iframe ready timeout"));
322
+ }, 3e4);
323
+ const checkMessage = /* @__PURE__ */ __name((event) => {
324
+ if (event.data?.messageBizId === MESSAGE_BIZ_ID && event.data?.type === "event" && event.data?.eventName === "ready") {
325
+ clearTimeout(timeout);
326
+ window.removeEventListener("message", checkMessage);
327
+ resolve();
328
+ }
329
+ }, "checkMessage");
330
+ window.addEventListener("message", checkMessage);
331
+ });
332
+ }
333
+ setupEventHandlers() {
334
+ const { events } = this.config;
335
+ if (!events) return;
336
+ if (events.onReady) {
337
+ this.messageBridge.onEvent("ready", events.onReady);
338
+ }
339
+ if (events.onError) {
340
+ this.messageBridge.onEvent("error", (data) => {
341
+ const msg = data?.message || "Unknown error";
342
+ events.onError(new Error(msg));
343
+ });
344
+ }
345
+ if (events.onMessage) {
346
+ this.messageBridge.onEvent("onMessage", events.onMessage);
347
+ }
348
+ }
349
+ reportInitMetrics(initStart) {
350
+ try {
351
+ const cost = performance.now() - initStart;
352
+ const Slardar = window.__Slardar;
353
+ if (Slardar?.sendEvent) {
354
+ Slardar.sendEvent({
355
+ name: "cui_init_total",
356
+ metrics: {
357
+ cost
358
+ },
359
+ categories: {
360
+ arch: "iframe-static",
361
+ appKey: this.config.appKey,
362
+ channelType: this.channelType
363
+ }
364
+ });
365
+ }
366
+ } catch {
367
+ }
368
+ }
369
+ createChatPanel() {
370
+ return {
371
+ sendMessage: /* @__PURE__ */ __name(async (data) => {
372
+ await this.messageBridge.send("sendMessage", data);
373
+ }, "sendMessage"),
374
+ clear: /* @__PURE__ */ __name(async () => {
375
+ await this.messageBridge.send("clear");
376
+ }, "clear"),
377
+ cancelMessage: /* @__PURE__ */ __name(async (messageItem) => {
378
+ await this.messageBridge.send("cancelMessage", messageItem);
379
+ }, "cancelMessage"),
380
+ clearAndStop: /* @__PURE__ */ __name(async () => {
381
+ await this.messageBridge.send("clearAndStop");
382
+ }, "clearAndStop"),
383
+ updateWelcomeMessage: /* @__PURE__ */ __name(async () => {
384
+ await this.messageBridge.send("updateWelcomeMessage");
385
+ }, "updateWelcomeMessage"),
386
+ updateConfig: /* @__PURE__ */ __name(async (config) => {
387
+ await this.messageBridge.send("updateConfig", config);
388
+ }, "updateConfig"),
389
+ observeSkill: /* @__PURE__ */ __name(async (skillId, name, skillType) => {
390
+ await this.messageBridge.send("observeSkill", {
391
+ skillId,
392
+ name,
393
+ skillType
394
+ });
395
+ }, "observeSkill"),
396
+ setCurrentSkill: /* @__PURE__ */ __name(async (skill) => {
397
+ await this.messageBridge.send("setCurrentSkill", skill);
398
+ }, "setCurrentSkill"),
399
+ destroy: /* @__PURE__ */ __name(async () => {
400
+ await this.destroy();
401
+ }, "destroy")
402
+ };
403
+ }
404
+ };
405
+
406
+ // src/index.ts
407
+ async function initAilyChat(root, config) {
408
+ const manager = new IframeManager(root, config);
409
+ return manager.init();
410
+ }
411
+ __name(initAilyChat, "initAilyChat");
412
+ // Annotate the CommonJS export names for ESM import in node:
413
+ 0 && (module.exports = {
414
+ IFRAME_BASE_URL,
415
+ MESSAGE_TIMEOUT,
416
+ getIframeBaseURL,
417
+ initAilyChat
418
+ });
419
+ //# sourceMappingURL=index.cjs.map