diff --git a/config/config.example.yaml b/config/config.example.yaml
index 2f13af3..58fe904 100644
--- a/config/config.example.yaml
+++ b/config/config.example.yaml
@@ -1,3 +1,5 @@
+language: zh-CN
+
server:
port: 8083
cors:
diff --git a/docker/agent-desk-lancedb.yaml b/docker/agent-desk-lancedb.yaml
index 73c53c7..9fdb643 100644
--- a/docker/agent-desk-lancedb.yaml
+++ b/docker/agent-desk-lancedb.yaml
@@ -1,3 +1,5 @@
+language: zh-CN
+
server:
port: 8083
cors:
diff --git a/docker/agent-desk-sqlite-lancedb.yaml b/docker/agent-desk-sqlite-lancedb.yaml
index 2593bd1..95b4eb7 100644
--- a/docker/agent-desk-sqlite-lancedb.yaml
+++ b/docker/agent-desk-sqlite-lancedb.yaml
@@ -1,3 +1,5 @@
+language: zh-CN
+
server:
port: 8083
cors:
diff --git a/docker/agent-desk.yaml b/docker/agent-desk.yaml
index 137b512..cce0540 100644
--- a/docker/agent-desk.yaml
+++ b/docker/agent-desk.yaml
@@ -1,3 +1,5 @@
+language: zh-CN
+
server:
port: 8083
cors:
diff --git a/internal/bootstrap/init.go b/internal/bootstrap/init.go
index 4773a80..21d1bae 100644
--- a/internal/bootstrap/init.go
+++ b/internal/bootstrap/init.go
@@ -4,6 +4,7 @@ import (
"agent-desk/internal/ai/rag/vectordb"
"agent-desk/internal/oidcclient"
"agent-desk/internal/pkg/config"
+ "agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/logx"
"agent-desk/internal/services/cronx"
"agent-desk/internal/wxwork"
@@ -20,6 +21,7 @@ func Init(configPath string) error {
return err
}
config.SetCurrent(cfg)
+ i18nx.SetDefaultLocale(cfg.LanguageOrDefault())
logx.Init(logx.Config{
Level: cfg.Logger.Level,
diff --git a/internal/bootstrap/routes.go b/internal/bootstrap/routes.go
index 1712958..138092d 100644
--- a/internal/bootstrap/routes.go
+++ b/internal/bootstrap/routes.go
@@ -11,7 +11,6 @@ import (
func registerApiAuthRoutes(group *gin.RouterGroup) {
group.POST("/login", api.Login)
group.POST("/logout", api.Logout)
- group.GET("/options", api.AuthOptions)
group.GET("/profile", api.Profile)
group.GET("/wxwork_callback", api.WxWorkCallback)
group.POST("/wxwork_exchange", api.WxWorkExchange)
diff --git a/internal/bootstrap/server.go b/internal/bootstrap/server.go
index 11c38ce..abcebba 100644
--- a/internal/bootstrap/server.go
+++ b/internal/bootstrap/server.go
@@ -26,6 +26,7 @@ import (
func NewServer() (*gin.Engine, error) {
cfg := config.Current()
+ i18nx.SetDefaultLocale(cfg.LanguageOrDefault())
gin.SetMode(gin.ReleaseMode)
printBanner()
@@ -156,6 +157,7 @@ func addRouter(app *gin.Engine) {
apiGroup := app.Group("/api")
apiGroup.GET("/health", api.Health)
+ apiGroup.GET("/config", api.PublicConfig)
registerApiAuthRoutes(apiGroup.Group("/auth"))
registerApiChannelRoutes(apiGroup.Group("/channel"))
registerApiCustomerRoutes(apiGroup.Group("/customer"))
diff --git a/internal/bootstrap/server_route_test.go b/internal/bootstrap/server_route_test.go
index 94b554a..592e6b1 100644
--- a/internal/bootstrap/server_route_test.go
+++ b/internal/bootstrap/server_route_test.go
@@ -32,6 +32,7 @@ func TestNewServerRegistersGinRoutes(t *testing.T) {
expected := []string{
http.MethodPost + " /api/auth/login",
+ http.MethodGet + " /api/config",
http.MethodGet + " /api/health",
http.MethodGet + " /api/auth/oidc_login",
http.MethodGet + " /api/auth/oidc_callback",
@@ -93,8 +94,9 @@ func TestNewServerHealthEndpointIsPublic(t *testing.T) {
}
}
-func TestNewServerExposesPublicAuthOptions(t *testing.T) {
+func TestNewServerExposesPublicConfig(t *testing.T) {
config.SetCurrent(&config.Config{
+ Language: "zh-CN",
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
@@ -116,7 +118,7 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
}
rec := httptest.NewRecorder()
- app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/options", nil))
+ app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/config", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d want %d", rec.Code, http.StatusOK)
@@ -125,8 +127,9 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
var body struct {
Success bool `json:"success"`
Data struct {
- WxWorkEnabled bool `json:"wxworkEnabled"`
- OIDCEnabled bool `json:"oidcEnabled"`
+ Language string `json:"language"`
+ WxWorkEnabled bool `json:"wxworkEnabled"`
+ OIDCEnabled bool `json:"oidcEnabled"`
} `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
@@ -135,6 +138,9 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
if !body.Success {
t.Fatalf("success=false, body=%s", rec.Body.String())
}
+ if body.Data.Language != "zh-CN" {
+ t.Fatalf("language=%q want zh-CN", body.Data.Language)
+ }
if !body.Data.WxWorkEnabled {
t.Fatalf("wxworkEnabled=false want true")
}
@@ -146,6 +152,29 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
}
}
+func TestNewServerDoesNotExposeLegacyAuthOptions(t *testing.T) {
+ config.SetCurrent(&config.Config{
+ Storage: config.StorageConfig{
+ Local: config.LocalStorageConfig{
+ Root: "storage",
+ BaseURL: "/storage",
+ },
+ },
+ })
+
+ app, err := NewServer()
+ if err != nil {
+ t.Fatalf("NewServer() error = %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/options", nil))
+
+ if rec.Code != http.StatusNotFound {
+ t.Fatalf("status=%d want %d, body=%s", rec.Code, http.StatusNotFound, rec.Body.String())
+ }
+}
+
func TestNewServerSeparatesAPIStaticAndSPA(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
diff --git a/internal/handlers/api/auth_handler.go b/internal/handlers/api/auth_handler.go
index 95d9a6b..9207315 100644
--- a/internal/handlers/api/auth_handler.go
+++ b/internal/handlers/api/auth_handler.go
@@ -30,9 +30,10 @@ func Login(ctx *gin.Context) {
httpx.WriteJSON(ctx, ret)
}
-func AuthOptions(ctx *gin.Context) {
+func PublicConfig(ctx *gin.Context) {
cfg := config.Current()
- httpx.WriteJSON(ctx, &response.AuthOptionsResponse{
+ httpx.WriteJSON(ctx, &response.PublicConfigResponse{
+ Language: cfg.LanguageOrDefault(),
WxWorkEnabled: cfg.WxWork.Enabled,
OIDCEnabled: cfg.OIDC.Enabled,
})
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index cb7722f..0e181fc 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -9,6 +9,7 @@ import (
)
type Config struct {
+ Language string `yaml:"language"`
Server ServerConfig `yaml:"server"`
DB DBConfig `yaml:"db"`
Logger LoggerConfig `yaml:"logger"`
@@ -21,6 +22,17 @@ type Config struct {
CustomerSession CustomerSessionConfig `yaml:"customerSession"`
}
+func (c Config) LanguageOrDefault() string {
+ switch strings.ToLower(strings.TrimSpace(c.Language)) {
+ case "zh", "zh-cn", "zh_cn", "zh-hans":
+ return "zh-CN"
+ case "en", "en-us", "en_us":
+ return "en-US"
+ default:
+ return "zh-CN"
+ }
+}
+
type WxWorkNotifyConfig struct {
Enabled bool `yaml:"enabled"`
ToUsers []int64 `yaml:"toUsers"`
diff --git a/internal/pkg/dto/response/auth_response.go b/internal/pkg/dto/response/auth_response.go
index f9e373b..c2c6e29 100644
--- a/internal/pkg/dto/response/auth_response.go
+++ b/internal/pkg/dto/response/auth_response.go
@@ -19,7 +19,8 @@ type LoginResponse struct {
Roles []string `json:"roles"`
}
-type AuthOptionsResponse struct {
- WxWorkEnabled bool `json:"wxworkEnabled"`
- OIDCEnabled bool `json:"oidcEnabled"`
+type PublicConfigResponse struct {
+ Language string `json:"language"`
+ WxWorkEnabled bool `json:"wxworkEnabled"`
+ OIDCEnabled bool `json:"oidcEnabled"`
}
diff --git a/internal/pkg/errorsx/errors.go b/internal/pkg/errorsx/errors.go
index ac561a4..f957c33 100644
--- a/internal/pkg/errorsx/errors.go
+++ b/internal/pkg/errorsx/errors.go
@@ -86,7 +86,7 @@ func (e *I18nError) Error() string {
if e == nil {
return ""
}
- return e.Message(i18nx.LocaleZhCN)
+ return e.Message(i18nx.DefaultLocale)
}
func (e *I18nError) Unwrap() error {
diff --git a/internal/pkg/i18nx/error.go b/internal/pkg/i18nx/error.go
index 53730e3..6b62483 100644
--- a/internal/pkg/i18nx/error.go
+++ b/internal/pkg/i18nx/error.go
@@ -20,7 +20,7 @@ func (e *Error) Error() string {
if e == nil {
return ""
}
- return e.Message(LocaleZhCN)
+ return e.Message(DefaultLocale)
}
func (e *Error) Message(locale string) string {
diff --git a/internal/pkg/i18nx/i18nx_test.go b/internal/pkg/i18nx/i18nx_test.go
index a02a6d5..b4772ed 100644
--- a/internal/pkg/i18nx/i18nx_test.go
+++ b/internal/pkg/i18nx/i18nx_test.go
@@ -9,27 +9,24 @@ import (
)
func TestNormalizeLocale(t *testing.T) {
- t.Parallel()
-
tests := []struct {
name string
in string
want string
}{
- {name: "default for blank", in: "", want: LocaleEnUS},
+ {name: "default for blank", in: "", want: LocaleZhCN},
{name: "exact chinese", in: "zh-CN", want: LocaleZhCN},
{name: "underscore chinese", in: "zh_CN", want: LocaleZhCN},
{name: "short chinese", in: "zh", want: LocaleZhCN},
{name: "exact english", in: "en-US", want: LocaleEnUS},
{name: "underscore english", in: "en_US", want: LocaleEnUS},
{name: "short english", in: "en", want: LocaleEnUS},
- {name: "unsupported falls back", in: "fr-FR", want: LocaleEnUS},
+ {name: "unsupported falls back", in: "fr-FR", want: LocaleZhCN},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
if got := NormalizeLocale(tt.in); got != tt.want {
t.Fatalf("NormalizeLocale(%q) = %q, want %q", tt.in, got, tt.want)
}
@@ -37,36 +34,33 @@ func TestNormalizeLocale(t *testing.T) {
}
}
-func TestResolveLocaleFromHeaders(t *testing.T) {
- t.Parallel()
-
+func TestResolveLocaleUsesDefaultLocale(t *testing.T) {
+ SetDefaultLocale(LocaleZhCN)
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/user/list", nil)
req.Header.Set("Accept-Language", "fr-FR, en-US;q=0.9, zh-CN;q=0.8")
- if got := ResolveRequestLocale(req); got != LocaleEnUS {
- t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleEnUS)
+ if got := ResolveRequestLocale(req); got != LocaleZhCN {
+ t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleZhCN)
}
}
-func TestResolveLocalePrefersXLocale(t *testing.T) {
- t.Parallel()
-
+func TestResolveLocaleIgnoresRequestLocaleHeaders(t *testing.T) {
+ SetDefaultLocale(LocaleZhCN)
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/user/list", nil)
req.Header.Set("X-Locale", "en-US")
req.Header.Set("Accept-Language", "zh-CN")
- if got := ResolveRequestLocale(req); got != LocaleEnUS {
- t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleEnUS)
+ if got := ResolveRequestLocale(req); got != LocaleZhCN {
+ t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleZhCN)
}
}
-func TestTranslateFallsBackToEnglish(t *testing.T) {
- t.Parallel()
-
+func TestTranslateUsesConfiguredDefaultForUnsupportedLocale(t *testing.T) {
+ SetDefaultLocale(LocaleZhCN)
if got := TLocale(LocaleEnUS, "error.auth.expired"); got != "Your session has expired. Please sign in again." {
t.Fatalf("english translation = %q", got)
}
- if got := TLocale("fr-FR", "error.auth.expired"); got != "Your session has expired. Please sign in again." {
+ if got := TLocale("fr-FR", "error.auth.expired"); got != "未登录或登录已过期" {
t.Fatalf("fallback translation = %q", got)
}
}
@@ -143,6 +137,7 @@ func TestGetfFallsBackToKey(t *testing.T) {
}
func TestMiddlewareStoresLocale(t *testing.T) {
+ SetDefaultLocale(LocaleZhCN)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(Middleware())
@@ -159,7 +154,7 @@ func TestMiddlewareStoresLocale(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
}
- if got := recorder.Body.String(); got != LocaleEnUS {
- t.Fatalf("middleware locale = %q, want %q", got, LocaleEnUS)
+ if got := recorder.Body.String(); got != LocaleZhCN {
+ t.Fatalf("middleware locale = %q, want %q", got, LocaleZhCN)
}
}
diff --git a/internal/pkg/i18nx/middleware.go b/internal/pkg/i18nx/middleware.go
index f067ba2..435ed8f 100644
--- a/internal/pkg/i18nx/middleware.go
+++ b/internal/pkg/i18nx/middleware.go
@@ -5,13 +5,11 @@ import (
"strings"
"github.com/gin-gonic/gin"
- "golang.org/x/text/language"
)
const (
- LocaleZhCN = "zh-CN"
- LocaleEnUS = "en-US"
- DefaultLocale = LocaleEnUS
+ LocaleZhCN = "zh-CN"
+ LocaleEnUS = "en-US"
)
var supportedLocales = map[string]string{
@@ -24,6 +22,12 @@ var supportedLocales = map[string]string{
"en_us": LocaleEnUS,
}
+var DefaultLocale = LocaleZhCN
+
+func SetDefaultLocale(locale string) {
+ DefaultLocale = NormalizeLocale(locale)
+}
+
func NormalizeLocale(value string) string {
key := strings.ToLower(strings.TrimSpace(value))
if key == "" {
@@ -35,19 +39,7 @@ func NormalizeLocale(value string) string {
return DefaultLocale
}
-func ResolveRequestLocale(req *http.Request) string {
- if req == nil {
- return DefaultLocale
- }
- if locale := normalizeSupportedLocale(req.Header.Get("X-Locale")); locale != "" {
- return locale
- }
- if locale := resolveAcceptLanguage(req.Header.Get("Accept-Language")); locale != "" {
- return locale
- }
- if locale := normalizeSupportedLocale(req.URL.Query().Get("locale")); locale != "" {
- return locale
- }
+func ResolveRequestLocale(_ *http.Request) string {
return DefaultLocale
}
@@ -57,31 +49,3 @@ func Middleware() gin.HandlerFunc {
ctx.Next()
}
}
-
-func normalizeSupportedLocale(value string) string {
- key := strings.ToLower(strings.TrimSpace(value))
- if key == "" {
- return ""
- }
- if locale, ok := supportedLocales[key]; ok {
- return locale
- }
- return ""
-}
-
-func resolveAcceptLanguage(value string) string {
- tags, _, err := language.ParseAcceptLanguage(value)
- if err != nil {
- return ""
- }
- for _, tag := range tags {
- if locale := normalizeSupportedLocale(tag.String()); locale != "" {
- return locale
- }
- base, _ := tag.Base()
- if locale := normalizeSupportedLocale(base.String()); locale != "" {
- return locale
- }
- }
- return ""
-}
diff --git a/web/app/dashboard/login/page.tsx b/web/app/dashboard/login/page.tsx
index 6e1fa4b..2106163 100644
--- a/web/app/dashboard/login/page.tsx
+++ b/web/app/dashboard/login/page.tsx
@@ -1,4 +1,3 @@
-import { LocaleSwitcher } from "@/components/locale-switcher"
import { LoginForm } from "@/components/login-form"
import { Suspense } from "react"
@@ -6,9 +5,6 @@ export default function LoginPage() {
return (
-
-
-
}>
diff --git a/web/components/legal-document-page.tsx b/web/components/legal-document-page.tsx
index 7543c24..67e1305 100644
--- a/web/components/legal-document-page.tsx
+++ b/web/components/legal-document-page.tsx
@@ -3,7 +3,6 @@
import Image from "next/image"
import Link from "next/link"
-import { LocaleSwitcher } from "@/components/locale-switcher"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useAppLocale, useI18n } from "@/i18n/provider"
import enUSMessages from "@/messages/en-US.json"
@@ -39,7 +38,7 @@ export function LegalDocumentPage({ type }: { type: LegalPageType }) {
return (
-
+
diff --git a/web/components/locale-switcher.tsx b/web/components/locale-switcher.tsx
deleted file mode 100644
index f7a75f3..0000000
--- a/web/components/locale-switcher.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client"
-
-import { LanguagesIcon } from "lucide-react"
-
-import { useAppLocale, useI18n } from "@/i18n/provider"
-import { SUPPORTED_LOCALES, type AppLocale } from "@/i18n/config"
-import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-
-export function LocaleSwitcher() {
- const t = useI18n()
- const { locale, setLocale } = useAppLocale()
-
- return (
-
- }
- aria-label={t("common.language")}
- >
-
-
-
- setLocale(value as AppLocale)}
- >
- {SUPPORTED_LOCALES.map((option) => (
-
- {t(`locale.${option}`)}
-
- ))}
-
-
-
- )
-}
diff --git a/web/components/login-form.tsx b/web/components/login-form.tsx
index bf3cf13..8129e6e 100644
--- a/web/components/login-form.tsx
+++ b/web/components/login-form.tsx
@@ -7,7 +7,8 @@ import { startTransition, useEffect, useState } from "react"
import { toast } from "sonner"
import { useAuth } from "@/components/auth-provider"
-import { fetchAuthOptions, loginWithPassword, type AuthOptions } from "@/lib/api/auth"
+import { loginWithPassword } from "@/lib/api/auth"
+import { fetchPublicConfig, type PublicConfig } from "@/lib/api/config"
import { useI18n } from "@/i18n/provider"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -40,15 +41,15 @@ export function LoginForm({
const { session } = useAuth()
const [isPending, setIsPending] = useState(false)
const [isWxWorkEnv, setIsWxWorkEnv] = useState(false)
- const [authOptions, setAuthOptions] = useState(null)
- const [authOptionsError, setAuthOptionsError] = useState(null)
+ const [publicConfig, setPublicConfig] = useState(null)
+ const [publicConfigError, setPublicConfigError] = useState(null)
const nextPath = searchParams.get("next")
const wxworkError = searchParams.get("wxworkError")
const oidcError = searchParams.get("oidcError")
const redirectPath =
nextPath && nextPath.startsWith("/") ? nextPath : "/dashboard"
const enabledProviderCount =
- Number(authOptions?.wxworkEnabled) + Number(authOptions?.oidcEnabled)
+ Number(publicConfig?.wxworkEnabled) + Number(publicConfig?.oidcEnabled)
useEffect(() => {
if (session) {
@@ -75,17 +76,17 @@ export function LoginForm({
useEffect(() => {
let cancelled = false
- void fetchAuthOptions()
+ void fetchPublicConfig()
.then((options) => {
if (!cancelled) {
- setAuthOptions(options)
- setAuthOptionsError(null)
+ setPublicConfig(options)
+ setPublicConfigError(null)
}
})
.catch((error) => {
if (!cancelled) {
- setAuthOptions(null)
- setAuthOptionsError(error instanceof Error ? error.message : "")
+ setPublicConfig(null)
+ setPublicConfigError(error instanceof Error ? error.message : "")
}
})
@@ -115,7 +116,7 @@ export function LoginForm({
}
}
- if (authOptionsError) {
+ if (publicConfigError) {
return (
@@ -124,7 +125,7 @@ export function LoginForm({
{t("auth.optionsLoadFailed")}
- {authOptionsError || t("api.requestFailed")}
+ {publicConfigError || t("api.requestFailed")}
@@ -133,7 +134,7 @@ export function LoginForm({
)
}
- if (!authOptions) {
+ if (!publicConfig) {
return (
@@ -206,7 +207,7 @@ export function LoginForm({
enabledProviderCount === 1 ? "grid-cols-1" : "grid-cols-2"
)}
>
- {authOptions.wxworkEnabled ? (
+ {publicConfig.wxworkEnabled ? (
) : null}
- {authOptions.oidcEnabled ? (
+ {publicConfig.oidcEnabled ? (
diff --git a/web/components/workbench-header.tsx b/web/components/workbench-header.tsx
index df0888b..db73e2c 100644
--- a/web/components/workbench-header.tsx
+++ b/web/components/workbench-header.tsx
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"
import { useState } from "react"
import { ChangePasswordDialog } from "@/components/change-password-dialog"
-import { LocaleSwitcher } from "@/components/locale-switcher"
import { PaletteToggle } from "@/components/palette-toggle"
import { RealtimeConnectionStatus } from "@/components/realtime-connection-status"
import { ThemeToggle } from "@/components/theme-toggle"
@@ -36,7 +35,6 @@ export function WorkbenchHeader() {
-
diff --git a/web/i18n/config.test.mjs b/web/i18n/config.test.mjs
index cc670cf..2eb6e9a 100644
--- a/web/i18n/config.test.mjs
+++ b/web/i18n/config.test.mjs
@@ -25,7 +25,7 @@ async function loadConfig() {
test("normalizes supported locale aliases", async () => {
const { DEFAULT_LOCALE, normalizeLocale } = await loadConfig()
- assert.equal(DEFAULT_LOCALE, "en-US")
+ assert.equal(DEFAULT_LOCALE, "zh-CN")
assert.equal(normalizeLocale("zh-CN"), "zh-CN")
assert.equal(normalizeLocale("zh_CN"), "zh-CN")
assert.equal(normalizeLocale("zh"), "zh-CN")
@@ -35,26 +35,11 @@ test("normalizes supported locale aliases", async () => {
assert.equal(normalizeLocale("fr-FR"), DEFAULT_LOCALE)
})
-test("resolves browser locale from stored value before navigator languages", async () => {
- const { resolveBrowserLocale } = await loadConfig()
+test("reads the configured locale without browser language detection", async () => {
+ const { configureLocale, readStoredLocale } = await loadConfig()
- assert.equal(
- resolveBrowserLocale({
- storedLocale: "en-US",
- navigatorLanguages: ["zh-CN"],
- }),
- "en-US"
- )
-})
-
-test("falls back through navigator languages", async () => {
- const { resolveBrowserLocale } = await loadConfig()
-
- assert.equal(
- resolveBrowserLocale({
- storedLocale: "",
- navigatorLanguages: ["fr-FR", "en"],
- }),
- "en-US"
- )
+ assert.equal(readStoredLocale(), "zh-CN")
+
+ configureLocale("en-US")
+ assert.equal(readStoredLocale(), "en-US")
})
diff --git a/web/i18n/config.ts b/web/i18n/config.ts
index 021d967..260b33b 100644
--- a/web/i18n/config.ts
+++ b/web/i18n/config.ts
@@ -1,8 +1,7 @@
export const SUPPORTED_LOCALES = ["zh-CN", "en-US"] as const
export type AppLocale = (typeof SUPPORTED_LOCALES)[number]
-export const DEFAULT_LOCALE: AppLocale = "en-US"
-export const LOCALE_STORAGE_KEY = "cs_ai_agent_locale"
+export const DEFAULT_LOCALE: AppLocale = "zh-CN"
const LOCALE_ALIASES: Record = {
zh: "zh-CN",
@@ -37,40 +36,13 @@ export function isSupportedLocale(
return SUPPORTED_LOCALES.includes(value as AppLocale)
}
-export function resolveBrowserLocale({
- storedLocale,
- navigatorLanguages,
-}: {
- storedLocale?: string | null
- navigatorLanguages?: readonly string[] | null
-}): AppLocale {
- if (isSupportedLocale(storedLocale)) {
- return storedLocale
- }
-
- for (const locale of navigatorLanguages ?? []) {
- const normalized = normalizeSupportedLocale(locale)
- if (normalized) {
- return normalized
- }
- }
-
- return DEFAULT_LOCALE
-}
+let configuredLocale: AppLocale = DEFAULT_LOCALE
export function readStoredLocale(): AppLocale {
- if (typeof window === "undefined") {
- return DEFAULT_LOCALE
- }
- return resolveBrowserLocale({
- storedLocale: window.localStorage.getItem(LOCALE_STORAGE_KEY),
- navigatorLanguages: window.navigator.languages,
- })
+ return configuredLocale
}
-export function writeStoredLocale(locale: AppLocale) {
- if (typeof window === "undefined") {
- return
- }
- window.localStorage.setItem(LOCALE_STORAGE_KEY, locale)
+export function configureLocale(locale: string | null | undefined): AppLocale {
+ configuredLocale = normalizeLocale(locale)
+ return configuredLocale
}
diff --git a/web/i18n/provider.tsx b/web/i18n/provider.tsx
index d345ffb..527bec8 100644
--- a/web/i18n/provider.tsx
+++ b/web/i18n/provider.tsx
@@ -12,10 +12,10 @@ import {
import {
DEFAULT_LOCALE,
type AppLocale,
- readStoredLocale,
- writeStoredLocale,
+ configureLocale,
} from "@/i18n/config"
import { translateMessage } from "@/i18n/messages"
+import { fetchPublicConfig } from "@/lib/api/config"
type LocaleContextValue = {
locale: AppLocale
@@ -34,11 +34,24 @@ export function AppI18nProvider({ children }: { children: ReactNode }) {
const [isLocaleReady, setIsLocaleReady] = useState(false)
useEffect(() => {
- const storedLocale = readStoredLocale()
- setLocaleState(storedLocale)
- document.documentElement.lang = storedLocale
- document.title = translateMessage(storedLocale, "app.metadataTitle")
- setIsLocaleReady(true)
+ let cancelled = false
+
+ fetchPublicConfig()
+ .then((config) => configureLocale(config.language))
+ .catch(() => configureLocale(DEFAULT_LOCALE))
+ .then((configuredLocale) => {
+ if (cancelled) {
+ return
+ }
+ setLocaleState(configuredLocale)
+ document.documentElement.lang = configuredLocale
+ document.title = translateMessage(configuredLocale, "app.metadataTitle")
+ setIsLocaleReady(true)
+ })
+
+ return () => {
+ cancelled = true
+ }
}, [])
useEffect(() => {
@@ -49,12 +62,7 @@ export function AppI18nProvider({ children }: { children: ReactNode }) {
() => ({
locale,
t: (key, values) => translateMessage(locale, key, values),
- setLocale: (nextLocale) => {
- setLocaleState(nextLocale)
- writeStoredLocale(nextLocale)
- document.documentElement.lang = nextLocale
- document.title = translateMessage(nextLocale, "app.metadataTitle")
- },
+ setLocale: () => {},
}),
[locale]
)
diff --git a/web/lib/api/auth.ts b/web/lib/api/auth.ts
index f520bbb..65ae6d1 100644
--- a/web/lib/api/auth.ts
+++ b/web/lib/api/auth.ts
@@ -6,17 +6,6 @@ export type LoginRequest = {
password: string
}
-export type AuthOptions = {
- wxworkEnabled: boolean
- oidcEnabled: boolean
-}
-
-export async function fetchAuthOptions() {
- return request("/api/auth/options", {
- skipAuth: true,
- })
-}
-
export async function loginWithPassword(payload: LoginRequest) {
const data = await request("/api/auth/login", {
method: "POST",
diff --git a/web/lib/api/client.ts b/web/lib/api/client.ts
index 3360357..70728f4 100644
--- a/web/lib/api/client.ts
+++ b/web/lib/api/client.ts
@@ -1,5 +1,4 @@
import { expireSession, readSession } from "@/lib/auth"
-import { readStoredLocale } from "@/i18n/config"
import { translateCurrentMessage } from "@/i18n/messages"
const API_BASE_URL =
@@ -50,9 +49,6 @@ function buildRequestHeaders(headers: HeadersInit | undefined, skipAuth?: boolea
) {
authHeaders.set("Content-Type", "application/json")
}
- const locale = readStoredLocale()
- authHeaders.set("Accept-Language", locale)
- authHeaders.set("X-Locale", locale)
return authHeaders
}
diff --git a/web/lib/sdk/agent-desk-sdk.test.mjs b/web/lib/sdk/agent-desk-sdk.test.mjs
index ddc7854..696ed29 100644
--- a/web/lib/sdk/agent-desk-sdk.test.mjs
+++ b/web/lib/sdk/agent-desk-sdk.test.mjs
@@ -50,14 +50,21 @@ async function loadSdk(config) {
const sandbox = {
URL,
console,
- fetch: async () => ({
- json: async () => ({
- success: true,
- data: {
- title: "\u5728\u7ebf\u5ba2\u670d",
- themeColor: "#2563eb",
- },
- }),
+ fetch: async (url) => ({
+ json: async () =>
+ String(url).endsWith("/api/config")
+ ? {
+ success: true,
+ data: {
+ language: "en-US",
+ },
+ }
+ : {
+ success: true,
+ data: {
+ themeColor: "#2563eb",
+ },
+ },
}),
document: {
body,
@@ -95,7 +102,7 @@ async function loadSdk(config) {
return sandbox
}
-async function flushPromises(count = 5) {
+async function flushPromises(count = 10) {
for (let i = 0; i < count; i += 1) {
await Promise.resolve()
}
@@ -133,6 +140,7 @@ test("launcher click creates chat iframe with a freshly resolved userToken", asy
)
assert.ok(launcher)
+ assert.equal(launcher.children.at(-1)?.textContent, "Support")
launcher.click()
await flushPromises()
diff --git a/web/lib/sdk/agent-desk-sdk.ts b/web/lib/sdk/agent-desk-sdk.ts
index a9fe92f..a74b6ec 100644
--- a/web/lib/sdk/agent-desk-sdk.ts
+++ b/web/lib/sdk/agent-desk-sdk.ts
@@ -7,6 +7,7 @@ import type {
type NormalizedAgentDeskConfig = AgentDeskConfig & {
baseUrl: string
channelId: string
+ language: string
position: "left" | "right"
themeColor: string
width: string
@@ -38,22 +39,23 @@ type WidgetConfigResponse = {
>>
}
-function getWidgetLocale() {
- try {
- const stored = window.localStorage?.getItem("cs_ai_agent_locale")
- const language = stored || document.documentElement.lang || window.navigator?.language || ""
- return language.toLowerCase().startsWith("zh") ? "zh-CN" : "en-US"
- } catch {
- return "en-US"
+type PublicConfigResponse = {
+ success?: boolean
+ data?: {
+ language?: string
}
}
-function getDefaultWidgetTitle() {
- return getWidgetLocale() === "en-US" ? "Support" : "\u5728\u7ebf\u5ba2\u670d"
+function normalizeWidgetLanguage(language: string | undefined) {
+ return String(language || "").toLowerCase().startsWith("en") ? "en-US" : "zh-CN"
}
-function getLauncherText() {
- return getWidgetLocale() === "en-US" ? "Support" : "\u5ba2\u670d"
+function getDefaultWidgetTitle(config?: NormalizedAgentDeskConfig | null) {
+ return normalizeWidgetLanguage(config?.language) === "en-US" ? "Support" : "\u5728\u7ebf\u5ba2\u670d"
+}
+
+function getLauncherText(config?: NormalizedAgentDeskConfig | null) {
+ return normalizeWidgetLanguage(config?.language) === "en-US" ? "Support" : "\u5ba2\u670d"
}
type FrameMessage =
@@ -65,8 +67,9 @@ type FrameMessage =
(function () {
const DEFAULT_CONFIG: Pick<
NormalizedAgentDeskConfig,
- "position" | "themeColor" | "width"
+ "language" | "position" | "themeColor" | "width"
> = {
+ language: "zh-CN",
position: "right",
themeColor: "#0f6cbd",
width: "380px",
@@ -103,6 +106,7 @@ type FrameMessage =
delete merged.apiBaseUrl
}
merged.channelId = String(merged.channelId || "")
+ merged.language = normalizeWidgetLanguage(String(merged.language || "zh-CN"))
if (merged.externalId) {
merged.externalId = String(merged.externalId)
}
@@ -209,6 +213,28 @@ type FrameMessage =
.catch(() => config)
}
+ function fetchPublicConfig(config: NormalizedAgentDeskConfig) {
+ const baseUrl = String(config.apiBaseUrl || config.baseUrl || "").replace(/\/$/, "")
+ if (!baseUrl || typeof fetch !== "function") {
+ return Promise.resolve(config)
+ }
+ return fetch(`${baseUrl}/api/config`, {
+ method: "GET",
+ cache: "no-store",
+ })
+ .then((response) => response.json() as Promise)
+ .then((payload) => {
+ if (!payload || payload.success === false) {
+ return config
+ }
+ return normalizeConfig({
+ ...config,
+ language: payload.data?.language || config.language,
+ })
+ })
+ .catch(() => config)
+ }
+
function clearFrameTimers() {
if (state.frameHideTimer) {
window.clearTimeout(state.frameHideTimer)
@@ -369,7 +395,7 @@ type FrameMessage =
state.frame = document.createElement("iframe")
state.frame.dataset.agentDeskWidget = "frame"
- state.frame.title = state.config.title || getDefaultWidgetTitle()
+ state.frame.title = state.config.title || getDefaultWidgetTitle(state.config)
state.frame.src = state.frameUrl.toString()
applyFrameLayout()
state.frame.style.display = "block"
@@ -435,7 +461,7 @@ type FrameMessage =
const text = document.createElement("span")
button.type = "button"
button.dataset.agentDeskWidget = "launcher"
- button.setAttribute("aria-label", config.title || getDefaultWidgetTitle())
+ button.setAttribute("aria-label", config.title || getDefaultWidgetTitle(config))
icon.setAttribute("viewBox", "0 0 24 24")
icon.setAttribute("fill", "none")
icon.setAttribute("stroke", "currentColor")
@@ -451,7 +477,7 @@ type FrameMessage =
path.setAttribute("d", pathData)
icon.appendChild(path)
})
- text.textContent = getLauncherText()
+ text.textContent = getLauncherText(config)
text.style.display = "block"
button.style.position = "fixed"
button.style.bottom = "24px"
@@ -504,15 +530,17 @@ type FrameMessage =
}
state.configLoading = true
- fetchWidgetConfig(state.config).then((nextConfig) => {
- state.configLoading = false
- state.config = normalizeConfig(nextConfig)
- if (state.button?.parentNode) {
- state.button.parentNode.removeChild(state.button)
- state.button = null
- }
- createLauncher()
- })
+ fetchPublicConfig(state.config)
+ .then((nextConfig) => fetchWidgetConfig(nextConfig))
+ .then((nextConfig) => {
+ state.configLoading = false
+ state.config = normalizeConfig(nextConfig)
+ if (state.button?.parentNode) {
+ state.button.parentNode.removeChild(state.button)
+ state.button = null
+ }
+ createLauncher()
+ })
}
function destroy() {
diff --git a/web/lib/sdk/config-types.ts b/web/lib/sdk/config-types.ts
index caf6a55..0fb2138 100644
--- a/web/lib/sdk/config-types.ts
+++ b/web/lib/sdk/config-types.ts
@@ -11,6 +11,7 @@ export type AgentDeskConfig = {
getUserToken?: () => string | Promise
title?: string
subtitle?: string
+ language?: string
position?: "left" | "right"
themeColor?: string
width?: string
diff --git a/web/public/sdk/agent-desk-sdk.min.js b/web/public/sdk/agent-desk-sdk.min.js
index bff2098..2d557b3 100644
--- a/web/public/sdk/agent-desk-sdk.min.js
+++ b/web/public/sdk/agent-desk-sdk.min.js
@@ -1 +1 @@
-var __rest=this&&this.__rest||function(e,t){var n={};for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&t.indexOf(i)<0&&(n[i]=e[i]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(i=Object.getOwnPropertySymbols(e);rString(e||"").trim())}catch(e){return Promise.reject(e)}}().then(e=>{if(!n.config)throw new Error("channelId is required");return n.frameUrl=function(e,t){const n=r(e),i=new URL(`${n}/support/chat/`);return i.searchParams.set("channelId",e.channelId),i.searchParams.set("baseUrl",e.baseUrl),e.apiBaseUrl&&i.searchParams.set("apiBaseUrl",e.apiBaseUrl),e.externalId&&i.searchParams.set("externalId",e.externalId),e.externalName&&i.searchParams.set("externalName",e.externalName),t&&i.searchParams.set("userToken",t),i}(n.config,e),n.frameConfig=a(n.config,e),n.frameUrl})}function s(){n.frameHideTimer&&(window.clearTimeout(n.frameHideTimer),n.frameHideTimer=null),n.frameDestroyTimer&&(window.clearTimeout(n.frameDestroyTimer),n.frameDestroyTimer=null)}function l(){const e=n.frame,t=n.config;if(e&&t){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"===t.position?"left bottom":"right bottom",n.isMaximized)return e.style.top="max(12px, env(safe-area-inset-top))",e.style.right="max(12px, env(safe-area-inset-right))",e.style.bottom="max(12px, env(safe-area-inset-bottom))",e.style.left="max(12px, env(safe-area-inset-left))",e.style.width="calc(100vw - max(12px, env(safe-area-inset-left)) - max(12px, env(safe-area-inset-right)))",e.style.maxWidth="none",e.style.height="calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom)))",void(e.style.borderRadius="20px");e.style.top="",e.style.bottom="max(88px, calc(72px + env(safe-area-inset-bottom)))",e.style.right="left"===t.position?"":"max(12px, env(safe-area-inset-right))",e.style.left="left"===t.position?"max(12px, env(safe-area-inset-left))":"",e.style.width=t.width||"380px",e.style.maxWidth="calc(100vw - max(12px, env(safe-area-inset-left)) - max(12px, env(safe-area-inset-right)))",e.style.height="min(760px, calc(100dvh - max(104px, calc(88px + env(safe-area-inset-bottom))) - max(12px, env(safe-area-inset-top))))",e.style.borderRadius="24px"}}function d(e){var t;if((null===(t=n.frame)||void 0===t?void 0:t.contentWindow)&&n.frameUrl)try{n.frame.contentWindow.postMessage(e,n.frameUrl.origin)}catch(e){console.error("[agent-desk-widget] postMessage failed",e)}}function c(){n.frame&&n.frameLoaded&&n.frameReady&&n.config&&(n.initSent||(n.initSent=!0,d({type:"agent-desk:init",payload:n.frameConfig||a(n.config,"")})),d({type:n.isOpen?"agent-desk:open":"agent-desk:minimize"}),d({type:"agent-desk:maximized",payload:{isMaximized:n.isMaximized}}))}function f(){const e=n.frame;if(e){if(s(),l(),e.style.display="block",n.isOpen)return e.style.visibility="visible",e.style.pointerEvents="auto",n.frameHideTimer=window.setTimeout(()=>{n.frame&&(n.frame.style.opacity="1",n.frame.style.transform="translate3d(0, 0, 0) scale(1)")},16),void c();e.style.pointerEvents="none",e.style.opacity="0",e.style.transform=n.isMaximized?"translate3d(0, 10px, 0) scale(0.985)":"translate3d(0, 16px, 0) scale(0.96)",n.frameHideTimer=window.setTimeout(()=>{n.frame&&!n.isOpen&&(n.frame.style.visibility="hidden")},n.animationDuration),c()}}function m(e){const t=e||window.AgentDeskConfig||{channelId:""};n.config=i(t);const a=r(n.config);t.baseUrl||(n.config.baseUrl=a),n.config.channelId?(n.configLoading=!0,function(e){const t=String(e.apiBaseUrl||e.baseUrl||"").replace(/\/$/,"");if(!t||!e.channelId||"function"!=typeof fetch)return Promise.resolve(e);const n=`${t}/api/channel/config?channelId=${encodeURIComponent(e.channelId)}`;return fetch(n,{method:"GET",cache:"no-store",headers:{"X-Channel-Id":e.channelId}}).then(e=>e.json()).then(t=>t&&!1!==t.success?function(e,t){if(!t)return e;const n=Object.assign({},e);return["title","subtitle","themeColor","position","width"].forEach(e=>{const i=t[e];null!=i&&(n[e]=i)}),n}(e,t.data||{}):e).catch(()=>e)}(n.config).then(e=>{var t;n.configLoading=!1,n.config=i(e),(null===(t=n.button)||void 0===t?void 0:t.parentNode)&&(n.button.parentNode.removeChild(n.button),n.button=null),function(){if(n.button)return n.button;const e=n.config;if(!e)return null;const t=document.createElement("button"),i=document.createElementNS("http://www.w3.org/2000/svg","svg"),r=document.createElement("span");t.type="button",t.dataset.agentDeskWidget="launcher",t.setAttribute("aria-label",e.title||getDefaultWidgetTitle()),i.setAttribute("viewBox","0 0 24 24"),i.setAttribute("fill","none"),i.setAttribute("stroke","currentColor"),i.setAttribute("stroke-width","2"),i.setAttribute("stroke-linecap","round"),i.setAttribute("stroke-linejoin","round"),i.setAttribute("aria-hidden","true"),i.style.width="24px",i.style.height="24px",i.style.flex="0 0 auto",["M3 11a9 9 0 1 1 18 0","M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z","M21 11h-3a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2z","M21 16v2a4 4 0 0 1-4 4h-5"].forEach(e=>{const t=document.createElementNS("http://www.w3.org/2000/svg","path");t.setAttribute("d",e),i.appendChild(t)}),r.textContent=getLauncherText(),r.style.display="block",t.style.position="fixed",t.style.bottom="24px",t.style.right="left"===e.position?"":"24px",t.style.left="left"===e.position?"24px":"",t.style.zIndex="2147483000",t.style.display="inline-flex",t.style.flexDirection="column",t.style.alignItems="center",t.style.justifyContent="center",t.style.gap="4px",t.style.width="64px",t.style.height="64px",t.style.border="0",t.style.borderRadius="999px",t.style.padding="0",t.style.background=e.themeColor||"#0f6cbd",t.style.color="#fff",t.style.font="600 13px/1 sans-serif",t.style.boxShadow="0 18px 40px rgba(15, 35, 65, 0.24)",t.style.cursor="pointer",t.appendChild(i),t.appendChild(r),t.addEventListener("click",()=>{if(n.isOpen)return n.isOpen=!1,void f();u()}),document.body.appendChild(t),n.button=t}()})):console.error("[agent-desk-widget] channelId is required")}function u(){return o().then(()=>{n.frame||(n.frame?n.frame:n.frameUrl&&n.config&&(n.frame=document.createElement("iframe"),n.frame.dataset.agentDeskWidget="frame",n.frame.title=n.config.title||getDefaultWidgetTitle(),n.frame.src=n.frameUrl.toString(),l(),n.frame.style.display="block",n.frame.style.visibility="hidden",n.frame.style.pointerEvents="none",n.frame.style.opacity="0",n.frame.style.transform="translate3d(0, 18px, 0) scale(0.96)",n.frame.addEventListener("load",()=>{n.frameLoaded=!0,f()}),document.body.appendChild(n.frame),n.frame)),n.frame&&(n.isOpen=!0,f())}).catch(e=>{console.error("[agent-desk-widget] open failed",e)})}t||(window.__CS_AI_AGENT_WIDGET_STATE__=n),window.AgentDeskWidget={mount:m,destroy:function(){var e,t;s(),(null===(e=n.frame)||void 0===e?void 0:e.parentNode)&&n.frame.parentNode.removeChild(n.frame),(null===(t=n.button)||void 0===t?void 0:t.parentNode)&&n.button.parentNode.removeChild(n.button),n.button=null,n.frame=null,n.frameLoaded=!1,n.frameReady=!1,n.initSent=!1,n.isOpen=!1,n.isMaximized=!1,n.configLoading=!1,n.frameConfig=null,n.frameUrl=null},open:()=>u(),close:()=>{n.isOpen=!1,f()},getChatUrl:()=>{var e;return n.config||m(window.AgentDeskConfig||{channelId:""}),(null===(e=n.config)||void 0===e?void 0:e.channelId)?o().then(e=>e.toString()):Promise.reject(new Error("channelId is required"))}},n.listenerBound||(window.addEventListener("message",function(e){if(!n.frame||e.source!==n.frame.contentWindow)return;const t=e.data||{};return"agent-desk:ready"===t.type?(n.frameReady=!0,void c()):"agent-desk:request-minimize"===t.type?(n.isOpen=!1,void f()):void("agent-desk:request-close"!==t.type?"agent-desk:request-toggle-maximize"===t.type&&(n.isMaximized=!n.isMaximized,f()):n.frame&&(s(),n.frame.style.pointerEvents="none",n.frame.style.opacity="0",n.frame.style.transform="translate3d(0, 18px, 0) scale(0.94)",n.frame.style.visibility="hidden",n.frameDestroyTimer=window.setTimeout(()=>{n.frame&&(n.frame.parentNode&&n.frame.parentNode.removeChild(n.frame),n.frame=null,n.frameLoaded=!1,n.frameReady=!1,n.initSent=!1,n.isOpen=!1,n.isMaximized=!1,s())},n.animationDuration)))}),n.listenerBound=!0),window.AgentDeskConfig&&m(window.AgentDeskConfig)}();
+var __rest=this&&this.__rest||function(e,t){var n={};for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&t.indexOf(i)<0&&(n[i]=e[i]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var a=0;for(i=Object.getOwnPropertySymbols(e);aString(e||"").trim())}catch(e){return Promise.reject(e)}}().then(e=>{if(!n.config)throw new Error("channelId is required");return n.frameUrl=function(e,t){const n=a(e),i=new URL(`${n}/support/chat/`);return i.searchParams.set("channelId",e.channelId),i.searchParams.set("baseUrl",e.baseUrl),e.apiBaseUrl&&i.searchParams.set("apiBaseUrl",e.apiBaseUrl),e.externalId&&i.searchParams.set("externalId",e.externalId),e.externalName&&i.searchParams.set("externalName",e.externalName),t&&i.searchParams.set("userToken",t),i}(n.config,e),n.frameConfig=r(n.config,e),n.frameUrl})}function s(){n.frameHideTimer&&(window.clearTimeout(n.frameHideTimer),n.frameHideTimer=null),n.frameDestroyTimer&&(window.clearTimeout(n.frameDestroyTimer),n.frameDestroyTimer=null)}function l(){const e=n.frame,t=n.config;if(e&&t){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"===t.position?"left bottom":"right bottom",n.isMaximized)return e.style.top="max(12px, env(safe-area-inset-top))",e.style.right="max(12px, env(safe-area-inset-right))",e.style.bottom="max(12px, env(safe-area-inset-bottom))",e.style.left="max(12px, env(safe-area-inset-left))",e.style.width="calc(100vw - max(12px, env(safe-area-inset-left)) - max(12px, env(safe-area-inset-right)))",e.style.maxWidth="none",e.style.height="calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom)))",void(e.style.borderRadius="20px");e.style.top="",e.style.bottom="max(88px, calc(72px + env(safe-area-inset-bottom)))",e.style.right="left"===t.position?"":"max(12px, env(safe-area-inset-right))",e.style.left="left"===t.position?"max(12px, env(safe-area-inset-left))":"",e.style.width=t.width||"380px",e.style.maxWidth="calc(100vw - max(12px, env(safe-area-inset-left)) - max(12px, env(safe-area-inset-right)))",e.style.height="min(760px, calc(100dvh - max(104px, calc(88px + env(safe-area-inset-bottom))) - max(12px, env(safe-area-inset-top))))",e.style.borderRadius="24px"}}function d(e){var t;if((null===(t=n.frame)||void 0===t?void 0:t.contentWindow)&&n.frameUrl)try{n.frame.contentWindow.postMessage(e,n.frameUrl.origin)}catch(e){console.error("[agent-desk-widget] postMessage failed",e)}}function c(){n.frame&&n.frameLoaded&&n.frameReady&&n.config&&(n.initSent||(n.initSent=!0,d({type:"agent-desk:init",payload:n.frameConfig||r(n.config,"")})),d({type:n.isOpen?"agent-desk:open":"agent-desk:minimize"}),d({type:"agent-desk:maximized",payload:{isMaximized:n.isMaximized}}))}function f(){const e=n.frame;if(e){if(s(),l(),e.style.display="block",n.isOpen)return e.style.visibility="visible",e.style.pointerEvents="auto",n.frameHideTimer=window.setTimeout(()=>{n.frame&&(n.frame.style.opacity="1",n.frame.style.transform="translate3d(0, 0, 0) scale(1)")},16),void c();e.style.pointerEvents="none",e.style.opacity="0",e.style.transform=n.isMaximized?"translate3d(0, 10px, 0) scale(0.985)":"translate3d(0, 16px, 0) scale(0.96)",n.frameHideTimer=window.setTimeout(()=>{n.frame&&!n.isOpen&&(n.frame.style.visibility="hidden")},n.animationDuration),c()}}function m(e){const t=e||window.AgentDeskConfig||{channelId:""};n.config=i(t);const r=a(n.config);t.baseUrl||(n.config.baseUrl=r),n.config.channelId?(n.configLoading=!0,function(e){const t=String(e.apiBaseUrl||e.baseUrl||"").replace(/\/$/,"");return t&&"function"==typeof fetch?fetch(`${t}/api/config`,{method:"GET",cache:"no-store"}).then(e=>e.json()).then(t=>{var n;return t&&!1!==t.success?i(Object.assign(Object.assign({},e),{language:(null===(n=t.data)||void 0===n?void 0:n.language)||e.language})):e}).catch(()=>e):Promise.resolve(e)}(n.config).then(e=>function(e){const t=String(e.apiBaseUrl||e.baseUrl||"").replace(/\/$/,"");if(!t||!e.channelId||"function"!=typeof fetch)return Promise.resolve(e);const n=`${t}/api/channel/config?channelId=${encodeURIComponent(e.channelId)}`;return fetch(n,{method:"GET",cache:"no-store",headers:{"X-Channel-Id":e.channelId}}).then(e=>e.json()).then(t=>t&&!1!==t.success?function(e,t){if(!t)return e;const n=Object.assign({},e);return["title","subtitle","themeColor","position","width"].forEach(e=>{const i=t[e];null!=i&&(n[e]=i)}),n}(e,t.data||{}):e).catch(()=>e)}(e)).then(e=>{var t;n.configLoading=!1,n.config=i(e),(null===(t=n.button)||void 0===t?void 0:t.parentNode)&&(n.button.parentNode.removeChild(n.button),n.button=null),function(){if(n.button)return n.button;const e=n.config;if(!e)return null;const t=document.createElement("button"),i=document.createElementNS("http://www.w3.org/2000/svg","svg"),a=document.createElement("span");t.type="button",t.dataset.agentDeskWidget="launcher",t.setAttribute("aria-label",e.title||getDefaultWidgetTitle(e)),i.setAttribute("viewBox","0 0 24 24"),i.setAttribute("fill","none"),i.setAttribute("stroke","currentColor"),i.setAttribute("stroke-width","2"),i.setAttribute("stroke-linecap","round"),i.setAttribute("stroke-linejoin","round"),i.setAttribute("aria-hidden","true"),i.style.width="24px",i.style.height="24px",i.style.flex="0 0 auto",["M3 11a9 9 0 1 1 18 0","M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z","M21 11h-3a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2z","M21 16v2a4 4 0 0 1-4 4h-5"].forEach(e=>{const t=document.createElementNS("http://www.w3.org/2000/svg","path");t.setAttribute("d",e),i.appendChild(t)}),a.textContent=getLauncherText(e),a.style.display="block",t.style.position="fixed",t.style.bottom="24px",t.style.right="left"===e.position?"":"24px",t.style.left="left"===e.position?"24px":"",t.style.zIndex="2147483000",t.style.display="inline-flex",t.style.flexDirection="column",t.style.alignItems="center",t.style.justifyContent="center",t.style.gap="4px",t.style.width="64px",t.style.height="64px",t.style.border="0",t.style.borderRadius="999px",t.style.padding="0",t.style.background=e.themeColor||"#0f6cbd",t.style.color="#fff",t.style.font="600 13px/1 sans-serif",t.style.boxShadow="0 18px 40px rgba(15, 35, 65, 0.24)",t.style.cursor="pointer",t.appendChild(i),t.appendChild(a),t.addEventListener("click",()=>{if(n.isOpen)return n.isOpen=!1,void f();u()}),document.body.appendChild(t),n.button=t}()})):console.error("[agent-desk-widget] channelId is required")}function u(){return o().then(()=>{n.frame||(n.frame?n.frame:n.frameUrl&&n.config&&(n.frame=document.createElement("iframe"),n.frame.dataset.agentDeskWidget="frame",n.frame.title=n.config.title||getDefaultWidgetTitle(n.config),n.frame.src=n.frameUrl.toString(),l(),n.frame.style.display="block",n.frame.style.visibility="hidden",n.frame.style.pointerEvents="none",n.frame.style.opacity="0",n.frame.style.transform="translate3d(0, 18px, 0) scale(0.96)",n.frame.addEventListener("load",()=>{n.frameLoaded=!0,f()}),document.body.appendChild(n.frame),n.frame)),n.frame&&(n.isOpen=!0,f())}).catch(e=>{console.error("[agent-desk-widget] open failed",e)})}t||(window.__CS_AI_AGENT_WIDGET_STATE__=n),window.AgentDeskWidget={mount:m,destroy:function(){var e,t;s(),(null===(e=n.frame)||void 0===e?void 0:e.parentNode)&&n.frame.parentNode.removeChild(n.frame),(null===(t=n.button)||void 0===t?void 0:t.parentNode)&&n.button.parentNode.removeChild(n.button),n.button=null,n.frame=null,n.frameLoaded=!1,n.frameReady=!1,n.initSent=!1,n.isOpen=!1,n.isMaximized=!1,n.configLoading=!1,n.frameConfig=null,n.frameUrl=null},open:()=>u(),close:()=>{n.isOpen=!1,f()},getChatUrl:()=>{var e;return n.config||m(window.AgentDeskConfig||{channelId:""}),(null===(e=n.config)||void 0===e?void 0:e.channelId)?o().then(e=>e.toString()):Promise.reject(new Error("channelId is required"))}},n.listenerBound||(window.addEventListener("message",function(e){if(!n.frame||e.source!==n.frame.contentWindow)return;const t=e.data||{};return"agent-desk:ready"===t.type?(n.frameReady=!0,void c()):"agent-desk:request-minimize"===t.type?(n.isOpen=!1,void f()):void("agent-desk:request-close"!==t.type?"agent-desk:request-toggle-maximize"===t.type&&(n.isMaximized=!n.isMaximized,f()):n.frame&&(s(),n.frame.style.pointerEvents="none",n.frame.style.opacity="0",n.frame.style.transform="translate3d(0, 18px, 0) scale(0.94)",n.frame.style.visibility="hidden",n.frameDestroyTimer=window.setTimeout(()=>{n.frame&&(n.frame.parentNode&&n.frame.parentNode.removeChild(n.frame),n.frame=null,n.frameLoaded=!1,n.frameReady=!1,n.initSent=!1,n.isOpen=!1,n.isMaximized=!1,s())},n.animationDuration)))}),n.listenerBound=!0),window.AgentDeskConfig&&m(window.AgentDeskConfig)}();