feat: add web channel configuration support and update widget response structure

This commit is contained in:
mlogclub
2026-04-24 22:24:19 +08:00
parent a262a865a0
commit ea94837db2
9 changed files with 400 additions and 114 deletions
@@ -3,6 +3,7 @@ package open
import (
"cs-agent/internal/pkg/dto/response"
"cs-agent/internal/pkg/irisx"
"cs-agent/internal/services"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/web"
@@ -17,9 +18,19 @@ func (c *ImWidgetController) AnyConfig() *web.JsonResult {
if channel == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
cfg, err := services.ChannelService.ParseWebChannelConfig(channel.ConfigJSON)
if err != nil {
return web.JsonErrorMsg("Web渠道配置不合法")
}
ret := response.WidgetConfigResponse{
ChannelID: channel.ChannelID,
ChannelID: channel.ChannelID,
Title: cfg.Title,
Subtitle: cfg.Subtitle,
WelcomeText: cfg.WelcomeText,
ThemeColor: cfg.ThemeColor,
Position: cfg.Position,
Width: cfg.Width,
}
return web.JsonData(ret)
}
+9
View File
@@ -15,3 +15,12 @@ type AuthPrincipal struct {
type WxWorkKFChannelConfig struct {
OpenKfID string `json:"openKfId"`
}
type WebChannelConfig struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
WelcomeText string `json:"welcomeText"`
ThemeColor string `json:"themeColor"`
Position string `json:"position"`
Width string `json:"width"`
}
+7 -1
View File
@@ -1,5 +1,11 @@
package response
type WidgetConfigResponse struct {
ChannelID string `json:"channelId"`
ChannelID string `json:"channelId"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
WelcomeText string `json:"welcomeText"`
ThemeColor string `json:"themeColor"`
Position string `json:"position"`
Width string `json:"width"`
}
+48
View File
@@ -159,6 +159,45 @@ func (s *channelService) ParseWxWorkKFChannelConfig(raw string) (*dto.WxWorkKFCh
return cfg, nil
}
func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfig, error) {
raw = strings.TrimSpace(raw)
cfg := &dto.WebChannelConfig{
Title: "在线客服",
Subtitle: "欢迎咨询",
WelcomeText: "",
ThemeColor: "#2563eb",
Position: "right",
Width: "380px",
}
if raw != "" {
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
return nil, err
}
}
cfg.Title = strings.TrimSpace(cfg.Title)
if cfg.Title == "" {
cfg.Title = "在线客服"
}
cfg.Subtitle = strings.TrimSpace(cfg.Subtitle)
cfg.WelcomeText = strings.TrimSpace(cfg.WelcomeText)
cfg.ThemeColor = strings.TrimSpace(cfg.ThemeColor)
if cfg.ThemeColor == "" {
cfg.ThemeColor = "#2563eb"
}
cfg.Position = strings.TrimSpace(cfg.Position)
if cfg.Position == "" {
cfg.Position = "right"
}
if cfg.Position != "left" && cfg.Position != "right" {
return nil, errorsx.InvalidParam("Web渠道配置 position 只能为 left 或 right")
}
cfg.Width = strings.TrimSpace(cfg.Width)
if cfg.Width == "" {
cfg.Width = "380px"
}
return cfg, nil
}
func (s *channelService) GetEnabledWxWorkKFChannelByOpenKfID(openKfID string) *models.Channel {
openKfID = strings.TrimSpace(openKfID)
if openKfID == "" {
@@ -229,6 +268,15 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe
if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil {
return nil, errorsx.InvalidParam("渠道标识已存在")
}
cfg, err := s.ParseWebChannelConfig(configJSON)
if err != nil {
return nil, errorsx.InvalidParam("Web渠道配置不合法")
}
configBytes, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
configJSON = string(configBytes)
case enums.ChannelTypeWxWorkKF:
if channelID == "" {
channelID = strs.UUID()
+151 -1
View File
@@ -37,11 +37,40 @@ const channelTypeOptions = [
{ value: "wxwork_kf", label: "企业微信客服" },
] as const
const widgetPositionOptions = [
{ value: "right", label: "右下角" },
{ value: "left", label: "左下角" },
] as const
type WebChannelConfig = {
title?: string
subtitle?: string
welcomeText?: string
themeColor?: string
position?: "left" | "right"
width?: string
}
const defaultWebChannelConfig: Required<WebChannelConfig> = {
title: "在线客服",
subtitle: "欢迎咨询",
welcomeText: "",
themeColor: "#2563eb",
position: "right",
width: "380px",
}
const schema = z.object({
channelType: z.enum(["web", "wxwork_kf"], "请选择渠道类型"),
aiAgentId: z.string().trim().regex(/^\d+$/, "请选择 AI Agent"),
name: z.string().trim().min(1, "渠道名称不能为空"),
openKfId: z.string().trim(),
widgetTitle: z.string().trim(),
widgetSubtitle: z.string().trim(),
widgetWelcomeText: z.string().trim(),
widgetThemeColor: z.string().trim(),
widgetPosition: z.enum(["left", "right"]),
widgetWidth: z.string().trim(),
remark: z.string().trim(),
})
@@ -58,6 +87,12 @@ const emptyForm: EditForm = {
aiAgentId: "",
name: "",
openKfId: "",
widgetTitle: defaultWebChannelConfig.title,
widgetSubtitle: defaultWebChannelConfig.subtitle,
widgetWelcomeText: defaultWebChannelConfig.welcomeText,
widgetThemeColor: defaultWebChannelConfig.themeColor,
widgetPosition: defaultWebChannelConfig.position,
widgetWidth: defaultWebChannelConfig.width,
remark: "",
}
@@ -73,15 +108,43 @@ function parseOpenKfId(configJson: string): string {
}
}
function parseWebChannelConfig(configJson: string): Required<WebChannelConfig> {
if (!configJson.trim()) {
return defaultWebChannelConfig
}
try {
const parsed = JSON.parse(configJson) as WebChannelConfig
const position = parsed.position === "left" ? "left" : "right"
return {
title: parsed.title?.trim() || defaultWebChannelConfig.title,
subtitle: parsed.subtitle?.trim() ?? defaultWebChannelConfig.subtitle,
welcomeText: parsed.welcomeText?.trim() ?? defaultWebChannelConfig.welcomeText,
themeColor:
parsed.themeColor?.trim() || defaultWebChannelConfig.themeColor,
position,
width: parsed.width?.trim() || defaultWebChannelConfig.width,
}
} catch {
return defaultWebChannelConfig
}
}
function buildForm(item: AdminChannel | null): EditForm {
if (!item) {
return emptyForm
}
const widgetConfig = parseWebChannelConfig(item.configJson)
return {
channelType: item.channelType === "wxwork_kf" ? "wxwork_kf" : "web",
aiAgentId: item.aiAgentId > 0 ? String(item.aiAgentId) : "",
name: item.name,
openKfId: parseOpenKfId(item.configJson),
widgetTitle: widgetConfig.title,
widgetSubtitle: widgetConfig.subtitle,
widgetWelcomeText: widgetConfig.welcomeText,
widgetThemeColor: widgetConfig.themeColor,
widgetPosition: widgetConfig.position,
widgetWidth: widgetConfig.width,
remark: item.remark || "",
}
}
@@ -91,7 +154,15 @@ function buildPayload(form: EditForm, status: number): CreateAdminChannelPayload
const configJson =
channelType === "wxwork_kf"
? JSON.stringify({ openKfId: form.openKfId.trim() })
: ""
: JSON.stringify({
title: form.widgetTitle.trim() || defaultWebChannelConfig.title,
subtitle: form.widgetSubtitle.trim(),
welcomeText: form.widgetWelcomeText.trim(),
themeColor:
form.widgetThemeColor.trim() || defaultWebChannelConfig.themeColor,
position: form.widgetPosition || defaultWebChannelConfig.position,
width: form.widgetWidth.trim() || defaultWebChannelConfig.width,
})
return {
channelType,
aiAgentId: Number(form.aiAgentId),
@@ -281,6 +352,85 @@ function ChannelFormBody({
) : null}
</div>
{channelType === "web" ? (
<div className="grid grid-cols-1 gap-4 rounded-md border p-4 sm:grid-cols-2">
<Field data-invalid={!!errors.widgetTitle}>
<FieldLabel htmlFor="channel-widget-title"></FieldLabel>
<FieldContent>
<Input id="channel-widget-title" {...register("widgetTitle")} />
<FieldError errors={[errors.widgetTitle]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.widgetSubtitle}>
<FieldLabel htmlFor="channel-widget-subtitle"></FieldLabel>
<FieldContent>
<Input
id="channel-widget-subtitle"
{...register("widgetSubtitle")}
/>
<FieldError errors={[errors.widgetSubtitle]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.widgetThemeColor}>
<FieldLabel htmlFor="channel-widget-theme-color"></FieldLabel>
<FieldContent>
<Input
id="channel-widget-theme-color"
placeholder="#2563eb"
{...register("widgetThemeColor")}
/>
<FieldError errors={[errors.widgetThemeColor]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.widgetPosition}>
<FieldLabel></FieldLabel>
<FieldContent>
<Controller
control={control}
name="widgetPosition"
render={({ field }) => (
<OptionCombobox
value={field.value}
options={[...widgetPositionOptions]}
placeholder="请选择挂载位置"
searchPlaceholder="搜索挂载位置"
emptyText="未找到挂载位置"
onChange={field.onChange}
/>
)}
/>
<FieldError errors={[errors.widgetPosition]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.widgetWidth}>
<FieldLabel htmlFor="channel-widget-width"></FieldLabel>
<FieldContent>
<Input
id="channel-widget-width"
placeholder="380px"
{...register("widgetWidth")}
/>
<FieldError errors={[errors.widgetWidth]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.widgetWelcomeText}>
<FieldLabel htmlFor="channel-widget-welcome-text"></FieldLabel>
<FieldContent>
<Input
id="channel-widget-welcome-text"
{...register("widgetWelcomeText")}
/>
<FieldError errors={[errors.widgetWelcomeText]} />
</FieldContent>
</Field>
</div>
) : null}
<Field data-invalid={!!errors.remark}>
<FieldLabel htmlFor="channel-remark"></FieldLabel>
<FieldContent>
+6 -89
View File
@@ -3,20 +3,12 @@
import { useEffect, useMemo, useState } from "react"
import type { KefuWidgetHostConfig } from "@/lib/kefu-widget-config"
import { generateUUID } from "@/lib/utils"
const STORAGE_KEY = "cs-agent-web-widget-test-config"
const INITIAL_CONFIG: KefuWidgetHostConfig = {
channelId: "",
baseUrl: "",
apiBaseUrl: "",
externalSource: "web_chat",
title: "在线客服",
subtitle: "欢迎咨询",
position: "right",
themeColor: "#2563eb",
width: "680px",
subject: "",
}
declare global {
@@ -30,10 +22,6 @@ declare global {
}
}
function generateRandomSubject() {
return `访客-${generateUUID().replace(/-/g, "").slice(0, 8)}`
}
function getDefaultConfig(): KefuWidgetHostConfig {
if (typeof window === "undefined") {
return INITIAL_CONFIG
@@ -44,27 +32,11 @@ function getDefaultConfig(): KefuWidgetHostConfig {
? (JSON.parse(savedText) as Partial<KefuWidgetHostConfig>)
: {}
const query = new URLSearchParams(window.location.search)
const origin = window.location.origin
return {
channelId: query.get("channelId") ?? savedConfig.channelId ?? "",
baseUrl: query.get("baseUrl") ?? savedConfig.baseUrl ?? origin,
apiBaseUrl:
query.get("apiBaseUrl") ??
savedConfig.apiBaseUrl ??
savedConfig.baseUrl ??
origin,
externalSource:
query.get("externalSource") ?? savedConfig.externalSource ?? "web_chat",
title: query.get("title") ?? savedConfig.title ?? "在线客服",
subtitle: query.get("subtitle") ?? savedConfig.subtitle ?? "欢迎咨询",
position:
(query.get("position") as "left" | "right" | null) ??
savedConfig.position ??
"right",
themeColor: query.get("themeColor") ?? savedConfig.themeColor ?? "#2563eb",
width: query.get("width") ?? savedConfig.width ?? "680px",
subject: query.get("subject") ?? savedConfig.subject ?? generateRandomSubject(),
baseUrl: "",
apiBaseUrl: "",
}
}
@@ -119,25 +91,12 @@ export function KefuWidgetDemo() {
}, [])
const snippet = useMemo(() => {
const scriptSrc = config.baseUrl
? `${config.baseUrl.replace(/\/$/, "")}/sdk/cs-ai-agent-sdk.min.js`
: "/sdk/cs-ai-agent-sdk.min.js"
return `<script>
window.CSAgentConfig = {
channelId: "${config.channelId || ""}",
baseUrl: "${config.baseUrl || ""}",
apiBaseUrl: "${config.apiBaseUrl || config.baseUrl || ""}",
externalSource: "${config.externalSource || "web_chat"}",
title: "${config.title || "在线客服"}",
subtitle: "${config.subtitle || ""}",
position: "${config.position || "right"}",
themeColor: "${config.themeColor || "#2563eb"}",
width: "${config.width || "380px"}",
subject: "${config.subject || ""}",
channelId: "${config.channelId || ""}"
};
</script>
<script async src="${scriptSrc}"></script>`
<script async src="/sdk/cs-ai-agent-sdk.min.js"></script>`
}, [config])
function updateField<K extends keyof KefuWidgetHostConfig>(
@@ -151,15 +110,8 @@ export function KefuWidgetDemo() {
const nextConfig: KefuWidgetHostConfig = {
...config,
channelId: config.channelId.trim(),
baseUrl: config.baseUrl.trim() || window.location.origin,
apiBaseUrl:
config.apiBaseUrl?.trim() || config.baseUrl.trim() || window.location.origin,
externalSource: config.externalSource?.trim() || "web_chat",
title: config.title?.trim() || "在线客服",
subtitle: config.subtitle?.trim() || "",
themeColor: config.themeColor?.trim() || "#2563eb",
width: config.width?.trim() || "380px",
subject: config.subject?.trim() || generateRandomSubject(),
baseUrl: "",
apiBaseUrl: "",
}
setConfig(nextConfig)
@@ -188,41 +140,6 @@ export function KefuWidgetDemo() {
value={config.channelId}
onChange={(value) => updateField("channelId", value)}
/>
<TextField
label="baseUrl"
value={config.baseUrl}
onChange={(value) => updateField("baseUrl", value)}
/>
<TextField
label="apiBaseUrl"
value={config.apiBaseUrl || ""}
onChange={(value) => updateField("apiBaseUrl", value)}
/>
<TextField
label="title"
value={config.title || ""}
onChange={(value) => updateField("title", value)}
/>
<TextField
label="subtitle"
value={config.subtitle || ""}
onChange={(value) => updateField("subtitle", value)}
/>
<TextField
label="themeColor"
value={config.themeColor || ""}
onChange={(value) => updateField("themeColor", value)}
/>
<TextField
label="width"
value={config.width || ""}
onChange={(value) => updateField("width", value)}
/>
<TextField
label="subject"
value={config.subject || ""}
onChange={(value) => updateField("subject", value)}
/>
</div>
<div className="mt-5 flex gap-2">
+3
View File
@@ -103,10 +103,13 @@ export type ImAsset = {
}
export type ImWidgetConfig = {
channelId?: string
title?: string
subtitle?: string
welcomeText?: string
themeColor?: string
position?: "left" | "right"
width?: string
}
const VISITOR_STORAGE_KEY = "cs_agent_im_visitor_id"
+82 -11
View File
@@ -16,6 +16,7 @@
initSent: false,
isOpen: false,
isMaximized: false,
configLoading: false,
frameHideTimer: null,
frameDestroyTimer: null,
config: null,
@@ -61,15 +62,62 @@
frameUrl.searchParams.set("baseUrl", config.baseUrl);
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
if (config.externalSource) frameUrl.searchParams.set("externalSource", config.externalSource);
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.subject) frameUrl.searchParams.set("subject", config.subject);
return frameUrl;
}
function mergeWidgetConfig(config, remoteConfig) {
if (!remoteConfig) {
return config;
}
var merged = {};
var key;
for (key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
merged[key] = config[key];
}
}
var remoteKeys = ["title", "subtitle", "welcomeText", "themeColor", "position", "width"];
for (var i = 0; i < remoteKeys.length; i += 1) {
key = remoteKeys[i];
if (
Object.prototype.hasOwnProperty.call(remoteConfig, key) &&
remoteConfig[key] !== undefined &&
remoteConfig[key] !== null
) {
merged[key] = remoteConfig[key];
}
}
return merged;
}
function fetchWidgetConfig(config) {
var baseUrl = String(config.apiBaseUrl || config.baseUrl || "").replace(/\/$/, "");
if (!baseUrl || !config.channelId || typeof fetch !== "function") {
return Promise.resolve(config);
}
var url = baseUrl + "/api/open/im/widget/config?channelId=" + encodeURIComponent(config.channelId);
return fetch(url, {
method: "GET",
cache: "no-store",
headers: {
"X-Channel-Id": config.channelId,
},
})
.then(function (response) {
return response.json();
})
.then(function (payload) {
if (!payload || payload.success === false) {
return config;
}
return mergeWidgetConfig(config, payload.data || {});
})
.catch(function () {
return config;
});
}
function clearFrameTimers() {
if (state.frameHideTimer) {
window.clearTimeout(state.frameHideTimer);
@@ -220,6 +268,9 @@
if (state.frame) {
return state.frame;
}
if (!state.frameUrl || !state.config) {
return null;
}
state.frame = document.createElement("iframe");
state.frame.dataset.csAgentWidget = "frame";
@@ -308,14 +359,31 @@
}
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");
var rawConfig = config || window.CSAgentConfig || {};
state.config = normalizeConfig(rawConfig);
var widgetBaseUrl = resolveWidgetBaseUrl(state.config);
if (!rawConfig.baseUrl) {
state.config.baseUrl = widgetBaseUrl;
}
if (!rawConfig.apiBaseUrl) {
state.config.apiBaseUrl = state.config.baseUrl;
}
if (!state.config.channelId) {
console.error("[cs-agent-widget] channelId is required");
return;
}
state.frameUrl = createFrameUrl(state.config);
createLauncher();
state.configLoading = true;
fetchWidgetConfig(state.config).then(function (nextConfig) {
state.configLoading = false;
state.config = normalizeConfig(nextConfig);
state.frameUrl = createFrameUrl(state.config);
if (state.button && state.button.parentNode) {
state.button.parentNode.removeChild(state.button);
state.button = null;
}
createLauncher();
});
}
function destroy() {
@@ -333,6 +401,7 @@
state.initSent = false;
state.isOpen = false;
state.isMaximized = false;
state.configLoading = false;
}
window.CSAgentWidget = {
@@ -342,6 +411,9 @@
if (!state.frame) {
createFrame();
}
if (!state.frame) {
return;
}
state.isOpen = true;
syncFrameVisibility();
},
@@ -360,4 +432,3 @@
mount(window.CSAgentConfig);
}
})();
+82 -11
View File
@@ -16,6 +16,7 @@
initSent: false,
isOpen: false,
isMaximized: false,
configLoading: false,
frameHideTimer: null,
frameDestroyTimer: null,
config: null,
@@ -61,15 +62,62 @@
frameUrl.searchParams.set("baseUrl", config.baseUrl);
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
if (config.externalSource) frameUrl.searchParams.set("externalSource", config.externalSource);
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.subject) frameUrl.searchParams.set("subject", config.subject);
return frameUrl;
}
function mergeWidgetConfig(config, remoteConfig) {
if (!remoteConfig) {
return config;
}
var merged = {};
var key;
for (key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
merged[key] = config[key];
}
}
var remoteKeys = ["title", "subtitle", "welcomeText", "themeColor", "position", "width"];
for (var i = 0; i < remoteKeys.length; i += 1) {
key = remoteKeys[i];
if (
Object.prototype.hasOwnProperty.call(remoteConfig, key) &&
remoteConfig[key] !== undefined &&
remoteConfig[key] !== null
) {
merged[key] = remoteConfig[key];
}
}
return merged;
}
function fetchWidgetConfig(config) {
var baseUrl = String(config.apiBaseUrl || config.baseUrl || "").replace(/\/$/, "");
if (!baseUrl || !config.channelId || typeof fetch !== "function") {
return Promise.resolve(config);
}
var url = baseUrl + "/api/open/im/widget/config?channelId=" + encodeURIComponent(config.channelId);
return fetch(url, {
method: "GET",
cache: "no-store",
headers: {
"X-Channel-Id": config.channelId,
},
})
.then(function (response) {
return response.json();
})
.then(function (payload) {
if (!payload || payload.success === false) {
return config;
}
return mergeWidgetConfig(config, payload.data || {});
})
.catch(function () {
return config;
});
}
function clearFrameTimers() {
if (state.frameHideTimer) {
window.clearTimeout(state.frameHideTimer);
@@ -220,6 +268,9 @@
if (state.frame) {
return state.frame;
}
if (!state.frameUrl || !state.config) {
return null;
}
state.frame = document.createElement("iframe");
state.frame.dataset.csAgentWidget = "frame";
@@ -308,14 +359,31 @@
}
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");
var rawConfig = config || window.CSAgentConfig || {};
state.config = normalizeConfig(rawConfig);
var widgetBaseUrl = resolveWidgetBaseUrl(state.config);
if (!rawConfig.baseUrl) {
state.config.baseUrl = widgetBaseUrl;
}
if (!rawConfig.apiBaseUrl) {
state.config.apiBaseUrl = state.config.baseUrl;
}
if (!state.config.channelId) {
console.error("[cs-agent-widget] channelId is required");
return;
}
state.frameUrl = createFrameUrl(state.config);
createLauncher();
state.configLoading = true;
fetchWidgetConfig(state.config).then(function (nextConfig) {
state.configLoading = false;
state.config = normalizeConfig(nextConfig);
state.frameUrl = createFrameUrl(state.config);
if (state.button && state.button.parentNode) {
state.button.parentNode.removeChild(state.button);
state.button = null;
}
createLauncher();
});
}
function destroy() {
@@ -333,6 +401,7 @@
state.initSent = false;
state.isOpen = false;
state.isMaximized = false;
state.configLoading = false;
}
window.CSAgentWidget = {
@@ -342,6 +411,9 @@
if (!state.frame) {
createFrame();
}
if (!state.frame) {
return;
}
state.isOpen = true;
syncFrameVisibility();
},
@@ -360,4 +432,3 @@
mount(window.CSAgentConfig);
}
})();