Skip to content

OpenAI AgentKit ChatKit Demo 实战

本文基于 OpenAI 最新发布的 AgentKit 及 ChatKit 组件,复现官方推荐的「后端创建 Session + 前端嵌入小部件」集成流程。你可以直接复制下方代码,在本地或生产环境中部署属于自己的 ChatKit 智能体体验。

架构与工作流

  • AgentKit 工作流: 在 OpenAI 平台的 Agent Builder 中完成设计与发布,得到可复用的 workflow_id。Demo 同时演示简单对话与复杂推理两套工作流切换。
  • FastAPI 服务端: 负责持有 OPENAI_API_KEY,通过 POST /v1/chatkit/sessions 为前端生成 client_secret,并代理 ChatKit CDN 以规避国内网络限制。
  • React + @openai/chatkit-react 前端: 使用 ChatKit 组件嵌入聊天体验,结合自定义调试面板、主题与宽度调节,便于团队调试与演示。
  • 安全与运维: Session 创建过程中不会泄露 API Key,Session 过期后前端自动刷新;日志中对敏感字段做遮罩,支持通过环境变量注入代理与自定义工作流。

ChatKit 架构示意

端到端流程

  1. 用户在前端点击开始对话时,@openai/chatkit-react 会调用 getClientSecret
  2. 前端向 FastAPI 的 /api/chatkit/session?workflow_type=xxx 发起请求。
  3. 服务端调用 OpenAI ChatKit 会话 API,返回 client_secretsession_id
  4. ChatKit 组件利用 client_secret 建立与 OpenAI 的安全长连接,加载对应工作流。
  5. 如会话超时或网络中断,SDK 自动重试,并在调试面板中输出日志。

目录结构

text
chatkit-demo/
├── backend/            # FastAPI 后端
│   ├── main.py
│   └── requirements.txt
└── frontend/           # React + Vite 前端
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── App.jsx
        ├── App.css
        ├── main.jsx
        └── index.css

环境准备

  • Python 3.11+, Node.js ≥ 18。
  • 设置环境变量:
    • OPENAI_API_KEY: 具备 AgentKit/ChatKit 权限的密钥。
    • WORKFLOW_ID(可选): 默认工作流,可在前端下拉切换。
    • HTTP_PROXY_FOR_REQUESTS(可选): 服务端调用 OpenAI API 时使用的代理。
    • DOMAIN_KEY(可选): 若需要在 iframe 中校验域名,可写入前端部署时的域名密钥。
  • 前端打包后静态文件由 FastAPI 通过 StaticFiles 提供,也可交给 Nginx / CDN。

后端: FastAPI + OpenAI SDK

关键特性

  • 在启动阶段移除系统代理环境变量,避免与 OpenAI 官方 SDK 冲突。
  • 通过 /api/workflows 暴露前端可选工作流列表,避免硬编码。
  • POST /api/chatkit/session 直接调度 REST API,同时兼容工作流版本与代理设定。
  • /cdn/chatkit.js 提供 CDN 代理,附带浏览器 Header 绕过 Cloudflare 校验。
  • 使用 uvicorn 热重载,便于本地开发。

backend/main.py

python
"""
ChatKit Demo 后端服务
基于 FastAPI + OpenAI ChatKit SDK
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response
from openai import OpenAI
from dotenv import load_dotenv
import os
import logging
import requests as req_lib

# 加载环境变量
load_dotenv()

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 初始化 FastAPI
app = FastAPI(
    title="ChatKit Demo API",
    description="OpenAI ChatKit 集成演示后端",
    version="1.0.0"
)

# CORS 配置 - 允许前端跨域访问
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境应该限制为具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 初始化 OpenAI 客户端
try:
    # 移除环境中的代理设置,避免与 OpenAI SDK 冲突
    os.environ.pop('HTTP_PROXY', None)
    os.environ.pop('HTTPS_PROXY', None)
    os.environ.pop('http_proxy', None)
    os.environ.pop('https_proxy', None)

    openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    logger.info("✅ OpenAI 客户端初始化成功")
except Exception as e:
    logger.error(f"❌ OpenAI 客户端初始化失败: {e}")
    openai_client = None

# 配置信息
WORKFLOWS = {
    "simple": "wf_68e753b2d73881909df7abd3e1ffa2160d6311b72f31e8f1",  # 简单工作流
    "complex": "wf_68e620cd70a48190a0a2cc41cbcbb49804b7fd1f17c65f8b"  # 复杂 reasoning 工作流
}
DEFAULT_WORKFLOW = os.getenv("WORKFLOW_ID", WORKFLOWS["simple"])
DOMAIN_KEY = os.getenv("DOMAIN_KEY", "")


@app.get("/api")
async def api_root():
    """API 根路径 - 健康检查"""
    return {
        "status": "running",
        "service": "ChatKit Demo API",
        "workflows": WORKFLOWS,
        "default_workflow": DEFAULT_WORKFLOW,
        "openai_connected": openai_client is not None
    }


@app.get("/api/workflows")
async def get_workflows():
    """获取可用的工作流列表"""
    return {
        "workflows": [
            {"id": "simple", "name": "简单对话", "workflow_id": WORKFLOWS["simple"]},
            {"id": "complex", "name": "复杂推理 (Reasoning)", "workflow_id": WORKFLOWS["complex"]}
        ],
        "default": "simple"
    }


@app.get("/health")
async def health_check():
    """健康检查端点"""
    if not openai_client:
        raise HTTPException(status_code=500, detail="OpenAI client not initialized")

    return {
        "status": "healthy",
        "openai_api_key_configured": bool(os.getenv("OPENAI_API_KEY")),
        "workflows": WORKFLOWS
    }


@app.get("/cdn/chatkit.js")
async def proxy_chatkit_js():
    """代理 ChatKit JS CDN 资源"""
    try:
        proxies = None
        if os.getenv("HTTP_PROXY_FOR_REQUESTS"):
            proxies = {
                "http": os.getenv("HTTP_PROXY_FOR_REQUESTS"),
                "https": os.getenv("HTTP_PROXY_FOR_REQUESTS")
            }

        # 添加浏览器Headers来绕过Cloudflare
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept": "*/*",
            "Accept-Language": "en-US,en;q=0.9",
            "Accept-Encoding": "gzip, deflate, br",
            "Referer": "https://platform.openai.com/",
            "Origin": "https://platform.openai.com",
            "Sec-Fetch-Dest": "script",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-site"
        }

        response = req_lib.get(
            "https://cdn.platform.openai.com/deployments/chatkit/chatkit.js",
            headers=headers,
            proxies=proxies,
            timeout=30
        )

        if response.status_code == 200:
            return Response(
                content=response.content,
                media_type="application/javascript",
                headers={
                    "Cache-Control": "public, max-age=3600",
                    "Access-Control-Allow-Origin": "*"
                }
            )
        else:
            raise HTTPException(status_code=response.status_code, detail=f"Failed to fetch ChatKit JS: {response.status_code}")

    except Exception as e:
        logger.error(f"❌ 代理 ChatKit JS 失败: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/chatkit/session")
async def create_chatkit_session(workflow_type: str = "simple"):
    """
    创建 ChatKit Session

    这是 ChatKit 工作的核心端点:
    1. 前端请求这个端点
    2. 服务端使用 OpenAI API Key 创建 session
    3. 返回 client_secret 给前端
    4. 前端使用 client_secret 连接 ChatKit

    参数:
    - workflow_type: 工作流类型 ("simple" 或 "complex")
    """
    try:
        if not openai_client:
            raise HTTPException(
                status_code=500,
                detail="OpenAI client not initialized. Check OPENAI_API_KEY."
            )

        # 获取对应的工作流 ID
        workflow_id = WORKFLOWS.get(workflow_type, WORKFLOWS["simple"])
        logger.info(f"🔄 创建 ChatKit Session for workflow: {workflow_type} ({workflow_id})")

        # 直接调用 REST API (因为 SDK 还未完全支持 ChatKit)
        import requests
        import uuid

        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
            "OpenAI-Beta": "chatkit_beta=v1"
        }

        payload = {
            "workflow": {
                "id": workflow_id,
                "version": "3"  # 使用发布的版本 3
            },
            "user": str(uuid.uuid4())  # 生成唯一用户ID
        }

        # 注意: domain_key 验证是在前端 ChatKit iframe 加载时进行
        # API 创建 session 时不需要 domain_key 参数

        # 使用代理(如果有)
        proxies = None
        if os.getenv("HTTP_PROXY_FOR_REQUESTS"):
            proxies = {
                "http": os.getenv("HTTP_PROXY_FOR_REQUESTS"),
                "https": os.getenv("HTTP_PROXY_FOR_REQUESTS")
            }

        response = requests.post(
            "https://api.openai.com/v1/chatkit/sessions",
            headers=headers,
            json=payload,
            proxies=proxies,
            timeout=30
        )

        if response.status_code != 200:
            raise Exception(f"API Error: {response.text}")

        session = response.json()

        logger.info(f"✅ ChatKit Session 创建成功: {session.get('id')} (workflow: {workflow_type})")

        return {
            "client_secret": session.get("client_secret"),
            "session_id": session.get("id"),
            "workflow_type": workflow_type,
            "workflow_id": workflow_id
        }

    except Exception as e:
        logger.error(f"❌ 创建 ChatKit Session 失败: {str(e)}")
        raise HTTPException(
            status_code=500,
            detail=f"Failed to create ChatKit session: {str(e)}"
        )


# 挂载静态文件 (React 打包后的前端页面)
# 注意: 这个要放在最后,确保 API 路由优先匹配
app.mount("/", StaticFiles(directory="../frontend/dist", html=True), name="static")


if __name__ == "__main__":
    import uvicorn

    port = int(os.getenv("PORT", 9000))
    logger.info(f"🚀 启动 ChatKit Demo API on port {port}")

    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=port,
        reload=True,  # 开发模式自动重载
        log_level="info",
        timeout_keep_alive=1800,  # 保持连接 30 分钟
        timeout_graceful_shutdown=10
    )

backend/requirements.txt

text
fastapi==0.115.6
uvicorn[standard]==0.34.0
python-dotenv==1.0.1
openai==1.58.1
requests==2.32.3

前端: React + ChatKit 组件

样式与交互亮点

  • 使用 useChatKit 暴露的 control 对象接管会话生命周期,支持自动刷新 client_secret
  • 内置调试日志面板,友好展示 Session ID、工作流信息及错误追踪,同时对敏感字段做脱敏处理。
  • 提供工作流切换、主题色调整、UI 密度与布局宽度调节,方便演示不同投放需求。
  • 移动端自动切换为纵向布局,确保演示兼容性。

frontend/package.json

json
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@openai/chatkit-react": "^0.0.0",
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

frontend/vite.config.js

javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:9000',
        changeOrigin: true,
      }
    }
  },
  build: {
    outDir: 'dist',
  }
})

frontend/src/main.jsx

javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

frontend/src/App.jsx

javascript
import { ChatKit, useChatKit } from '@openai/chatkit-react';
import { useEffect, useState, useCallback, useRef } from 'react';
import './App.css';

function App() {
  const [isReady, setIsReady] = useState(false);
  const [error, setError] = useState(null);
  const [debugInfo, setDebugInfo] = useState([]);
  const [showStylePanel, setShowStylePanel] = useState(false);

  // 工作流类型 - 从 localStorage 读取,默认为 simple
  const [workflowType, setWorkflowType] = useState(() => {
    return localStorage.getItem('chatkit_workflow_type') || 'simple';
  });

  // ChatKit 样式配置
  const [themeConfig, setThemeConfig] = useState({
    colorScheme: 'light',
    accentColor: '#5B7FFF',
    accentLevel: 2,
    radius: 'round',
    density: 'normal',
  });

  // 布局宽度配置 (固定像素)
  const [debugWidth, setDebugWidth] = useState(600); // 调试面板宽度 (px)
  const [chatKitWidth, setChatKitWidth] = useState(600); // ChatKit 宽度 (px)
  const [isDragging, setIsDragging] = useState(false);

  // 隐藏敏感信息的工具函数
  const maskSensitive = (text) => {
    // 隐藏 session_id 和 client_secret
    return text
      .replace(/(cksess_[a-f0-9]{48})/g, (match) => match.substring(0, 12) + '***')
      .replace(/(Client Secret: )([a-zA-Z0-9_-]{20})/g, '$1***');
  };

  const addDebug = useCallback((message) => {
    const timestamp = new Date().toLocaleTimeString();
    const maskedMessage = maskSensitive(message);
    setDebugInfo(prev => [...prev, `[${timestamp}] ${maskedMessage}`]);
    console.log(message);
  }, []);

  const { control } = useChatKit({
    api: {
      async getClientSecret(currentClientSecret) {
        if (currentClientSecret) {
          addDebug('[Session] 会话过期,正在刷新...');
        } else {
          addDebug(`[Session] 首次创建会话... (工作流: ${workflowType === 'simple' ? '简单对话' : '复杂推理'})`);
        }

        try {
          const response = await fetch(`/api/chatkit/session?workflow_type=${workflowType}`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
          });

          if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
          }

          const data = await response.json();
          addDebug(`[Session] 创建成功: ${data.session_id}`);
          addDebug(`[Workflow] 使用工作流: ${data.workflow_type} (${data.workflow_id})`);
          addDebug(`[Auth] Client Secret: ${data.client_secret.substring(0, 20)}...`);

          return data.client_secret;
        } catch (error) {
          addDebug(`[Error] 获取 client_secret 失败: ${error.message}`);
          setError(error.message);
          throw error;
        }
      },
    },
    // 主题配置 - 可动态调整
    theme: {
      colorScheme: themeConfig.colorScheme,
      color: {
        accent: {
          primary: themeConfig.accentColor,
          level: themeConfig.accentLevel
        }
      },
      radius: themeConfig.radius,
      density: themeConfig.density,
      typography: {
        fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
      }
    },
    // 欢迎屏幕配置
    startScreen: {
      greeting: '您好,我是 AI 助手。有什么可以帮您的吗?',
      prompts: [
        { label: '自我介绍', prompt: '介绍一下你自己' },
        { label: '功能说明', prompt: '你能帮我做什么?' },
        { label: '使用帮助', prompt: '如何使用这个对话界面?' },
      ],
    },
    // 输入框配置
    composer: {
      placeholder: '输入消息...',
    },
    onError: (error) => {
      const errorMsg = error.message || JSON.stringify(error);
      addDebug(`[Error] ChatKit 错误: ${errorMsg}`);

      // 检测是否为网络连接错误或流式响应中断
      if (errorMsg.includes('ERR_CONNECTION_CLOSED') ||
          errorMsg.includes('ERR_NETWORK') ||
          errorMsg.includes('fetch failed') ||
          errorMsg.includes('stream') ||
          errorMsg.includes('conversation')) {
        addDebug('[Recovery] 检测到连接中断 (可能是长时间推理导致),ChatKit 会自动重连...');
        // ChatKit 内部有重试机制,不需要手动处理
        // 这种情况通常发生在复杂的 reasoning workflow 中
      } else {
        addDebug(`[Error] 未知错误类型,显示给用户: ${errorMsg}`);
        setError(`ChatKit 错误: ${error.message || '未知错误'}`);
      }
    },
  });

  // 使用 ref 标记是否已初始化,避免重复执行
  const isInitialized = useRef(false);
  const isReadyRef = useRef(false);

  // 同步 isReady 状态到 ref
  useEffect(() => {
    isReadyRef.current = isReady;
  }, [isReady]);

  useEffect(() => {
    if (isInitialized.current) return;
    isInitialized.current = true;

    const currentWorkflow = localStorage.getItem('chatkit_workflow_type') || 'simple';
    addDebug('[Init] 组件初始化...');
    addDebug(`[Config] 当前工作流: ${currentWorkflow === 'simple' ? '简单对话 (Simple)' : '复杂推理 (Reasoning)'}`);

    // 检查 ChatKit Web Component 是否已加载
    const checkChatKit = () => {
      if (customElements.get('openai-chatkit')) {
        addDebug('[Init] ChatKit Web Component 已就绪');
        setIsReady(true);
      } else {
        addDebug('[Init] 等待 ChatKit Web Component 加载...');
        customElements.whenDefined('openai-chatkit').then(() => {
          addDebug('[Init] ChatKit Web Component 已就绪');
          setIsReady(true);
        });
      }
    };

    checkChatKit();

    // 超时检测 - 使用 ref 获取最新的 isReady 状态
    const timeout = setTimeout(() => {
      if (!isReadyRef.current) {
        addDebug('[Error] ChatKit 加载超时');
        setError('ChatKit SDK 加载超时,请检查网络连接或CDN是否可访问');
      }
    }, 15000);

    return () => clearTimeout(timeout);
  }, [addDebug]); // 只依赖 addDebug,它是稳定的 useCallback

  // 拖拽调整宽度 - 使用 useCallback 避免不必要的重新渲染
  const handleMouseDown = useCallback((e) => {
    e.preventDefault();
    setIsDragging(true);
  }, []);

  const handleMouseMove = useCallback((e) => {
    const debugPanel = document.querySelector('.debug-panel');
    if (debugPanel) {
      const rect = debugPanel.getBoundingClientRect();
      const newWidth = e.clientX - rect.left;
      // 限制宽度在 300px - 1200px 之间
      const clampedWidth = Math.min(Math.max(newWidth, 300), 1200);
      setDebugWidth(clampedWidth);
    }
  }, []);

  const handleMouseUp = useCallback(() => {
    setIsDragging(false);
  }, []);

  useEffect(() => {
    if (isDragging) {
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      return () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
    }
  }, [isDragging, handleMouseMove, handleMouseUp]);

  return (
    <div className="app">
      <div className="header">
        <h1>OpenAI ChatKit Demo</h1>
        <p>基于 AgentKit Workflow 的智能对话演示</p>

        {/* 工作流选择器 */}
        <div className="workflow-selector">
          <label htmlFor="workflow-select">选择工作流:</label>
          <select
            id="workflow-select"
            value={workflowType}
            onChange={(e) => {
              const newWorkflow = e.target.value;
              setWorkflowType(newWorkflow);
              // 保存到 localStorage
              localStorage.setItem('chatkit_workflow_type', newWorkflow);
              addDebug(`[Config] 切换工作流: ${newWorkflow === 'simple' ? '简单对话' : '复杂推理'}`);
              addDebug('[Info] 已保存选择,点击刷新按钮以应用新工作流');
            }}
            className="workflow-select"
          >
            <option value="simple">简单对话 (Simple)</option>
            <option value="complex">复杂推理 (Reasoning)</option>
          </select>
          <button
            onClick={() => window.location.reload()}
            className="refresh-btn"
            title="重新加载以应用新工作流"
          >
            🔄 刷新
          </button>
        </div>
      </div>

      {/* 主内容区域 - 并排布局 */}
      <div className="main-content">
        {/* 调试信息面板 */}
        <div className="debug-panel" style={{ width: `${debugWidth}px` }}>
          <div className="debug-header">
            <strong>调试日志</strong>
            <span className={`status-badge ${isReady ? 'ready' : 'loading'}`}>
              {isReady ? '就绪' : '加载中'}
            </span>
          </div>
          <div className="debug-content">
            {debugInfo.map((msg, idx) => (
              <div key={idx} className="debug-line">{msg}</div>
            ))}
          </div>
        </div>

        {/* 拖拽分隔条 */}
        <div
          className={`resize-handle ${isDragging ? 'dragging' : ''}`}
          onMouseDown={handleMouseDown}
        >
          <div className="resize-handle-inner"></div>
        </div>

        {/* ChatKit 容器 */}
        <div className="chat-container" style={{ width: `${chatKitWidth}px` }}>
          {error ? (
            <div className="error-state">
              <h3>加载失败</h3>
              <p>{error}</p>
              <button onClick={() => window.location.reload()} className="reload-btn">
                重新加载
              </button>
            </div>
          ) : !isReady ? (
            <div className="loading-state">
              <div className="spinner"></div>
              <p>正在加载 ChatKit...</p>
            </div>
          ) : (
            <ChatKit control={control} className="chatkit-widget" />
          )}
        </div>
      </div>

      <div className="footer">
        <span>Powered by OpenAI AgentKit</span>
        <button
          className="style-toggle-btn"
          onClick={() => setShowStylePanel(!showStylePanel)}
        >
          {showStylePanel ? '隐藏' : '显示'}样式配置
        </button>
      </div>

      {/* 样式配置面板 */}
      {showStylePanel && (
        <div className="style-panel">
          <h3>ChatKit 样式配置</h3>

          <div className="style-group">
            <label>配色方案</label>
            <select
              value={themeConfig.colorScheme}
              onChange={(e) => setThemeConfig({...themeConfig, colorScheme: e.target.value})}
            >
              <option value="light">浅色</option>
              <option value="dark">深色</option>
            </select>
          </div>

          <div className="style-group">
            <label>主题色</label>
            <input
              type="color"
              value={themeConfig.accentColor}
              onChange={(e) => setThemeConfig({...themeConfig, accentColor: e.target.value})}
            />
            <span className="color-value">{themeConfig.accentColor}</span>
          </div>

          <div className="style-group">
            <label>颜色强度 (1-3)</label>
            <input
              type="range"
              min="1"
              max="3"
              value={themeConfig.accentLevel}
              onChange={(e) => setThemeConfig({...themeConfig, accentLevel: parseInt(e.target.value)})}
            />
            <span className="range-value">{themeConfig.accentLevel}</span>
          </div>

          <div className="style-group">
            <label>圆角风格</label>
            <select
              value={themeConfig.radius}
              onChange={(e) => setThemeConfig({...themeConfig, radius: e.target.value})}
            >
              <option value="round">圆角</option>
              <option value="square">直角</option>
            </select>
          </div>

          <div className="style-group">
            <label>UI 密度</label>
            <select
              value={themeConfig.density}
              onChange={(e) => setThemeConfig({...themeConfig, density: e.target.value})}
            >
              <option value="compact">紧凑</option>
              <option value="normal">正常</option>
            </select>
          </div>

          <div className="style-divider"></div>

          <div className="style-group">
            <label>调试面板宽度 ({debugWidth}px)</label>
            <input
              type="range"
              min="300"
              max="1200"
              step="10"
              value={debugWidth}
              onChange={(e) => setDebugWidth(parseInt(e.target.value))}
            />
            <span className="range-value">{debugWidth}px</span>
          </div>

          <div className="style-group">
            <label>ChatKit 宽度 ({chatKitWidth}px)</label>
            <input
              type="range"
              min="400"
              max="1500"
              step="10"
              value={chatKitWidth}
              onChange={(e) => setChatKitWidth(parseInt(e.target.value))}
            />
            <span className="range-value">{chatKitWidth}px</span>
          </div>

          <div className="style-note">
            提示: ChatKit 样式修改后需重新发送消息生效,布局宽度实时生效
          </div>
        </div>
      )}
    </div>
  );
}

export default App;

frontend/src/App.css

css
.app {
  width: 100%;
  padding: 20px;
}

.header {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 30px;
}

.header h1 {
  font-size: 2.2rem;
  margin-bottom: 8px;
  font-weight: 600;
  color: #1a202c;
}

.header p {
  font-size: 1rem;
  color: #64748b;
  font-weight: 400;
}

/* 工作流选择器 */
.workflow-selector {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 16px;
  padding: 12px 20px;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
  max-width: 500px;
  margin-left: auto;
  margin-right: auto;
}

.workflow-selector label {
  font-size: 0.875rem;
  color: #475569;
  font-weight: 500;
}

.workflow-select {
  padding: 6px 12px;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  font-size: 0.875rem;
  background: white;
  cursor: pointer;
  transition: border-color 0.2s;
  min-width: 200px;
}

.workflow-select:hover {
  border-color: #5B7FFF;
}

.workflow-select:focus {
  outline: none;
  border-color: #5B7FFF;
  box-shadow: 0 0 0 3px rgba(91, 127, 255, 0.1);
}

.refresh-btn {
  padding: 6px 12px;
  background: #5B7FFF;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.875rem;
  font-weight: 500;
  transition: background 0.2s;
}

.refresh-btn:hover {
  background: #4c6eef;
}

/* 主内容区域 - 并排布局 */
.main-content {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  min-height: 600px;
  position: relative;
}

/* 调试面板 */
.debug-panel {
  background: white;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.debug-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  background: #f8fafc;
  border-bottom: 1px solid #e2e8f0;
}

.debug-header strong {
  font-size: 0.875rem;
  color: #475569;
  font-weight: 600;
}

.status-badge {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 12px;
  font-weight: 500;
}

.status-badge.ready {
  background: #dcfce7;
  color: #166534;
}

.status-badge.loading {
  background: #fef3c7;
  color: #92400e;
}

.debug-content {
  padding: 12px 16px;
  flex: 1;
  overflow-y: auto;
  font-family: 'Courier New', 'Consolas', monospace;
  font-size: 0.8rem;
  color: #334155;
  background: #fafafa;
}

.debug-line {
  margin: 4px 0;
  line-height: 1.5;
  word-break: break-all;
}

/* 拖拽分隔条 */
.resize-handle {
  width: 10px;
  background: transparent;
  cursor: col-resize;
  position: relative;
  flex-shrink: 0;
  transition: background 0.2s;
  user-select: none;
}

.resize-handle:hover,
.resize-handle.dragging {
  background: #5B7FFF;
}

.resize-handle-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 2px;
  height: 40px;
  background: #94a3b8;
  border-radius: 1px;
}

.resize-handle:hover .resize-handle-inner,
.resize-handle.dragging .resize-handle-inner {
  background: white;
}

/* 聊天容器 */
.chat-container {
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  overflow: hidden;
  border: 1px solid #e2e8f0;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.chatkit-widget {
  width: 100%;
  height: 100%;
  flex: 1;
}

/* 错误状态 */
.error-state {
  padding: 60px 40px;
  text-align: center;
  color: #dc2626;
}

.error-state h3 {
  font-size: 1.25rem;
  margin-bottom: 12px;
  font-weight: 600;
}

.error-state p {
  color: #64748b;
  margin-bottom: 24px;
  font-size: 0.95rem;
}

.reload-btn {
  padding: 10px 24px;
  background: #5B7FFF;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 500;
  transition: background 0.2s;
}

.reload-btn:hover {
  background: #4c6eef;
}

/* 加载状态 */
.loading-state {
  padding: 60px 40px;
  text-align: center;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid #e2e8f0;
  border-top: 3px solid #5B7FFF;
  border-radius: 50%;
  margin: 0 auto 20px;
  animation: spin 1s linear infinite;
}

.loading-state p {
  color: #64748b;
  font-size: 0.95rem;
}

/* 页脚 */
.footer {
  text-align: center;
  padding: 20px;
  color: #94a3b8;
  font-size: 0.875rem;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 20px;
}

.style-toggle-btn {
  padding: 8px 16px;
  background: white;
  color: #5B7FFF;
  border: 1px solid #5B7FFF;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.875rem;
  font-weight: 500;
  transition: all 0.2s;
}

.style-toggle-btn:hover {
  background: #5B7FFF;
  color: white;
}

/* 样式配置面板 */
.style-panel {
  position: fixed;
  right: 20px;
  bottom: 80px;
  width: 320px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  border: 1px solid #e2e8f0;
  padding: 20px;
  z-index: 1000;
}

.style-panel h3 {
  margin: 0 0 20px 0;
  font-size: 1.1rem;
  color: #1a202c;
  font-weight: 600;
}

.style-group {
  margin-bottom: 16px;
}

.style-group label {
  display: block;
  margin-bottom: 8px;
  font-size: 0.875rem;
  color: #475569;
  font-weight: 500;
}

.style-group select,
.style-group input[type="color"] {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  font-size: 0.875rem;
  background: white;
  cursor: pointer;
}

.style-group input[type="color"] {
  height: 40px;
  padding: 4px;
}

.style-group input[type="range"] {
  width: calc(100% - 40px);
  margin-right: 10px;
}

.color-value,
.range-value {
  display: inline-block;
  margin-left: 10px;
  font-size: 0.875rem;
  color: #64748b;
  font-family: monospace;
}

.style-divider {
  height: 1px;
  background: #e2e8f0;
  margin: 16px 0;
}

.style-note {
  margin-top: 16px;
  padding: 12px;
  background: #fef3c7;
  border-radius: 6px;
  font-size: 0.8rem;
  color: #92400e;
  line-height: 1.5;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* 响应式设计 */
@media (max-width: 968px) {
  .header h1 {
    font-size: 1.8rem;
  }

  /* 移动端切换为垂直布局 */
  .main-content {
    flex-direction: column;
    gap: 16px;
  }

  .debug-panel {
    width: 100% !important;
    border-radius: 8px;
    border: 1px solid #e2e8f0;
    max-height: 200px;
  }

  .resize-handle {
    display: none;
  }

  .chat-container {
    width: 100% !important;
    border-radius: 8px;
    border: 1px solid #e2e8f0;
    min-height: 500px;
  }
}

frontend/src/index.css

css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  background: #f5f7fa;
  min-height: 100vh;
  margin: 0;
  padding: 0;
}

#root {
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

部署与调试建议

  • 本地开发: 启动顺序为 uvicorn main:app --reloadnpm run dev。Vite 通过代理转发 /api 调用,避免手动配置跨域。
  • 生产部署: 将前端打包 (npm run build) 后由 FastAPI StaticFiles 或 Nginx 托管,同时用 systemdsupervisor 常驻后端进程。
  • 日志观察: 前端调试面板&后端 logging 会同时记录 Session ID,便于排查失败场景;如需更严格脱敏可增加自定义过滤器。
  • 工作流管理: 在 OpenAI 平台发布新版本后更新 WORKFLOW_ID 或直接在前端切换,以验证不同推理配置对业务指标的影响。
  • 安全加固: 生产环境务必限制 CORS 来源,并将 DOMAIN_KEY 注入部署域名,阻止未授权站点加载 ChatKit。

使用以上代码即可在 Heliki 社区文档中完整呈现 AgentKit ChatKit Demo,帮助读者快速上手官方推荐的集成方案。