ClawHost 源码实现深度分析

1. 项目概览

ClawHost 是一个基于 Kubernetes 的 OpenClaw Bot 托管平台,用 Go 语言编写,通过 REST API 和反向代理实现 Bot 的全生命周期管理。项目采用 Go + Next.js 混合架构,代码总量约 8,576 行 Go 代码(不含前端),核心依赖包括 Echo web 框架、GORM ORM、client-go K8s 客户端。

GitHub 仓库: https://github.com/fastclaw-ai/clawhost

技术栈

层级技术
语言Go 1.24
Web 框架Echo v4.13
数据库PostgreSQL (GORM)
K8s 客户端client-go v0.32
WebSocketgorilla/websocket
配置管理Viper (TOML)
CLICobra
前端Next.js (静态导出,embed 嵌入)
部署Helm Chart + Docker 多阶段构建

2. 代码结构和模块划分


clawhost/
├── main.go                          # 入口(7行)
├── cmd/
│   ├── root.go                      # Cobra 根命令
│   └── server.go                    # 服务启动、路由注册(227行,核心)
├── handler/
│   ├── api/v1/                      # REST API 处理器
│   │   ├── bot_create.go            # Bot CRUD
│   │   ├── bot_start.go             # 生命周期管理
│   │   ├── bot_stop.go
│   │   ├── bot_delete.go
│   │   ├── bot_upgrade.go           # 镜像升级(单个/批量)
│   │   ├── bot_restart_all.go       # 全量重建重启
│   │   ├── bot_channel.go           # IM 渠道管理
│   │   ├── bot_connect.go           # 连接信息
│   │   ├── skill_list.go            # Skills 管理
│   │   ├── skill_update.go
│   │   ├── app.go                   # App(租户)管理
│   │   └── ...
│   └── proxy/
│       └── proxy.go                 # HTTP/WebSocket 反向代理(426行,最大文件)
├── middleware/
│   └── auth.go                      # 三层认证中间件
├── model/
│   ├── bot.go                       # Bot 数据模型
│   ├── app.go                       # App(租户)模型
│   └── openclaw_config.go           # OpenClaw 配置映射
├── service/k8s/
│   ├── client.go                    # K8s 客户端初始化
│   ├── deployment.go                # Deployment 编排(683行)
│   ├── service.go                   # ClusterIP Service 管理
│   ├── gateway.go                   # Gateway WebSocket 客户端
│   ├── botconfig.go                 # 配置构建与合并
│   ├── config_sync.go               # DB ↔ Pod 配置同步
│   ├── exec.go                      # Pod 内命令执行
│   ├── approve.go                   # 设备自动审批
│   ├── channel.go                   # Channel 配置操作
│   └── workspace.go                 # Agent 工作区管理
├── util/
│   ├── config.go                    # Viper 配置加载
│   ├── db.go                        # 数据库连接
│   └── response.go                  # 统一响应格式
├── web/
│   ├── admin_embed.go               # Go embed 嵌入 Next.js 静态文件
│   └── admin/                       # Next.js Admin Panel 源码
├── deploy/helm/clawhost/            # Helm Chart
└── Dockerfile                       # 多阶段构建

模块职责分析

代码按经典的 Handler → Service → Model 三层架构组织:

3. K8s 编排实现细节

3.1 Deployment 创建

每个 Bot 对应一个独立的 K8s Deployment,命名规则为 oc-{botID[:8]},截取前 8 位以满足 K8s 63 字符名称限制。

核心函数 buildDeploymentSpecservice/k8s/deployment.go:96)构建完整的 Deployment 对象:


// service/k8s/deployment.go
func GetDeploymentName(botID string) string {
    return fmt.Sprintf("oc-%s", getShortID(botID))
}

func buildDeploymentSpec(botID, userID string, config *BotConfig) *appsv1.Deployment {
    // ...
    labels := map[string]string{
        "app":     "openclaw",
        "bot-id":  botID,
        "user-id": userID,
    }
    replicas := int32(1)
    // ...
}

Pod 结构由以下部分组成:

1. Init Container (alpine:3.19):修复 PVC 目录权限

`go

initCmd := "chown -R 1000:1000 /data && chmod -R 755 /data"

`

2. 主容器 (openclaw):运行 OpenClaw Gateway

- UID/GID: 1000(非 root)

- 端口: 18789(可配置)

- 启动命令:openclaw gateway --port 18789 --bind lan --allow-unconfigured --dev

- 资源默认:100m~500m CPU,128Mi~512Mi 内存

3. Sidecar 容器 (chatclaw,可选):当 chatclaw.image 配置时自动注入

- 端口: 3000

- 与主容器共享 PVC,通过 SubPath 隔离数据

- 独立的资源限制(默认 100m~1000m CPU,256Mi~1Gi 内存)

存储设计:所有 Bot 共享一个 PVC(openclaw-shared-data),通过 SubPath 实现 Bot 级隔离:


VolumeMounts: []corev1.VolumeMount{
    {
        Name:      "data",
        MountPath: "/home/node/.openclaw",
        SubPath:   botID,  // 每个 Bot 独立目录
    },
}

3.2 首次启动配置注入

首次启动时,容器启动命令中内联写入 openclaw.json 配置:


// 只在首次启动时写入配置,避免覆盖用户通过 OpenClaw UI 修改的配置
return []string{"sh", "-c", fmt.Sprintf(`if [ ! -f /home/node/.openclaw/openclaw.json ]; then
cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG'
%s
EOFCONFIG
fi
# Patch config: clean up invalid keys and ensure controlUi is set
if command -v node > /dev/null 2>&1 && [ -f /home/node/.openclaw/openclaw.json ]; then
  node -e "..."
fi
exec openclaw gateway --port %d --bind lan --allow-unconfigured --dev`, configJSON, gatewayPort)}

这个设计保证了:

3.3 Service 创建

每个 Bot 创建一个 ClusterIP Service:


// service/k8s/service.go
func CreateService(ctx context.Context, botID, userID string) (string, error) {
    serviceName := GetServiceName(botID) // oc-{botID[:8]}-svc
    ports := []corev1.ServicePort{
        {Name: "gateway", Port: gatewayPort, ...},
    }
    if ChatClawEnabled() {
        ports = append(ports, corev1.ServicePort{
            Name: "chatclaw", Port: ccPort, ...
        })
    }
    // ...
    return fmt.Sprintf("%s.%s.svc.cluster.local:%d", ...)
}

Service 端点格式:oc-{shortID}-svc.{namespace}.svc.cluster.local:18789

3.4 镜像拉取策略


// service/k8s/deployment.go
func imagePullPolicy(image string) corev1.PullPolicy {
    if viper.GetBool("kubernetes.local_dev") {
        return corev1.PullIfNotPresent  // 本地开发模式
    }
    if !strings.Contains(image, ":") || strings.HasSuffix(strings.ToLower(image), ":latest") {
        return corev1.PullAlways  // latest 标签始终拉取
    }
    return corev1.PullIfNotPresent
}

3.5 无 Ingress Controller

ClawHost 不为每个 Bot 创建 Ingress。Helm Chart 中只有一个 Ingress 给 ClawHost 管理面板本身。Bot 流量通过 ClawHost 的内置反向代理路由到 Pod,避免了 Ingress 资源爆炸的问题。

4. API 设计

4.1 API 端点总览

ClawHost 暴露三组 API:

租户 API(`/bot/api/v1`)— Bearer Token 认证

方法路径功能
POST`/bots`创建 Bot
GET`/bots`列出 Bot
GET`/bots/:id`获取 Bot 详情
PUT`/bots/:id`更新 Bot
DELETE`/bots/:id`删除 Bot
POST`/bots/:id/start`启动 Bot
POST`/bots/:id/stop`停止 Bot
POST`/bots/:id/restart`重启 Bot
GET`/bots/:id/status`获取运行状态
GET`/bots/:id/connect`获取连接信息
POST`/bots/:id/reset-token`重置访问令牌
GET`/bots/:id/skills`列出 Skills
PUT`/bots/:id/skills/:name`更新 Skill
DELETE`/bots/:id/skills/:name`删除 Skill
POST`/bots/:id/channels`添加渠道
GET`/bots/:id/channels`列出渠道
DELETE`/bots/:id/channels/:channel`移除渠道
GET`/bots/:id/channels/:channel/pairing`渠道配对列表
POST`/bots/:id/channels/:channel/pairing/approve`批准配对
POST`/bots/:id/channels/:channel/pairing/revoke`撤销配对
GET`/bots/:id/channels/:channel/pairing/users`已配对用户
POST`/bots/:id/channels/wechat/login`微信扫码登录
GET`/bots/:id/channels/wechat/login/status`登录状态
GET`/bots/:id/channels/wechat/accounts`微信账号列表
DELETE`/bots/:id/channels/wechat/accounts/:id`移除微信账号
GET`/bots/:id/devices`设备列表
POST`/bots/:id/devices/:id/approve`批准设备
DELETE`/bots/:id/devices/:id`撤销设备
GET`/bots/:id/config/models`模型提供商列表
POST`/bots/:id/config/models`添加模型提供商
PUT`/bots/:id/config/models/:provider`更新提供商
DELETE`/bots/:id/config/models/:provider`删除提供商
GET`/bots/:id/config/defaults`Agent 默认配置
PUT`/bots/:id/config/defaults`设置 Agent 默认
GET`/bots/:id/config/raw`读取原始配置
PUT`/bots/:id/config/raw`写入原始配置

管理员 API(`/bot/api/v1/admin`)— Admin Token 认证

方法路径功能
GET`/verify`验证 Admin Token
GET`/config`获取全局配置
POST`/apps`创建 App
GET`/apps`列出 App
PUT`/apps/:id`更新 App
DELETE`/apps/:id`删除 App
POST`/apps/:id/reset-token`重置 App Token
POST`/bots`管理员创建 Bot
GET`/bots`管理员列出所有 Bot
POST`/bots/:id/start`管理员启动 Bot
POST`/bots/:id/stop`管理员停止 Bot
DELETE`/bots/:id`管理员删除 Bot
POST`/bots/upgrade`批量升级所有 Bot
POST`/bots/:id/upgrade`升级单个 Bot
POST`/bots/restart`全量重建重启

代理路由

路径模式功能
`/proxy/:bot_id/*`反向代理到 Bot Pod
`*.{bot_domain}/*`子域名路由(通配符 Ingress)

4.2 统一响应格式


// util/response.go
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

所有 API 返回统一的 {code, message, data} 结构。

4.3 认证机制

ClawHost 实现了三层认证


// middleware/auth.go
// 1. BearerAuth — App 级 API Token
func BearerAuth() echo.MiddlewareFunc { ... }
    // 先查 apps 表匹配 token → 再 fallback 到配置文件 token

// 2. BotOwnerAuth — Bot 所属权校验
func BotOwnerAuth() echo.MiddlewareFunc { ... }
    // 验证 app.ID == bot.AppID

// 3. AdminAuth — 管理员固定 Token
func AdminAuth() echo.MiddlewareFunc { ... }
    // 对比 config 中的 admin_token

5. 数据库模型

5.1 Bot 模型


// model/bot.go
type Bot struct {
    ID          string     `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
    AppID       string     `json:"app_id" gorm:"index"`
    UserID      string     `json:"user_id" gorm:"index"`
    Name        string     `json:"name"`
    Slug        string     `json:"slug" gorm:"uniqueIndex"`
    Status      BotStatus  `json:"status"`
    Endpoint    string     `json:"endpoint"`
    AccessToken string     `json:"access_token"`
    Config      string     `json:"-" gorm:"type:text"`         // JSON, 存储完整 openclaw.json
    ExpiresAt   *time.Time `json:"expires_at,omitempty"`
    CreatedAt   time.Time  `json:"created_at"`
    UpdatedAt   time.Time  `json:"updated_at"`
}

Bot 状态机


const (
    BotStatusCreated  BotStatus = "created"
    BotStatusRunning  BotStatus = "running"
    BotStatusStopped  BotStatus = "stopped"
    BotStatusError    BotStatus = "error"
)

关键设计:

5.2 App 模型(租户)


// model/app.go
type App struct {
    ID          string    `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    APIToken    string    `json:"api_token" gorm:"uniqueIndex"`
    IsActive    bool      `json:"is_active" gorm:"default:true"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

App 是多租户的基础单元,每个 App 拥有一个 API Token 用于操作其名下的 Bot。

5.3 OpenClaw 配置模型


// model/openclaw_config.go
type OpenClawConfig struct {
    Gateway  *GatewayConfig            `json:"gateway,omitempty"`
    Models   *ModelsConfig             `json:"models,omitempty"`
    Agents   *AgentsConfig             `json:"agents,omitempty"`
    Channels ChannelsConfig            `json:"channels,omitempty"`
    Plugins  map[string]interface{}    `json:"plugins,omitempty"`
}

type GatewayConfig struct {
    Port        int               `json:"port,omitempty"`
    Mode        string            `json:"mode,omitempty"`
    Auth        *GatewayAuthConfig `json:"auth,omitempty"`
    ControlUI   *ControlUIConfig  `json:"controlUi,omitempty"`
    // ...
}

type ModelsConfig struct {
    Mode      string                        `json:"mode"`
    Providers map[string]*ProviderConfig    `json:"providers"`
}

这套模型完整映射了 OpenClaw 的 openclaw.json 配置,支持在数据库和 Pod 之间双向同步。

5.4 自动迁移


// cmd/server.go
db.AutoMigrate(&model.Bot{}, &model.App{})

使用 GORM 的 AutoMigrate,无手动 SQL migration 文件,适合早期迭代。

6. 代理层实现

代理层是 ClawHost 最复杂的模块(426 行),位于 handler/proxy/proxy.go

6.1 HTTP 反向代理


// handler/proxy/proxy.go
func ProxyToBot(c echo.Context) error {
    botIdentifier := c.Param("bot_id")
    
    // 支持 UUID 和 Slug 两种标识
    if isUUID(botIdentifier) {
        bot, err = model.GetBotByID(botIdentifier)
    } else {
        bot, err = model.GetBotBySlug(botIdentifier)
    }
    
    // 路由决策:API 路径 → OpenClaw Gateway,其他 → ChatClaw WebUI
    isAPIPath := strings.HasPrefix(remainingPath, "/v1/")
    isWS := isWebSocketRequest(c.Request())
    
    if isAPIPath || isWS {
        targetHost, err = k8s.GetServiceEndpoint(ctx, bot.ID)
    } else {
        targetHost, err = k8s.GetWebUIEndpoint(ctx, bot.ID)
    }
    // ...
}

路由策略

6.2 WebSocket 代理

WebSocket 代理实现了完整的双向消息转发:


func proxyWebSocket(c echo.Context, targetHost, path, botID, accessToken string) error {
    // 升级客户端连接
    clientConn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
    
    // 连接后端 WebSocket(先尝试连接,处理 NOT_PAIRED)
    backendConn, resp, err := websocket.DefaultDialer.Dial(backendURL.String(), requestHeader)
    
    // NOT_PAIRED 自动重试
    if err != nil && isNotPairedResponse(body) {
        k8s.AutoApproveAllPending(ctx, botID, accessToken)
        backendConn, _, err = websocket.DefaultDialer.Dial(...)  // 重试
    }
    
    // 双向消息转发
    go func() { /* Client → Backend */ }()
    go func() { /* Backend → Client */ }()
}

6.3 自动设备审批

这是代理层最巧妙的设计。OpenClaw Gateway 需要设备配对认证,但在托管场景下用户不便手动配对。ClawHost 通过多层自动审批解决:

1. HTTP 响应拦截


proxy.ModifyResponse = func(resp *http.Response) error {
    if isNotPairedResponse(body) {
        go k8s.AutoApproveAllPending(ctx, botID, token)
    }
    return nil
}

2. WebSocket 握手重试:收到 NOT_PAIRED 时同步审批后重试连接

3. WebSocket 消息检测:首条消息如果是 NOT_PAIRED,后台触发审批

4. 轮询审批器:用户首次带 token 访问时,启动 16 秒轮询


func autoApprovePoller(botID, accessToken string) {
    // 防止重复轮询
    activePollersMu.Lock()
    if activePollers[botID] { ... }
    // 每 2 秒检查一次,持续 16 秒
    for i := 0; i < 8; i++ {
        time.Sleep(2 * time.Second)
        k8s.AutoApproveAllPending(ctx, botID, accessToken)
    }
}

6.4 Session Cookie

代理层使用 HMAC-SHA256 签名的 session cookie 避免每次请求携带 token:


func sessionCookieValue(botID, accessToken string) string {
    mac := hmac.New(sha256.New, []byte(accessToken))
    mac.Write([]byte(botID))
    return hex.EncodeToString(mac.Sum(nil))
}

首次认证后设置 7 天有效的 HttpOnly cookie,后续请求无需 ?token= 参数。

7. Admin Panel 实现

7.1 架构

Admin Panel 使用 Next.js 构建,静态导出后通过 Go 的 embed 包嵌入到二进制中:


// web/admin_embed.go
package web
import "embed"
//go:embed all:admin/out
var AdminFS embed.FS

// cmd/server.go
adminFS, err := fs.Sub(web.AdminFS, "admin/out")
adminHandler := http.FileServer(http.FS(adminFS))
e.GET("/admin/*", echo.WrapHandler(http.StripPrefix("/admin", adminHandler)))

7.2 Docker 多阶段构建


# 第一阶段:构建前端
FROM node:20-alpine AS frontend
WORKDIR /app/web/admin
COPY web/admin/package.json web/admin/package-lock.json ./
RUN npm ci
COPY web/admin/ .
RUN npx next build --webpack

# 第二阶段:构建 Go 二进制
FROM golang:1.24-alpine AS builder
COPY --from=frontend /app/web/admin/out ./web/admin/out
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o clawhost .

# 第三阶段:最终镜像
FROM alpine:latest
COPY --from=builder /app/clawhost .
EXPOSE 18080
CMD ["./clawhost", "server"]

这种设计使得最终镜像只包含一个静态二进制,前端嵌入其中,无需额外的 web 服务器。

7.3 前端技术栈

Admin Panel 使用 shadcn/ui 组件库,基于 React + Tailwind CSS,提供 Bot 管理、App 管理等功能界面。

8. 安全模型

8.1 多租户隔离

ClawHost 通过 App → Bot 两级关系实现多租户:


App (租户) ──→ API Token ──→ Bearer Auth 中间件
    │
    └── Bot 1 (AppID = app.ID)
    └── Bot 2 (AppID = app.ID)

8.2 Pod 安全


SecurityContext: &corev1.SecurityContext{
    RunAsUser:                func() *int64 { v := int64(1000); return &v }(),
    RunAsGroup:               func() *int64 { v := int64(1000); return &v }(),
    AllowPrivilegeEscalation: func() *bool { v := false; return &v }(),
}

8.3 K8s RBAC


# deploy/helm/clawhost/templates/rbac.yaml
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/exec", "pods/log", "services", "persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

ClawHost 使用 Role(非 ClusterRole),权限限制在 namespace 内,遵循最小权限原则。

8.4 Gateway 认证

ClawHost 通过以下方式保护 Bot Gateway:

9. Bot 生命周期管理

9.1 创建 → 启动流程


1. POST /bot/api/v1/bots (CreateBot)
   ├── 验证参数(name, user_id, slug)
   ├── 生成 UUID 和 AccessToken
   ├── 存储到 PostgreSQL(status = "created")
   └── 返回 Bot 信息 + access_url

2. POST /bot/api/v1/bots/:id/start (StartBot)
   ├── 从 DB 读取配置,转换为 K8s 配置
   ├── 创建 K8s Deployment(buildDeploymentSpec)
   ├── 创建 K8s ClusterIP Service
   ├── 更新 DB 状态为 "running"
   ├── 异步写入配置到 Pod(go func)
   └── 返回 Bot 信息(含 endpoint)

关键细节:配置写入是异步的(go func),不阻塞启动响应。配置通过 ExecInPod 执行 cat > 命令写入:


// service/k8s/botconfig.go
command := []string{"sh", "-c", fmt.Sprintf(
    "cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG'\n%s\nEOFCONFIG", 
    string(configJSON),
)}
_, err = ExecInPod(ctx, namespace, podName, "openclaw", command)

9.2 升级流程


// handler/api/v1/bot_upgrade.go
func UpgradeBot(c echo.Context) error {
    // 检查当前镜像是否已是目标版本
    currentImage, _ := k8s.GetDeploymentImage(ctx, bot.ID)
    if currentImage == image { return "skipped" }
    
    // 更新 Deployment 镜像(触发滚动更新)
    k8s.UpdateDeploymentImage(ctx, bot.ID, image)
}

批量升级使用并发:


// UpgradeAllBots
var wg sync.WaitGroup
for _, bot := range runningBots {
    wg.Add(1)
    go func(bot model.Bot) {
        defer wg.Done()
        k8s.UpdateDeploymentImage(ctx, bot.ID, image)
    }(bot)
}
wg.Wait()

9.3 全量重建重启

RestartAllBots 不仅重启,而是完全重建 Deployment(ReplaceDeployment),确保 Pod spec 与最新配置一致(如 ChatClaw sidecar 注入):


// handler/api/v1/bot_restart_all.go
func RestartAllBots(c echo.Context) error {
    for _, bot := range runningBots {
        k8sConfig := convertToK8sConfig(bot, openclawConfig)
        k8s.ReplaceDeployment(ctx, bot.ID, bot.UserID, bot.AccessToken, k8sConfig)
    }
}

9.4 删除流程


// handler/api/v1/bot_delete.go
func DeleteBot(c echo.Context) error {
    if bot.Status == model.BotStatusRunning {
        k8s.DeleteDeployment(ctx, bot.ID)
        k8s.DeleteService(ctx, bot.ID)
    }
    model.DeleteBot(bot.ID)
    // TODO: Clean up NAS data directory
}

⚠️ 注意代码中有一个 TODO:删除 Bot 时不清理 PVC 上的数据目录

10. 配置同步机制

ClawHost 在 PostgreSQL 和 Pod 之间维护配置的双向同步。

10.1 DB → Pod(写配置到 Pod)


// service/k8s/config_sync.go
// 完整同步(会覆盖 gateway 配置)
func SyncConfigToPod(ctx context.Context, botID string) error

// 分区同步(只同步指定 section,保留 gateway 不变)
func SyncSectionsToPod(ctx context.Context, botID string, sections ...string) error

分区同步是一个精巧的设计。它使用 Pod 内的 Node.js 进行 JSON 合并,确保未修改的 section 保持字节级一致,避免 OpenClaw 的热重载检测到误变更:


nodeScript := fmt.Sprintf(
    `const fs=require("fs");`+
    `const p="/home/node/.openclaw/openclaw.json";`+
    `let c={};try{c=JSON.parse(fs.readFileSync(p,"utf8"))}catch(e){}`+
    `Object.assign(c,JSON.parse(Buffer.from("%s","base64").toString()));`+
    `fs.writeFileSync(p,JSON.stringify(c,null,2)+"\n")`,
    patchB64)

10.2 Pod → DB(从 Pod 读配置)


func SyncConfigToDatabase(ctx context.Context, botID string) error {
    config, _ := ReadOpenClawConfig(ctx, botID)  // ExecInPod + cat
    bot.SetOpenClawConfig(config)
    model.UpdateBot(bot)
}

10.3 配置合并策略

当更新 Models、Agents 或 Channels 时,只同步相应 section:


func UpdateModelsConfig(ctx context.Context, botID string, ...) error {
    // 1. 更新 DB 中的配置
    config.Models = modelsConfig
    bot.SetOpenClawConfig(config)
    model.UpdateBot(bot)
    
    // 2. 只同步 models section 到 Pod
    if bot.Status == model.BotStatusRunning {
        return SyncSectionsToPod(ctx, botID, "models")
    }
}

这确保了修改模型配置不会意外触发 gateway 重启。

11. Gateway WebSocket 客户端

ClawHost 实现了一个完整的 OpenClaw Gateway WebSocket 客户端(service/k8s/gateway.go),用于设备管理:

11.1 连接协议


func NewGatewayClient(ctx context.Context, endpoint, accessToken string) (*GatewayClient, error) {
    // 先尝试 wss://(OpenClaw v2026.2.19+ 要求安全传输)
    // 失败则 fallback 到 ws://
    
    // 禁用 HTTP 代理,防止代理软件破坏 WebSocket 帧
    noProxy := func(*http.Request) (*url.URL, error) { return nil, nil }
}

11.2 认证握手


func (c *GatewayClient) handleAuth(ctx context.Context, accessToken string) error {
    // 1. 读取 connect.challenge 事件
    // 2. 发送 connect 请求(role: "operator", scopes: ["operator.admin", ...])
    // 3. 等待认证响应
    authReq := map[string]any{
        "type": "req", "id": "connect-1", "method": "connect",
        "params": map[string]any{
            "minProtocol": 3, "maxProtocol": 3,
            "client": map[string]any{"id": "cli", "version": "1.0.0", ...},
            "role": "operator",
            "scopes": []string{"operator.admin", "operator.read", ...},
            "auth": map[string]any{"token": accessToken},
        },
    }
}

11.3 设备管理操作

通过 WebSocket RPC 调用:

12. Helm Chart 结构


deploy/helm/clawhost/
├── Chart.yaml                    # v0.1.0
├── values.yaml                   # 默认配置
└── templates/
    ├── deployment.yaml           # ClawHost 自身的 Deployment
    ├── service.yaml              # ClawHost Service
    ├── ingress.yaml              # 管理面板 Ingress
    ├── configmap.yaml            # config.toml 配置映射
    ├── secret.yaml               # admin-token + db-password
    ├── rbac.yaml                 # ServiceAccount + Role + RoleBinding
    ├── postgresql.yaml           # 可选的内置 PostgreSQL
    ├── pvc.yaml                  # Bot 共享存储
    └── _helpers.tpl              # 模板函数

values.yaml 关键配置项


server:
  port: 18080
  image:
    repository: clawhost/clawhost
    tag: latest

kubernetes:
  namespace: "default"

openclaw:
  image: "openclaw/openclaw:latest"
  gatewayPort: 18789
  cpuLimit: "500m"
  memoryLimit: "512Mi"

chatclaw:
  image: ""              # 留空则不启用 sidecar
  port: 3000

domain:
  botDomain: "clawhost.ai"

storage:
  pvcName: "openclaw-shared-data"
  storageSize: "50Gi"

postgresql:
  enabled: true          # 内置 PostgreSQL(开发用)

ConfigMap 模板自动生成 config.toml,从 Helm values 注入所有配置项。

13. Skills/Channels/Devices 动态管理

13.1 Skills 管理

Skills 通过 ExecInPod 执行 OpenClaw CLI 命令管理:


// handler/api/v1/skill_list.go
output, err := k8s.ExecInPod(ctx, namespace, podName, "openclaw",
    []string{"node", "/app/openclaw.mjs", "skills", "list", "--json"})

13.2 Channels 管理

Channels 管理更为精细,支持多账号结构:


// service/k8s/channel.go
// channels.{channel}.accounts.{accountName}
func AddChannelToBot(ctx context.Context, botID, accessToken, channel, account string, 
    channelConfig map[string]interface{}) error {
    // 分离 channel 级配置(dmPolicy, enabled)和 account 级配置(botToken 等)
    // 合并到 openclaw.json 中
    // OpenClaw 会热重载配置变更
}

配置结构示例:


{
  "channels": {
    "telegram": {
      "enabled": true,
      "dmPolicy": "open",
      "accounts": {
        "mybot": {
          "botToken": "xxx"
        }
      }
    }
  }
}

13.3 Devices 管理

设备管理有两条路径:

1. Gateway WebSocket API(快速):通过 GatewayClient 直接调用

2. CLI Fallback(兼容):通过 ExecInPod 执行 openclaw devices 命令


// service/k8s/gateway.go
func ListBotDevicesViaGateway(ctx context.Context, botID, accessToken string) (*DeviceListResult, error) {
    client, _ := NewGatewayClient(ctx, endpoint, accessToken)
    pending, _ := client.GetPendingPairRequests(ctx)
    paired, _ := client.GetPairedNodes(ctx)
    return &DeviceListResult{Pending: pending, Paired: paired}, nil
}

14. 代码质量评估

14.1 优点

1. 清晰的架构分层:Handler/Service/Model 分离明确

2. 配置同步设计精巧:分区同步避免不必要的 gateway 重启

3. 自动设备审批:多层防护确保 WebUI 访问不需手动配对

4. 安全意识:非 root 容器、HMAC session cookie、RBAC 最小权限

5. 向后兼容:新旧 OpenClaw 版本的 wss/ws 降级、legacy token 支持

6. 前端嵌入:单二进制部署,无额外依赖

14.2 不足

1. 无测试:整个项目零测试文件(未找到任何 *_test.go),这是最大的风险

2. 无数据库 migration:依赖 GORM AutoMigrate,生产环境不够可控

3. 错误处理不够细致:部分地方直接 fmt.Errorf 包装,没有自定义错误类型

4. PVC 数据不清理:删除 Bot 时不删除 PVC 数据(有 TODO 标记)

5. 并发控制有限autoApprovePoller 使用简单的 map+mutex,高并发下可能有竞争

6. 日志不规范:混用 fmt.Printfc.Logger(),无结构化日志

7. 硬编码:部分路径硬编码(如 /home/node/.openclaw),虽然合理但缺乏抽象

8. 缺少优雅停机cmd/server.go 中直接 e.Start(),无 graceful shutdown

14.3 代码指标

指标数据
Go 代码总行数8,576
最大文件`service/k8s/deployment.go` (683行)
最复杂模块`handler/proxy/proxy.go` (426行)
测试覆盖率**0%**
外部依赖数~15 个直接依赖
Docker 镜像大小Alpine base,预计 20-30MB

15. 总结

ClawHost 是一个设计务实、功能完整的 OpenClaw 托管控制面。它用约 8,500 行 Go 代码实现了:

核心架构选择(无 Ingress per bot、共享 PVC + SubPath、内置反向代理)在小规模部署下合理高效。但缺乏测试是最大的技术债,建议在生产化前补充关键路径的集成测试。

适用场景:中小规模(数十到数百个 Bot)的私有化部署,特别适合需要集中管理多个 OpenClaw 实例的团队。