Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 189 additions & 24 deletions static/embed.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
(function () {
console.log('[IFRAME] hello, embed injected')

const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

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

let isMinMode = true;
button.id = "openim-customer-service-embed-button";
iframe.id = "openim-customer-service-embed-iframe";

let isMinMode = false;
let isInit = false;
const iframeSrc = "https://web.rentsoft.cn";
const allowedOrigins = [iframeSrc];

function initUI() {
Object.assign(button.style, {
Expand All @@ -24,8 +31,15 @@
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.2)",
cursor: "pointer",
});
button.style.backgroundColor = '#2160fd'
button.style.padding = '8px'
const iconImgEl = document.createElement('img');
iconImgEl.src = '/logo/iframe-enter-icon-logo.svg';
iconImgEl.style.width = '100%';
iconImgEl.style.height = '100%';
button.appendChild(iconImgEl);

iframe.src = "https://openim-bot.open-sora.ai/open-kf-chatbot";
iframe.src = iframeSrc;
Object.assign(iframe.style, {
position: "fixed",
zIndex: "10000",
Expand All @@ -48,6 +62,8 @@
};

button.onclick = function () {
console.log('[embed] click button')

const isMinWidth = window.matchMedia("(max-width: 768px)").matches || isMobile;
const isHidden = iframe.style.transform === "scale(0)";

Expand All @@ -65,30 +81,61 @@
}
};

window.addEventListener('message', function (event) {
if (event.data.event === 'closeIframe') {
iframe.style.transform = "scale(0)";
iframe.style.opacity = 0;
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
if (event.data.event === 'toogleSize') {
iframe.style.width = isMinMode ? "720px" : "420px";
iframe.style.height = isMinMode ? "80vh" : "60vh";
isMinMode = !isMinMode;
}
if (event.data.event === 'getConfig') {
if(event.data.data){
button.innerHTML = `<img src="${event.data.data}" style="width: 50px; height: 50px;"/>`
}else {
button.innerHTML = "Ask";
button.style.color = "white";
button.style.backgroundColor = "#2160fd";
let isGetConfig = false;
const rpc = setupParentRPCListener({
allowedOrigins: allowedOrigins, // ★ 必填:子页面来源
debug: true,
handler: async function ({ action, data }) {
if (action === 'closeIframe') {
iframe.style.transform = "scale(0)";
iframe.style.opacity = 0;
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
document.body.appendChild(button);
}
if (action === 'toogleSize') {
iframe.style.width = isMinMode ? "720px" : "420px";
iframe.style.height = isMinMode ? "80vh" : "60vh";
isMinMode = !isMinMode;
}
if (action === 'getConfig') {
console.log('get config', data, isGetConfig);
if(isGetConfig) return;
// if(data){
// button.style.backgroundColor = '#2160fd'
// button.style.padding = '8px'
// const iconImgEl = document.createElement('img');
// iconImgEl.src = data.iconUrl;
// iconImgEl.style.width = '100%';
// iconImgEl.style.height = '100%';
// button.appendChild(iconImgEl);
// }else {
// button.innerHTML = "Ask";
// button.style.color = "white";
// button.style.backgroundColor = "#2160fd";
// }
document.body.appendChild(button);
isGetConfig = true;
}
},
});

// 在页面关闭/刷新时清理监听,避免内存泄漏,并使变量被有效使用
try {
window.addEventListener('beforeunload', function () {
try {
if (rpc && typeof rpc.dispose === 'function') {
rpc.dispose();
}
} catch (e) {
/* ignore dispose errors */
console.error(e);
}
});
} catch (e) {
/* ignore addEventListener errors */
console.error(e);
}

window.onload = initUI;

function adjustIframeStyleForSmallScreens() {
Expand All @@ -114,7 +161,7 @@
}
} else {
Object.assign(iframe.style, {
width: isMinMode ? "420px" : "720px",
width: isMinMode ? "420px" : "640px",
height: isMinMode ? "60vh" : "80vh",
right: "20px",
bottom: "100px",
Expand All @@ -131,3 +178,121 @@

window.addEventListener('resize', adjustIframeStyleForSmallScreens);
})();

// parent-rpc.js
/**
* 在父页面注册一个 RPC 监听器,安全响应来自 iframe 的请求。
* - allowedOrigins: 白名单来源数组(必填!)
* - handler: async ({ action, data, event }) => any 业务处理函数
* - respondTTL: 已响应请求ID的记忆时长,防止重复响应(ms)
* - debug: 打印调试日志
*
* 使用指南:
* ```js
* import { setupParentRPCListener } from "/parent-rpc.js";
* const rpc = setupParentRPCListener({
* allowedOrigins: ["https://child.example.com"], // ★ 必填:子页面来源
* debug: true,
* handler: async ({ action, data }) => {
* switch (action) {
* case "ping":
* return { pong: true, now: Date.now() };
* case "sum":
* const { a, b } = data;
* return { sum: a + b };
* default:
* throw new Error("Unknown action: " + action);
* }
* },
* });
*
* // 如果需要移除监听:
* // rpc.dispose();
* ```
*/
function setupParentRPCListener({
allowedOrigins,
handler,
respondTTL = 60_000,
debug = false,
} = {}) {
if (!Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
throw new Error("[setupParentRPCListener] 'allowedOrigins' is required.");
}
if (typeof handler !== "function") {
throw new Error("[setupParentRPCListener] 'handler' must be a function.");
}

const responded = new Map(); // id -> expireAt

const gc = () => {
const now = Date.now();
for (const [id, expireAt] of responded.entries()) {
if (expireAt <= now) responded.delete(id);
}
};
const gcTimer = setInterval(gc, Math.min(respondTTL, 10_000));

function log(...args) { if (debug) console.log("[ParentRPC]", ...args); }

function isAllowedOrigin(origin) {
return allowedOrigins.includes(origin);
}

function isValidRequest(data) {
return data
&& data.__rpc === true
&& data.dir === "REQUEST"
&& typeof data.id === "string"
&& typeof data.action === "string";
}

async function onMessage(event) {
try {
const { origin, data, source } = event;
if (!isAllowedOrigin(origin)) return;
if (!isValidRequest(data)) return;

const { id, action, payload } = data;

// 去重同一请求ID(例如 iframe 重试导致的重复投递)
if (responded.has(id)) {
log("duplicate request id, ignoring:", id);
return;
}

log("request <-", { id, origin, action, payload });

let result, isError = false, errorMsg = "";
try {
result = await handler({ action, data: payload, event });
} catch (e) {
isError = true;
errorMsg = e?.message || String(e);
}

const response = isError
? { __rpc: true, dir: "RESPONSE", id, ok: false, error: { message: errorMsg } }
: { __rpc: true, dir: "RESPONSE", id, ok: true, result };

// 回复给来源窗口(通常是 iframe.contentWindow)
if (source && typeof source.postMessage === "function") {
source.postMessage(response, origin);
responded.set(id, Date.now() + respondTTL);
log("response ->", { id, ok: !isError });
}
} catch (err) {
log("onMessage error:", err);
}
}

window.addEventListener("message", onMessage);

return {
dispose() {
clearInterval(gcTimer);
window.removeEventListener("message", onMessage);
responded.clear();
}
};
}
10 changes: 10 additions & 0 deletions static/logo/iframe-enter-icon-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading