diff --git a/AGENTS.md b/AGENTS.md index 5b4752d..5dac35e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -286,10 +286,9 @@ func BuildXxx(item *models.Xxx) *response.Xxx { ### 8.2 路径分层 -- `/api/dashboard/*`:业务后台接口,默认归属 -- `/api/admin/*`:平台总后台接口 -- `/api/open/*`:开放接口;IM 优先使用 `/api/open/im/*` -- `/api/auth/*`:认证接口,可未登录访问 +- `/api/dashboard/*`:业务后台接口 +- `/api/third/*`:第三方平台调用接口 +- `/api/*`:开放接口 禁止新增 `/api/v1` 这类版本前缀。 @@ -305,8 +304,7 @@ func BuildXxx(item *models.Xxx) *response.Xxx { ### 8.4 路由注册 - 业务后台统一在 `internal/bootstrap/server.go` 中通过 `mvc.Configure(app.Party("/api/dashboard"), ...)` 注册 -- 平台接口统一通过 `mvc.Configure(app.Party("/api/admin"), ...)` 注册 -- 开放接口按领域归档,如 `mvc.Configure(app.Party("/api/open/im"), ...)` +- 开放接口按领域归档,如 `mvc.Configure(app.Party("/api"), ...)` - 在分组内部通过 `m.Party("/xxx").Handle(...)` 挂载资源 - 不要为每个资源单独再写一层顶级 `mvc.Configure(app.Party("/api/dashboard/xxx"), ...)` - 认证与鉴权中间件优先挂在 `/api/dashboard` 或 `/api/admin` 这一层 diff --git a/internal/bootstrap/server.go b/internal/bootstrap/server.go index 56fccdf..a4fbe56 100644 --- a/internal/bootstrap/server.go +++ b/internal/bootstrap/server.go @@ -10,7 +10,6 @@ import ( _ "cs-agent/internal/ai/runtime" "cs-agent/internal/controllers/api" "cs-agent/internal/controllers/dashboard" - "cs-agent/internal/controllers/open" "cs-agent/internal/controllers/third" "cs-agent/internal/middleware" "cs-agent/internal/pkg/config" @@ -95,10 +94,20 @@ func addRouter(app *iris.Application) { app.Get("/api/dashboard/ws", middleware.AuthMiddleware, middleware.DashboardWsMiddleware) app.Get("/api/open/im/ws", middleware.OpenImWsMiddleware) - mvc.Configure(app.Party("/api/auth"), func(m *mvc.Application) { - m.Handle(new(api.AuthController)) + mvc.Configure(app.Party("/api"), func(m *mvc.Application) { + m.Party("/auth").Handle(new(api.AuthController)) + + m.Party("/channel", middleware.ChannelContextMiddleware).Handle(new(api.ChannelController)) + m.Party("/conversation", middleware.ChannelContextMiddleware).Handle(new(api.ConversationController)) + m.Party("/message", middleware.ChannelContextMiddleware).Handle(new(api.MessageController)) }) + // mvc.Configure(app.Party("/api/open/im", middleware.ChannelContextMiddleware), func(m *mvc.Application) { + // m.Party("/widget").Handle(new(api.ImWidgetController)) + // m.Party("/conversation").Handle(new(api.ConversationController)) + // m.Party("/message").Handle(new(api.MessageController)) + // }) + mvc.Configure(app.Party("/api/dashboard", middleware.AuthMiddleware), func(m *mvc.Application) { m.Party("/dashboard").Handle(new(dashboard.DashboardController)) m.Party("/user").Handle(new(dashboard.UserController)) @@ -131,12 +140,6 @@ func addRouter(app *iris.Application) { m.Party("/mcp").Handle(new(dashboard.MCPController)) }) - mvc.Configure(app.Party("/api/open/im", middleware.OpenImContextMiddleware), func(m *mvc.Application) { - m.Party("/widget").Handle(new(open.ImWidgetController)) - m.Party("/conversation").Handle(new(open.ImConversationController)) - m.Party("/message").Handle(new(open.ImMessageController)) - }) - mvc.Configure(app.Party("/api/third"), func(m *mvc.Application) { m.Party("/wechat").Handle(new(third.WechatController)) }) diff --git a/internal/controllers/open/im_widget_controller.go b/internal/controllers/api/channel_controller.go similarity index 78% rename from internal/controllers/open/im_widget_controller.go rename to internal/controllers/api/channel_controller.go index 9be6e5a..bffda2a 100644 --- a/internal/controllers/open/im_widget_controller.go +++ b/internal/controllers/api/channel_controller.go @@ -1,20 +1,19 @@ -package open +package api 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" ) -type ImWidgetController struct { +type ChannelController struct { Ctx iris.Context } -func (c *ImWidgetController) AnyConfig() *web.JsonResult { - channel := irisx.GetChannel(c.Ctx) +func (c *ChannelController) AnyConfig() *web.JsonResult { + channel := services.ChannelService.GetEnabledChannel(c.Ctx) if channel == nil { return web.JsonErrorMsg("接入渠道未初始化") } diff --git a/internal/controllers/open/im_conversation_controller.go b/internal/controllers/api/conversation_controller.go similarity index 80% rename from internal/controllers/open/im_conversation_controller.go rename to internal/controllers/api/conversation_controller.go index 79473eb..4ae9235 100644 --- a/internal/controllers/open/im_conversation_controller.go +++ b/internal/controllers/api/conversation_controller.go @@ -1,4 +1,4 @@ -package open +package api import ( "cs-agent/internal/builders" @@ -12,12 +12,12 @@ import ( "github.com/mlogclub/simple/web/params" ) -type ImConversationController struct { +type ConversationController struct { Ctx iris.Context } -func (c *ImConversationController) GetBy(id int64) *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *ConversationController) GetBy(id int64) *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) @@ -40,8 +40,8 @@ func (c *ImConversationController) GetBy(id int64) *web.JsonResult { return web.JsonData(detail) } -func (c *ImConversationController) PostCreate_or_match() *web.JsonResult { - channel := irisx.GetChannel(c.Ctx) +func (c *ConversationController) PostCreate_or_match() *web.JsonResult { + channel := services.ChannelService.GetEnabledChannel(c.Ctx) if channel == nil { return web.JsonErrorMsg("接入渠道未初始化") } @@ -57,8 +57,8 @@ func (c *ImConversationController) PostCreate_or_match() *web.JsonResult { return web.JsonData(builders.BuildConversation(item)) } -func (c *ImConversationController) PostClose() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *ConversationController) PostClose() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) diff --git a/internal/controllers/open/im_message_controller.go b/internal/controllers/api/message_controller.go similarity index 87% rename from internal/controllers/open/im_message_controller.go rename to internal/controllers/api/message_controller.go index 95f8978..33b0c8b 100644 --- a/internal/controllers/open/im_message_controller.go +++ b/internal/controllers/api/message_controller.go @@ -1,4 +1,4 @@ -package open +package api import ( "cs-agent/internal/builders" @@ -15,12 +15,12 @@ import ( "github.com/spf13/cast" ) -type ImMessageController struct { +type MessageController struct { Ctx iris.Context } -func (c *ImMessageController) AnyList() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *MessageController) AnyList() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) @@ -53,8 +53,8 @@ func (c *ImMessageController) AnyList() *web.JsonResult { return web.JsonCursorData(results, cast.ToString(nextCursor), hasMore) } -func (c *ImMessageController) PostSend() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *MessageController) PostSend() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) @@ -74,8 +74,8 @@ func (c *ImMessageController) PostSend() *web.JsonResult { return web.JsonData(builders.BuildMessage(item)) } -func (c *ImMessageController) PostRead() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *MessageController) PostRead() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) @@ -93,8 +93,8 @@ func (c *ImMessageController) PostRead() *web.JsonResult { return web.JsonSuccess() } -func (c *ImMessageController) PostUpload_image() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *MessageController) PostUpload_image() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) @@ -138,8 +138,8 @@ func (c *ImMessageController) PostUpload_image() *web.JsonResult { return web.JsonData(builders.BuildAsset(item)) } -func (c *ImMessageController) PostUpload_attachment() *web.JsonResult { - if irisx.GetChannel(c.Ctx) == nil { +func (c *MessageController) PostUpload_attachment() *web.JsonResult { + if services.ChannelService.GetEnabledChannel(c.Ctx) == nil { return web.JsonErrorMsg("接入渠道未初始化") } external := irisx.GetExternalInfo(c.Ctx) diff --git a/internal/middleware/open_im_middleware.go b/internal/middleware/open_im_middleware.go index ce80a7e..832ee1a 100644 --- a/internal/middleware/open_im_middleware.go +++ b/internal/middleware/open_im_middleware.go @@ -3,38 +3,13 @@ package middleware import ( "cs-agent/internal/pkg/irisx" "cs-agent/internal/pkg/openidentity" - "cs-agent/internal/services" - "strings" "github.com/kataras/iris/v12" "github.com/mlogclub/simple/web" ) -// OpenImContextMiddleware 校验 X-Channel-Id / channelId 对应启用 web 渠道;除 /api/open/im/widget 外解析并缓存外部访客身份。 -func OpenImContextMiddleware(ctx iris.Context) { - channelID := strings.TrimSpace(ctx.GetHeader("X-Channel-Id")) - if channelID == "" { - channelID = strings.TrimSpace(ctx.URLParam("channelId")) - } - if channelID == "" { - ctx.StopExecution() - _ = ctx.JSON(web.JsonErrorMsg("channelId不能为空")) - return - } - channel := services.ChannelService.GetEnabledWebChannelByChannelID(channelID) - if channel == nil { - ctx.StopExecution() - _ = ctx.JSON(web.JsonErrorMsg("接入渠道不存在或已停用")) - return - } - irisx.SetOpenImChannel(ctx, channel) - - path := ctx.Path() - if strings.Contains(path, "/open/im/widget") { - ctx.Next() - return - } - +// ChannelContextMiddleware 校验 X-Channel-Id / channelId 对应启用 web 渠道 +func ChannelContextMiddleware(ctx iris.Context) { ext, err := openidentity.GetExternalInfo(ctx) if err != nil { ctx.StopExecution() diff --git a/internal/middleware/ws_middleware.go b/internal/middleware/ws_middleware.go index 90f62ab..573a8e5 100644 --- a/internal/middleware/ws_middleware.go +++ b/internal/middleware/ws_middleware.go @@ -1,12 +1,9 @@ package middleware import ( - "cs-agent/internal/models" - "cs-agent/internal/pkg/errorsx" "cs-agent/internal/pkg/openidentity" "cs-agent/internal/services" "log/slog" - "strings" "github.com/kataras/iris/v12" "github.com/mlogclub/simple/web" @@ -16,9 +13,7 @@ func DashboardWsMiddleware(ctx iris.Context) { principal := services.AuthService.GetAuthPrincipal(ctx) if principal == nil { if _, err := services.AuthService.Authenticate(ctx); err != nil { - _ = ctx.StopWithJSON(iris.StatusUnauthorized, map[string]any{ - "message": err.Error(), - }) + _ = ctx.StopWithJSON(iris.StatusUnauthorized, web.JsonError(err)) return } principal = services.AuthService.GetAuthPrincipal(ctx) @@ -31,14 +26,11 @@ func DashboardWsMiddleware(ctx iris.Context) { } func OpenImWsMiddleware(ctx iris.Context) { - channel, err := resolveEnabledChannelForWS(ctx) - if err != nil { - _ = ctx.StopWithJSON(iris.StatusBadRequest, map[string]any{ - "message": err.Error(), - }) + channel := services.ChannelService.GetEnabledChannel(ctx) + if channel == nil { + _ = ctx.StopWithJSON(iris.StatusBadRequest, web.JsonErrorMsg("接入渠道不存在或已停用")) return } - // 与 Open IM HTTP 一致:优先站内 AuthPrincipal;否则使用外部访客身份(Header/query,见 openidentity)。 // 二者不应在业务上同时作为「客户身份」使用;本入口在 principal 非空时不再解析 external,避免语义冲突。 principal := services.AuthService.GetAuthPrincipal(ctx) @@ -46,9 +38,7 @@ func OpenImWsMiddleware(ctx iris.Context) { if principal == nil { ext, err := openidentity.GetExternalInfo(ctx) if err != nil { - _ = ctx.StopWithJSON(iris.StatusUnauthorized, map[string]any{ - "message": err.Error(), - }) + _ = ctx.StopWithJSON(iris.StatusUnauthorized, web.JsonError(err)) return } external = ext @@ -59,26 +49,3 @@ func OpenImWsMiddleware(ctx iris.Context) { return } } - -func resolveEnabledChannelForWS(ctx iris.Context) (*models.Channel, error) { - channel, rsp := requireEnabledChannel(ctx) - if rsp == nil { - return channel, nil - } - return nil, errorsx.InvalidParam("接入渠道不存在或已停用") -} - -func requireEnabledChannel(ctx iris.Context) (*models.Channel, *web.JsonResult) { - channelID := strings.TrimSpace(ctx.GetHeader("X-Channel-Id")) - if channelID == "" { - channelID = strings.TrimSpace(ctx.URLParam("channelId")) - } - if channelID == "" { - return nil, web.JsonErrorMsg("channelId不能为空") - } - channel := services.ChannelService.GetEnabledWebChannelByChannelID(channelID) - if channel == nil { - return nil, web.JsonErrorMsg("接入渠道不存在或已停用") - } - return channel, nil -} diff --git a/internal/pkg/irisx/context.go b/internal/pkg/irisx/context.go index d9eac8b..ee1d5a2 100644 --- a/internal/pkg/irisx/context.go +++ b/internal/pkg/irisx/context.go @@ -1,10 +1,11 @@ package irisx import ( - "cs-agent/internal/models" "cs-agent/internal/pkg/openidentity" "github.com/kataras/iris/v12" + "github.com/mlogclub/simple/common/strs" + "github.com/mlogclub/simple/web/params" ) const ( @@ -12,16 +13,16 @@ const ( ctxKeyOpenImExternalInfo = "openImExternalInfo" ) -func SetOpenImChannel(ctx iris.Context, channel *models.Channel) { - ctx.Values().Set(ctxKeyOpenImChannel, channel) -} +// func SetOpenImChannel(ctx iris.Context, channel *models.Channel) { +// ctx.Values().Set(ctxKeyOpenImChannel, channel) +// } -// GetChannel 返回 OpenImContextMiddleware 注入的接入渠道(未走中间件时为 nil)。 -func GetChannel(ctx iris.Context) *models.Channel { - v := ctx.Values().Get(ctxKeyOpenImChannel) - channel, _ := v.(*models.Channel) - return channel -} +// // GetChannel 返回 OpenImContextMiddleware 注入的接入渠道(未走中间件时为 nil)。 +// func GetChannel(ctx iris.Context) *models.Channel { +// v := ctx.Values().Get(ctxKeyOpenImChannel) +// channel, _ := v.(*models.Channel) +// return channel +// } func SetOpenImExternalInfo(ctx iris.Context, ext *openidentity.ExternalInfo) { ctx.Values().Set(ctxKeyOpenImExternalInfo, ext) @@ -33,3 +34,13 @@ func GetExternalInfo(ctx iris.Context) *openidentity.ExternalInfo { ext, _ := v.(*openidentity.ExternalInfo) return ext } + +func GetChannelID(ctx iris.Context) string { + if channelID := ctx.GetHeader("X-Channel-ID"); strs.IsNotBlank(channelID) { + return channelID + } + if channelID, _ := params.Get(ctx, "channelId"); strs.IsNotBlank(channelID) { + return channelID + } + return "" +} diff --git a/internal/repositories/channel_repository.go b/internal/repositories/channel_repository.go index 0fc2b5d..b29b28a 100644 --- a/internal/repositories/channel_repository.go +++ b/internal/repositories/channel_repository.go @@ -3,6 +3,7 @@ package repositories import ( "cs-agent/internal/models" + "github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/sqls" "github.com/mlogclub/simple/web/params" "gorm.io/gorm" @@ -62,12 +63,12 @@ func (r *channelRepository) FindPageByCnd(db *gorm.DB, cnd *sqls.Cnd) (list []mo return } -func (r *channelRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (list []models.Channel) { +func (r *channelRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (list []models.Channel) { db.Raw(sqlStr, paramArr...).Scan(&list) return } -func (r *channelRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (count int64) { +func (r *channelRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (count int64) { db.Raw(sqlStr, paramArr...).Count(&count) return } @@ -100,3 +101,9 @@ func (r *channelRepository) Delete(db *gorm.DB, id int64) { db.Delete(&models.Channel{}, "id = ?", id) } +func (r *channelRepository) GetByChannelID(db *gorm.DB, channelID string) *models.Channel { + if strs.IsBlank(channelID) { + return nil + } + return r.FindOne(db, sqls.NewCnd().Where("channel_id = ?", channelID)) +} diff --git a/internal/services/channel_service.go b/internal/services/channel_service.go index b4a4126..655bdf5 100644 --- a/internal/services/channel_service.go +++ b/internal/services/channel_service.go @@ -6,12 +6,14 @@ import ( "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" + "cs-agent/internal/pkg/irisx" "cs-agent/internal/pkg/utils" "cs-agent/internal/repositories" "encoding/json" "strings" "time" + "github.com/kataras/iris/v12" "github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/sqls" "github.com/mlogclub/simple/web/params" @@ -217,12 +219,16 @@ func (s *channelService) GetEnabledWxWorkKFChannelByOpenKfID(openKfID string) *m return nil } -func (s *channelService) GetEnabledWebChannelByChannelID(channelID string) *models.Channel { - channelID = strings.TrimSpace(channelID) - if channelID == "" { +func (s *channelService) GetEnabledChannel(ctx iris.Context) *models.Channel { + channelID := irisx.GetChannelID(ctx) + channel := repositories.ChannelRepository.GetByChannelID(sqls.DB(), channelID) + if channel == nil { return nil } - return s.Take("channel_type = ? AND channel_id = ? AND status = ?", enums.ChannelTypeWeb, channelID, enums.StatusOk) + if channel.Status != enums.StatusOk { + return nil + } + return channel } func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRequest) (*models.Channel, error) { diff --git a/web/lib/api/im.ts b/web/lib/api/im.ts index 842dfaf..639c4ec 100644 --- a/web/lib/api/im.ts +++ b/web/lib/api/im.ts @@ -196,7 +196,7 @@ function toQueryString(query?: Record) { } export function fetchImConversationDetail(id: number) { - return request(`/api/open/im/conversation/${id}`, { + return request(`/api/conversation/${id}`, { ...createRequestOptions(), }) } @@ -205,21 +205,21 @@ export function fetchImMessages( query?: Record ) { return request>( - `/api/open/im/message/list${toQueryString(query)}`, + `/api/message/list${toQueryString(query)}`, createRequestOptions() ) } /** 外部身份仅通过 createImHeaders()(X-External-*)传递,无 JSON body */ export function createOrMatchImConversation() { - return request("/api/open/im/conversation/create_or_match", { + return request("/api/conversation/create_or_match", { ...createRequestOptions({ method: "POST" }), }) } export function fetchImWidgetConfig() { return request( - `/api/open/im/widget/config${toQueryString({ + `/api/channel/config${toQueryString({ channelId: getRuntimeImConfig().channelId, })}`, createRequestOptions() @@ -227,7 +227,7 @@ export function fetchImWidgetConfig() { } export function closeImConversation(conversationId: number) { - return request("/api/open/im/conversation/close", { + return request("/api/conversation/close", { ...createRequestOptions({ method: "POST", body: JSON.stringify({ conversationId }), @@ -242,7 +242,7 @@ export function sendImMessage(payload: { payload?: string clientMsgId?: string }) { - return request("/api/open/im/message/send", { + return request("/api/message/send", { ...createRequestOptions({ method: "POST", body: JSON.stringify(payload), @@ -251,7 +251,7 @@ export function sendImMessage(payload: { } export function markImMessageRead(conversationId: number, messageId = 0) { - return request("/api/open/im/message/read", { + return request("/api/message/read", { ...createRequestOptions({ method: "POST", body: JSON.stringify({ conversationId, messageId }), @@ -263,7 +263,7 @@ export function uploadImImage(conversationId: number, file: File) { const formData = new FormData() formData.set("conversationId", String(conversationId)) formData.set("file", file) - return request("/api/open/im/message/upload_image", { + return request("/api/message/upload_image", { ...createRequestOptions({ method: "POST", body: formData, @@ -275,7 +275,7 @@ export function uploadImAttachment(conversationId: number, file: File) { const formData = new FormData() formData.set("conversationId", String(conversationId)) formData.set("file", file) - return request("/api/open/im/message/upload_attachment", { + return request("/api/message/upload_attachment", { ...createRequestOptions({ method: "POST", body: formData, diff --git a/web/lib/sdk/cs-ai-agent-sdk.js b/web/lib/sdk/cs-ai-agent-sdk.js index c5abd4f..87263de 100644 --- a/web/lib/sdk/cs-ai-agent-sdk.js +++ b/web/lib/sdk/cs-ai-agent-sdk.js @@ -100,7 +100,7 @@ if (!baseUrl || !config.channelId || typeof fetch !== "function") { return Promise.resolve(config); } - var url = baseUrl + "/api/open/im/widget/config?channelId=" + encodeURIComponent(config.channelId); + var url = baseUrl + "/api/channel/config?channelId=" + encodeURIComponent(config.channelId); return fetch(url, { method: "GET", cache: "no-store", diff --git a/web/public/sdk/cs-ai-agent-sdk.min.js b/web/public/sdk/cs-ai-agent-sdk.min.js index 4376810..95a7b3b 100644 --- a/web/public/sdk/cs-ai-agent-sdk.min.js +++ b/web/public/sdk/cs-ai-agent-sdk.min.js @@ -1 +1 @@ -!function(){var e={position:"right",themeColor:"#0f6cbd",width:"380px",externalSource:"web_chat"},t=window.__CS_AGENT_WIDGET_STATE__;function n(t){var n,i={};for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&(i[n]=e[n]);for(n in t=t||{})Object.prototype.hasOwnProperty.call(t,n)&&(i[n]=t[n]);return i.baseUrl=String(i.baseUrl||window.location.origin).replace(/\/$/,""),i.apiBaseUrl?i.apiBaseUrl=String(i.apiBaseUrl).replace(/\/$/,""):delete i.apiBaseUrl,i.channelId=String(i.channelId||""),i.externalSource=String(i.externalSource||"web_chat"),i}function i(e){var t=document.currentScript;return t&&t.src?t.src.replace(/\/sdk\/cs-ai-agent-sdk\.min\.js(?:\?.*)?$/,""):String(e.widgetBaseUrl||e.baseUrl||window.location.origin).replace(/\/$/,"")}function r(){t.frameHideTimer&&(window.clearTimeout(t.frameHideTimer),t.frameHideTimer=null),t.frameDestroyTimer&&(window.clearTimeout(t.frameDestroyTimer),t.frameDestroyTimer=null)}function a(){var e=t.frame,n=t.config;if(e&&n){if(e.style.position="fixed",e.style.border="0",e.style.overflow="hidden",e.style.background="#fff",e.style.zIndex="2147483000",e.style.boxShadow="0 28px 80px rgba(15, 35, 65, 0.28)",e.style.willChange="top,right,bottom,left,width,height,opacity,transform,border-radius",e.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",e.style.transformOrigin="left"===n.position?"left bottom":"right bottom",t.isMaximized)return e.style.top="20px",e.style.right="20px",e.style.bottom="20px",e.style.left="20px",e.style.width="calc(100vw - 40px)",e.style.maxWidth="none",e.style.height="calc(100vh - 40px)",void(e.style.borderRadius="24px");e.style.top="",e.style.bottom="88px",e.style.right="left"===n.position?"":"24px",e.style.left="left"===n.position?"24px":"",e.style.width=n.width||"380px",e.style.maxWidth="calc(100vw - 24px)",e.style.height="min(760px, calc(100vh - 112px))",e.style.borderRadius="28px"}}function o(e){if(t.frame&&t.frame.contentWindow&&t.frameUrl)try{t.frame.contentWindow.postMessage(e,t.frameUrl.origin)}catch(e){console.error("[cs-agent-widget] postMessage failed",e)}}function s(){t.frame&&t.frameLoaded&&t.frameReady&&(t.initSent||(t.initSent=!0,o({type:"cs-agent:init",payload:t.config})),o({type:t.isOpen?"cs-agent:open":"cs-agent:minimize"}),o({type:"cs-agent:maximized",payload:{isMaximized:t.isMaximized}}))}function l(){var e=t.frame;if(e){if(r(),a(),e.style.display="block",t.isOpen)return e.style.visibility="visible",e.style.pointerEvents="auto",t.frameHideTimer=window.setTimeout(function(){t.frame&&(t.frame.style.opacity="1",t.frame.style.transform="translate3d(0, 0, 0) scale(1)")},16),void s();e.style.pointerEvents="none",e.style.opacity="0",e.style.transform=t.isMaximized?"translate3d(0, 10px, 0) scale(0.985)":"translate3d(0, 16px, 0) scale(0.96)",t.frameHideTimer=window.setTimeout(function(){t.frame&&!t.isOpen&&(t.frame.style.visibility="hidden")},t.animationDuration),s()}}function c(){return t.frame?t.frame:t.frameUrl&&t.config?(t.frame=document.createElement("iframe"),t.frame.dataset.csAgentWidget="frame",t.frame.title=t.config.title||"\u5728\u7ebf\u5ba2\u670d",t.frame.src=t.frameUrl.toString(),a(),t.frame.style.display="block",t.frame.style.visibility="hidden",t.frame.style.pointerEvents="none",t.frame.style.opacity="0",t.frame.style.transform="translate3d(0, 18px, 0) scale(0.96)",t.frame.addEventListener("load",function(){t.frameLoaded=!0,l()}),document.body.appendChild(t.frame),t.frame):null}function d(e){var r=e||window.CSAgentConfig||{};t.config=n(r);var a=i(t.config);r.baseUrl||(t.config.baseUrl=a),t.config.channelId?(t.configLoading=!0,function(e){var t=String(e.apiBaseUrl||e.baseUrl||"").replace(/\/$/,"");if(!t||!e.channelId||"function"!=typeof fetch)return Promise.resolve(e);var n=t+"/api/open/im/widget/config?channelId="+encodeURIComponent(e.channelId);return fetch(n,{method:"GET",cache:"no-store",headers:{"X-Channel-Id":e.channelId}}).then(function(e){return e.json()}).then(function(t){return t&&!1!==t.success?function(e,t){if(!t)return e;var n,i={};for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&(i[n]=e[n]);for(var r=["title","subtitle","themeColor","position","width"],a=0;a