# ClawPanel Docker 多实例管理 — 技术规划 > 版本: v1.0 | 日期: 2026-03-08 ## 1. 问题分析 ### 1.1 现状 ClawPanel 当前架构是 **单实例管理**: ``` 浏览器 → ClawPanel 前端 │ ├── /__api/* → dev-api.js → 读写本机 ~/.openclaw/ 文件 ├── /ws → 代理到本机 Gateway:18789 (WebSocket) └── 静态文件 → dist/ ``` **所有页面**(模型配置、Agent 管理、Gateway 设置、日志、聊天等)操作的都是: - 本机文件系统上的 `~/.openclaw/openclaw.json` - 本机运行的 Gateway 进程(端口 18789) ### 1.2 Phase 1 已完成 Docker 集群页面实现了 **容器生命周期管理**(通过 Docker Socket API): - 启动/停止/重启/删除容器 - 部署新容器(端口映射、数据卷、环境变量) - 查看容器日志 - 多节点管理(本机 + 远程 Docker 主机) ### 1.3 缺口 Docker 页面能管容器的"壳",但 **无法管理容器里的 OpenClaw**: - 无法配置某个容器内的模型 - 无法查看某个容器内的 Gateway 日志 - 无法管理某个容器内的 Agent - 聊天功能只连本机 Gateway --- ## 2. 目标架构 ### 2.1 核心思路:API 代理 + 实例切换 ``` ┌──────────────────────────────────────────────────┐ │ ClawPanel 前端 │ │ ┌────────────────────────────────────────────┐ │ │ │ 实例切换器: [ ● 本机 ▼ ] │ │ │ │ [ ○ prod-server (Docker) ] │ │ │ │ [ ○ dev-box (远程) ] │ │ │ │ [ + 添加实例 ] │ │ │ └────────────────────────────────────────────┘ │ │ │ │ 现有页面(模型/Agent/Gateway/日志/聊天...) │ │ │ │ │ │ api.readOpenclawConfig() │ │ │ api.listAgents() │ │ ▼ │ │ tauri-api.js → webInvoke('read_openclaw_config') │ │ │ │ │ 自动附带 instanceId │ └──────────────────┼───────────────────────────────┘ ▼ dev-api.js (本机后端) │ ┌────────┼────────┐ ▼ ▼ ▼ 本机文件 代理转发 代理转发 ~/.openclaw ↓ ↓ 实例 A 实例 B http://host http://192.168.1.100 :18790 :1420 /__api/* /__api/* ``` **关键点:每个 Docker 容器运行 full 镜像,内含完整的 ClawPanel (serve.js) + Gateway。** 因此每个容器已经有自己的 `/__api/*` 端点,我们只需要代理请求过去。 ### 2.2 WebSocket 连接 ``` 切换实例时: wsClient.disconnect() ← 断开旧连接 wsClient.connect(newHost, newToken) ← 连接新实例的 Gateway ``` WebSocket 连接信息从目标实例的配置中读取(通过代理 API 获取 `read_openclaw_config`)。 ### 2.3 自动组网流程 部署新容器时自动完成: ``` 用户点击「部署容器」 │ ├─ 1. Docker API 创建容器(端口映射 hostPort→1420, hostPort→18789) ├─ 2. 启动容器,等待健康检查通过 ├─ 3. 探测容器 Panel 端点:GET http://hostIP:hostPort/__api/check_installation ├─ 4. 自动写入实例注册表 ~/.openclaw/instances.json └─ 5. 前端自动刷新实例列表 ``` --- ## 3. 数据结构 ### 3.1 实例注册表 文件位置:`~/.openclaw/instances.json` ```json { "activeId": "local", "instances": [ { "id": "local", "name": "本机", "type": "local", "endpoint": null, "gatewayPort": 18789, "addedAt": 1741420800, "note": "" }, { "id": "docker-abc123", "name": "openclaw-prod", "type": "docker", "endpoint": "http://127.0.0.1:18790", "gatewayPort": 18789, "containerId": "abc123def456", "nodeId": "local", "addedAt": 1741420900, "note": "生产环境" }, { "id": "remote-1", "name": "办公室服务器", "type": "remote", "endpoint": "http://192.168.1.100:1420", "gatewayPort": 18789, "addedAt": 1741421000, "note": "" } ] } ``` **三种实例类型:** | type | 说明 | 来源 | |------|------|------| | `local` | 本机 OpenClaw | 始终存在,不可删除 | | `docker` | Docker 容器内的 OpenClaw | 部署容器时自动注册 | | `remote` | 远程服务器上的 OpenClaw | 用户手动添加 | ### 3.2 实例状态(运行时,不持久化) ```js { id: 'docker-abc123', online: true, // 健康检查结果 version: '2026.3.5', // OpenClaw 版本 gatewayRunning: true, // Gateway 状态 lastCheck: 1741420999, // 上次检查时间 } ``` --- ## 4. 改动清单 ### 4.1 后端 dev-api.js #### 4.1.1 实例注册表管理(新增) ``` 新增 handlers: instance_list → 读取 instances.json instance_add → 添加实例(手动或自动) instance_remove → 删除实例 instance_set_active → 切换活跃实例 instance_health_check → 健康检查单个实例 instance_health_all → 批量健康检查 ``` #### 4.1.2 API 代理转发(核心改动) 改造 `_apiMiddleware`: ```js // 伪代码 async function _apiMiddleware(req, res, next) { if (!req.url?.startsWith('/__api/')) return next() const cmd = extractCmd(req.url) const body = await readBody(req) // 实例管理命令 → 始终本机处理 if (cmd.startsWith('instance_') || cmd.startsWith('docker_') || ALWAYS_LOCAL.has(cmd)) { return handleLocally(cmd, body, res) } // 获取当前活跃实例 const active = getActiveInstance() if (active.type === 'local') { // 本机 → 直接处理(现有逻辑不变) return handleLocally(cmd, body, res) } // 远程/Docker 实例 → 代理转发 return proxyToInstance(active, cmd, body, res) } ``` **始终在本机处理的命令(ALWAYS_LOCAL):** - `instance_*` — 实例管理本身 - `docker_*` — Docker 容器管理 - `auth_*` — 认证 - `read_panel_config` / `write_panel_config` — 本地面板配置 - `assistant_*` — AI 助手(操作本机文件系统) **通过代理转发的命令:** - `read_openclaw_config` / `write_openclaw_config` — 目标实例的配置 - `get_services_status` / `start_service` / `stop_service` — 目标实例的服务 - `list_agents` / `add_agent` / `delete_agent` — 目标实例的 Agent - `read_log_tail` / `search_log` — 目标实例的日志 - `get_version_info` / `upgrade_openclaw` — 目标实例的版本 - `list_memory_files` / `read_memory_file` — 目标实例的记忆文件 - `read_mcp_config` / `write_mcp_config` — 目标实例的 MCP 配置 - 等其他 OpenClaw 相关命令 #### 4.1.3 代理转发实现 ```js async function proxyToInstance(instance, cmd, body, res) { const url = `${instance.endpoint}/__api/${cmd}` try { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const data = await resp.text() res.writeHead(resp.status, { 'Content-Type': 'application/json' }) res.end(data) } catch (e) { res.writeHead(502, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: `实例 ${instance.name} 不可达: ${e.message}` })) } } ``` #### 4.1.4 Docker 部署自动注册 修改 `docker_create_container` handler: - 容器创建并启动后,自动等待健康检查 - 通过 `GET http://hostIP:panelPort/__api/check_installation` 验证 - 健康检查通过后自动写入 `instances.json` - 返回结果包含 `instanceId` ### 4.2 前端 tauri-api.js #### 4.2.1 新增实例管理 API ```js // 实例管理 instanceList: () => cachedInvoke('instance_list', {}, 10000), instanceAdd: (instance) => { invalidate('instance_list'); return invoke('instance_add', instance) }, instanceRemove: (id) => { invalidate('instance_list'); return invoke('instance_remove', { id }) }, instanceSetActive: (id) => { invalidate('instance_list'); _cache.clear(); return invoke('instance_set_active', { id }) }, instanceHealthCheck: (id) => invoke('instance_health_check', { id }), instanceHealthAll: () => invoke('instance_health_all'), ``` **注意 `instanceSetActive` 清空全部缓存**,因为切换实例后所有缓存数据都过期了。 #### 4.2.2 无需改动的部分 现有的 `api.readOpenclawConfig()`、`api.listAgents()` 等方法 **完全不变**。 代理逻辑在后端 `_apiMiddleware` 层透明处理。 ### 4.3 前端 app-state.js 新增: ```js let _activeInstance = { id: 'local', name: '本机', type: 'local' } let _instanceListeners = [] export function getActiveInstance() { return _activeInstance } export function onInstanceChange(fn) { ... } export async function switchInstance(id) { // 1. 调后端切换 await api.instanceSetActive(id) // 2. 更新本地状态 _activeInstance = instances.find(i => i.id === id) // 3. 清缓存 invalidate() // 清 API 缓存 // 4. 断开旧 WebSocket wsClient.disconnect() // 5. 重新检测状态 await detectOpenclawStatus() // 6. 连接新实例的 Gateway WebSocket connectToActiveGateway() // 7. 通知所有监听者(侧边栏、页面刷新) _instanceListeners.forEach(fn => fn(_activeInstance)) } ``` ### 4.4 前端 sidebar.js 在侧边栏顶部 logo 下方添加实例切换器: ```html