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 |
| WebSocket | gorilla/websocket |
| 配置管理 | Viper (TOML) |
| CLI | Cobra |
| 前端 | 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 三层架构组织:
- cmd/server.go 是路由注册中心,所有 API 端点在此定义(约 130 行路由声明)
- handler/api/v1/ 负责请求解析和响应,不含业务逻辑
- service/k8s/ 是真正的核心层,封装了所有 K8s 操作和配置管理
- handler/proxy/ 独立于 API 层,作为反向代理服务 Bot 流量
- model/ 使用 GORM 定义数据库模型,支持自动迁移
3. K8s 编排实现细节
3.1 Deployment 创建
每个 Bot 对应一个独立的 K8s Deployment,命名规则为 oc-{botID[:8]},截取前 8 位以满足 K8s 63 字符名称限制。
核心函数 buildDeploymentSpec(service/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)}
这个设计保证了:
- 首次启动时自动注入网关配置
- 重启时不覆盖用户已修改的配置
- 使用
node -e清理无效字段(如auth.scopes) - 自动拷贝预装插件(
openclaw-weixin)
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"
)
关键设计:
- UUID 主键:使用 PostgreSQL 的
gen_random_uuid() - Slug:唯一索引,支持自定义短标识或自动生成
- AccessToken:创建时自动生成,用于 Gateway 认证
- Config:以 JSON 文本存储完整的
openclaw.json配置
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)
}
// ...
}
路由策略:
/v1/*路径和 WebSocket 请求 → 直接路由到 OpenClaw Gateway 端口(18789)- 其他请求 → 优先路由到 ChatClaw WebUI 端口(3000),若不可用则 fallback 到 Gateway
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)
- 每个 App 有独立的 API Token
- BotOwnerAuth 中间件校验
bot.AppID == app.ID - Admin API 使用独立的 admin_token,与租户 token 完全隔离
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 }(),
}
- 非 root 运行(UID 1000)
- 禁止权限提升
- Init container 以 root 运行但只做 chown,且
ReadOnlyRootFilesystem: true
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:
- Token 模式:
gateway.auth.mode = "token",使用 Bot 的 AccessToken - 禁用设备认证:
dangerouslyDisableDeviceAuth = true,因为配对由 ClawHost 代理层自动处理 - 可信代理:配置私有网络地址段(10.0.0.0/8 等)为可信代理
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 调用:
node.pair.list— 获取待配对设备node.pair.approve— 批准配对node.list— 获取已配对设备node.revoke— 撤销设备
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.Printf 和 c.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 代码实现了:
- 完整的 Bot 生命周期管理
- 智能的反向代理(自动设备审批)
- 精细的配置同步(分区同步避免不必要重启)
- 多租户 App 隔离
- ChatClaw sidecar 注入
- 嵌入式 Admin Panel
核心架构选择(无 Ingress per bot、共享 PVC + SubPath、内置反向代理)在小规模部署下合理高效。但缺乏测试是最大的技术债,建议在生产化前补充关键路径的集成测试。
适用场景:中小规模(数十到数百个 Bot)的私有化部署,特别适合需要集中管理多个 OpenClaw 实例的团队。