From f287727c59d520571ce72a393f29ceae8d85aae4 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 28 Apr 2026 18:31:47 +0800 Subject: [PATCH 1/2] feat: add JWT authentication support to KefuWidgetDemo --- web/components/kefu/widget-demo.tsx | 223 ++++++++++++++++++++++++---- web/package.json | 1 + web/pnpm-lock.yaml | 11 +- 3 files changed, 205 insertions(+), 30 deletions(-) diff --git a/web/components/kefu/widget-demo.tsx b/web/components/kefu/widget-demo.tsx index 6e9922d..8ad0ad5 100644 --- a/web/components/kefu/widget-demo.tsx +++ b/web/components/kefu/widget-demo.tsx @@ -1,16 +1,28 @@ "use client" +import { SignJWT } from "jose" import { useEffect, useMemo, useState } from "react" import type { KefuWidgetHostConfig } from "@/lib/kefu-widget-config" const STORAGE_KEY = "cs-agent-web-widget-test-config" +const DEFAULT_JWT_TTL_MINUTES = "30" const INITIAL_CONFIG: KefuWidgetHostConfig = { channelId: "", baseUrl: "", apiBaseUrl: "", } +type AuthMode = "guest" | "jwt" + +type WidgetDemoConfig = KefuWidgetHostConfig & { + authMode?: AuthMode + jwtSecret?: string + jwtUserId?: string + jwtName?: string + jwtTtlMinutes?: string +} + declare global { interface Window { CSAgentWidget?: { @@ -22,14 +34,14 @@ declare global { } } -function getDefaultConfig(): KefuWidgetHostConfig { +function getDefaultConfig(): WidgetDemoConfig { if (typeof window === "undefined") { return INITIAL_CONFIG } const savedText = window.localStorage.getItem(STORAGE_KEY) const savedConfig = savedText - ? (JSON.parse(savedText) as Partial) + ? (JSON.parse(savedText) as Partial) : {} const query = new URLSearchParams(window.location.search) @@ -37,6 +49,11 @@ function getDefaultConfig(): KefuWidgetHostConfig { channelId: query.get("channelId") ?? savedConfig.channelId ?? "", baseUrl: "", apiBaseUrl: "", + authMode: (query.get("authMode") as AuthMode | null) ?? savedConfig.authMode ?? "guest", + jwtSecret: savedConfig.jwtSecret ?? "", + jwtUserId: query.get("userId") ?? savedConfig.jwtUserId ?? "demo-user-001", + jwtName: query.get("name") ?? savedConfig.jwtName ?? "测试用户", + jwtTtlMinutes: savedConfig.jwtTtlMinutes ?? DEFAULT_JWT_TTL_MINUTES, } } @@ -69,10 +86,79 @@ function injectWidget(config: KefuWidgetHostConfig) { document.body.appendChild(script) } +function buildWidgetConfig(config: WidgetDemoConfig, userToken: string): WidgetDemoConfig { + return { + ...config, + channelId: config.channelId.trim(), + baseUrl: "", + apiBaseUrl: "", + userToken, + } +} + +async function signUserToken(config: WidgetDemoConfig) { + const userId = (config.jwtUserId || "").trim() + const name = (config.jwtName || "").trim() + const secret = (config.jwtSecret || "").trim() + const ttl = Number(config.jwtTtlMinutes || DEFAULT_JWT_TTL_MINUTES) + + if (!userId) { + throw new Error("请填写 userId") + } + if (!name) { + throw new Error("请填写用户名称") + } + if (!secret) { + throw new Error("请填写 JWT Secret") + } + if (!Number.isFinite(ttl) || ttl <= 0) { + throw new Error("有效期必须大于 0") + } + + return new SignJWT({ userId, name }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt() + .setExpirationTime(`${ttl}m`) + .sign(new TextEncoder().encode(secret)) +} + export function KefuWidgetDemo() { - const [config, setConfig] = useState(INITIAL_CONFIG) + const [config, setConfig] = useState({ + ...INITIAL_CONFIG, + authMode: "guest", + jwtSecret: "", + jwtUserId: "demo-user-001", + jwtName: "测试用户", + jwtTtlMinutes: DEFAULT_JWT_TTL_MINUTES, + }) const [status, setStatus] = useState("请填写 channelId") const [origin, setOrigin] = useState("") + const [generatedToken, setGeneratedToken] = useState("") + + async function mountWidget(configToMount: WidgetDemoConfig) { + let userToken = "" + if (configToMount.authMode === "jwt") { + userToken = await signUserToken(configToMount) + } + + const nextConfig = buildWidgetConfig(configToMount, userToken) + setConfig(nextConfig) + setGeneratedToken(userToken) + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(nextConfig)) + + if (!nextConfig.channelId) { + removeMountedWidget() + setStatus("请填写 channelId") + return + } + + injectWidget(nextConfig) + setStatus( + nextConfig.authMode === "jwt" + ? "Widget 已挂载:JWT 用户模式" + : "Widget 已挂载:访客模式" + ) + } useEffect(() => { const timer = window.setTimeout(() => { @@ -82,7 +168,11 @@ export function KefuWidgetDemo() { setStatus(initialConfig.channelId ? "Widget 已挂载" : "请填写 channelId") if (initialConfig.channelId) { - injectWidget(initialConfig) + void mountWidget(initialConfig).catch((error) => { + removeMountedWidget() + setGeneratedToken("") + setStatus(error instanceof Error ? error.message : "生成 userToken 失败") + }) } }, 0) @@ -97,40 +187,34 @@ export function KefuWidgetDemo() { ? `${origin}/sdk/cs-ai-agent-sdk.min.js` : "/sdk/cs-ai-agent-sdk.min.js" + const configLines = [` channelId: "${config.channelId || ""}"`] + if (config.authMode === "jwt") { + configLines.push(` userToken: "${generatedToken || "业务系统后端签发的 JWT"}"`) + } + return ` ` - }, [config, origin]) + }, [config, generatedToken, origin]) - function updateField( + function updateField( key: K, - value: KefuWidgetHostConfig[K] + value: WidgetDemoConfig[K] ) { setConfig((current) => ({ ...current, [key]: value })) } - function handleMount() { - const nextConfig: KefuWidgetHostConfig = { - ...config, - channelId: config.channelId.trim(), - baseUrl: "", - apiBaseUrl: "", - } - - setConfig(nextConfig) - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(nextConfig)) - - if (!nextConfig.channelId) { + async function handleMount() { + try { + await mountWidget(config) + } catch (error) { removeMountedWidget() - setStatus("请填写 channelId") - return + setGeneratedToken("") + setStatus(error instanceof Error ? error.message : "生成 userToken 失败") } - - injectWidget(nextConfig) - setStatus("Widget 已挂载") } return ( @@ -146,12 +230,47 @@ export function KefuWidgetDemo() { value={config.channelId} onChange={(value) => updateField("channelId", value)} /> + updateField("authMode", value)} + options={[ + { label: "访客", value: "guest" }, + { label: "JWT 用户", value: "jwt" }, + ]} + /> + {config.authMode === "jwt" ? ( +
+ updateField("jwtUserId", value)} + /> + updateField("jwtName", value)} + /> + updateField("jwtSecret", value)} + type="password" + /> + updateField("jwtTtlMinutes", value)} + type="number" + /> +
+ ) : null}