feat: add web channel configuration support and update widget response structure
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user