"use client"; import dynamic from "next/dynamic"; import Image from "next/image"; import { useEffect, useState } from "react"; import { ArrowRight, Bot, ChartColumn, Cpu, DatabaseZap, GitBranch, MessageSquareText, ShieldCheck, } from "lucide-react"; import { generateUUID } from "@/lib/utils"; import type { WidgetHostConfig } from "@/lib/widget/config"; const STORAGE_KEY = "cs-agent-widget-test-config"; const GITHUB_URL = "https://github.com/huabeitech/cs-ai-agent"; const GITEE_URL = "https://gitee.com/huabeitech/cs-ai-agent"; const OFFICIAL_SITE_URL = "https://aiagent.huabei.pro"; type TestConfig = WidgetHostConfig; const featureCards = [ { title: "先让 AI 处理重复路径", description: "把常见问题和标准流程优先交给 Agent,降低人工压力。", icon: MessageSquareText, }, { title: "人工接管不脱离上下文", description: "在低置信度、命中规则或用户要求时,带着会话上下文平滑转人工。", icon: Bot, }, { title: "知识、人工和工单形成闭环", description: "把知识检索、人工接管、外部系统调用和工单流程放进同一条客服链路。", icon: GitBranch, }, { title: "面向客服运营而不是 Demo", description: "强调稳定运行、可观测流程和结构化升级,不把项目做成聊天壳子。", icon: ShieldCheck, }, ]; const scopeCards = [ { title: "会话运行时", description: "接收用户消息、维护会话状态并触发回复执行。", icon: MessageSquareText, }, { title: "AI 知识检索", description: "基于 RAG 的企业知识库,提升答案准确性。", icon: DatabaseZap, }, { title: "AI Agent 运行时", description: "自定义 Skill,通过 MCP 协议对接企业业务系统。", icon: Cpu, }, { title: "人工协同", description: "在 AI 不该继续时,把会话转给人工客服或指定客服组。", icon: Bot, }, { title: "工单闭环", description: "从会话创建工单,跟踪处理过程,直到最终关闭。", icon: ChartColumn, }, ]; const flowSteps = [ { step: "01", title: "用户发起咨询", description: "用户通过 Web Widget 或开放接口进入系统,创建新会话或匹配已有会话。", }, { step: "02", title: "系统装载上下文", description: "加载会话历史、AI 配置、可用 Skill、知识库和工具能力,为当前问题准备执行上下文。", }, { step: "03", title: "AI 优先回复", description: "AI Agent 结合历史消息、知识检索结果和可用工具,优先处理常见问题与标准流程。", }, { step: "04", title: "按需调用能力", description: "在回复过程中执行 Skill、通过 MCP 调用外部系统,或触发特定业务流程。", }, { step: "05", title: "转交人工处理", description: "当 AI 无法继续有效处理,或用户希望由人工接待时,会话带着上下文转交客服。", }, { step: "06", title: "转入工单流程", description: "当问题需要跨时段、跨角色持续跟进时,可以从会话直接转入工单流程。", }, ]; function getWidgetRootPath(pathname: string): string { return pathname.startsWith("/widget") ? "/widget" : ""; } function getWidgetSdkUrl(baseUrl: string, pathname: string): string { return `${baseUrl.replace(/\/$/, "")}${getWidgetRootPath(pathname)}/sdk/cs-agent-widget.js`; } function generateRandomSubject(): string { return `设备会话-${generateUUID().replace(/-/g, "").slice(0, 8)}`; } function buildDefaultConfig(baseUrl: string): TestConfig { return { channelId: "", baseUrl, apiBaseUrl: baseUrl, title: "IoT 智能客服", subtitle: "设备告警、工单协同与在线服务统一入口", position: "right", themeColor: "#2563eb", width: "680px", subject: generateRandomSubject(), }; } function getInitialConfig(): TestConfig { if (typeof window === "undefined") { return buildDefaultConfig(""); } const origin = window.location.origin; const query = new URLSearchParams(window.location.search); const savedText = window.localStorage.getItem(STORAGE_KEY); const savedConfig = savedText ? (JSON.parse(savedText) as Partial) : {}; return { ...buildDefaultConfig(origin), ...savedConfig, channelId: query.get("channelId") ?? savedConfig.channelId ?? "", baseUrl: query.get("baseUrl") ?? savedConfig.baseUrl ?? origin, apiBaseUrl: query.get("apiBaseUrl") ?? savedConfig.apiBaseUrl ?? savedConfig.baseUrl ?? origin, width: query.get("width") ?? savedConfig.width ?? "680px", subject: query.get("subject") ?? savedConfig.subject ?? generateRandomSubject(), }; } function removeMountedWidget() { if (typeof window === "undefined") { return; } document .querySelectorAll( '[data-cs-agent-widget="launcher"], [data-cs-agent-widget="frame"], [data-cs-agent-widget="script"]', ) .forEach((node) => node.remove()); delete window.CSAgentConfig; delete window.__CS_AGENT_WIDGET_CONFIG__; delete (window as Window & { __CS_AGENT_WIDGET_LOADED__?: boolean }) .__CS_AGENT_WIDGET_LOADED__; } function injectWidget(config: TestConfig) { removeMountedWidget(); window.CSAgentConfig = config; const script = document.createElement("script"); script.async = true; script.src = getWidgetSdkUrl(window.location.origin, window.location.pathname); script.dataset.csAgentWidget = "script"; document.body.appendChild(script); } function WidgetHomePageInner() { const [config, setConfig] = useState(getInitialConfig); const [status, setStatus] = useState(() => getInitialConfig().channelId ? "Widget 已挂载" : "请先填写 channelId", ); useEffect(() => { if (config.channelId) { injectWidget(config); } else { removeMountedWidget(); } return () => { removeMountedWidget(); }; }, [config]); function updateField( key: K, value: TestConfig[K], ) { setConfig((current) => ({ ...current, [key]: value })); } function handleApply() { const nextConfig = { ...config, channelId: config.channelId.trim(), baseUrl: config.baseUrl.trim() || window.location.origin, apiBaseUrl: config.apiBaseUrl?.trim() || config.baseUrl.trim() || window.location.origin, title: config.title?.trim() || "IoT 智能客服", subtitle: config.subtitle?.trim() || "设备告警、工单协同与在线服务统一入口", themeColor: config.themeColor?.trim() || "#2563eb", width: config.width?.trim() || "680px", subject: config.subject?.trim() || generateRandomSubject(), }; setConfig(nextConfig); window.localStorage.setItem(STORAGE_KEY, JSON.stringify(nextConfig)); if (!nextConfig.channelId) { removeMountedWidget(); setStatus("请先填写 channelId"); return; } injectWidget(nextConfig); setStatus("Widget 已挂载"); } const sdkUrl = getWidgetSdkUrl(config.baseUrl, window.location.pathname); const snippet = ` `; return (

AI 优先接待,人工无缝协同

贝壳 AI 客服是一个以 AI Agent 为核心的智能客服系统,把在线会话、 知识库检索、工单流转和人工接管串成一条完整链路。

{[ { label: "接待模式", value: "AI + 人工协同" }, { label: "回答方式", value: "知识库 RAG 驱动" }, { label: "运行能力", value: "Skills + MCP + 工单流程" }, ].map((item) => (
{item.label}
{item.value}
))}
产品能力

面向真实客服场景的开源系统

用一套系统处理用户咨询、AI 回复、知识检索、人工接管和后续工单。

{featureCards.map((item) => { const Icon = item.icon; return (

{item.title}

{item.description}

); })}
覆盖完整客服链路

从第一条消息到问题关闭,关键环节都在同一套系统里。

{scopeCards.map((item) => { const Icon = item.icon; return (
{item.title}
{item.description}
); })}
客服处理闭环

从用户发起咨询,到 AI 回复、人工接管、工单跟踪,核心流程都在同一套系统里完成。

{flowSteps.map((item) => (
{item.step}
{item.title}
{item.description}
))}
接入方式

保留在线配置与接入脚本,直接拿来演示和集成

这里可以直接填写 `channelId`、调整宿主参数并挂载 Widget,也可以复制脚本到业务系统完成基础接入。

Widget 配置
宿主页面接入时需要的基础参数。
接入脚本
复制到宿主页面即可完成基础接入。
                  {snippet}
                
); } const WidgetHomePage = dynamic(async () => WidgetHomePageInner, { ssr: false, }); export default WidgetHomePage;