引言
OpenClaw 提供了功能完善的 Control UI(Web 控制面板),但在实际业务场景中,开发者往往需要将 AI 对话能力集成到自己的应用或系统中——比如企业内部的客服页面、CRM 系统的悬浮客服窗、或是特定业务流程的对话界面。
本文将深入讲解如何开发自定义前端页面,通过 WebSocket 协议直接与 OpenClaw Gateway 通信,实现与 AI Agent 的实时对话。
一、OpenClaw Gateway 架构回顾
在开始开发前,需要理解 OpenClaw 的核心架构:
Gateway:OpenClaw 的核心守护进程,负责管理所有通讯渠道和 AI Agent WebSocket API:所有客户端(包括 Control UI、CLI、macOS app)都通过 WebSocket 与 Gateway 通信 默认端口: 18789绑定地址:默认 127.0.0.1(loopback),可配置
[前端应用] <--WebSocket--> [Gateway :18789] <--> [AI Model]
|
[WhatsApp/Telegram/Discord...]
二、WebSocket 协议详解
2.1 协议格式
OpenClaw 使用 JSON 文本帧作为 WebSocket 通信格式。消息分为三类:
req | ||
res | ||
event |
2.2 连接握手
WebSocket 连接建立后,第一帧必须是 connect 请求:
{
"type": "req",
"id": "conn-001",
"method": "connect",
"params": {
"hello": {
"version": "1.0",
"agentId": "main",
"platform": "web"
},
"auth": {
"token": "your-gateway-token"
}
}
}
响应:
{
"type": "res",
"id": "conn-001",
"ok": true,
"payload": {
"hello": "ok",
"snapshot": {
"presence": [...],
"health": {...}
}
}
}
2.3 核心 API 方法
与对话相关的核心方法:
chat.history | |
chat.send | |
chat.abort | |
chat.inject |
三、自定义前端开发实战
3.1 项目结构
my-openclaw-chat/
├── index.html
├── styles.css
├── app.js
└── package.json (可选)
3.2 核心代码实现
第一步:建立 WebSocket 连接
class OpenClawClient {
constructor(options = {}) {
this.url = options.url || 'ws://127.0.0.1:18789';
this.token = options.token || '';
this.agentId = options.agentId || 'main';
this.ws = null;
this.requestId = 0;
this.handlers = new Map();
this.sessionKey = options.sessionKey || null;
}
// 建立连接
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
// 发送 connect 握手
this.sendRequest('connect', {
hello: {
version: '1.0',
agentId: this.agentId,
platform: 'web'
},
auth: {
token: this.token
}
}).then(resolve).catch(reject);
};
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
};
});
}
// 发送请求
sendRequest(method, params = {}) {
return new Promise((resolve, reject) => {
const id = `req-${++this.requestId}`;
this.handlers.set(id, { resolve, reject });
this.ws.send(JSON.stringify({
type: 'req',
id,
method,
params
}));
});
}
// 处理消息
handleMessage(message) {
if (message.type === 'res' && message.id) {
const handler = this.handlers.get(message.id);
if (handler) {
if (message.ok) {
handler.resolve(message.payload);
} else {
handler.reject(new Error(message.error || 'Unknown error'));
}
this.handlers.delete(message.id);
}
} else if (message.type === 'event') {
this.handleEvent(message);
}
}
// 处理事件
handleEvent(event) {
console.log('Event:', event.event, event.payload);
// 根据事件类型处理
}
}
第二步:获取会话历史
// 获取聊天历史
async getChatHistory(maxChars = 10000) {
const response = await this.sendRequest('chat.history', {
sessionKey: this.sessionKey,
maxChars
});
return response.messages || [];
}
响应数据结构:
{
"messages": [
{
"role": "user",
"content": "你好,请介绍一下自己"
},
{
"role": "assistant",
"content": "你好!我是..."
}
],
"sessionKey": "agent:main:web:abc123"
}
第三步:发送消息
// 发送消息(流式响应)
async sendMessage(content, onChunk) {
const idempotencyKey = `msg-${Date.now()}-${Math.random()}`;
// 发送请求
const response = await this.sendRequest('chat.send', {
sessionKey: this.sessionKey,
message: {
role: 'user',
content
},
idempotencyKey
});
// 返回 runId 用于后续处理
return {
runId: response.runId,
status: response.status// "started"
};
}
第四步:监听流式响应
// 在 handleEvent 中处理 agent 事件
handleEvent(event) {
if (event.event === 'agent') {
const { runId, state, summary, content } = event.payload;
switch (state) {
case 'streaming':
// 流式输出片段
if (onChunk) onChunk(content);
break;
case 'done':
// 完成
console.log('Agent response done:', summary);
break;
case 'error':
console.error('Agent error:', event.payload.error);
break;
}
} else if (event.event === 'chat') {
// 聊天消息事件
console.log('Chat event:', event.payload);
}
}
3.3 完整前端示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenClaw 对话</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px; margin: 0 auto; padding: 20px;
background: #f5f5f5;
}
#chat-container {
background: white; border-radius: 12px; overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#messages {
height: 500px; overflow-y: auto; padding: 20px;
}
.message {
margin-bottom: 16px; padding: 12px 16px; border-radius: 8px;
max-width: 80%;
}
.message.user {
background: #007AFF; color: white; margin-left: auto;
}
.message.assistant {
background: #E9E9EB; color: black;
}
.message.tool-call {
background: #FFF3CD; border: 1px solid #FFEEBA;
font-size: 12px; font-family: monospace;
}
#input-area {
display: flex; padding: 16px; border-top: 1px solid #eee;
gap: 12px;
}
#message-input {
flex: 1; padding: 12px; border: 1px solid #ddd;
border-radius: 8px; font-size: 16px;
}
#send-btn {
padding: 12px 24px; background: #007AFF; color: white;
border: none; border-radius: 8px; cursor: pointer; font-size: 16px;
}
#send-btn:disabled { background: #ccc; }
.typing { color: #666; font-style: italic; }
</style>
</head>
<body>
<h1>🤖 OpenClaw AI 对话</h1>
<div id="chat-container">
<div id="messages"></div>
<div id="input-area">
<input type="text" id="message-input" placeholder="输入消息..." />
<button id="send-btn">发送</button>
</div>
</div>
<script>
// OpenClaw 客户端类(简化版)
class OpenClawChat {
constructor(token, sessionKey = null) {
this.url = 'ws://127.0.0.1:18789';
this.token = token;
this.sessionKey = sessionKey;
this.ws = null;
this.requestId = 0;
this.handlers = {};
this.onMessageCallback = null;
this.onToolCallCallback = null;
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.sendRequest('connect', {
hello: { version: '1.0', agentId: 'main', platform: 'web' },
auth: { token: this.token }
}).then(resolve).catch(reject);
};
this.ws.onmessage = (e) => this.handleMessage(JSON.parse(e.data));
this.ws.onerror = reject;
this.ws.onclose = () => console.log('Disconnected');
});
}
sendRequest(method, params) {
return new Promise((resolve, reject) => {
const id = `req-${++this.requestId}`;
this.handlers[id] = { resolve, reject };
this.ws.send(JSON.stringify({ type: 'req', id, method, params }));
});
}
handleMessage(msg) {
if (msg.type === 'res' && this.handlers[msg.id]) {
const h = this.handlers[msg.id];
msg.ok ? h.resolve(msg.payload) : h.reject(new Error(msg.error));
delete this.handlers[msg.id];
} else if (msg.type === 'event') {
if (msg.event === 'agent') this.handleAgentEvent(msg.payload);
else if (msg.event === 'chat') this.onMessageCallback?.(msg.payload);
}
}
handleAgentEvent(payload) {
if (payload.state === 'streaming') {
this.onChunkCallback?.(payload.content);
}
}
async sendMessage(content) {
const key = `msg-${Date.now()}`;
return this.sendRequest('chat.send', {
sessionKey: this.sessionKey,
message: { role: 'user', content },
idempotencyKey: key
});
}
async getHistory() {
return this.sendRequest('chat.history', { sessionKey: this.sessionKey });
}
}
// === 应用逻辑 ===
const TOKEN = 'your-gateway-token-here'; // 替换为你的 Token
const chat = new OpenClawChat(TOKEN);
let currentAssistantMessage = null;
// UI 元素
const messagesDiv = document.getElementById('messages');
const input = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
// 添加消息到界面
function addMessage(role, content, isHtml = false) {
const div = document.createElement('div');
div.className = `message ${role}`;
if (isHtml) div.innerHTML = content;
else div.textContent = content;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return div;
}
// 发送消息
async function send() {
const content = input.value.trim();
if (!content) return;
input.value = '';
sendBtn.disabled = true;
// 显示用户消息
addMessage('user', content);
// 显示 AI 响应占位
currentAssistantMessage = addMessage('assistant', '正在思考...');
try {
let assistantContent = '';
// 设置流式回调
chat.onChunkCallback = (chunk) => {
assistantContent += chunk;
currentAssistantMessage.textContent = assistantContent;
};
// 发送消息
await chat.sendMessage(content);
} catch (error) {
currentAssistantMessage.textContent = '错误: ' + error.message;
} finally {
sendBtn.disabled = false;
chat.onChunkCallback = null;
}
}
// 初始化
async function init() {
try {
await chat.connect();
console.log('Connected to OpenClaw');
// 获取历史记录
const history = await chat.getHistory();
if (history.messages) {
history.messages.forEach(msg => {
addMessage(msg.role, msg.content);
});
}
// 绑定事件
sendBtn.onclick = send;
input.onkeypress = (e) => { if (e.key === 'Enter') send(); };
} catch (error) {
addMessage('assistant', '连接失败: ' + error.message);
}
}
init();
</script>
</body>
</html>
四、设备配对与认证处理
4.1 配对机制
首次连接时,如果设备未配对,会收到错误:
{
"type": "res",
"ok": false,
"error": "pairing required"
}
解决方案:
# 1. 查看待配对请求
openclaw devices list
# 2. 审批设备
openclaw devices approve <requestId>
4.2 本地信任
本地连接(127.0.0.1 或 localhost)自动信任,无需配对。
4.3 Token 获取
# 获取当前 Gateway Token
openclaw config get gateway.auth.token
五、远程访问方案
如果前端部署在不同于 Gateway 的服务器,需要通过 SSH 隧道或 Tailscale 访问。
5.1 SSH 隧道方案
# 在前端服务器上建立隧道
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host
前端连接地址不变:ws://127.0.0.1:18789
5.2 Tailscale 方案
# 在 Gateway 主机上启用 Tailscale Serve
openclaw gateway --tailscale serve
前端连接地址:ws://<magicdns>:18789
六、安全最佳实践
6.1 认证安全
6.2 配置建议
生产环境建议配置 gateway.trustedProxies:
{
"gateway": {
"auth": {
"mode": "token",
"token": "your-secure-token"
}
}
}
6.3 前端安全
不要在前端代码中硬编码 Token,建议通过后端代理转发 使用环境变量管理敏感配置 生产环境考虑部署后端 API 代理
七、进阶功能
7.1 工具调用展示
Agent 可能调用工具(搜索、执行命令等),前端应展示:
chat.onToolCallCallback = (toolCall) => {
addMessage('tool-call', `🔧 调用工具: ${toolCall.name}\n${JSON.stringify(toolCall.arguments)}`);
};
7.2 会话管理
// 创建新会话
const newSession = await chat.sendRequest('sessions.create', {
agentId: 'main',
mainKey: 'web'// 标识来源
});
// 切换会话
chat.sessionKey = newSession.sessionKey;
7.3 中断响应
// 中止正在进行的响应
await chat.sendRequest('chat.abort', {
sessionKey: chat.sessionKey
});
八、常见问题排查
openclaw gateway | ||
gateway.auth.token | ||
openclaw devices approve | ||
结语
通过 WebSocket API,开发者可以完全脱离 Control UI,构建自定义的前端对话界面。核心要点总结:
协议层:基于 JSON 的 WebSocket 文本帧协议 认证:Token + 设备配对机制 核心方法: chat.send(发送)、chat.history(历史)、chat.abort(中断)远程访问:SSH 隧道或 Tailscale 安全:生产环境使用后端代理转发,避免前端暴露 Token
OpenClaw 的 WebSocket API 为企业提供了灵活的集成能力——无论是简单的客服对话框,还是复杂的 AI 驱动的业务流程,都可以基于此构建。
参考资料:OpenClaw 官方文档 https://docs.openclaw.ai
夜雨聆风