refactor: update Kefu widget configuration to use dynamic user token retrieval
- Changed `userToken` property to `getUserToken` function in KefuWidgetHostConfig for dynamic token fetching. - Introduced KefuWidgetRuntimeConfig to manage runtime-specific configurations. - Updated readKefuWidgetConfig and setKefuWidgetConfig functions to accommodate new configuration structure. - Enhanced cs-ai-agent-sdk.js to handle user token resolution and frame configuration. - Added tests for dynamic user token retrieval in cs-ai-agent-sdk.test.mjs. - Removed deprecated cs-agent-widget.js file as part of the refactor.
This commit is contained in:
+1
-1
Submodule docs updated: 077a8032c5...9141c24e39
@@ -29,7 +29,9 @@ declare global {
|
|||||||
CSAgentWidget?: {
|
CSAgentWidget?: {
|
||||||
mount: (config: KefuWidgetHostConfig) => void
|
mount: (config: KefuWidgetHostConfig) => void
|
||||||
destroy: () => void
|
destroy: () => void
|
||||||
|
open: () => Promise<void>
|
||||||
close: () => void
|
close: () => void
|
||||||
|
getChatUrl: () => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,14 +88,16 @@ function injectWidget(config: KefuWidgetHostConfig) {
|
|||||||
document.body.appendChild(script)
|
document.body.appendChild(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWidgetConfig(config: WidgetDemoConfig, userToken: string): WidgetDemoConfig {
|
function buildWidgetConfig(config: WidgetDemoConfig): KefuWidgetHostConfig {
|
||||||
return {
|
const nextConfig: KefuWidgetHostConfig = {
|
||||||
...config,
|
|
||||||
channelId: config.channelId.trim(),
|
channelId: config.channelId.trim(),
|
||||||
baseUrl: "",
|
baseUrl: "",
|
||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
userToken,
|
|
||||||
}
|
}
|
||||||
|
if (config.authMode === "jwt") {
|
||||||
|
nextConfig.getUserToken = () => signUserToken(config)
|
||||||
|
}
|
||||||
|
return nextConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signUserToken(config: WidgetDemoConfig) {
|
async function signUserToken(config: WidgetDemoConfig) {
|
||||||
@@ -134,19 +138,23 @@ export function KefuWidgetDemo() {
|
|||||||
const [status, setStatus] = useState("请填写 channelId")
|
const [status, setStatus] = useState("请填写 channelId")
|
||||||
const [origin, setOrigin] = useState("")
|
const [origin, setOrigin] = useState("")
|
||||||
const [generatedToken, setGeneratedToken] = useState("")
|
const [generatedToken, setGeneratedToken] = useState("")
|
||||||
|
const [latestDirectChatUrl, setLatestDirectChatUrl] = useState("")
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [snippetCopied, setSnippetCopied] = useState(false)
|
const [snippetCopied, setSnippetCopied] = useState(false)
|
||||||
|
|
||||||
async function mountWidget(configToMount: WidgetDemoConfig) {
|
async function mountWidget(configToMount: WidgetDemoConfig) {
|
||||||
let userToken = ""
|
const cleanConfig = {
|
||||||
if (configToMount.authMode === "jwt") {
|
...configToMount,
|
||||||
userToken = await signUserToken(configToMount)
|
channelId: configToMount.channelId.trim(),
|
||||||
|
baseUrl: "",
|
||||||
|
apiBaseUrl: "",
|
||||||
|
getUserToken: undefined,
|
||||||
}
|
}
|
||||||
|
const nextConfig = buildWidgetConfig(cleanConfig)
|
||||||
const nextConfig = buildWidgetConfig(configToMount, userToken)
|
setConfig(cleanConfig)
|
||||||
setConfig(nextConfig)
|
setGeneratedToken("")
|
||||||
setGeneratedToken(userToken)
|
setLatestDirectChatUrl("")
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(nextConfig))
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanConfig))
|
||||||
|
|
||||||
if (!nextConfig.channelId) {
|
if (!nextConfig.channelId) {
|
||||||
removeMountedWidget()
|
removeMountedWidget()
|
||||||
@@ -156,7 +164,7 @@ export function KefuWidgetDemo() {
|
|||||||
|
|
||||||
injectWidget(nextConfig)
|
injectWidget(nextConfig)
|
||||||
setStatus(
|
setStatus(
|
||||||
nextConfig.authMode === "jwt"
|
cleanConfig.authMode === "jwt"
|
||||||
? "Widget 已挂载:JWT 用户模式"
|
? "Widget 已挂载:JWT 用户模式"
|
||||||
: "Widget 已挂载:访客模式"
|
: "Widget 已挂载:访客模式"
|
||||||
)
|
)
|
||||||
@@ -173,7 +181,7 @@ export function KefuWidgetDemo() {
|
|||||||
void mountWidget(initialConfig).catch((error) => {
|
void mountWidget(initialConfig).catch((error) => {
|
||||||
removeMountedWidget()
|
removeMountedWidget()
|
||||||
setGeneratedToken("")
|
setGeneratedToken("")
|
||||||
setStatus(error instanceof Error ? error.message : "生成 userToken 失败")
|
setStatus(error instanceof Error ? error.message : "挂载 Widget 失败")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
@@ -191,7 +199,11 @@ export function KefuWidgetDemo() {
|
|||||||
|
|
||||||
const configLines = [` channelId: "${config.channelId || ""}"`]
|
const configLines = [` channelId: "${config.channelId || ""}"`]
|
||||||
if (config.authMode === "jwt") {
|
if (config.authMode === "jwt") {
|
||||||
configLines.push(` userToken: "${generatedToken || "业务系统后端签发的 JWT"}"`)
|
configLines.push(` async getUserToken() {
|
||||||
|
const res = await fetch("/api/kefu/user-token", { credentials: "include" });
|
||||||
|
const data = await res.json();
|
||||||
|
return data.userToken;
|
||||||
|
}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<script>
|
return `<script>
|
||||||
@@ -200,22 +212,7 @@ ${configLines.join(",\n")}
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script async src="${scriptSrc}"></script>`
|
<script async src="${scriptSrc}"></script>`
|
||||||
}, [config, generatedToken, origin])
|
}, [config.authMode, config.channelId, origin])
|
||||||
|
|
||||||
const directChatUrl = useMemo(() => {
|
|
||||||
const base = origin || ""
|
|
||||||
const channelId = (config.channelId || "").trim()
|
|
||||||
if (!base || !channelId) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL("/kefu/chat/", base)
|
|
||||||
url.searchParams.set("channelId", channelId)
|
|
||||||
if (config.authMode === "jwt" && generatedToken) {
|
|
||||||
url.searchParams.set("userToken", generatedToken)
|
|
||||||
}
|
|
||||||
return url.toString()
|
|
||||||
}, [config.authMode, config.channelId, generatedToken, origin])
|
|
||||||
|
|
||||||
function updateField<K extends keyof WidgetDemoConfig>(
|
function updateField<K extends keyof WidgetDemoConfig>(
|
||||||
key: K,
|
key: K,
|
||||||
@@ -230,17 +227,42 @@ ${configLines.join(",\n")}
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
removeMountedWidget()
|
removeMountedWidget()
|
||||||
setGeneratedToken("")
|
setGeneratedToken("")
|
||||||
setStatus(error instanceof Error ? error.message : "生成 userToken 失败")
|
setStatus(error instanceof Error ? error.message : "挂载 Widget 失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCopyDirectUrl() {
|
async function handleCopyDirectUrl() {
|
||||||
if (!directChatUrl || typeof navigator === "undefined") {
|
if (!window.CSAgentWidget || typeof navigator === "undefined") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await navigator.clipboard.writeText(directChatUrl)
|
try {
|
||||||
setCopied(true)
|
const url = await window.CSAgentWidget.getChatUrl()
|
||||||
window.setTimeout(() => setCopied(false), 1600)
|
setLatestDirectChatUrl(url)
|
||||||
|
if (config.authMode === "jwt") {
|
||||||
|
setGeneratedToken(new URL(url).searchParams.get("userToken") || "")
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
setCopied(true)
|
||||||
|
window.setTimeout(() => setCopied(false), 1600)
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error instanceof Error ? error.message : "生成客服链接失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpenDirectChat() {
|
||||||
|
if (!window.CSAgentWidget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = await window.CSAgentWidget.getChatUrl()
|
||||||
|
setLatestDirectChatUrl(url)
|
||||||
|
if (config.authMode === "jwt") {
|
||||||
|
setGeneratedToken(new URL(url).searchParams.get("userToken") || "")
|
||||||
|
}
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer")
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error instanceof Error ? error.message : "生成客服链接失败")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCopySnippet() {
|
async function handleCopySnippet() {
|
||||||
@@ -353,13 +375,13 @@ ${configLines.join(",\n")}
|
|||||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row">
|
<div className="mt-2 flex flex-col gap-2 sm:flex-row">
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={directChatUrl || "请先填写 channelId 并挂载"}
|
value={latestDirectChatUrl || "点击复制或新窗口打开时生成最新链接"}
|
||||||
className="h-9 min-w-0 flex-1 rounded-md border border-slate-200 px-3 font-mono text-xs outline-none"
|
className="h-9 min-w-0 flex-1 rounded-md border border-slate-200 px-3 font-mono text-xs outline-none"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!directChatUrl}
|
disabled={!config.channelId}
|
||||||
onClick={() => void handleCopyDirectUrl()}
|
onClick={() => void handleCopyDirectUrl()}
|
||||||
className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
|
className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
@@ -367,12 +389,8 @@ ${configLines.join(",\n")}
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!directChatUrl}
|
disabled={!config.channelId}
|
||||||
onClick={() => {
|
onClick={() => void handleOpenDirectChat()}
|
||||||
if (directChatUrl) {
|
|
||||||
window.open(directChatUrl, "_blank", "noopener,noreferrer")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="rounded-md bg-slate-950 px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
className="rounded-md bg-slate-950 px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
新窗口打开
|
新窗口打开
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
setKefuWidgetConfig,
|
setKefuWidgetConfig,
|
||||||
type KefuWidgetHostConfig,
|
type KefuWidgetRuntimeConfig,
|
||||||
} from "@/lib/kefu-widget-config"
|
} from "@/lib/kefu-widget-config"
|
||||||
|
|
||||||
type HostBridgeOptions = {
|
type HostBridgeOptions = {
|
||||||
@@ -32,7 +32,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
|||||||
const data = event.data as
|
const data = event.data as
|
||||||
| {
|
| {
|
||||||
type?: string
|
type?: string
|
||||||
payload?: KefuWidgetHostConfig | { isMaximized?: boolean }
|
payload?: KefuWidgetRuntimeConfig | { isMaximized?: boolean }
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
if (!data?.type) {
|
if (!data?.type) {
|
||||||
@@ -40,7 +40,7 @@ export function bindKefuHostBridge(options: HostBridgeOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
|
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
|
||||||
setKefuWidgetConfig(data.payload as KefuWidgetHostConfig)
|
setKefuWidgetConfig(data.payload as KefuWidgetRuntimeConfig)
|
||||||
options.onInit?.()
|
options.onInit?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export type KefuWidgetHostConfig = {
|
|||||||
externalId?: string
|
externalId?: string
|
||||||
/** 访客展示名,仅用于首次换取客服会话 token */
|
/** 访客展示名,仅用于首次换取客服会话 token */
|
||||||
externalName?: string
|
externalName?: string
|
||||||
/** 业务系统签发的前台用户 JWT,仅用于首次换取客服会话 token */
|
/** 打开客服前按需获取业务系统签发的前台用户 JWT */
|
||||||
userToken?: string
|
getUserToken?: () => string | Promise<string>
|
||||||
title?: string
|
title?: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
position?: "left" | "right"
|
position?: "left" | "right"
|
||||||
@@ -15,15 +15,20 @@ export type KefuWidgetHostConfig = {
|
|||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type KefuWidgetRuntimeConfig = Omit<KefuWidgetHostConfig, "getUserToken"> & {
|
||||||
|
/** 仅用于 /kefu/chat 运行时换取客服会话 token,不属于 CSAgentConfig 接入参数 */
|
||||||
|
userToken?: string
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CSAgentConfig?: KefuWidgetHostConfig
|
CSAgentConfig?: KefuWidgetHostConfig
|
||||||
__CS_AGENT_WIDGET_CONFIG__?: KefuWidgetHostConfig
|
__CS_AGENT_WIDGET_CONFIG__?: KefuWidgetRuntimeConfig
|
||||||
__CS_AGENT_WIDGET_STATE__?: unknown
|
__CS_AGENT_WIDGET_STATE__?: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readKefuWidgetConfig(): KefuWidgetHostConfig {
|
export function readKefuWidgetConfig(): KefuWidgetRuntimeConfig {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return {
|
return {
|
||||||
channelId: "",
|
channelId: "",
|
||||||
@@ -33,7 +38,7 @@ export function readKefuWidgetConfig(): KefuWidgetHostConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
const fallback: KefuWidgetHostConfig = {
|
const fallback: KefuWidgetRuntimeConfig = {
|
||||||
channelId:
|
channelId:
|
||||||
query.get("channelId") ??
|
query.get("channelId") ??
|
||||||
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() ??
|
process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() ??
|
||||||
@@ -56,10 +61,20 @@ export function readKefuWidgetConfig(): KefuWidgetHostConfig {
|
|||||||
width: query.get("width") ?? undefined,
|
width: query.get("width") ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.__CS_AGENT_WIDGET_CONFIG__ ?? window.CSAgentConfig ?? fallback
|
if (window.__CS_AGENT_WIDGET_CONFIG__) {
|
||||||
|
return window.__CS_AGENT_WIDGET_CONFIG__
|
||||||
|
}
|
||||||
|
if (window.CSAgentConfig) {
|
||||||
|
const { getUserToken: _getUserToken, ...hostConfig } = window.CSAgentConfig
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
...hostConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setKefuWidgetConfig(config: KefuWidgetHostConfig) {
|
export function setKefuWidgetConfig(config: KefuWidgetRuntimeConfig) {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
frameHideTimer: null,
|
frameHideTimer: null,
|
||||||
frameDestroyTimer: null,
|
frameDestroyTimer: null,
|
||||||
config: null,
|
config: null,
|
||||||
|
frameConfig: null,
|
||||||
frameUrl: null,
|
frameUrl: null,
|
||||||
animationDuration: 260,
|
animationDuration: 260,
|
||||||
};
|
};
|
||||||
@@ -49,8 +50,8 @@
|
|||||||
if (merged.externalId) {
|
if (merged.externalId) {
|
||||||
merged.externalId = String(merged.externalId);
|
merged.externalId = String(merged.externalId);
|
||||||
}
|
}
|
||||||
if (merged.userToken) {
|
if (typeof merged.getUserToken !== "function") {
|
||||||
merged.userToken = String(merged.userToken);
|
delete merged.getUserToken;
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
return String(config.widgetBaseUrl || config.baseUrl || window.location.origin).replace(/\/$/, "");
|
return String(config.widgetBaseUrl || config.baseUrl || window.location.origin).replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFrameUrl(config) {
|
function createFrameUrl(config, userToken) {
|
||||||
var widgetBaseUrl = resolveWidgetBaseUrl(config);
|
var widgetBaseUrl = resolveWidgetBaseUrl(config);
|
||||||
var frameUrl = new URL(widgetBaseUrl + "/kefu/chat/");
|
var frameUrl = new URL(widgetBaseUrl + "/kefu/chat/");
|
||||||
frameUrl.searchParams.set("channelId", config.channelId);
|
frameUrl.searchParams.set("channelId", config.channelId);
|
||||||
@@ -71,10 +72,49 @@
|
|||||||
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
|
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
|
||||||
if (config.externalId) frameUrl.searchParams.set("externalId", config.externalId);
|
if (config.externalId) frameUrl.searchParams.set("externalId", config.externalId);
|
||||||
if (config.externalName) frameUrl.searchParams.set("externalName", config.externalName);
|
if (config.externalName) frameUrl.searchParams.set("externalName", config.externalName);
|
||||||
if (config.userToken) frameUrl.searchParams.set("userToken", config.userToken);
|
if (userToken) frameUrl.searchParams.set("userToken", userToken);
|
||||||
return frameUrl;
|
return frameUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFrameConfig(config, userToken) {
|
||||||
|
var payload = {};
|
||||||
|
var key;
|
||||||
|
for (key in config) {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(config, key) &&
|
||||||
|
key !== "getUserToken"
|
||||||
|
) {
|
||||||
|
payload[key] = config[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userToken) {
|
||||||
|
payload.userToken = userToken;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUserToken() {
|
||||||
|
var config = state.config || {};
|
||||||
|
if (typeof config.getUserToken !== "function") {
|
||||||
|
return Promise.resolve("");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Promise.resolve(config.getUserToken()).then(function (token) {
|
||||||
|
return String(token || "").trim();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareFrameUrl() {
|
||||||
|
return resolveUserToken().then(function (userToken) {
|
||||||
|
state.frameUrl = createFrameUrl(state.config, userToken);
|
||||||
|
state.frameConfig = createFrameConfig(state.config, userToken);
|
||||||
|
return state.frameUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function mergeWidgetConfig(config, remoteConfig) {
|
function mergeWidgetConfig(config, remoteConfig) {
|
||||||
if (!remoteConfig) {
|
if (!remoteConfig) {
|
||||||
return config;
|
return config;
|
||||||
@@ -199,7 +239,7 @@
|
|||||||
state.initSent = true;
|
state.initSent = true;
|
||||||
postToFrame({
|
postToFrame({
|
||||||
type: "cs-agent:init",
|
type: "cs-agent:init",
|
||||||
payload: state.config,
|
payload: state.frameConfig || createFrameConfig(state.config, ""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,11 +428,13 @@
|
|||||||
button.appendChild(text);
|
button.appendChild(text);
|
||||||
|
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
if (!state.frame) {
|
if (state.isOpen) {
|
||||||
createFrame();
|
state.isOpen = false;
|
||||||
|
syncFrameVisibility();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
state.isOpen = !state.isOpen;
|
|
||||||
syncFrameVisibility();
|
openWidget();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(button);
|
document.body.appendChild(button);
|
||||||
@@ -416,7 +458,6 @@
|
|||||||
fetchWidgetConfig(state.config).then(function (nextConfig) {
|
fetchWidgetConfig(state.config).then(function (nextConfig) {
|
||||||
state.configLoading = false;
|
state.configLoading = false;
|
||||||
state.config = normalizeConfig(nextConfig);
|
state.config = normalizeConfig(nextConfig);
|
||||||
state.frameUrl = createFrameUrl(state.config);
|
|
||||||
if (state.button && state.button.parentNode) {
|
if (state.button && state.button.parentNode) {
|
||||||
state.button.parentNode.removeChild(state.button);
|
state.button.parentNode.removeChild(state.button);
|
||||||
state.button = null;
|
state.button = null;
|
||||||
@@ -441,25 +482,48 @@
|
|||||||
state.isOpen = false;
|
state.isOpen = false;
|
||||||
state.isMaximized = false;
|
state.isMaximized = false;
|
||||||
state.configLoading = false;
|
state.configLoading = false;
|
||||||
|
state.frameConfig = null;
|
||||||
|
state.frameUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWidget() {
|
||||||
|
return prepareFrameUrl()
|
||||||
|
.then(function () {
|
||||||
|
if (!state.frame) {
|
||||||
|
createFrame();
|
||||||
|
}
|
||||||
|
if (!state.frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.isOpen = true;
|
||||||
|
syncFrameVisibility();
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.error("[cs-agent-widget] open failed", error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.CSAgentWidget = {
|
window.CSAgentWidget = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
destroy: destroy,
|
destroy: destroy,
|
||||||
open: function () {
|
open: function () {
|
||||||
if (!state.frame) {
|
return openWidget();
|
||||||
createFrame();
|
|
||||||
}
|
|
||||||
if (!state.frame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.isOpen = true;
|
|
||||||
syncFrameVisibility();
|
|
||||||
},
|
},
|
||||||
close: function () {
|
close: function () {
|
||||||
state.isOpen = false;
|
state.isOpen = false;
|
||||||
syncFrameVisibility();
|
syncFrameVisibility();
|
||||||
},
|
},
|
||||||
|
getChatUrl: function () {
|
||||||
|
if (!state.config) {
|
||||||
|
mount(window.CSAgentConfig || {});
|
||||||
|
}
|
||||||
|
if (!state.config || !state.config.channelId) {
|
||||||
|
return Promise.reject(new Error("channelId is required"));
|
||||||
|
}
|
||||||
|
return prepareFrameUrl().then(function (frameUrl) {
|
||||||
|
return frameUrl.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!state.listenerBound) {
|
if (!state.listenerBound) {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
import test from "node:test"
|
||||||
|
import vm from "node:vm"
|
||||||
|
|
||||||
|
function createElement(tagName) {
|
||||||
|
return {
|
||||||
|
tagName,
|
||||||
|
children: [],
|
||||||
|
dataset: {},
|
||||||
|
style: {},
|
||||||
|
attributes: {},
|
||||||
|
listeners: {},
|
||||||
|
parentNode: null,
|
||||||
|
contentWindow: {},
|
||||||
|
setAttribute(name, value) {
|
||||||
|
this.attributes[name] = String(value)
|
||||||
|
},
|
||||||
|
appendChild(child) {
|
||||||
|
child.parentNode = this
|
||||||
|
this.children.push(child)
|
||||||
|
return child
|
||||||
|
},
|
||||||
|
removeChild(child) {
|
||||||
|
this.children = this.children.filter((item) => item !== child)
|
||||||
|
child.parentNode = null
|
||||||
|
return child
|
||||||
|
},
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
this.listeners[type] = handler
|
||||||
|
},
|
||||||
|
click() {
|
||||||
|
this.listeners.click?.()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSdk(config) {
|
||||||
|
const source = await readFile(new URL("./cs-ai-agent-sdk.js", import.meta.url), "utf8")
|
||||||
|
const body = createElement("body")
|
||||||
|
const sandbox = {
|
||||||
|
URL,
|
||||||
|
console,
|
||||||
|
fetch: async () => ({
|
||||||
|
json: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
title: "在线客服",
|
||||||
|
themeColor: "#2563eb",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
document: {
|
||||||
|
body,
|
||||||
|
currentScript: {
|
||||||
|
src: "https://chat.example/sdk/cs-ai-agent-sdk.min.js",
|
||||||
|
},
|
||||||
|
createElement,
|
||||||
|
createElementNS: (_namespace, tagName) => createElement(tagName),
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
CSAgentConfig: config,
|
||||||
|
location: {
|
||||||
|
origin: "https://host.example",
|
||||||
|
},
|
||||||
|
addEventListener() {},
|
||||||
|
clearTimeout() {},
|
||||||
|
setTimeout(handler) {
|
||||||
|
handler()
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sandbox.window.window = sandbox.window
|
||||||
|
sandbox.window.document = sandbox.document
|
||||||
|
sandbox.window.fetch = sandbox.fetch
|
||||||
|
sandbox.window.console = console
|
||||||
|
sandbox.window.URL = URL
|
||||||
|
sandbox.window.setTimeout = sandbox.window.setTimeout
|
||||||
|
sandbox.window.clearTimeout = sandbox.window.clearTimeout
|
||||||
|
|
||||||
|
vm.runInNewContext(source, sandbox)
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
return sandbox
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushPromises(count = 5) {
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
await Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getChatUrl resolves a fresh userToken for each call", async () => {
|
||||||
|
let calls = 0
|
||||||
|
const sandbox = await loadSdk({
|
||||||
|
channelId: "ch_1",
|
||||||
|
baseUrl: "https://api.example",
|
||||||
|
getUserToken: async () => `token_${++calls}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(typeof sandbox.window.CSAgentWidget.getChatUrl, "function")
|
||||||
|
|
||||||
|
const first = await sandbox.window.CSAgentWidget.getChatUrl()
|
||||||
|
const second = await sandbox.window.CSAgentWidget.getChatUrl()
|
||||||
|
|
||||||
|
assert.equal(new URL(first).searchParams.get("userToken"), "token_1")
|
||||||
|
assert.equal(new URL(second).searchParams.get("userToken"), "token_2")
|
||||||
|
assert.equal(calls, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("launcher click creates chat iframe with a freshly resolved userToken", async () => {
|
||||||
|
const sandbox = await loadSdk({
|
||||||
|
channelId: "ch_1",
|
||||||
|
baseUrl: "https://api.example",
|
||||||
|
getUserToken: async () => "click_token",
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const launcher = sandbox.document.body.children.find(
|
||||||
|
(child) => child.dataset.csAgentWidget === "launcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(launcher)
|
||||||
|
|
||||||
|
launcher.click()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const frame = sandbox.document.body.children.find(
|
||||||
|
(child) => child.dataset.csAgentWidget === "frame"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(frame)
|
||||||
|
assert.equal(new URL(frame.src).searchParams.get("userToken"), "click_token")
|
||||||
|
})
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var DEFAULT_CONFIG = {
|
|
||||||
position: "right",
|
|
||||||
themeColor: "#0f6cbd",
|
|
||||||
width: "380px",
|
|
||||||
};
|
|
||||||
|
|
||||||
var state = window.__CS_AGENT_WIDGET_STATE__;
|
|
||||||
if (!state) {
|
|
||||||
state = {
|
|
||||||
button: null,
|
|
||||||
frame: null,
|
|
||||||
frameLoaded: false,
|
|
||||||
frameReady: false,
|
|
||||||
initSent: false,
|
|
||||||
isOpen: false,
|
|
||||||
isMaximized: false,
|
|
||||||
frameHideTimer: null,
|
|
||||||
frameDestroyTimer: null,
|
|
||||||
config: null,
|
|
||||||
frameUrl: null,
|
|
||||||
animationDuration: 260,
|
|
||||||
};
|
|
||||||
window.__CS_AGENT_WIDGET_STATE__ = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeConfig(config) {
|
|
||||||
var merged = {};
|
|
||||||
var key;
|
|
||||||
for (key in DEFAULT_CONFIG) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(DEFAULT_CONFIG, key)) {
|
|
||||||
merged[key] = DEFAULT_CONFIG[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config = config || {};
|
|
||||||
for (key in config) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(config, key)) {
|
|
||||||
merged[key] = config[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merged.baseUrl = String(merged.baseUrl || window.location.origin).replace(/\/$/, "");
|
|
||||||
merged.apiBaseUrl = String(merged.apiBaseUrl || merged.baseUrl).replace(/\/$/, "");
|
|
||||||
merged.channelId = String(merged.channelId || "");
|
|
||||||
if (merged.externalId) {
|
|
||||||
merged.externalId = String(merged.externalId);
|
|
||||||
}
|
|
||||||
if (merged.userToken) {
|
|
||||||
merged.userToken = String(merged.userToken);
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWidgetBaseUrl(config) {
|
|
||||||
var currentScript = document.currentScript;
|
|
||||||
if (currentScript && currentScript.src) {
|
|
||||||
return currentScript.src.replace(/\/sdk\/cs-agent-widget\.js(?:\?.*)?$/, "");
|
|
||||||
}
|
|
||||||
return String(config.widgetBaseUrl || config.baseUrl || window.location.origin).replace(/\/$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFrameUrl(config) {
|
|
||||||
var widgetBaseUrl = resolveWidgetBaseUrl(config);
|
|
||||||
var frameUrl = new URL(widgetBaseUrl + "/kefu/chat/");
|
|
||||||
frameUrl.searchParams.set("channelId", config.channelId);
|
|
||||||
frameUrl.searchParams.set("baseUrl", config.baseUrl);
|
|
||||||
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
|
|
||||||
if (config.title) frameUrl.searchParams.set("title", config.title);
|
|
||||||
if (config.subtitle) frameUrl.searchParams.set("subtitle", config.subtitle);
|
|
||||||
if (config.position) frameUrl.searchParams.set("position", config.position);
|
|
||||||
if (config.themeColor) frameUrl.searchParams.set("themeColor", config.themeColor);
|
|
||||||
if (config.width) frameUrl.searchParams.set("width", config.width);
|
|
||||||
if (config.externalId) frameUrl.searchParams.set("externalId", config.externalId);
|
|
||||||
if (config.externalName) frameUrl.searchParams.set("externalName", config.externalName);
|
|
||||||
if (config.userToken) frameUrl.searchParams.set("userToken", config.userToken);
|
|
||||||
return frameUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFrameTimers() {
|
|
||||||
if (state.frameHideTimer) {
|
|
||||||
window.clearTimeout(state.frameHideTimer);
|
|
||||||
state.frameHideTimer = null;
|
|
||||||
}
|
|
||||||
if (state.frameDestroyTimer) {
|
|
||||||
window.clearTimeout(state.frameDestroyTimer);
|
|
||||||
state.frameDestroyTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFrameLayout() {
|
|
||||||
var frame = state.frame;
|
|
||||||
var config = state.config;
|
|
||||||
if (!frame || !config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.style.position = "fixed";
|
|
||||||
frame.style.border = "0";
|
|
||||||
frame.style.overflow = "hidden";
|
|
||||||
frame.style.background = "#fff";
|
|
||||||
frame.style.zIndex = "2147483000";
|
|
||||||
frame.style.boxShadow = "0 28px 80px rgba(15, 35, 65, 0.28)";
|
|
||||||
frame.style.willChange = "top,right,bottom,left,width,height,opacity,transform,border-radius";
|
|
||||||
frame.style.transition =
|
|
||||||
"top 260ms cubic-bezier(0.22, 1, 0.36, 1), right 260ms cubic-bezier(0.22, 1, 0.36, 1), bottom 260ms cubic-bezier(0.22, 1, 0.36, 1), left 260ms cubic-bezier(0.22, 1, 0.36, 1), width 260ms cubic-bezier(0.22, 1, 0.36, 1), height 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, transform 260ms cubic-bezier(0.22, 1, 0.36, 1), border-radius 260ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 260ms ease";
|
|
||||||
frame.style.transformOrigin =
|
|
||||||
config.position === "left" ? "left bottom" : "right bottom";
|
|
||||||
|
|
||||||
if (state.isMaximized) {
|
|
||||||
frame.style.top = "20px";
|
|
||||||
frame.style.right = "20px";
|
|
||||||
frame.style.bottom = "20px";
|
|
||||||
frame.style.left = "20px";
|
|
||||||
frame.style.width = "calc(100vw - 40px)";
|
|
||||||
frame.style.maxWidth = "none";
|
|
||||||
frame.style.height = "calc(100vh - 40px)";
|
|
||||||
frame.style.borderRadius = "24px";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.style.top = "";
|
|
||||||
frame.style.bottom = "88px";
|
|
||||||
frame.style.right = config.position === "left" ? "" : "24px";
|
|
||||||
frame.style.left = config.position === "left" ? "24px" : "";
|
|
||||||
frame.style.width = config.width || "380px";
|
|
||||||
frame.style.maxWidth = "calc(100vw - 24px)";
|
|
||||||
frame.style.height = "min(760px, calc(100vh - 112px))";
|
|
||||||
frame.style.borderRadius = "28px";
|
|
||||||
}
|
|
||||||
|
|
||||||
function postToFrame(message) {
|
|
||||||
if (!state.frame || !state.frame.contentWindow || !state.frameUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
state.frame.contentWindow.postMessage(message, state.frameUrl.origin);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[cs-agent-widget] postMessage failed", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushFrameState() {
|
|
||||||
if (!state.frame || !state.frameLoaded || !state.frameReady) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.initSent) {
|
|
||||||
state.initSent = true;
|
|
||||||
postToFrame({
|
|
||||||
type: "cs-agent:init",
|
|
||||||
payload: state.config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
postToFrame({ type: state.isOpen ? "cs-agent:open" : "cs-agent:minimize" });
|
|
||||||
postToFrame({
|
|
||||||
type: "cs-agent:maximized",
|
|
||||||
payload: { isMaximized: state.isMaximized },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncFrameVisibility() {
|
|
||||||
var frame = state.frame;
|
|
||||||
if (!frame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearFrameTimers();
|
|
||||||
applyFrameLayout();
|
|
||||||
frame.style.display = "block";
|
|
||||||
|
|
||||||
if (state.isOpen) {
|
|
||||||
frame.style.visibility = "visible";
|
|
||||||
frame.style.pointerEvents = "auto";
|
|
||||||
state.frameHideTimer = window.setTimeout(function () {
|
|
||||||
if (!state.frame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.frame.style.opacity = "1";
|
|
||||||
state.frame.style.transform = "translate3d(0, 0, 0) scale(1)";
|
|
||||||
}, 16);
|
|
||||||
flushFrameState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.style.pointerEvents = "none";
|
|
||||||
frame.style.opacity = "0";
|
|
||||||
frame.style.transform = state.isMaximized
|
|
||||||
? "translate3d(0, 10px, 0) scale(0.985)"
|
|
||||||
: "translate3d(0, 16px, 0) scale(0.96)";
|
|
||||||
state.frameHideTimer = window.setTimeout(function () {
|
|
||||||
if (!state.frame || state.isOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.frame.style.visibility = "hidden";
|
|
||||||
}, state.animationDuration);
|
|
||||||
flushFrameState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyFrame() {
|
|
||||||
if (!state.frame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearFrameTimers();
|
|
||||||
state.frame.style.pointerEvents = "none";
|
|
||||||
state.frame.style.opacity = "0";
|
|
||||||
state.frame.style.transform = "translate3d(0, 18px, 0) scale(0.94)";
|
|
||||||
state.frame.style.visibility = "hidden";
|
|
||||||
state.frameDestroyTimer = window.setTimeout(function () {
|
|
||||||
if (!state.frame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.frame.parentNode) {
|
|
||||||
state.frame.parentNode.removeChild(state.frame);
|
|
||||||
}
|
|
||||||
state.frame = null;
|
|
||||||
state.frameLoaded = false;
|
|
||||||
state.frameReady = false;
|
|
||||||
state.initSent = false;
|
|
||||||
state.isOpen = false;
|
|
||||||
state.isMaximized = false;
|
|
||||||
clearFrameTimers();
|
|
||||||
}, state.animationDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFrame() {
|
|
||||||
if (state.frame) {
|
|
||||||
return state.frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.frame = document.createElement("iframe");
|
|
||||||
state.frame.dataset.csAgentWidget = "frame";
|
|
||||||
state.frame.title = state.config.title || "在线客服";
|
|
||||||
state.frame.src = state.frameUrl.toString();
|
|
||||||
applyFrameLayout();
|
|
||||||
state.frame.style.display = "block";
|
|
||||||
state.frame.style.visibility = "hidden";
|
|
||||||
state.frame.style.pointerEvents = "none";
|
|
||||||
state.frame.style.opacity = "0";
|
|
||||||
state.frame.style.transform = "translate3d(0, 18px, 0) scale(0.96)";
|
|
||||||
state.frame.addEventListener("load", function () {
|
|
||||||
state.frameLoaded = true;
|
|
||||||
syncFrameVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(state.frame);
|
|
||||||
return state.frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowMessage(event) {
|
|
||||||
if (!state.frame || event.source !== state.frame.contentWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = event.data || {};
|
|
||||||
if (data.type === "cs-agent:ready") {
|
|
||||||
state.frameReady = true;
|
|
||||||
flushFrameState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "cs-agent:request-minimize") {
|
|
||||||
state.isOpen = false;
|
|
||||||
syncFrameVisibility();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "cs-agent:request-close") {
|
|
||||||
destroyFrame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "cs-agent:request-toggle-maximize") {
|
|
||||||
state.isMaximized = !state.isMaximized;
|
|
||||||
syncFrameVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLauncher() {
|
|
||||||
if (state.button) {
|
|
||||||
return state.button;
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = state.config;
|
|
||||||
var button = document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.dataset.csAgentWidget = "launcher";
|
|
||||||
button.setAttribute("aria-label", config.title || "在线客服");
|
|
||||||
button.textContent = config.title || "在线客服";
|
|
||||||
button.style.position = "fixed";
|
|
||||||
button.style.bottom = "24px";
|
|
||||||
button.style.right = config.position === "left" ? "" : "24px";
|
|
||||||
button.style.left = config.position === "left" ? "24px" : "";
|
|
||||||
button.style.zIndex = "2147483000";
|
|
||||||
button.style.border = "0";
|
|
||||||
button.style.borderRadius = "999px";
|
|
||||||
button.style.padding = "14px 18px";
|
|
||||||
button.style.background = config.themeColor || "#0f6cbd";
|
|
||||||
button.style.color = "#fff";
|
|
||||||
button.style.font = "600 14px/1 sans-serif";
|
|
||||||
button.style.boxShadow = "0 18px 40px rgba(15, 35, 65, 0.24)";
|
|
||||||
button.style.cursor = "pointer";
|
|
||||||
|
|
||||||
button.addEventListener("click", function () {
|
|
||||||
if (!state.frame) {
|
|
||||||
createFrame();
|
|
||||||
}
|
|
||||||
state.isOpen = !state.isOpen;
|
|
||||||
syncFrameVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(button);
|
|
||||||
state.button = button;
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mount(config) {
|
|
||||||
state.config = normalizeConfig(config || window.CSAgentConfig || {});
|
|
||||||
if (!state.config.channelId || !state.config.baseUrl) {
|
|
||||||
console.error("[cs-agent-widget] channelId and baseUrl are required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.frameUrl = createFrameUrl(state.config);
|
|
||||||
createLauncher();
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroy() {
|
|
||||||
clearFrameTimers();
|
|
||||||
if (state.frame && state.frame.parentNode) {
|
|
||||||
state.frame.parentNode.removeChild(state.frame);
|
|
||||||
}
|
|
||||||
if (state.button && state.button.parentNode) {
|
|
||||||
state.button.parentNode.removeChild(state.button);
|
|
||||||
}
|
|
||||||
state.button = null;
|
|
||||||
state.frame = null;
|
|
||||||
state.frameLoaded = false;
|
|
||||||
state.frameReady = false;
|
|
||||||
state.initSent = false;
|
|
||||||
state.isOpen = false;
|
|
||||||
state.isMaximized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.CSAgentWidget = {
|
|
||||||
mount: mount,
|
|
||||||
destroy: destroy,
|
|
||||||
open: function () {
|
|
||||||
if (!state.frame) {
|
|
||||||
createFrame();
|
|
||||||
}
|
|
||||||
state.isOpen = true;
|
|
||||||
syncFrameVisibility();
|
|
||||||
},
|
|
||||||
close: function () {
|
|
||||||
state.isOpen = false;
|
|
||||||
syncFrameVisibility();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!state.listenerBound) {
|
|
||||||
window.addEventListener("message", handleWindowMessage);
|
|
||||||
state.listenerBound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.CSAgentConfig) {
|
|
||||||
mount(window.CSAgentConfig);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user