openclaw-studio-dashboard

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OpenClaw 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
undefined
bash
undefined

Run latest version with npx

使用npx运行最新版本

npx -y openclaw-studio@latest
npx -y openclaw-studio@latest

Default gateway URL: ws://localhost:18789

默认网关URL:ws://localhost:18789

undefined
undefined

From Source

从源码安装

bash
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run dev
bash
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run dev

Setup Helper

设置助手

bash
undefined
bash
undefined

Configure gateway URL/token without opening UI

无需打开UI即可配置网关URL/令牌

npm run studio:setup
undefined
npm run studio:setup
undefined

Connection Architecture

连接架构

OpenClaw Studio uses a two-path architecture:
  1. Browser → Studio: HTTP + SSE (
    /api/runtime/*
    ,
    /api/intents/*
    )
  2. Studio → Gateway: Server-owned WebSocket to upstream OpenClaw
Critical concept:
ws://localhost:18789
means "gateway on the Studio host", not "gateway on your browser's machine".
OpenClaw Studio采用双路径架构:
  1. 浏览器 → Studio:HTTP + SSE(
    /api/runtime/*
    ,
    /api/intents/*
  2. Studio → Gateway:服务器托管的WebSocket连接至上游OpenClaw
核心概念
ws://localhost:18789
表示“Studio所在主机上的网关”,而非“浏览器所在机器上的网关”。

Configuration Patterns

配置模式

Deployment Scenarios

部署场景

A. Both Local (Same Computer)

A. 本地部署(同一计算机)

bash
undefined
bash
undefined

Start 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.token

B. 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:18789
In 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
undefined

Start 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

undefined
undefined

Environment Variables

环境变量

bash
undefined
bash
undefined

Gateway 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
undefined
HOST=0.0.0.0 # 公开绑定 - 需要STUDIO_ACCESS_TOKEN
undefined

Configuration 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 DB
Studio设置存储在
~/.openclaw/openclaw-studio/
目录下:
~/.openclaw/
├── openclaw.json              # OpenClaw Gateway配置
└── openclaw-studio/
    ├── settings.json          # 网关URL/令牌
    └── runtime.db            # 控制平面运行时数据库

API & Key Commands

API与关键命令

Studio Setup

Studio设置

bash
undefined
bash
undefined

Development 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
undefined
npm run verify:native-runtime:repair npm run verify:native-runtime:check
undefined

TypeScript 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/bash
bash
#!/bin/bash

setup-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中输入网关令牌

undefined
undefined

Example 2: Production Cloud Setup

示例2:云端生产环境部署

bash
#!/bin/bash
bash
#!/bin/bash

deploy-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"
undefined
npm 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"
undefined

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

Pattern 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
undefined
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()
    );
    
    // 通知订阅者
    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
undefined

Verify 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
/studio
bash
undefined
cat ~/.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'; // ✅
问题
/studio
路径下资源404
bash
undefined

Native Module Issues

Studio必须在根路径下提供服务

如果需要/studio路径:

1. 在next.config.js中设置basePath

2. 重新构建Studio

next.config.js

Problem:
better_sqlite3.node
/
NODE_MODULE_VERSION
mismatch
bash
undefined
module.exports = { basePath: '/studio', // ... }
undefined

Auto-repair (recommended)

原生模块问题

npm run verify:native-runtime:repair
问题
better_sqlite3.node
/
NODE_MODULE_VERSION
不匹配
bash
undefined

Manual 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

```bash
node -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错误

```bash

Reset runtime DB if corrupted

检查状态目录权限

rm ~/.openclaw/openclaw-studio/runtime.db npm run dev
undefined
ls -la ~/.openclaw/openclaw-studio/

Access Token Issues

如果数据库损坏,重置运行时数据库

Problem: 401 "Studio access token required"
bash
undefined
rm ~/.openclaw/openclaw-studio/runtime.db npm run dev
undefined

Set access token for public binds

访问令牌问题

export STUDIO_ACCESS_TOKEN=your-secure-token
问题:401错误“需要Studio访问令牌”
bash
undefined

Access UI with token once to set cookie

为公开绑定设置访问令牌

export STUDIO_ACCESS_TOKEN=your-secure-token

Or set in browser

使用令牌访问UI一次以设置Cookie

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

Key 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.md
typescript
// 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:
    docs/ui-guide.md
    - Agent creation, cron jobs, exec approvals
  • PI Chat Streaming:
    docs/pi-chat-streaming.md
    - SSE event handling, replay, tool calls
  • Permissions:
    docs/permissions-sandboxing.md
    - Tool policies, sandbox config, approval flows
  • Architecture:
    ARCHITECTURE.md
    - Modules and data flow
  • 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指南
    docs/ui-guide.md
    - Agent创建、定时任务、执行审批
  • AI聊天流
    docs/pi-chat-streaming.md
    - SSE事件处理、回放、工具调用
  • 权限管理
    docs/permissions-sandboxing.md
    - 工具策略、沙箱配置、审批流程
  • 架构文档
    ARCHITECTURE.md
    - 模块与数据流
  • Discord社区https://discord.gg/EFkFHbZw