openclaw-studio-dashboard
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOpenClaw Studio Dashboard
OpenClaw Studio 仪表板
Skill by ara.so — Hermes Skills collection.
OpenClaw Studio is a clean web dashboard for OpenClaw Gateway that provides a unified interface to connect gateways, manage agents, chat, handle approvals, and configure jobs. Built with TypeScript/Next.js, it runs a server-owned control plane architecture with SSE streaming for real-time runtime events.
由ara.so提供的Skill——Hermes Skills合集。
OpenClaw Studio是一款为OpenClaw Gateway打造的简洁Web仪表板,提供统一界面用于连接网关、管理Agent、聊天、处理审批以及配置任务。它基于TypeScript/Next.js构建,采用服务器托管的控制平面架构,通过SSE流实现实时运行时事件。
What OpenClaw Studio Does
OpenClaw Studio 功能介绍
- Gateway Connection: Connect to local or remote OpenClaw Gateway instances via WebSocket
- Agent Management: Create, configure, and monitor AI agents with tool policies and sandbox settings
- Chat Interface: Stream conversations with PI agents including tool calls and thinking traces
- Approval Workflows: Manage exec approvals and runtime permissions
- Job Configuration: Set up and monitor cron jobs
- Runtime Streaming: Real-time event streaming over SSE with replay/history
- 网关连接:通过WebSocket连接本地或远程OpenClaw Gateway实例
- Agent管理:创建、配置并监控带有工具策略和沙箱设置的AI Agent
- 聊天界面:与AI Agent进行流式对话,包含工具调用和思考轨迹
- 审批工作流:管理执行审批和运行时权限
- 任务配置:设置并监控定时任务
- 运行时流:通过SSE实现实时事件流,支持回放/历史记录
Installation & Startup
安装与启动
Quick Start (Recommended)
快速开始(推荐)
bash
undefinedbash
undefinedRun latest version with npx
使用npx运行最新版本
npx -y openclaw-studio@latest
npx -y openclaw-studio@latest
Opens on http://localhost:3000
Default gateway URL: ws://localhost:18789
默认网关URL:ws://localhost:18789
undefinedundefinedFrom Source
从源码安装
bash
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run devbash
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run devSetup Helper
设置助手
bash
undefinedbash
undefinedConfigure gateway URL/token without opening UI
无需打开UI即可配置网关URL/令牌
npm run studio:setup
undefinednpm run studio:setup
undefinedConnection Architecture
连接架构
OpenClaw Studio uses a two-path architecture:
- Browser → Studio: HTTP + SSE (,
/api/runtime/*)/api/intents/* - Studio → Gateway: Server-owned WebSocket to upstream OpenClaw
Critical concept: means "gateway on the Studio host", not "gateway on your browser's machine".
ws://localhost:18789OpenClaw Studio采用双路径架构:
- 浏览器 → Studio:HTTP + SSE(,
/api/runtime/*)/api/intents/* - Studio → Gateway:服务器托管的WebSocket连接至上游OpenClaw
核心概念:表示“Studio所在主机上的网关”,而非“浏览器所在机器上的网关”。
ws://localhost:18789Configuration Patterns
配置模式
Deployment Scenarios
部署场景
A. Both Local (Same Computer)
A. 本地部署(同一计算机)
bash
undefinedbash
undefinedStart Studio
启动Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm run dev
npx -y openclaw-studio@latest
cd openclaw-studio
npm run dev
In Studio UI:
在Studio UI中:
- Upstream URL: ws://localhost:18789
- 上游URL:ws://localhost:18789
- Upstream Token: <your-gateway-token>
- 上游令牌:<your-gateway-token>
Get gateway token:
```bash
openclaw config get gateway.auth.token
获取网关令牌:
```bash
openclaw config get gateway.auth.tokenB. Gateway in Cloud, Studio Local
B. 网关云端部署,Studio本地部署
Option 1: Tailscale Serve (Recommended)
On gateway host:
bash
tailscale serve --yes --bg --https 443 http://127.0.0.1:18789In Studio (local laptop):
- Upstream URL:
wss://<gateway-host>.ts.net - Upstream Token:
<gateway-token>
Option 2: SSH Tunnel
From laptop:
bash
ssh -L 18789:127.0.0.1:18789 user@<gateway-host>In Studio:
- Upstream URL:
ws://localhost:18789 - Upstream Token:
<gateway-token>
选项1:Tailscale Serve(推荐)
在网关主机上:
bash
tailscale serve --yes --bg --https 443 http://127.0.0.1:18789在本地Studio(笔记本)中:
- 上游URL:
wss://<gateway-host>.ts.net - 上游令牌:
<gateway-token>
选项2:SSH隧道
从笔记本执行:
bash
ssh -L 18789:127.0.0.1:18789 user@<gateway-host>在Studio中:
- 上游URL:
ws://localhost:18789 - 上游令牌:
<gateway-token>
C. Both in Cloud (Always-On)
C. 云端部署(持续运行)
On Studio VPS:
bash
undefined在Studio VPS上:
bash
undefinedStart Studio
启动Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run dev
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run dev
If OpenClaw is on same VPS:
如果OpenClaw在同一VPS上:
- Upstream URL: ws://localhost:18789
- 上游URL:ws://localhost:18789
- Upstream Token: <gateway-token>
- 上游令牌:<gateway-token>
Expose Studio over Tailscale
通过Tailscale暴露Studio
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
Access from anywhere: https://<studio-host>.ts.net
从任意位置访问:https://<studio-host>.ts.net
undefinedundefinedEnvironment Variables
环境变量
bash
undefinedbash
undefinedGateway connection (optional, can set in UI)
网关连接(可选,可在UI中设置)
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
Studio access control (required for public binds)
Studio访问控制(公开绑定必填)
STUDIO_ACCESS_TOKEN=your-secure-token-here
STUDIO_ACCESS_TOKEN=your-secure-token-here
OpenClaw state directory
OpenClaw状态目录
OPENCLAW_STATE_DIR=~/.openclaw
OPENCLAW_STATE_DIR=~/.openclaw
Bind host (default: 127.0.0.1)
绑定主机(默认:127.0.0.1)
HOST=0.0.0.0 # Public bind - requires STUDIO_ACCESS_TOKEN
undefinedHOST=0.0.0.0 # 公开绑定 - 需要STUDIO_ACCESS_TOKEN
undefinedConfiguration Files
配置文件
Studio settings are stored in :
~/.openclaw/openclaw-studio/~/.openclaw/
├── openclaw.json # OpenClaw Gateway config
└── openclaw-studio/
├── settings.json # Gateway URL/token
└── runtime.db # Control-plane runtime DBStudio设置存储在目录下:
~/.openclaw/openclaw-studio/~/.openclaw/
├── openclaw.json # OpenClaw Gateway配置
└── openclaw-studio/
├── settings.json # 网关URL/令牌
└── runtime.db # 控制平面运行时数据库API & Key Commands
API与关键命令
Studio Setup
Studio设置
bash
undefinedbash
undefinedDevelopment mode with auto-restart
开发模式(自动重启)
npm run dev
npm run dev
Production build
生产构建
npm run build
npm run start
npm run build
npm run start
Turbo dev mode
Turbo开发模式
npm run dev:turbo
npm run dev:turbo
Verify/repair native dependencies
验证/修复原生依赖
npm run verify:native-runtime:repair
npm run verify:native-runtime:check
undefinednpm run verify:native-runtime:repair
npm run verify:native-runtime:check
undefinedTypeScript API Patterns
TypeScript API模式
Connecting to Gateway
连接至网关
typescript
// In Studio UI components
import { useGatewayConnection } from '@/hooks/useGatewayConnection';
export function DashboardPage() {
const { connected, connect, disconnect, status } = useGatewayConnection();
const handleConnect = async () => {
try {
await connect({
url: 'ws://localhost:18789',
token: process.env.GATEWAY_TOKEN
});
} catch (error) {
console.error('Connection failed:', error);
}
};
return (
<div>
<button onClick={handleConnect} disabled={connected}>
{connected ? 'Connected' : 'Connect'}
</button>
<span>Status: {status}</span>
</div>
);
}typescript
// 在Studio UI组件中
import { useGatewayConnection } from '@/hooks/useGatewayConnection';
export function DashboardPage() {
const { connected, connect, disconnect, status } = useGatewayConnection();
const handleConnect = async () => {
try {
await connect({
url: 'ws://localhost:18789',
token: process.env.GATEWAY_TOKEN
});
} catch (error) {
console.error('连接失败:', error);
}
};
return (
<div>
<button onClick={handleConnect} disabled={connected}>
{connected ? '已连接' : '连接'}
</button>
<span>状态: {status}</span>
</div>
);
}Streaming Runtime Events
流式运行时事件
typescript
// Server-side SSE streaming
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Subscribe to runtime events
const subscription = runtimeStore.subscribe((event) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
});
// Replay history
const history = await runtimeStore.getHistory();
for (const event of history) {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
}
return () => subscription.unsubscribe();
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}typescript
// 服务器端SSE流
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 订阅运行时事件
const subscription = runtimeStore.subscribe((event) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
});
// 回放历史记录
const history = await runtimeStore.getHistory();
for (const event of history) {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
}
return () => subscription.unsubscribe();
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}Creating Agents
创建Agent
typescript
// Agent creation API
interface AgentConfig {
name: string;
systemPrompt: string;
toolPolicy: 'all' | 'allowlist' | 'denylist';
allowedTools?: string[];
sandboxEnabled: boolean;
execApprovalRequired: boolean;
}
export async function createAgent(config: AgentConfig) {
const response = await fetch('/api/intents/agent-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_name: config.name,
system_prompt: config.systemPrompt,
tool_policy: config.toolPolicy,
allowed_tools: config.allowedTools,
sandbox_config: {
enabled: config.sandboxEnabled,
mounts: config.sandboxEnabled ? [
{ host: '/tmp', container: '/workspace', readonly: false }
] : []
},
exec_approval_required: config.execApprovalRequired
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create agent');
}
return response.json();
}typescript
// Agent创建API
interface AgentConfig {
name: string;
systemPrompt: string;
toolPolicy: 'all' | 'allowlist' | 'denylist';
allowedTools?: string[];
sandboxEnabled: boolean;
execApprovalRequired: boolean;
}
export async function createAgent(config: AgentConfig) {
const response = await fetch('/api/intents/agent-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_name: config.name,
system_prompt: config.systemPrompt,
tool_policy: config.toolPolicy,
allowed_tools: config.allowedTools,
sandbox_config: {
enabled: config.sandboxEnabled,
mounts: config.sandboxEnabled ? [
{ host: '/tmp', container: '/workspace', readonly: false }
] : []
},
exec_approval_required: config.execApprovalRequired
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || '创建Agent失败');
}
return response.json();
}Chat with Streaming
流式聊天
typescript
// Client-side chat with SSE
import { useEffect, useState } from 'react';
export function ChatInterface({ agentId }: { agentId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [thinking, setThinking] = useState<string[]>([]);
useEffect(() => {
// Connect to runtime stream
const eventSource = new EventSource('/api/runtime/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'tool_call':
setMessages(prev => [...prev, {
role: 'assistant',
content: `Using tool: ${data.tool_name}`,
metadata: { type: 'tool', ...data }
}]);
break;
case 'thinking':
setThinking(prev => [...prev, data.content]);
break;
case 'transcript':
setMessages(prev => [...prev, {
role: data.role,
content: data.content
}]);
setThinking([]);
break;
}
});
return () => eventSource.close();
}, [agentId]);
const sendMessage = async (content: string) => {
await fetch('/api/intents/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
message: content
})
});
};
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{thinking.length > 0 && (
<div className="thinking">
{thinking.map((t, i) => <p key={i}>{t}</p>)}
</div>
)}
<input onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}} />
</div>
);
}typescript
// 客户端流式聊天(基于SSE)
import { useEffect, useState } from 'react';
export function ChatInterface({ agentId }: { agentId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [thinking, setThinking] = useState<string[]>([]);
useEffect(() => {
// 连接至运行时流
const eventSource = new EventSource('/api/runtime/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'tool_call':
setMessages(prev => [...prev, {
role: 'assistant',
content: `使用工具: ${data.tool_name}`,
metadata: { type: 'tool', ...data }
}]);
break;
case 'thinking':
setThinking(prev => [...prev, data.content]);
break;
case 'transcript':
setMessages(prev => [...prev, {
role: data.role,
content: data.content
}]);
setThinking([]);
break;
}
});
return () => eventSource.close();
}, [agentId]);
const sendMessage = async (content: string) => {
await fetch('/api/intents/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
message: content
})
});
};
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{thinking.length > 0 && (
<div className="thinking">
{thinking.map((t, i) => <p key={i}>{t}</p>)}
</div>
)}
<input onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}} />
</div>
);
}Real-World Examples
实际应用示例
Example 1: Local Development Setup
示例1:本地开发环境设置
bash
#!/bin/bashbash
#!/bin/bashsetup-local-dev.sh
setup-local-dev.sh
Start OpenClaw Gateway (in separate terminal)
启动OpenClaw Gateway(在单独终端中)
openclaw gateway start
openclaw gateway start
Get gateway token
获取网关令牌
GATEWAY_TOKEN=$(openclaw config get gateway.auth.token)
GATEWAY_TOKEN=$(openclaw config get gateway.auth.token)
Start Studio
启动Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npx -y openclaw-studio@latest
cd openclaw-studio
Configure via environment
通过环境变量配置
export NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
npm run dev
export NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
npm run dev
Enter gateway token in UI
在UI中输入网关令牌
undefinedundefinedExample 2: Production Cloud Setup
示例2:云端生产环境部署
bash
#!/bin/bashbash
#!/bin/bashdeploy-studio-cloud.sh
deploy-studio-cloud.sh
On your VPS running OpenClaw Gateway
在运行OpenClaw Gateway的VPS上
cd /opt
npx -y openclaw-studio@latest
cd openclaw-studio
cd /opt
npx -y openclaw-studio@latest
cd openclaw-studio
Install dependencies
安装依赖
npm install
npm install
Build for production
生产构建
npm run build
npm run build
Set access token for public access
设置访问令牌(公开访问必填)
export STUDIO_ACCESS_TOKEN=$(openssl rand -hex 32)
export STUDIO_ACCESS_TOKEN=$(openssl rand -hex 32)
Expose over Tailscale
通过Tailscale暴露服务
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
Run production server
启动生产服务器
npm run start
echo "Studio available at: https://$(tailscale status --json | jq -r '.Self.DNSName')/"
echo "Access token: $STUDIO_ACCESS_TOKEN"
echo "Use: https://your-host.ts.net/?access_token=$STUDIO_ACCESS_TOKEN"
undefinednpm run start
echo "Studio访问地址: https://$(tailscale status --json | jq -r '.Self.DNSName')/"
echo "访问令牌: $STUDIO_ACCESS_TOKEN"
echo "访问链接: https://your-host.ts.net/?access_token=$STUDIO_ACCESS_TOKEN"
undefinedExample 3: Agent with Sandbox & Approvals
示例3:带沙箱与审批的Agent
typescript
// create-secure-agent.ts
import { createAgent } from './lib/agent-api';
async function createSecureCodeAgent() {
const agent = await createAgent({
name: 'secure-code-assistant',
systemPrompt: `You are a secure code assistant.
Always ask before executing commands.
Work within the sandboxed environment only.`,
toolPolicy: 'allowlist',
allowedTools: [
'bash',
'read_file',
'write_file',
'search_files'
],
sandboxEnabled: true,
execApprovalRequired: true
});
console.log('Agent created:', agent.id);
return agent;
}
// Usage
createSecureCodeAgent().catch(console.error);typescript
// create-secure-agent.ts
import { createAgent } from './lib/agent-api';
async function createSecureCodeAgent() {
const agent = await createAgent({
name: 'secure-code-assistant',
systemPrompt: `你是一名安全代码助手。
执行命令前务必先询问。
仅在沙箱环境内工作。`,
toolPolicy: 'allowlist',
allowedTools: [
'bash',
'read_file',
'write_file',
'search_files'
],
sandboxEnabled: true,
execApprovalRequired: true
});
console.log('Agent已创建:', agent.id);
return agent;
}
// 使用示例
createSecureCodeAgent().catch(console.error);Example 4: Cron Job Configuration
示例4:定时任务配置
typescript
// configure-cron-job.ts
interface CronJobConfig {
agentId: string;
schedule: string;
task: string;
enabled: boolean;
}
async function createCronJob(config: CronJobConfig) {
const response = await fetch('/api/intents/job-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: config.agentId,
schedule: config.schedule,
task_prompt: config.task,
enabled: config.enabled
})
});
return response.json();
}
// Schedule daily backup
await createCronJob({
agentId: 'agent-123',
schedule: '0 0 * * *', // Daily at midnight
task: 'Create backup of project files and upload to storage',
enabled: true
});typescript
// configure-cron-job.ts
interface CronJobConfig {
agentId: string;
schedule: string;
task: string;
enabled: boolean;
}
async function createCronJob(config: CronJobConfig) {
const response = await fetch('/api/intents/job-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: config.agentId,
schedule: config.schedule,
task_prompt: config.task,
enabled: config.enabled
})
});
return response.json();
}Common Patterns
配置每日备份任务
Pattern 1: Conditional Gateway URL
—
typescript
// lib/gateway-config.ts
export function getGatewayUrl(): string {
// Check environment variable first
if (process.env.NEXT_PUBLIC_GATEWAY_URL) {
return process.env.NEXT_PUBLIC_GATEWAY_URL;
}
// Check saved settings
const settings = loadStudioSettings();
if (settings?.gatewayUrl) {
return settings.gatewayUrl;
}
// Default to localhost
return 'ws://localhost:18789';
}
export function isSecureConnection(url: string): boolean {
return url.startsWith('wss://');
}await createCronJob({
agentId: 'agent-123',
schedule: '0 0 * * *', // 每日午夜执行
task: '创建项目文件备份并上传至存储服务',
enabled: true
});
undefinedPattern 2: Access Token Management
通用模式
—
模式1:条件式网关URL
typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const requiredToken = process.env.STUDIO_ACCESS_TOKEN;
// No token required for loopback-only binds
if (!requiredToken) {
return NextResponse.next();
}
// Check cookie first
const cookieToken = request.cookies.get('studio_access_token')?.value;
if (cookieToken === requiredToken) {
return NextResponse.next();
}
// Check query param (for initial setup)
const queryToken = request.nextUrl.searchParams.get('access_token');
if (queryToken === requiredToken) {
const response = NextResponse.next();
response.cookies.set('studio_access_token', requiredToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 30 // 30 days
});
return response;
}
return new NextResponse('Unauthorized', { status: 401 });
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
};typescript
// lib/gateway-config.ts
export function getGatewayUrl(): string {
// 优先检查环境变量
if (process.env.NEXT_PUBLIC_GATEWAY_URL) {
return process.env.NEXT_PUBLIC_GATEWAY_URL;
}
// 检查已保存的设置
const settings = loadStudioSettings();
if (settings?.gatewayUrl) {
return settings.gatewayUrl;
}
// 默认使用本地地址
return 'ws://localhost:18789';
}
export function isSecureConnection(url: string): boolean {
return url.startsWith('wss://');
}Pattern 3: Runtime Event Handling
模式2:访问令牌管理
typescript
// lib/runtime-store.ts
import Database from 'better-sqlite3';
export class RuntimeStore {
private db: Database.Database;
private subscribers: Set<(event: RuntimeEvent) => void>;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.subscribers = new Set();
this.initDb();
}
private initDb() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS runtime_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
agent_id TEXT,
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_timestamp
ON runtime_events(agent_id, timestamp);
`);
}
async storeEvent(event: RuntimeEvent) {
const stmt = this.db.prepare(`
INSERT INTO runtime_events (event_type, agent_id, payload, timestamp)
VALUES (?, ?, ?, ?)
`);
stmt.run(
event.type,
event.agentId,
JSON.stringify(event),
Date.now()
);
// Notify subscribers
this.subscribers.forEach(fn => fn(event));
}
async getHistory(agentId?: string, limit = 100): Promise<RuntimeEvent[]> {
const query = agentId
? `SELECT payload FROM runtime_events
WHERE agent_id = ?
ORDER BY timestamp DESC LIMIT ?`
: `SELECT payload FROM runtime_events
ORDER BY timestamp DESC LIMIT ?`;
const stmt = this.db.prepare(query);
const rows = agentId
? stmt.all(agentId, limit)
: stmt.all(limit);
return rows.map(row => JSON.parse(row.payload)).reverse();
}
subscribe(callback: (event: RuntimeEvent) => void) {
this.subscribers.add(callback);
return {
unsubscribe: () => this.subscribers.delete(callback)
};
}
}typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const requiredToken = process.env.STUDIO_ACCESS_TOKEN;
// 仅回环绑定无需令牌
if (!requiredToken) {
return NextResponse.next();
}
// 优先检查Cookie
const cookieToken = request.cookies.get('studio_access_token')?.value;
if (cookieToken === requiredToken) {
return NextResponse.next();
}
// 检查查询参数(用于初始设置)
const queryToken = request.nextUrl.searchParams.get('access_token');
if (queryToken === requiredToken) {
const response = NextResponse.next();
response.cookies.set('studio_access_token', requiredToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 30 // 30天
});
return response;
}
return new NextResponse('未授权', { status: 401 });
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
};Troubleshooting
模式3:运行时事件处理
Connection Issues
—
Problem: UI loads but "Connect" fails
bash
undefinedtypescript
// lib/runtime-store.ts
import Database from 'better-sqlite3';
export class RuntimeStore {
private db: Database.Database;
private subscribers: Set<(event: RuntimeEvent) => void>;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.subscribers = new Set();
this.initDb();
}
private initDb() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS runtime_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
agent_id TEXT,
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_timestamp
ON runtime_events(agent_id, timestamp);
`);
}
async storeEvent(event: RuntimeEvent) {
const stmt = this.db.prepare(`
INSERT INTO runtime_events (event_type, agent_id, payload, timestamp)
VALUES (?, ?, ?, ?)
`);
stmt.run(
event.type,
event.agentId,
JSON.stringify(event),
Date.now()
);
// 通知订阅者
this.subscribers.forEach(fn => fn(event));
}
async getHistory(agentId?: string, limit = 100): Promise<RuntimeEvent[]> {
const query = agentId
? `SELECT payload FROM runtime_events
WHERE agent_id = ?
ORDER BY timestamp DESC LIMIT ?`
: `SELECT payload FROM runtime_events
ORDER BY timestamp DESC LIMIT ?`;
const stmt = this.db.prepare(query);
const rows = agentId
? stmt.all(agentId, limit)
: stmt.all(limit);
return rows.map(row => JSON.parse(row.payload)).reverse();
}
subscribe(callback: (event: RuntimeEvent) => void) {
this.subscribers.add(callback);
return {
unsubscribe: () => this.subscribers.delete(callback)
};
}
}Check gateway is running
故障排除
—
连接问题
openclaw gateway status
问题:UI加载正常但“连接”按钮点击后失败
bash
undefinedVerify gateway URL in Studio settings
检查网关是否运行
cat ~/.openclaw/openclaw-studio/settings.json
openclaw gateway status
If Studio is remote, remember localhost means Studio host
验证Studio设置中的网关URL
Use Tailscale Serve or SSH tunnel if needed
—
**Problem**: `EPROTO` / "wrong version number"
```typescript
// Wrong: using wss:// to non-TLS endpoint
const url = 'wss://localhost:18789'; // ❌
// Right: use ws:// for plain HTTP
const url = 'ws://localhost:18789'; // ✅
// Right: use wss:// for Tailscale HTTPS
const url = 'wss://gateway.ts.net'; // ✅Problem: Assets 404 under
/studiobash
undefinedcat ~/.openclaw/openclaw-studio/settings.json
Studio must be served at root path
如果Studio在远程主机,注意localhost指的是Studio所在主机
If you need /studio path:
必要时使用Tailscale Serve或SSH隧道
1. Set basePath in next.config.js
—
2. Rebuild Studio
—
next.config.js
—
module.exports = {
basePath: '/studio',
// ...
}
undefined
**问题**:`EPROTO` / “版本号错误”
```typescript
// 错误:对非TLS端点使用wss://
const url = 'wss://localhost:18789'; // ❌
// 正确:对普通HTTP使用ws://
const url = 'ws://localhost:18789'; // ✅
// 正确:对Tailscale HTTPS使用wss://
const url = 'wss://gateway.ts.net'; // ✅问题:路径下资源404
/studiobash
undefinedNative Module Issues
Studio必须在根路径下提供服务
—
如果需要/studio路径:
—
1. 在next.config.js中设置basePath
—
2. 重新构建Studio
—
next.config.js
Problem: / mismatch
better_sqlite3.nodeNODE_MODULE_VERSIONbash
undefinedmodule.exports = {
basePath: '/studio',
// ...
}
undefinedAuto-repair (recommended)
原生模块问题
npm run verify:native-runtime:repair
问题: / 不匹配
better_sqlite3.nodeNODE_MODULE_VERSIONbash
undefinedManual fix
自动修复(推荐)
npm rebuild better-sqlite3
npm install
npm run verify:native-runtime:repair
Check node/npm versions match
手动修复
node -v && node -p "process.versions.modules"
which node && which npm
npm rebuild better-sqlite3
npm install
If using nvm, ensure same node version
检查node/npm版本是否一致
nvm use 20
npm install
**Problem**: SQLite errors on startup
```bashnode -v && node -p "process.versions.modules"
which node && which npm
Check permissions on state directory
如果使用nvm,确保使用相同的node版本
ls -la ~/.openclaw/openclaw-studio/
nvm use 20
npm install
**问题**:启动时出现SQLite错误
```bashReset runtime DB if corrupted
检查状态目录权限
rm ~/.openclaw/openclaw-studio/runtime.db
npm run dev
undefinedls -la ~/.openclaw/openclaw-studio/
Access Token Issues
如果数据库损坏,重置运行时数据库
Problem: 401 "Studio access token required"
bash
undefinedrm ~/.openclaw/openclaw-studio/runtime.db
npm run dev
undefinedSet access token for public binds
访问令牌问题
export STUDIO_ACCESS_TOKEN=your-secure-token
问题:401错误“需要Studio访问令牌”
bash
undefinedAccess UI with token once to set cookie
为公开绑定设置访问令牌
export STUDIO_ACCESS_TOKEN=your-secure-token
Or set in browser
使用令牌访问UI一次以设置Cookie
undefinedDebugging Connection Path
或在浏览器中访问
typescript
// debug-connection.ts
export function debugConnection() {
console.log('Studio runtime environment:');
console.log('- Node version:', process.version);
console.log('- Platform:', process.platform);
console.log('- CWD:', process.cwd());
console.log('- State dir:', process.env.OPENCLAW_STATE_DIR || '~/.openclaw');
const isServer = typeof window === 'undefined';
console.log('- Running on:', isServer ? 'server' : 'browser');
if (isServer) {
const os = require('os');
console.log('- Hostname:', os.hostname());
console.log('- Network interfaces:',
Object.keys(os.networkInterfaces()));
}
console.log('\nGateway connection:');
console.log('- URL:', process.env.NEXT_PUBLIC_GATEWAY_URL || 'ws://localhost:18789');
console.log('- Token set:', !!process.env.GATEWAY_TOKEN);
console.log('- Access token required:', !!process.env.STUDIO_ACCESS_TOKEN);
}undefinedKey Files Reference
连接路径调试
openclaw-studio/
├── app/
│ ├── api/
│ │ ├── runtime/
│ │ │ └── stream/route.ts # SSE streaming endpoint
│ │ └── intents/
│ │ ├── agent-create/route.ts
│ │ ├── chat/route.ts
│ │ └── job-create/route.ts
│ ├── dashboard/
│ │ ├── page.tsx # Main dashboard
│ │ └── agents/page.tsx
│ └── layout.tsx
├── lib/
│ ├── runtime-store.ts # SQLite runtime DB
│ ├── gateway-client.ts # WebSocket client
│ └── settings.ts # Settings persistence
├── hooks/
│ └── useGatewayConnection.ts
├── docs/
│ ├── ui-guide.md
│ ├── pi-chat-streaming.md
│ ├── permissions-sandboxing.md
│ └── color-system.md
├── next.config.js
├── package.json
└── ARCHITECTURE.mdtypescript
// debug-connection.ts
export function debugConnection() {
console.log('Studio运行时环境:');
console.log('- Node版本:', process.version);
console.log('- 平台:', process.platform);
console.log('- 当前工作目录:', process.cwd());
console.log('- 状态目录:', process.env.OPENCLAW_STATE_DIR || '~/.openclaw');
const isServer = typeof window === 'undefined';
console.log('- 运行位置:', isServer ? '服务器' : '浏览器');
if (isServer) {
const os = require('os');
console.log('- 主机名:', os.hostname());
console.log('- 网络接口:',
Object.keys(os.networkInterfaces()));
}
console.log('\n网关连接信息:');
console.log('- URL:', process.env.NEXT_PUBLIC_GATEWAY_URL || 'ws://localhost:18789');
console.log('- 令牌已设置:', !!process.env.GATEWAY_TOKEN);
console.log('- 需要访问令牌:', !!process.env.STUDIO_ACCESS_TOKEN);
}Additional Resources
关键文件参考
- UI Guide: - Agent creation, cron jobs, exec approvals
docs/ui-guide.md - PI Chat Streaming: - SSE event handling, replay, tool calls
docs/pi-chat-streaming.md - Permissions: - Tool policies, sandbox config, approval flows
docs/permissions-sandboxing.md - Architecture: - Modules and data flow
ARCHITECTURE.md - Discord: https://discord.gg/EFkFHbZw
openclaw-studio/
├── app/
│ ├── api/
│ │ ├── runtime/
│ │ │ └── stream/route.ts # SSE流端点
│ │ └── intents/
│ │ ├── agent-create/route.ts
│ │ ├── chat/route.ts
│ │ └── job-create/route.ts
│ ├── dashboard/
│ │ ├── page.tsx # 主仪表板
│ │ └── agents/page.tsx
│ └── layout.tsx
├── lib/
│ ├── runtime-store.ts # SQLite运行时数据库
│ ├── gateway-client.ts # WebSocket客户端
│ └── settings.ts # 设置持久化
├── hooks/
│ └── useGatewayConnection.ts
├── docs/
│ ├── ui-guide.md
│ ├── pi-chat-streaming.md
│ ├── permissions-sandboxing.md
│ └── color-system.md
├── next.config.js
├── package.json
└── ARCHITECTURE.md—
额外资源
—
- UI指南:- Agent创建、定时任务、执行审批
docs/ui-guide.md - AI聊天流:- SSE事件处理、回放、工具调用
docs/pi-chat-streaming.md - 权限管理:- 工具策略、沙箱配置、审批流程
docs/permissions-sandboxing.md - 架构文档:- 模块与数据流
ARCHITECTURE.md - Discord社区:https://discord.gg/EFkFHbZw