Appearance
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 过期后前端自动刷新;日志中对敏感字段做遮罩,支持通过环境变量注入代理与自定义工作流。

端到端流程
- 用户在前端点击开始对话时,
@openai/chatkit-react会调用getClientSecret。 - 前端向 FastAPI 的
/api/chatkit/session?workflow_type=xxx发起请求。 - 服务端调用 OpenAI ChatKit 会话 API,返回
client_secret与session_id。 - ChatKit 组件利用
client_secret建立与 OpenAI 的安全长连接,加载对应工作流。 - 如会话超时或网络中断,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 --reload与npm run dev。Vite 通过代理转发/api调用,避免手动配置跨域。 - 生产部署: 将前端打包 (
npm run build) 后由 FastAPIStaticFiles或 Nginx 托管,同时用systemd或supervisor常驻后端进程。 - 日志观察: 前端调试面板&后端
logging会同时记录 Session ID,便于排查失败场景;如需更严格脱敏可增加自定义过滤器。 - 工作流管理: 在 OpenAI 平台发布新版本后更新
WORKFLOW_ID或直接在前端切换,以验证不同推理配置对业务指标的影响。 - 安全加固: 生产环境务必限制 CORS 来源,并将
DOMAIN_KEY注入部署域名,阻止未授权站点加载 ChatKit。
使用以上代码即可在 Heliki 社区文档中完整呈现 AgentKit ChatKit Demo,帮助读者快速上手官方推荐的集成方案。
