乐于分享
好东西不私藏

自定义前端页面与 OpenClaw 对话:技术实战指南

自定义前端页面与 OpenClaw 对话:技术实战指南

引言

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
客户端 → Gateway
请求消息
res
Gateway → 客户端
响应消息
event
Gateway → 客户端
服务端推送事件

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
发送消息并触发 Agent 响应
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',
agentIdthis.agentId,
platform'web'
          },
auth: {
tokenthis.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', {
sessionKeythis.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', {
sessionKeythis.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-width800pxmargin0 auto; padding20px;
background#f5f5f5;
    }
#chat-container {
background: white; border-radius12pxoverflow: hidden;
box-shadow2px 8px rgba(0,0,0,0.1);
    }
#messages {
height500pxoverflow-y: auto; padding20px;
    }
.message {
margin-bottom16pxpadding12px 16pxborder-radius8px;
max-width80%;
    }
.message.user {
background#007AFFcolor: white; margin-left: auto;
    }
.message.assistant {
background#E9E9EBcolor: black;
    }
.message.tool-call {
background#FFF3CDborder1px solid #FFEEBA;
font-size12pxfont-family: monospace;
    }
#input-area {
display: flex; padding16pxborder-top1px solid #eee;
gap12px;
    }
#message-input {
flex1padding12pxborder1px solid #ddd;
border-radius8pxfont-size16px;
    }
#send-btn {
padding12px 24pxbackground#007AFFcolor: white;
border: none; border-radius8pxcursor: pointer; font-size16px;
    }
#send-btn:disabled { background#ccc; }
.typing { color#666font-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: { tokenthis.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', {
sessionKeythis.sessionKey,
message: { role'user', content },
idempotencyKey: key
        });
      }

async getHistory() {
return this.sendRequest('chat.history', { sessionKeythis.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 认证安全

风险
防护措施
Token 泄露
最小化 Token 分发范围,定期轮换
中间人攻击
远程访问使用 SSH 隧道或 HTTPS
未授权访问
启用设备配对机制,配置白名单

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
});

八、常见问题排查

问题
可能原因
解决方案
连接失败
Gateway 未运行
启动 openclaw gateway
认证失败
Token 错误
检查 gateway.auth.token
pairing required
设备未配对
执行 openclaw devices approve
消息发送失败
sessionKey 无效
检查会话是否正确初始化
远程连接超时
防火墙/隧道未建立
检查 SSH 隧道或 Tailscale 状态

结语

通过 WebSocket API,开发者可以完全脱离 Control UI,构建自定义的前端对话界面。核心要点总结:

  1. 协议层:基于 JSON 的 WebSocket 文本帧协议
  2. 认证:Token + 设备配对机制
  3. 核心方法chat.send(发送)、chat.history(历史)、chat.abort(中断)
  4. 远程访问:SSH 隧道或 Tailscale
  5. 安全:生产环境使用后端代理转发,避免前端暴露 Token

OpenClaw 的 WebSocket API 为企业提供了灵活的集成能力——无论是简单的客服对话框,还是复杂的 AI 驱动的业务流程,都可以基于此构建。


参考资料:OpenClaw 官方文档 https://docs.openclaw.ai

中小企业基于 OpenClaw 二次开发内部系统的可行性分析
不会写需求文档?让AI帮你搞定!手把手教你用AI写PRD,再交给AI开发
2026年OpenCode完全入门指南:从零开始用AI写代码