Skip to content

Commit a9bddbe

Browse files
Merge pull request #279 from openimsdk/chan
feat: embed intelligent customer support iframe
2 parents 265207f + 29d6ea7 commit a9bddbe

File tree

2 files changed

+199
-24
lines changed

2 files changed

+199
-24
lines changed

static/embed.js

Lines changed: 189 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
(function () {
2+
console.log('[IFRAME] hello, embed injected')
3+
24
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
35

46
const button = document.createElement('div');
57
const iframe = document.createElement('iframe');
68

7-
let isMinMode = true;
9+
button.id = "openim-customer-service-embed-button";
10+
iframe.id = "openim-customer-service-embed-iframe";
11+
12+
let isMinMode = false;
813
let isInit = false;
14+
const iframeSrc = "https://web.rentsoft.cn";
15+
const allowedOrigins = [iframeSrc];
916

1017
function initUI() {
1118
Object.assign(button.style, {
@@ -24,8 +31,15 @@
2431
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.2)",
2532
cursor: "pointer",
2633
});
34+
button.style.backgroundColor = '#2160fd'
35+
button.style.padding = '8px'
36+
const iconImgEl = document.createElement('img');
37+
iconImgEl.src = '/logo/iframe-enter-icon-logo.svg';
38+
iconImgEl.style.width = '100%';
39+
iconImgEl.style.height = '100%';
40+
button.appendChild(iconImgEl);
2741

28-
iframe.src = "https://openim-bot.open-sora.ai/open-kf-chatbot";
42+
iframe.src = iframeSrc;
2943
Object.assign(iframe.style, {
3044
position: "fixed",
3145
zIndex: "10000",
@@ -48,6 +62,8 @@
4862
};
4963

5064
button.onclick = function () {
65+
console.log('[embed] click button')
66+
5167
const isMinWidth = window.matchMedia("(max-width: 768px)").matches || isMobile;
5268
const isHidden = iframe.style.transform === "scale(0)";
5369

@@ -65,30 +81,61 @@
6581
}
6682
};
6783

68-
window.addEventListener('message', function (event) {
69-
if (event.data.event === 'closeIframe') {
70-
iframe.style.transform = "scale(0)";
71-
iframe.style.opacity = 0;
72-
document.body.style.overflow = '';
73-
document.documentElement.style.overflow = '';
74-
}
75-
if (event.data.event === 'toogleSize') {
76-
iframe.style.width = isMinMode ? "720px" : "420px";
77-
iframe.style.height = isMinMode ? "80vh" : "60vh";
78-
isMinMode = !isMinMode;
79-
}
80-
if (event.data.event === 'getConfig') {
81-
if(event.data.data){
82-
button.innerHTML = `<img src="${event.data.data}" style="width: 50px; height: 50px;"/>`
83-
}else {
84-
button.innerHTML = "Ask";
85-
button.style.color = "white";
86-
button.style.backgroundColor = "#2160fd";
84+
let isGetConfig = false;
85+
const rpc = setupParentRPCListener({
86+
allowedOrigins: allowedOrigins, // ★ 必填:子页面来源
87+
debug: true,
88+
handler: async function ({ action, data }) {
89+
if (action === 'closeIframe') {
90+
iframe.style.transform = "scale(0)";
91+
iframe.style.opacity = 0;
92+
document.body.style.overflow = '';
93+
document.documentElement.style.overflow = '';
8794
}
88-
document.body.appendChild(button);
89-
}
95+
if (action === 'toogleSize') {
96+
iframe.style.width = isMinMode ? "720px" : "420px";
97+
iframe.style.height = isMinMode ? "80vh" : "60vh";
98+
isMinMode = !isMinMode;
99+
}
100+
if (action === 'getConfig') {
101+
console.log('get config', data, isGetConfig);
102+
if(isGetConfig) return;
103+
// if(data){
104+
// button.style.backgroundColor = '#2160fd'
105+
// button.style.padding = '8px'
106+
// const iconImgEl = document.createElement('img');
107+
// iconImgEl.src = data.iconUrl;
108+
// iconImgEl.style.width = '100%';
109+
// iconImgEl.style.height = '100%';
110+
// button.appendChild(iconImgEl);
111+
// }else {
112+
// button.innerHTML = "Ask";
113+
// button.style.color = "white";
114+
// button.style.backgroundColor = "#2160fd";
115+
// }
116+
document.body.appendChild(button);
117+
isGetConfig = true;
118+
}
119+
},
90120
});
91121

122+
// 在页面关闭/刷新时清理监听,避免内存泄漏,并使变量被有效使用
123+
try {
124+
window.addEventListener('beforeunload', function () {
125+
try {
126+
if (rpc && typeof rpc.dispose === 'function') {
127+
rpc.dispose();
128+
}
129+
} catch (e) {
130+
/* ignore dispose errors */
131+
console.error(e);
132+
}
133+
});
134+
} catch (e) {
135+
/* ignore addEventListener errors */
136+
console.error(e);
137+
}
138+
92139
window.onload = initUI;
93140

94141
function adjustIframeStyleForSmallScreens() {
@@ -114,7 +161,7 @@
114161
}
115162
} else {
116163
Object.assign(iframe.style, {
117-
width: isMinMode ? "420px" : "720px",
164+
width: isMinMode ? "420px" : "640px",
118165
height: isMinMode ? "60vh" : "80vh",
119166
right: "20px",
120167
bottom: "100px",
@@ -131,3 +178,121 @@
131178

132179
window.addEventListener('resize', adjustIframeStyleForSmallScreens);
133180
})();
181+
182+
// parent-rpc.js
183+
/**
184+
* 在父页面注册一个 RPC 监听器,安全响应来自 iframe 的请求。
185+
* - allowedOrigins: 白名单来源数组(必填!)
186+
* - handler: async ({ action, data, event }) => any 业务处理函数
187+
* - respondTTL: 已响应请求ID的记忆时长,防止重复响应(ms)
188+
* - debug: 打印调试日志
189+
*
190+
* 使用指南:
191+
* ```js
192+
* import { setupParentRPCListener } from "/parent-rpc.js";
193+
* const rpc = setupParentRPCListener({
194+
* allowedOrigins: ["https://child.example.com"], // ★ 必填:子页面来源
195+
* debug: true,
196+
* handler: async ({ action, data }) => {
197+
* switch (action) {
198+
* case "ping":
199+
* return { pong: true, now: Date.now() };
200+
* case "sum":
201+
* const { a, b } = data;
202+
* return { sum: a + b };
203+
* default:
204+
* throw new Error("Unknown action: " + action);
205+
* }
206+
* },
207+
* });
208+
*
209+
* // 如果需要移除监听:
210+
* // rpc.dispose();
211+
* ```
212+
*/
213+
function setupParentRPCListener({
214+
allowedOrigins,
215+
handler,
216+
respondTTL = 60_000,
217+
debug = false,
218+
} = {}) {
219+
if (!Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
220+
throw new Error("[setupParentRPCListener] 'allowedOrigins' is required.");
221+
}
222+
if (typeof handler !== "function") {
223+
throw new Error("[setupParentRPCListener] 'handler' must be a function.");
224+
}
225+
226+
const responded = new Map(); // id -> expireAt
227+
228+
const gc = () => {
229+
const now = Date.now();
230+
for (const [id, expireAt] of responded.entries()) {
231+
if (expireAt <= now) responded.delete(id);
232+
}
233+
};
234+
const gcTimer = setInterval(gc, Math.min(respondTTL, 10_000));
235+
236+
function log(...args) { if (debug) console.log("[ParentRPC]", ...args); }
237+
238+
function isAllowedOrigin(origin) {
239+
return allowedOrigins.includes(origin);
240+
}
241+
242+
function isValidRequest(data) {
243+
return data
244+
&& data.__rpc === true
245+
&& data.dir === "REQUEST"
246+
&& typeof data.id === "string"
247+
&& typeof data.action === "string";
248+
}
249+
250+
async function onMessage(event) {
251+
try {
252+
const { origin, data, source } = event;
253+
if (!isAllowedOrigin(origin)) return;
254+
if (!isValidRequest(data)) return;
255+
256+
const { id, action, payload } = data;
257+
258+
// 去重同一请求ID(例如 iframe 重试导致的重复投递)
259+
if (responded.has(id)) {
260+
log("duplicate request id, ignoring:", id);
261+
return;
262+
}
263+
264+
log("request <-", { id, origin, action, payload });
265+
266+
let result, isError = false, errorMsg = "";
267+
try {
268+
result = await handler({ action, data: payload, event });
269+
} catch (e) {
270+
isError = true;
271+
errorMsg = e?.message || String(e);
272+
}
273+
274+
const response = isError
275+
? { __rpc: true, dir: "RESPONSE", id, ok: false, error: { message: errorMsg } }
276+
: { __rpc: true, dir: "RESPONSE", id, ok: true, result };
277+
278+
// 回复给来源窗口(通常是 iframe.contentWindow)
279+
if (source && typeof source.postMessage === "function") {
280+
source.postMessage(response, origin);
281+
responded.set(id, Date.now() + respondTTL);
282+
log("response ->", { id, ok: !isError });
283+
}
284+
} catch (err) {
285+
log("onMessage error:", err);
286+
}
287+
}
288+
289+
window.addEventListener("message", onMessage);
290+
291+
return {
292+
dispose() {
293+
clearInterval(gcTimer);
294+
window.removeEventListener("message", onMessage);
295+
responded.clear();
296+
}
297+
};
298+
}
Lines changed: 10 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)