docs: add cross-repo architecture chain maps and risk analysis
- Add 4 chain maps: task-execution, artifact-lifecycle, session-recovery, bridge-distributed - Add cross-repo call analysis with top-10 fragile points - Update AGENTS.md with 'Cross-Repo Architecture Chain Maps' section - Document artifact path gap: OpenClaw tools output to ~/.openclaw/media/ but plugin export scans tasks/<session>/<run>/
This commit is contained in:
parent
f8449d42e7
commit
3522bb7b99
3
.gitignore
vendored
3
.gitignore
vendored
@ -67,3 +67,6 @@ app.*.map.json
|
||||
|
||||
# Gradle artifacts (including third_party)
|
||||
**/.gradle/
|
||||
|
||||
# Repomix — dynamically generated, not committed
|
||||
repomix-output.xml
|
||||
|
||||
16
AGENTS.md
16
AGENTS.md
@ -4,6 +4,22 @@
|
||||
- For any change that touches gateway auth, `.env`, secure storage, tokens, passwords, TLS, file upload, native entitlements, packaging, or release-sensitive settings, follow the security rules in this file and [docs/security/secure-development-rules.md](docs/security/secure-development-rules.md).
|
||||
- For non-trivial implementation work, default to the worktree-first execution flow in this file without asking the user to restate that preference each time.
|
||||
|
||||
## Cross-Repo Architecture Chain Maps
|
||||
|
||||
When modifying code that crosses repo boundaries (app ↔ bridge ↔ OpenClaw ↔ plugins), consult the corresponding chain map first. Each map documents the full call flow, protocol boundaries, data structures, and known fragile points across all participating repositories.
|
||||
|
||||
Required reading before modifying:
|
||||
- **Task execution**: [chain-map-task-execution.md](docs/architecture/chain-map-task-execution.md) — full path from `sendChatMessage()` through bridge routing, OpenClaw gateway, to plugin artifact export.
|
||||
- **Artifact lifecycle**: [chain-map-artifact-lifecycle.md](docs/architecture/chain-map-artifact-lifecycle.md) — prepare → execute → export → snapshot → download → sync. Documents the critical path gap where OpenClaw tools save to `~/.openclaw/media/` or `/tmp/` instead of `tasks/<session>/<run>/`.
|
||||
- **Session recovery**: [chain-map-session-recovery.md](docs/architecture/chain-map-session-recovery.md) — app restart, bridge restart, network interruption, gateway unreachable scenarios and their state machines.
|
||||
- **Bridge distributed**: [chain-map-bridge-distributed.md](docs/architecture/chain-map-bridge-distributed.md) — primary→edge forwarding topology, session stickiness, VPN transport, hop limits.
|
||||
- **Overview**: [cross-repo-call-analysis-2026-06-05.md](docs/architecture/cross-repo-call-analysis-2026-06-05.md) — complete cross-repo module relationship map, protocol boundaries, key data structures, and top-10 fragile points.
|
||||
|
||||
When any change touches the bridge protocol, artifact paths, session state, or routing/recovery logic:
|
||||
- Verify the change against each affected chain map.
|
||||
- If the change introduces a new call path, data field, or protocol behavior, update the corresponding chain map in the same PR.
|
||||
- Pay special attention to the artifact path gap: OpenClaw tools produce output in `~/.openclaw/media/` and `/tmp/openclaw/` but the export plugin only scans `tasks/<session>/<run>/`. Any feature that adds OpenClaw tool usage must ensure outputs land in the task-scoped directory.
|
||||
|
||||
## Default Task Mode
|
||||
|
||||
- Default to an isolated `git worktree` for non-trivial tasks. Create the worktree from `main`, do the work there, merge back to `main`, then remove the temporary worktree when done.
|
||||
|
||||
201
docs/ai-context/architecture-map.md
Normal file
201
docs/ai-context/architecture-map.md
Normal file
@ -0,0 +1,201 @@
|
||||
# Architecture Map — 跨仓库架构地图
|
||||
|
||||
> 生成日期: 2026-06-05 | 三个仓库的拓扑关系和协议边界
|
||||
|
||||
---
|
||||
|
||||
## 整体拓扑
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户桌面 (localhost) │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌───────────────────────────┐ │
|
||||
│ │ xworkmate-app │ │ xworkmate-bridge │ │
|
||||
│ │ (Flutter/Dart) │ │ (Go, :8787) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌──────────────────┐ │ │ ┌───────────────────────┐ │ │
|
||||
│ │ │ AppController │ │ │ │ HTTP/WS Handler │ │ │
|
||||
│ │ │ Desktop │ │ │ │ /acp (WS) │ │ │
|
||||
│ │ └────────┬─────────┘ │ │ │ /acp/rpc (HTTP POST) │ │ │
|
||||
│ │ │ │ │ └───────────┬───────────┘ │ │
|
||||
│ │ ┌────────▼─────────┐ │ │ │ │ │
|
||||
│ │ │ GatewayRuntime │ │◄───┼── ACP ──────┘ │ │
|
||||
│ │ │ (WS + ACP Client)│ │ │ JSON-RPC 2.0 │ │
|
||||
│ │ └────────┬─────────┘ │ │ │ │
|
||||
│ │ │ │ │ ┌───────────────────────┐ │ │
|
||||
│ │ ┌────────▼─────────┐ │ │ │ Router │ │ │
|
||||
│ │ │ GoTaskService │ │ │ │ (provider selection) │ │ │
|
||||
│ │ │ (任务分派) │ │ │ └───────┬───────┬───────┘ │ │
|
||||
│ │ └──────────────────┘ │ │ │ │ │ │
|
||||
│ └──────────────────────┘ │ ┌────▼──┐ ┌──▼──────┐ │ │
|
||||
│ │ │ codex │ │ opencode│ │ │
|
||||
│ │ │(CLI) │ │ (serve) │ │ │
|
||||
│ │ └───────┘ └─────────┘ │ │
|
||||
│ │ ┌───────┐ ┌─────────┐ │ │
|
||||
│ │ │ gemini│ │ hermes │ │ │
|
||||
│ │ │(CLI) │ │ (CLI) │ │ │
|
||||
│ │ └───────┘ └─────────┘ │ │
|
||||
│ └─────────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────┼────────────────┤
|
||||
│ │ OpenClaw 网关 │
|
||||
│ │ (ws://127.0.0.1:18789) │
|
||||
│ │ / openclaw.svc.plus:443 │
|
||||
│ └─────────────────┬───────────────┘
|
||||
│ │
|
||||
│ ┌─────────────────▼───────────────┐
|
||||
│ │ openclaw-multi-session-plugins │
|
||||
│ │ (TypeScript, npm 插件) │
|
||||
│ │ │
|
||||
│ │ 网关方法: │
|
||||
│ │ xworkmate.artifacts.* │
|
||||
│ │ xworkmate.agents.run │
|
||||
│ │ │
|
||||
│ │ Agent 工具: │
|
||||
│ │ openclaw_multi_session_* │
|
||||
│ └─────────────────────────────────┘
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
云端服务
|
||||
|
||||
┌──────────────────┐ ┌──────────────────────────────┐
|
||||
│ accounts.svc.plus│ │ xworkmate-bridge.svc.plus │
|
||||
│ (账户/MFA) │ │ (托管 Bridge, 多租户) │
|
||||
└──────────────────┘ └──────────────────────────────┘
|
||||
|
||||
┌──────────────────┐
|
||||
│ ollama.svc.plus │
|
||||
│ ollama.com │
|
||||
│ (LLM 推理) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 协议边界
|
||||
|
||||
### 边界 1: App ↔ Bridge (ACP JSON-RPC)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 协议 | JSON-RPC 2.0 |
|
||||
| 传输 | WebSocket (`/acp`) + HTTP POST/SSE (`/acp/rpc`) |
|
||||
| 认证 | Bearer token (BRIDGE_AUTH_TOKEN) |
|
||||
| 方向 | App → Bridge (请求), Bridge → App (SSE 推送) |
|
||||
| 关键方法 | `session.start`, `session.message`, `acp.capabilities`, `xworkmate.gateway.*` |
|
||||
|
||||
### 边界 2: Bridge ↔ AI Providers (ACP JSON-RPC)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 协议 | JSON-RPC 2.0 |
|
||||
| 传输 | WebSocket (codex) / HTTP (opencode, gemini, hermes) |
|
||||
| 认证 | Bearer token |
|
||||
| 方向 | Bridge → Provider (请求/响应) |
|
||||
| 特殊 | gemini/hermes 通过子进程 stdio 中转(adapter 模式) |
|
||||
|
||||
### 边界 3: Bridge ↔ OpenClaw Gateway (Gateway RPC)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 协议 | 自定义 WebSocket RPC(Ed25519 加密握手) |
|
||||
| 传输 | WebSocket |
|
||||
| 默认端点 | `ws://127.0.0.1:18789` (本地) / `wss://openclaw.svc.plus:443` (云端) |
|
||||
| 关键方法 | `chat.send`, `xworkmate.artifacts.*`, `agent.wait` |
|
||||
| 方向 | 双向 (Bridge 发起请求 + 订阅推送事件) |
|
||||
|
||||
### 边界 4: OpenClaw Gateway ↔ Plugin (内部插件 API)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 协议 | OpenClaw Plugin SDK (内存调用) |
|
||||
| 方法 | `xworkmate.artifacts.prepare/export/list/read`, `xworkmate.agents.run` |
|
||||
| 工具 | `openclaw_multi_session_artifacts`, `openclaw_multi_session_agents` |
|
||||
|
||||
---
|
||||
|
||||
## 数据流方向
|
||||
|
||||
```
|
||||
用户输入
|
||||
│
|
||||
▼
|
||||
xworkmate-app ──── session.start ────► xworkmate-bridge
|
||||
│ │
|
||||
│ ├──► codex/opencode/gemini/hermes
|
||||
│ │ (AI agent 执行)
|
||||
│ │
|
||||
│ └──► OpenClaw Gateway
|
||||
│ │
|
||||
│ chat.send │
|
||||
│ ◄─────────────┘
|
||||
│ SSE stream (token by token)
|
||||
│ xworkmate.gateway.push
|
||||
◄─────────────────────────────────
|
||||
│
|
||||
│ 工件下载:
|
||||
│ GET /artifacts/openclaw/download?ref=<signed>
|
||||
├──────────────────────────────────► xworkmate-bridge
|
||||
│ │
|
||||
│ └──► xworkmate.artifacts.read
|
||||
│ │
|
||||
│ ▼
|
||||
│ openclaw-multi-session-plugins
|
||||
│
|
||||
▼
|
||||
本地文件: ~/.xworkmate/threads/<session>/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键配置项
|
||||
|
||||
### xworkmate-app (config/settings.yaml)
|
||||
```yaml
|
||||
bridge:
|
||||
server_url: "https://xworkmate-bridge.svc.plus"
|
||||
auth_token: ""
|
||||
|
||||
accounts:
|
||||
base_url: "https://accounts.svc.plus"
|
||||
```
|
||||
|
||||
### xworkmate-bridge (环境变量 / config.yaml)
|
||||
```yaml
|
||||
upstream:
|
||||
gateway_url: "ws://127.0.0.1:18789" # GATEWAY_RPC_URL
|
||||
codex_url: "" # CODEX_RPC_URL
|
||||
opencode_url: "http://127.0.0.1:38993" # OPENCODE_RPC_URL
|
||||
gemini_url: "http://127.0.0.1:8791" # GEMINI_RPC_URL
|
||||
hermes_url: "http://127.0.0.1:3920" # HERMES_RPC_URL
|
||||
|
||||
distributed_nodes:
|
||||
- id: xworkmate-bridge
|
||||
role: primary
|
||||
endpoint: "http://172.29.10.1:8787"
|
||||
- id: cn-xworkmate-bridge
|
||||
role: edge
|
||||
endpoint: "http://172.29.10.2:8787"
|
||||
```
|
||||
|
||||
### openclaw-multi-session-plugins (openclaw.plugin.json)
|
||||
```json
|
||||
{
|
||||
"gatewayMethods": [
|
||||
"xworkmate.artifacts.prepare",
|
||||
"xworkmate.artifacts.export",
|
||||
"xworkmate.artifacts.list",
|
||||
"xworkmate.artifacts.read",
|
||||
"xworkmate.agents.run"
|
||||
],
|
||||
"config": {
|
||||
"bridgeUrl": "",
|
||||
"bridgeToken": "",
|
||||
"workspaceDir": "~/.openclaw/workspace",
|
||||
"maxFiles": 1000,
|
||||
"artifactRefSigningSecret": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
271
docs/ai-context/chain-map.md
Normal file
271
docs/ai-context/chain-map.md
Normal file
@ -0,0 +1,271 @@
|
||||
# Chain Map — 跨仓库调用链
|
||||
|
||||
> 生成日期: 2026-06-05 | 文件级调用链,标注协议边界
|
||||
|
||||
---
|
||||
|
||||
## Chain 1: 用户发起 AI 对话 (主流程)
|
||||
|
||||
```
|
||||
xworkmate-app xworkmate-bridge
|
||||
───────────── ────────────────
|
||||
lib/app/app_controller_desktop.dart
|
||||
→ handleChatSend()
|
||||
└─ lib/runtime/gateway_runtime_core.dart
|
||||
GatewayRuntime.sendMessage()
|
||||
│
|
||||
├── (ACP WebSocket 路径)
|
||||
│ └─ lib/runtime/gateway_acp_client.dart
|
||||
│ GatewayAcpClient.request(method: "session.start", ...)
|
||||
│ → WebSocket /acp ──────────────────► internal/acp/http_handler.go
|
||||
│ HandleWebSocket()
|
||||
│ └─ internal/acp/rpc_handler.go
|
||||
│ handleRequest("session.start", ...)
|
||||
│ └─ internal/router/
|
||||
│ Resolve(provider)
|
||||
│ └─ Provider handler
|
||||
│
|
||||
└── (ACP HTTP/SSE 路径)
|
||||
└─ lib/runtime/gateway_acp_client.dart
|
||||
GatewayAcpClient.request()
|
||||
→ HTTP POST /acp/rpc ──────────────► internal/acp/http_handler.go
|
||||
HandleRPC()
|
||||
└─ internal/acp/rpc_handler.go
|
||||
handleRequest(...)
|
||||
```
|
||||
|
||||
### 涉及的 key files:
|
||||
| 层 | 文件 | 作用 |
|
||||
|----|------|------|
|
||||
| app | `lib/app/app_controller_desktop.dart` | 用户输入入口 |
|
||||
| app | `lib/runtime/gateway_runtime_core.dart` | Gateway runtime 核心 |
|
||||
| app | `lib/runtime/gateway_acp_client.dart` | ACP 客户端 (JSON-RPC 封装) |
|
||||
| app | `lib/runtime/acp_endpoint_paths.dart` | 端点路径解析 |
|
||||
| bridge | `internal/acp/http_handler.go` | HTTP/WS 请求入口 |
|
||||
| bridge | `internal/acp/rpc_handler.go` | JSON-RPC 方法分发 |
|
||||
| bridge | `internal/router/` | 提供商路由 |
|
||||
|
||||
### 协议: ACP JSON-RPC 2.0 over WebSocket (主) / HTTP SSE (后备)
|
||||
### 断点风险:
|
||||
- ACP 客户端 120s 超时 → 长任务可能超时
|
||||
- SSE 流中断后降级为轮询 (`xworkmate.tasks.get`)
|
||||
- WebSocket 断线需重连 (30s ping/10s 超时)
|
||||
|
||||
---
|
||||
|
||||
## Chain 2: OpenClaw 网关任务执行
|
||||
|
||||
```
|
||||
xworkmate-app xworkmate-bridge
|
||||
───────────── ────────────────
|
||||
lib/runtime/external_code_agent_acp_desktop_transport.dart
|
||||
ExternalCodeAgentAcpTransport
|
||||
→ session.start(provider="gateway")
|
||||
└─ lib/runtime/gateway_acp_client.dart
|
||||
→ POST /gateway/openclaw ────────────────► internal/acp/http_handler.go
|
||||
│ HandleOpenClawRPC()
|
||||
│ └─ internal/acp/gateway.go
|
||||
│ Orchestrator.Process()
|
||||
│ │
|
||||
│ ├─ 本地 gateway
|
||||
│ │ └─ internal/gatewayruntime/runtime.go
|
||||
│ │ GatewayRuntime.Connect()
|
||||
│ │ → WebSocket ────► OpenClaw Gateway
|
||||
│ │ ws://127.0.0.1:18789
|
||||
│ │ │
|
||||
│ │ ├─ chat.send
|
||||
│ │ ├─ agent.wait
|
||||
│ │ └─ xworkmate.artifacts.*
|
||||
│ │ │
|
||||
│ │ ▼
|
||||
│ │ openclaw-multi-session-plugins
|
||||
│ │ ─────────────────────────────
|
||||
│ │ index.ts → register()
|
||||
│ │ xworkmate.artifacts.prepare
|
||||
│ │ xworkmate.artifacts.export
|
||||
│ │ xworkmate.artifacts.read
|
||||
│ │
|
||||
│ └─ 分布式转发
|
||||
│ └─ internal/acp/distributed_forwarder.go
|
||||
│ ForwardToPeer()
|
||||
│ → HTTP POST → 远端 bridge /acp/rpc
|
||||
│
|
||||
◄── SSE stream (token by token) ────────────
|
||||
◄── xworkmate.gateway.push (chat events) ───
|
||||
|
||||
lib/app/app_controller_openclaw_task_queue.dart
|
||||
OpenClawTaskQueue
|
||||
→ 本地队列管理 (max 5 active, 20 queued)
|
||||
→ 持久化 & 恢复 (pollOpenClawTaskAssociationInternal)
|
||||
```
|
||||
|
||||
### 涉及的 key files:
|
||||
| 层 | 文件 | 作用 |
|
||||
|----|------|------|
|
||||
| app | `lib/runtime/external_code_agent_acp_desktop_transport.dart` | 任务传输层 |
|
||||
| app | `lib/runtime/go_task_service_client.dart` | 任务接口定义 |
|
||||
| app | `lib/runtime/go_task_service_desktop_service.dart` | 桌面任务服务 |
|
||||
| app | `lib/app/app_controller_openclaw_task_queue.dart` | 客户端任务队列 |
|
||||
| bridge | `internal/acp/gateway.go` | OpenClaw 集成 |
|
||||
| bridge | `internal/acp/http_handler.go` | `/gateway/openclaw` 端点 |
|
||||
| bridge | `internal/gatewayruntime/runtime.go` | 网关 WS 客户端 |
|
||||
| bridge | `internal/acp/distributed_forwarder.go` | 分布式转发 |
|
||||
| plugins | `index.ts` | 插件入口 |
|
||||
| plugins | `src/exportArtifacts.ts` | 工件逻辑 |
|
||||
|
||||
### 协议: ACP JSON-RPC → Gateway RPC (WebSocket, Ed25519 握手)
|
||||
### 断点风险:
|
||||
- 网关 WebSocket 断连 → 任务丢失
|
||||
- 分布式转发 hop=3 限制 → 深层拓扑不可达
|
||||
- 任务轮询恢复依赖 `xworkmate.tasks.get` → 非实时
|
||||
|
||||
---
|
||||
|
||||
## Chain 3: 工件下载流
|
||||
|
||||
```
|
||||
xworkmate-app xworkmate-bridge
|
||||
───────────── ────────────────
|
||||
lib/app/app_controller_desktop_thread_storage.dart
|
||||
syncArtifactsFromBridge()
|
||||
→ GET /artifacts/openclaw/download ───────────► internal/acp/http_handler.go
|
||||
?ref=<signed>&t=<expiry> HandleArtifactDownload()
|
||||
└─ internal/gatewayruntime/runtime.go
|
||||
gateway.RequestByMode(
|
||||
"openclaw",
|
||||
"xworkmate.artifacts.read",
|
||||
params
|
||||
)
|
||||
│
|
||||
▼
|
||||
openclaw-multi-session-plugins
|
||||
─────────────────────────────
|
||||
src/exportArtifacts.ts
|
||||
xworkmate.artifacts.read()
|
||||
→ 验证 HMAC 签名
|
||||
→ 读取文件内容
|
||||
→ 返回 artifact
|
||||
```
|
||||
|
||||
### 涉及的 key files:
|
||||
| 层 | 文件 | 作用 |
|
||||
|----|------|------|
|
||||
| app | `lib/app/app_controller_desktop_thread_storage.dart` | 工件同步 |
|
||||
| bridge | `internal/acp/http_handler.go` | 下载端点 |
|
||||
| bridge | `internal/gatewayruntime/runtime.go` | 网关调用 |
|
||||
| plugins | `src/exportArtifacts.ts` | read 逻辑 |
|
||||
|
||||
### 安全: HMAC-SHA256 签名绑定 (workspaceRoot, session, run, path, size, hash)
|
||||
### 断点风险:
|
||||
- 签名过期 (24h) → 下载失败
|
||||
- 签名密钥不一致 → 验证失败
|
||||
|
||||
---
|
||||
|
||||
## Chain 4: MCP 配置生成
|
||||
|
||||
```
|
||||
xworkmate-app xworkmate-bridge
|
||||
───────────── ────────────────
|
||||
lib/runtime/codex_config_bridge.dart
|
||||
CodexConfigBridge.generate()
|
||||
→ 写入 ~/.codex/config.toml
|
||||
[mcp_servers.xworkmate]
|
||||
command = "openclaw-mcp"
|
||||
args = ["--gateway", "https://xworkmate-bridge.svc.plus"]
|
||||
# BEGIN XWORKMATE MANAGED MCP BLOCK
|
||||
...
|
||||
# END XWORKMATE MANAGED MCP BLOCK
|
||||
|
||||
lib/runtime/opencode_config_bridge.dart
|
||||
OpencodeConfigBridge.generate()
|
||||
→ 写入 ~/.opencode/config.toml
|
||||
[mcp_servers.xworkmate]
|
||||
url = "https://xworkmate-bridge.svc.plus/acp"
|
||||
# 或 type="stdio" + command="openclaw-mcp" + args=["--gateway", url]
|
||||
```
|
||||
|
||||
### 涉及的 key files:
|
||||
| 层 | 文件 | 作用 |
|
||||
|----|------|------|
|
||||
| app | `lib/runtime/codex_config_bridge.dart` | Codex CLI 配置 |
|
||||
| app | `lib/runtime/opencode_config_bridge.dart` | OpenCode CLI 配置 |
|
||||
| bridge | `internal/mounts/reconcile.go` | MCP 配置管理 (服务端) |
|
||||
|
||||
### 断点风险:
|
||||
- 配置 block 标记 (`# BEGIN XWORKMATE MANAGED MCP BLOCK`) 冲突 → 覆盖用户配置
|
||||
- Gateway URL 变更 → 需重新生成配置
|
||||
|
||||
---
|
||||
|
||||
## Chain 5: 多 Agent 编排 (Plugin → Bridge 反向调用)
|
||||
|
||||
```
|
||||
openclaw-multi-session-plugins xworkmate-bridge
|
||||
───────────────────────────── ────────────────
|
||||
src/bridgeAgents.ts
|
||||
run(input)
|
||||
→ fetch(bridgeUrl, {
|
||||
method: "POST",
|
||||
body: { jsonrpc: "2.0",
|
||||
method: "session.start",
|
||||
params: {
|
||||
sessionId: "openclaw:<sessionKey>",
|
||||
multiAgent: true,
|
||||
mode: "multi-agent",
|
||||
routing: { orchestrationMode, steps, ... }
|
||||
}
|
||||
}
|
||||
})
|
||||
→ HTTP POST /acp/rpc ──────────────────────► internal/acp/rpc_handler.go
|
||||
handleRequest("session.start", ...)
|
||||
└─ internal/router/
|
||||
→ Orchestrator.Process()
|
||||
→ 多 agent 协作
|
||||
→ 结果返回插件
|
||||
|
||||
src/exportArtifacts.ts
|
||||
→ 结果写入 artifactDirectory
|
||||
→ multi-agent-result.json
|
||||
→ multi-agent-result.md
|
||||
```
|
||||
|
||||
### 涉及的 key files:
|
||||
| 层 | 文件 | 作用 |
|
||||
|----|------|------|
|
||||
| plugins | `src/bridgeAgents.ts` | Bridge HTTP 调用 |
|
||||
| bridge | `internal/acp/rpc_handler.go` | RPC 分发 |
|
||||
| bridge | `internal/acp/orchestrator.go` | 多 agent 编排 |
|
||||
|
||||
### 协议: HTTP JSON-RPC (插件 → Bridge)
|
||||
### 断点风险:
|
||||
- 插件配置中 bridgeUrl 指向错误 → 调用失败
|
||||
- 双向循环依赖: plugins 调 bridge, bridge 调 plugins
|
||||
|
||||
---
|
||||
|
||||
## 跨仓库调用矩阵
|
||||
|
||||
```
|
||||
调用方
|
||||
app bridge plugins
|
||||
┌───────┬───────┬───────┐
|
||||
app │ - │ ACP │ - │
|
||||
被调 bridge │ - │ - │ HTTP │
|
||||
方 plugins │ - │ GW │ - │
|
||||
└───────┴───────┴───────┘
|
||||
|
||||
ACP = JSON-RPC 2.0 over WebSocket/HTTP SSE
|
||||
HTTP = JSON-RPC 2.0 over HTTP POST
|
||||
GW = OpenClaw Gateway RPC over WebSocket (Ed25519)
|
||||
```
|
||||
|
||||
## 调用链复杂度评分
|
||||
|
||||
| Chain | 跨仓库跳数 | 协议变换 | 风险等级 |
|
||||
|-------|-----------|---------|---------|
|
||||
| Chain 1 (AI 对话) | 2 (app→bridge→provider) | 1 (ACP) | **中** |
|
||||
| Chain 2 (OpenClaw 任务) | 4 (app→bridge→gateway→plugins) | 2 (ACP + GW RPC) | **高** |
|
||||
| Chain 3 (工件下载) | 3 (app→bridge→gateway→plugins) | 2 (HTTPS + GW RPC) | **中** |
|
||||
| Chain 4 (MCP 配置) | 1 (app→本地文件) | 0 | **低** |
|
||||
| Chain 5 (多 Agent) | 2 (plugins→bridge→provider) + 2 (bridge→gateway→plugins) | 2 (HTTP + GW RPC) | **高/循环** |
|
||||
327
docs/ai-context/module-boundary.md
Normal file
327
docs/ai-context/module-boundary.md
Normal file
@ -0,0 +1,327 @@
|
||||
# Module Boundary — 模块边界与接口契约
|
||||
|
||||
> 生成日期: 2026-06-05 | 跨仓库接口定义、协议规范、安全边界
|
||||
|
||||
---
|
||||
|
||||
## 1. App ↔ Bridge: ACP 协议边界
|
||||
|
||||
### 端点定义
|
||||
|
||||
```
|
||||
# WebSocket (主通道)
|
||||
ws://<bridge-host>:8787/acp
|
||||
|
||||
# HTTP RPC (后备通道)
|
||||
POST https://<bridge-host>/acp/rpc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <BRIDGE_AUTH_TOKEN>
|
||||
```
|
||||
|
||||
### JSON-RPC 2.0 请求格式
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "<request-id>",
|
||||
"method": "<method-name>",
|
||||
"params": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 方法清单
|
||||
|
||||
| 方法 | 方向 | 描述 | 调用方文件 |
|
||||
|------|------|------|-----------|
|
||||
| `health` | App→Bridge | 健康检查 | `gateway_runtime_core.dart` |
|
||||
| `acp.capabilities` | App→Bridge | 查询提供商能力 | `gateway_acp_client.dart` |
|
||||
| `session.start` | App→Bridge | 启动 AI 会话 | `external_code_agent_acp_desktop_transport.dart` |
|
||||
| `session.message` | App→Bridge | 发送会话消息 | 同上 |
|
||||
| `session.cancel` | App→Bridge | 取消会话 | 同上 |
|
||||
| `session.close` | App→Bridge | 关闭会话 | 同上 |
|
||||
| `xworkmate.gateway.connect` | App→Bridge | 连接远程网关 | `gateway_runtime_session_client.dart` |
|
||||
| `xworkmate.gateway.request` | App→Bridge | 网关请求 | 同上 |
|
||||
| `xworkmate.gateway.disconnect` | App→Bridge | 断开网关 | 同上 |
|
||||
| `xworkmate.routing.resolve` | App→Bridge | 解析路由 | `gateway_acp_client.dart` |
|
||||
| `xworkmate.tasks.get` | App→Bridge | 查询任务状态 | `external_code_agent_acp_desktop_transport.dart` |
|
||||
| `xworkmate.tasks.cancel` | App→Bridge | 取消任务 | 同上 |
|
||||
| `xworkmate.desktop.offer` | App→Bridge | WebRTC SDP offer | bridge `rpc_handler.go` |
|
||||
| `xworkmate.jobs.*` | App→Bridge | 后台作业管理 | bridge `rpc_handler.go` |
|
||||
| `system.logs` | App→Bridge | 获取系统日志 | bridge `rpc_handler.go` |
|
||||
|
||||
### SSE 推送事件
|
||||
|
||||
| 事件 | 方向 | 描述 |
|
||||
|------|------|------|
|
||||
| `xworkmate.gateway.snapshot` | Bridge→App | 网关状态快照 |
|
||||
| `xworkmate.gateway.log` | Bridge→App | 网关日志 |
|
||||
| `xworkmate.gateway.push` | Bridge→App | 网关推送 (chat.run 等) |
|
||||
|
||||
### 认证
|
||||
|
||||
- Bridge 验证 `Authorization: Bearer <token>` 头
|
||||
- Bridge 验证 `Origin` 头 (白名单: `https://xworkmate.svc.plus`, `http://localhost:*`, `http://127.0.0.1:*`)
|
||||
- SSL 证书错误自动重试 (最多 5 次)
|
||||
|
||||
### 超时与重试
|
||||
|
||||
- 请求超时: 120s
|
||||
- TLS 握手失败: 最多 5 次重试
|
||||
- 连接超时: 最多 2 次重试
|
||||
- WebSocket: 30s ping 间隔, 10s 连接超时
|
||||
|
||||
---
|
||||
|
||||
## 2. Bridge ↔ AI Providers: 提供商适配器边界
|
||||
|
||||
### 通用协议
|
||||
|
||||
所有提供商通过 ACP JSON-RPC 2.0 通信。Bridge 作为客户端,Provider 作为服务端。
|
||||
|
||||
```
|
||||
Bridge ── HTTP POST / WebSocket ──► Provider (codex/opencode/gemini/hermes)
|
||||
Headers: Authorization: Bearer <BRIDGE_AUTH_TOKEN>
|
||||
```
|
||||
|
||||
### Codex 适配器
|
||||
|
||||
```
|
||||
传输: WebSocket (主) / HTTP (后备)
|
||||
配置: CODEX_RPC_URL
|
||||
认证: Bearer token
|
||||
```
|
||||
|
||||
### OpenCode 适配器
|
||||
|
||||
```
|
||||
传输: HTTP
|
||||
端点: http://127.0.0.1:38993
|
||||
启动: bridge 启动 opencode serve 子进程
|
||||
配置: OPENCODE_RPC_URL
|
||||
```
|
||||
|
||||
### Gemini 适配器
|
||||
|
||||
```
|
||||
传输: HTTP
|
||||
端点: http://127.0.0.1:8791
|
||||
启动: bridge 通过 stdio 启动 gemini CLI 子进程
|
||||
→ adapter 将 stdio JSON-RPC 转换为 HTTP
|
||||
配置: GEMINI_RPC_URL
|
||||
```
|
||||
|
||||
### Hermes 适配器
|
||||
|
||||
```
|
||||
传输: HTTP
|
||||
端点: http://127.0.0.1:3920
|
||||
启动: bridge 通过 stdio 启动 hermes CLI 子进程
|
||||
→ adapter 将 stdio JSON-RPC 转换为 HTTP
|
||||
配置: HERMES_RPC_URL
|
||||
```
|
||||
|
||||
### 提供商选择逻辑
|
||||
|
||||
Bridge 的 Router 模块根据以下因素选择提供商:
|
||||
1. 请求中的 `routing.provider` 参数
|
||||
2. 提供商可用性 (health check)
|
||||
3. 会话亲和性 (session 绑定到特定 provider)
|
||||
|
||||
---
|
||||
|
||||
## 3. Bridge ↔ OpenClaw Gateway: 网关 RPC 边界
|
||||
|
||||
### 连接
|
||||
|
||||
```
|
||||
Bridge ── WebSocket ──► OpenClaw Gateway
|
||||
ws://127.0.0.1:18789 (本地)
|
||||
wss://openclaw.svc.plus:443 (云端)
|
||||
|
||||
Headless 模式: 无 SSL, 直接 WS 连接
|
||||
```
|
||||
|
||||
### 认证 (Ed25519 加密握手)
|
||||
|
||||
1. Bridge 生成 Ed25519 密钥对
|
||||
2. Bridge 发送 `connect` 消息,附带公钥和设备标识
|
||||
3. Gateway 返回加密 challenge
|
||||
4. Bridge 使用私钥签名 challenge
|
||||
5. Gateway 验证签名 → 建立可信连接
|
||||
|
||||
### 设备配对 (可选)
|
||||
|
||||
```
|
||||
Bridge → Gateway: device.pair.request
|
||||
Gateway → Bridge (push): device.pair.approval_pending
|
||||
App → Bridge → Gateway: device.pair.approve / device.pair.reject
|
||||
```
|
||||
|
||||
### 网关 RPC 方法
|
||||
|
||||
| 方法 | 方向 | 描述 |
|
||||
|------|------|------|
|
||||
| `connect` | Bridge→Gateway | 认证连接 |
|
||||
| `chat.send` | Bridge→Gateway | 发送 agent 执行请求 |
|
||||
| `agent.wait` | Bridge→Gateway | 等待 agent 完成 |
|
||||
| `health` | Bridge→Gateway | 健康检查 |
|
||||
| `skills.status` | Bridge→Gateway | 技能状态 |
|
||||
| `channels.status` | Bridge→Gateway | 通道状态 |
|
||||
| `models.list` | Bridge→Gateway | 模型列表 |
|
||||
| `cron.list` | Bridge→Gateway | 定时任务列表 |
|
||||
| `system-presence` | Bridge→Gateway | 系统在线状态 |
|
||||
| `xworkmate.artifacts.prepare` | Bridge→Gateway→Plugin | 工件准备 |
|
||||
| `xworkmate.artifacts.export` | Bridge→Gateway→Plugin | 工件导出 |
|
||||
| `xworkmate.artifacts.list` | Bridge→Gateway→Plugin | 工件列表 |
|
||||
| `xworkmate.artifacts.read` | Bridge→Gateway→Plugin | 工件读取 |
|
||||
|
||||
### 推送事件 (Gateway→Bridge)
|
||||
|
||||
| 事件 | 描述 |
|
||||
|------|------|
|
||||
| `chat.run` | Agent 执行进度事件 |
|
||||
| `chat.error` | Agent 执行错误 |
|
||||
| `health` | 网关健康状态 |
|
||||
| `device.pair.approval_pending` | 设备配对请求 |
|
||||
| `device.pair.update` | 设备配对状态变更 |
|
||||
|
||||
---
|
||||
|
||||
## 4. OpenClaw Gateway ↔ Plugin: 插件 API 边界
|
||||
|
||||
### 插件注册 (openclaw.plugin.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "openclaw-multi-session-plugins",
|
||||
"version": "0.1.15",
|
||||
"openclaw": {
|
||||
"extensions": ["./dist/index.js"]
|
||||
},
|
||||
"gatewayMethods": [
|
||||
"xworkmate.artifacts.prepare",
|
||||
"xworkmate.artifacts.export",
|
||||
"xworkmate.artifacts.list",
|
||||
"xworkmate.artifacts.read",
|
||||
"xworkmate.agents.run"
|
||||
],
|
||||
"tools": {
|
||||
"openclaw_multi_session_artifacts": { "sessionScoped": true },
|
||||
"openclaw_multi_session_agents": { "sessionScoped": true }
|
||||
},
|
||||
"config": {
|
||||
"workspaceDir": "~/.openclaw/workspace",
|
||||
"maxFiles": 1000,
|
||||
"maxInlineBytes": 1048576,
|
||||
"artifactRefSigningSecret": "",
|
||||
"bridgeUrl": "",
|
||||
"bridgeToken": "",
|
||||
"bridgeTimeoutMs": 120000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网关方法契约
|
||||
|
||||
| 方法 | 输入 | 输出 |
|
||||
|------|------|------|
|
||||
| `xworkmate.artifacts.prepare` | `{sessionKey, runId, workspaceDir?}` | `{artifactScope, artifactDirectory, scopeKind}` |
|
||||
| `xworkmate.artifacts.export` | `{sessionKey, runId, artifactScope?, sinceUnixMs?, ...}` | `{artifacts[], manifestMarkdown, warnings[]}` |
|
||||
| `xworkmate.artifacts.list` | `{sessionKey, runId, ...}` | `{artifacts[] (不含内容), manifestMarkdown}` |
|
||||
| `xworkmate.artifacts.read` | `{sessionKey, runId, artifactScope?, relativePath?, artifactRef?}` | `{artifacts[0], manifestMarkdown}` |
|
||||
| `xworkmate.agents.run` | `{sessionKey, runId, taskPrompt, mode, steps?, participants?, ...}` | `{bridgeResult, artifacts[]}` |
|
||||
|
||||
### 安全边界
|
||||
|
||||
```
|
||||
artifactRef = HMAC-SHA256(workspaceRoot, sessionKey, runId, relativePath, size, sha256)
|
||||
|
||||
验证条件:
|
||||
1. artifactRef 签名有效 (HMAC 密钥匹配)
|
||||
2. artifactRef 未过期 (24h TTL)
|
||||
3. path 在 workspaceRoot 内 (无路径穿越)
|
||||
4. path 非符号链接
|
||||
5. artifactScope 与 sessionKey/runId 匹配 (无跨会话借用)
|
||||
|
||||
跳过目录: .git, .openclaw, .xworkmate, node_modules
|
||||
忽略文件: .gitignore, artifact-ignore.md 匹配
|
||||
```
|
||||
|
||||
### Agent 工具边界
|
||||
|
||||
```
|
||||
工具: openclaw_multi_session_artifacts
|
||||
输入: { action: "list"|"read", artifactScope?, relativePath? }
|
||||
输出: { artifacts[]|manifestMarkdown }
|
||||
安全: sessionKey/runId 由 OpenClaw 运行时注入,
|
||||
Agent 无法覆盖 (在 tool factory 中解构排除)
|
||||
|
||||
工具: openclaw_multi_session_agents
|
||||
输入: { taskPrompt, mode, steps?, participants?, maxTurns? }
|
||||
输出: { result, artifacts[] }
|
||||
安全: sessionKey/runId 同样由运行时注入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Plugin → Bridge 反向调用边界
|
||||
|
||||
### HTTP JSON-RPC 调用
|
||||
|
||||
```
|
||||
Plugin (bridgeAgents.ts)
|
||||
→ POST {bridgeUrl}/acp/rpc
|
||||
Headers:
|
||||
Authorization: Bearer <bridgeToken>
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "openclaw-<timestamp>",
|
||||
"method": "session.start",
|
||||
"params": {
|
||||
"sessionId": "openclaw:<sessionKey>",
|
||||
"threadId": "<sessionKey>",
|
||||
"taskPrompt": "...",
|
||||
"workingDirectory": "<artifactDirectory>",
|
||||
"multiAgent": true,
|
||||
"mode": "multi-agent",
|
||||
"routing": {
|
||||
"orchestrationMode": "...",
|
||||
"steps": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
```
|
||||
环境变量:
|
||||
XWORKMATE_BRIDGE_URL — bridge 基础 URL (自动追加 /acp/rpc)
|
||||
XWORKMATE_BRIDGE_TOKEN — bridge 认证 token
|
||||
|
||||
插件配置 (openclaw.plugin.json):
|
||||
bridgeUrl: "https://xworkmate-bridge.svc.plus"
|
||||
bridgeToken: "..."
|
||||
bridgeTimeoutMs: 120000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 边界脆弱点汇总
|
||||
|
||||
| 边界 | 脆弱点 | 严重程度 |
|
||||
|------|--------|---------|
|
||||
| App↔Bridge | SSE 流中断 → 降级为轮询 | **中** |
|
||||
| App↔Bridge | 120s 超时对长任务不够 | **中** |
|
||||
| App↔Bridge | WebSocket 断线重连无消息队列持久化 | **中** |
|
||||
| Bridge↔Provider | Gemini/Hermes 子进程崩溃 | **高** |
|
||||
| Bridge↔Provider | Codex MCP 配置注入冲突 | **中** |
|
||||
| Bridge↔Gateway | WebSocket 断连 → 任务状态丢失 | **高** |
|
||||
| Bridge↔Gateway | Ed25519 密钥轮换无自动化 | **低** |
|
||||
| Gateway↔Plugin | artifactRef 签名密钥不一致 | **高** |
|
||||
| Gateway↔Plugin | 24h 签名过期 → 历史工件不可读 | **中** |
|
||||
| Plugin→Bridge | bridgeUrl/bridgeToken 配置错误 | **高** |
|
||||
| Plugin→Bridge | 循环依赖 (plugin 调 bridge, bridge 调 plugin) | **高** |
|
||||
| 全部 | 多仓库版本耦合 (app 1.1.4 + bridge latest + plugins 0.1.15) | **中** |
|
||||
290
docs/ai-context/refactor-notes.md
Normal file
290
docs/ai-context/refactor-notes.md
Normal file
@ -0,0 +1,290 @@
|
||||
# Refactor Notes — 重构建议与架构审核
|
||||
|
||||
> 生成日期: 2026-06-05 | 基于 chain-map 和 module-boundary 的架构分析
|
||||
|
||||
---
|
||||
|
||||
## 总体评估
|
||||
|
||||
三个仓库形成了一个**清晰的 3 层架构**:App → Bridge → (Providers + Gateway + Plugins)。架构在职责拆分上是合理的,但在以下几个方面存在可改进的空间:
|
||||
|
||||
1. **循环依赖**: Plugin → Bridge (HTTP) + Bridge → Plugin (Gateway RPC) 形成调用环
|
||||
2. **协议层过多**: Chain 2 经过 4 层跳转 (App → Bridge → Gateway → Plugin)
|
||||
3. **容错不足**: 多处依赖未处理的状态丢失场景
|
||||
4. **配置分散**: App/Bridge/Plugin 各自维护连接配置,缺乏统一管理
|
||||
|
||||
---
|
||||
|
||||
## 问题 1: Plugin ↔ Bridge 循环依赖
|
||||
|
||||
### 现状
|
||||
|
||||
```
|
||||
Bridge → (Gateway RPC) → Plugin (xworkmate.artifacts.*)
|
||||
Plugin → (HTTP JSON-RPC) → Bridge (session.start, multiAgent)
|
||||
```
|
||||
|
||||
### 问题
|
||||
|
||||
- 两个方向使用不同协议 (Gateway RPC vs HTTP JSON-RPC),增加调试难度
|
||||
- 循环引用导致: Bridge 故障 → Plugin 不可用 → Bridge 的 agent 调用失败 → Plugin 的 bridgeAgents 也无法工作
|
||||
- 版本升级需要同步两个方向
|
||||
|
||||
### 建议
|
||||
|
||||
**方案 A (推荐): 统一为单向调用**
|
||||
|
||||
将 Plugin 中的 `bridgeAgents.ts` 功能移到 Bridge 内部:
|
||||
|
||||
```
|
||||
Bridge
|
||||
internal/
|
||||
acp/
|
||||
orchestrator.go ← 集成当前 bridgeAgents 逻辑
|
||||
gateway.go ← 保留 xworkmate.artifacts.* 调用
|
||||
```
|
||||
|
||||
Plugin 变为纯工件管理 (只被调,不反向调用):
|
||||
|
||||
```
|
||||
Plugin (简化后)
|
||||
src/exportArtifacts.ts ← 只保留 prepare/export/list/read
|
||||
删除 src/bridgeAgents.ts ← 移到 Bridge
|
||||
```
|
||||
|
||||
**方案 B: 统一协议方向**
|
||||
|
||||
Plugin ↔ Bridge 全部走 Gateway RPC (去掉 Plugin 中的 HTTP 调用):
|
||||
|
||||
- Bridge 新增 `xworkmate.bridge.*` 网关方法供 Plugin 调用
|
||||
- Plugin 通过 `api.callGatewayMethod()` 而非 `fetch()` 调用 Bridge
|
||||
|
||||
### 影响范围
|
||||
|
||||
- 方案 A: 改动 Bridge 内部 + 删除 Plugin 的 bridgeAgents.ts
|
||||
- 方案 B: 改动 Bridge (新增网关方法) + Plugin (改用网关调用)
|
||||
|
||||
---
|
||||
|
||||
## 问题 2: Chain 2 协议层过多
|
||||
|
||||
### 现状
|
||||
|
||||
```
|
||||
App ──(ACP/WS)──► Bridge ──(GW RPC/WS)──► Gateway ──(Plugin API)──► Plugin
|
||||
跳数: 4 协议变换: 2 次
|
||||
```
|
||||
|
||||
### 问题
|
||||
|
||||
- 每层增加延迟和故障点
|
||||
- 错误信息层层包装,难以定位根因
|
||||
- Gateway 层是性能瓶颈 (WebSocket 单连接复用)
|
||||
|
||||
### 建议
|
||||
|
||||
**短期优化**:
|
||||
|
||||
在 Bridge 中增加 Gateway 连接池(当前为单连接):
|
||||
```go
|
||||
// gatewayruntime/pool.go (新增)
|
||||
type GatewayPool struct {
|
||||
conns []*GatewayRuntime
|
||||
maxSize int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *GatewayPool) Acquire() *GatewayRuntime { ... }
|
||||
func (p *GatewayPool) Release(rt *GatewayRuntime) { ... }
|
||||
```
|
||||
|
||||
**中期优化**:
|
||||
|
||||
Bridge 缓存工件元数据,减少实时 Gateway 调用:
|
||||
|
||||
```go
|
||||
// internal/acp/artifact_cache.go (新增)
|
||||
type ArtifactCache struct {
|
||||
cache map[string]*CachedArtifact
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// 命中缓存时跳过 Gateway→Plugin 调用
|
||||
func (c *ArtifactCache) GetOrFetch(sessionKey, runId string) { ... }
|
||||
```
|
||||
|
||||
**长期考虑**:
|
||||
|
||||
如果 Plugin 始终与 Bridge 在同一主机,考虑内嵌 Plugin 为 Bridge 内部模块(避免 Gateway RPC 中转)。
|
||||
|
||||
---
|
||||
|
||||
## 问题 3: 容错与恢复
|
||||
|
||||
### 3.1 SSE 流中断降级为轮询
|
||||
|
||||
**现状** (`external_code_agent_acp_desktop_transport.dart`):
|
||||
```
|
||||
SSE 流中断 → 降级为 xworkmate.tasks.get 轮询 → 非实时
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- 在 Bridge 中维护任务状态的增量日志,支持断点续传
|
||||
- App 重连时发送 `session.resume` 而非重新 `session.start`
|
||||
|
||||
### 3.2 Gateway WebSocket 断连 → 任务状态丢失
|
||||
|
||||
**现状** (`gatewayruntime/runtime.go`): 断连后未持久化任务状态。
|
||||
|
||||
**建议**:
|
||||
- Bridge 中维护 `tasks` map,断连时标记为 `STALE`
|
||||
- 重连后自动查询任务最新状态
|
||||
- 超过 TTL 的 STALE 任务发出 `session.cancel` 通知 App
|
||||
|
||||
### 3.3 Gemini/Hermes 子进程崩溃
|
||||
|
||||
**现状**: 子进程崩溃后无自动重启。
|
||||
|
||||
**建议**:
|
||||
```go
|
||||
// internal/geminiadapter/process_manager.go (增强)
|
||||
type ProcessManager struct {
|
||||
cmd *exec.Cmd
|
||||
restartMax int // 最大重启次数
|
||||
backoff time.Duration // 退避策略
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) Start() {
|
||||
for i := 0; i < pm.restartMax; i++ {
|
||||
if err := pm.run(); err != nil {
|
||||
time.Sleep(pm.backoff * time.Duration(1<<i))
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 问题 4: 配置管理分散
|
||||
|
||||
### 现状
|
||||
|
||||
| 组件 | 配置位置 | 管理方式 |
|
||||
|------|---------|---------|
|
||||
| App | `config/settings.yaml` | 本地文件 |
|
||||
| App | `config/feature_flags.yaml` | 本地文件 |
|
||||
| Bridge | 环境变量 / `config.yaml` | Ansible 部署 |
|
||||
| Plugin | `openclaw.plugin.json` / 环境变量 | 插件清单 |
|
||||
|
||||
### 问题
|
||||
|
||||
- `bridgeUrl` 和 `bridgeToken` 在 App/Bridge/Plugin 三处独立配置
|
||||
- 配置不一致导致调试困难
|
||||
- 无版本化配置管理
|
||||
|
||||
### 建议
|
||||
|
||||
**集中配置源**: 使用 accounts.svc.plus 作为配置中心。
|
||||
|
||||
```
|
||||
accounts.svc.plus
|
||||
GET /api/config/bridge → { serverUrl, authToken, providers... }
|
||||
GET /api/config/plugin → { bridgeUrl, bridgeToken, workspaceDir... }
|
||||
```
|
||||
|
||||
**本地配置缓存**:
|
||||
- App 首次启动从 accounts 拉取配置
|
||||
- Bridge 启动时从 accounts 拉取 providers 配置
|
||||
- Plugin 从 OpenClaw 网关配置中继承 bridge 连接信息
|
||||
|
||||
---
|
||||
|
||||
## 问题 5: 无版本兼容性检查
|
||||
|
||||
### 现状
|
||||
|
||||
三个仓库独立发布,无版本契约:
|
||||
- xworkmate-app: 1.1.4
|
||||
- xworkmate-bridge: (无显式版本)
|
||||
- openclaw-multi-session-plugins: 0.1.15
|
||||
|
||||
### 建议
|
||||
|
||||
**在 ACP 协议中增加版本协商**:
|
||||
|
||||
```json
|
||||
// acp.capabilities 响应中增加
|
||||
{
|
||||
"protocolVersion": "1.0.0",
|
||||
"minCompatibleVersion": "1.0.0",
|
||||
"componentVersions": {
|
||||
"app": "1.1.4",
|
||||
"bridge": "0.2.0",
|
||||
"gateway": "2026.5.28",
|
||||
"plugins": "0.1.15"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**App 启动时校验**:
|
||||
```dart
|
||||
// gateway_runtime_core.dart
|
||||
final caps = await gatewayRuntime.getCapabilities();
|
||||
if (!isCompatible(caps.minCompatibleVersion)) {
|
||||
showUpdateDialog();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重构优先级矩阵
|
||||
|
||||
| 优先级 | 问题 | 改动量 | 影响 | 建议时序 |
|
||||
|--------|------|--------|------|---------|
|
||||
| **P0** | Plugin↔Bridge 循环依赖 | 中 (移动 bridgeAgents) | 消除循环 + 简化协议 | 本周 |
|
||||
| **P0** | Gateway 断连任务丢失 | 小 (增加持久化) | 核心可靠性 | 本周 |
|
||||
| **P1** | Gemini/Hermes 进程崩溃 | 小 (增加重启) | 提供商可用性 | 本周 |
|
||||
| **P1** | 配置管理分散 | 大 (集中配置) | 运维效率 | 本月 |
|
||||
| **P2** | Chain 2 协议层过多 | 大 (连接池+缓存) | 性能 + 延迟 | 本月 |
|
||||
| **P2** | 版本兼容性检查 | 小 (协议扩展) | 升级安全 | 本月 |
|
||||
| **P3** | SSE 降级轮询优化 | 中 (增量日志) | 用户体验 | 下月 |
|
||||
|
||||
---
|
||||
|
||||
## 各仓库具体改动清单
|
||||
|
||||
### xworkmate-app
|
||||
|
||||
- [ ] `lib/runtime/gateway_acp_client.dart`: 增加版本兼容性检查
|
||||
- [ ] `lib/runtime/external_code_agent_acp_desktop_transport.dart`: 支持 session.resume
|
||||
- [ ] `lib/runtime/runtime_models_account.dart`: 从 accounts 拉取集中配置
|
||||
- [ ] `config/settings.yaml`: 减少硬编码 URL,改为从 accounts 获取
|
||||
|
||||
### xworkmate-bridge
|
||||
|
||||
- [ ] `internal/acp/orchestrator.go`: 集成 bridgeAgents 逻辑(方案A)
|
||||
- [ ] `internal/gatewayruntime/runtime.go`: 增加连接池 + 任务状态持久化
|
||||
- [ ] `internal/gatewayruntime/pool.go`: 新增连接池
|
||||
- [ ] `internal/acp/artifact_cache.go`: 新增工件缓存
|
||||
- [ ] `internal/geminiadapter/` + `internal/hermesadapter/`: 增加子进程自动重启
|
||||
- [ ] `internal/acp/rpc_handler.go`: 支持 session.resume
|
||||
|
||||
### openclaw-multi-session-plugins
|
||||
|
||||
- [ ] `src/bridgeAgents.ts`: 删除或重构为单向网关调用(方案B)
|
||||
- [ ] `openclaw.plugin.json`: 简化配置(从网关继承 bridge 信息)
|
||||
- [ ] `src/exportArtifacts.ts`: 增加 artifactRef TTL 可配置
|
||||
|
||||
---
|
||||
|
||||
## 不做的事情
|
||||
|
||||
| 项目 | 原因 |
|
||||
|------|------|
|
||||
| 引入 gRPC 替代 JSON-RPC | 现有协议工作正常,切换成本高于收益 |
|
||||
| 合并 App 和 Bridge 为单体 | 拆分有明确的价值 (独立部署、技术栈隔离) |
|
||||
| 引入消息队列 | 当前规模不需要,会增加运维复杂度 |
|
||||
| 统一为单一编程语言 | Dart/Go/TS 各有适用场景,不必强行统一 |
|
||||
161
docs/ai-context/repo-summary.md
Normal file
161
docs/ai-context/repo-summary.md
Normal file
@ -0,0 +1,161 @@
|
||||
# Repo Summary — 三个仓库核心职责
|
||||
|
||||
> 生成日期: 2026-06-05 | 基于目录结构、README、依赖清单、入口文件、配置文件
|
||||
|
||||
---
|
||||
|
||||
## 1. xworkmate-app (Flutter/Dart)
|
||||
|
||||
### 核心职责
|
||||
XWorkmate 桌面客户端 — AI 协作工具的 GUI 前端。负责用户交互、会话管理、配置生成、任务队列。
|
||||
|
||||
### 入口模块
|
||||
| 入口 | 路径 | 作用 |
|
||||
|------|------|------|
|
||||
| main | `lib/main.dart` | 加载 FeatureManifest,启动 XWorkmateApp |
|
||||
| app | `lib/app/app.dart` | 顶层 Widget 树 |
|
||||
| controller | `lib/app/app_controller_desktop.dart` | 桌面版主控制器(所有平台逻辑入口) |
|
||||
|
||||
### 关键目录
|
||||
```
|
||||
lib/
|
||||
app/ — 应用级编排 (controller, gateway, runtime helpers, task queue)
|
||||
runtime/ — 服务层: ACP 客户端、Gateway runtime、Account client、任务服务
|
||||
features/ — UI 特性模块 (按功能拆分)
|
||||
models/ — 数据模型
|
||||
widgets/ — 可复用组件
|
||||
i18n/ — 国际化
|
||||
config/
|
||||
settings.yaml — 默认服务 URL 配置
|
||||
feature_flags.yaml — 功能开关
|
||||
```
|
||||
|
||||
### 主要调用链
|
||||
```
|
||||
GUI Event
|
||||
→ AppControllerDesktop
|
||||
→ GatewayRuntime (.connect, .chat, .session)
|
||||
→ GatewayAcpClient (JSON-RPC 2.0)
|
||||
→ HTTP POST /acp/rpc 或 WebSocket /acp
|
||||
→ xworkmate-bridge
|
||||
|
||||
GUI Event
|
||||
→ GoTaskServiceClient (DesktopGoTaskService)
|
||||
→ ExternalCodeAgentAcpTransport
|
||||
→ GatewayAcpClient (.session.start / .session.message)
|
||||
→ xworkmate-bridge
|
||||
|
||||
配置生成
|
||||
→ CodexConfigBridge / OpencodeConfigBridge
|
||||
→ 写入 ~/.codex/config.toml / ~/.opencode/config.toml
|
||||
→ 注入 MCP server 配置 (openclaw-mcp --gateway <url>)
|
||||
```
|
||||
|
||||
### 与其他仓库的关系
|
||||
- **xworkmate-bridge**: 通过 ACP JSON-RPC (HTTP/SSE/WebSocket) 通信。是 app 的唯一上游。
|
||||
- **openclaw-multi-session-plugins**: 不直接通信。通过 bridge 中转。
|
||||
- **openclaw.svc.plus** / **accounts.svc.plus**: REST/WebSocket 外部服务。
|
||||
|
||||
### 技术栈
|
||||
Dart/Flutter, `http` 包, `web_socket_channel`, `flutter_webrtc`, 无 gRPC 依赖。
|
||||
|
||||
---
|
||||
|
||||
## 2. xworkmate-bridge (Go)
|
||||
|
||||
### 核心职责
|
||||
ACP 控制平面网关 — 连接 Flutter 前端与多个 AI agent 后端的中间层。负责协议适配、路由、分布式转发、工件管理和 WebRTC 桌面流。
|
||||
|
||||
### 入口模块
|
||||
| 入口 | 路径 | 作用 |
|
||||
|------|------|------|
|
||||
| main | `main.go` | CLI 入口 (`serve` / `adapter` / `stdio` / `version`) |
|
||||
| serve cmd | `cmd/` | `serve` 命令启动 HTTP/WS 服务器 |
|
||||
| http handler | `internal/acp/http_handler.go` | 路由注册 (所有端点) |
|
||||
| rpc handler | `internal/acp/rpc_handler.go` | JSON-RPC 方法分发 |
|
||||
|
||||
### 关键目录
|
||||
```
|
||||
internal/
|
||||
acp/ — ACP 协议实现 (HTTP handler, RPC handler, gateway, config)
|
||||
router/ — 提供商路由决策引擎
|
||||
gatewayruntime/ — OpenClaw 网关 WebSocket 客户端
|
||||
geminiadapter/ — Gemini CLI stdio 适配器
|
||||
hermesadapter/ — Hermes CLI stdio 适配器
|
||||
opencodeadapter/ — OpenCode HTTP 适配器
|
||||
mounts/ — 提供商 MCP 配置管理
|
||||
main.go — 入口
|
||||
Dockerfile — 容器化部署
|
||||
```
|
||||
|
||||
### 主要调用链
|
||||
```
|
||||
xworkmate-app
|
||||
→ POST /acp/rpc (session.start)
|
||||
→ rpc_handler.handleRequest()
|
||||
→ router.Resolve(provider)
|
||||
→ codex: WebSocket/HTTP → codex endpoint
|
||||
→ opencode: HTTP → 127.0.0.1:38993 (opencode serve 子进程)
|
||||
→ gemini: HTTP → 127.0.0.1:8791 (gemini adapter 子进程)
|
||||
→ hermes: HTTP → 127.0.0.1:3920 (hermes adapter 子进程)
|
||||
→ openclaw: WebSocket → OpenClaw gateway (ws://127.0.0.1:18789)
|
||||
|
||||
xworkmate-app
|
||||
→ POST /gateway/openclaw (session.start, provider="gateway")
|
||||
→ acp/gateway.go → gatewayruntime/runtime.go
|
||||
→ WebSocket → OpenClaw gateway
|
||||
→ chat.send → agent 执行
|
||||
→ xworkmate.artifacts.* → openclaw-multi-session-plugins
|
||||
```
|
||||
|
||||
### 与其他仓库的关系
|
||||
- **xworkmate-app**: 被调用方。暴露 `/acp` (WS) 和 `/acp/rpc` (HTTP) 端点。
|
||||
- **openclaw-multi-session-plugins**: 通过 OpenClaw 网关 RPC 间接调用(`xworkmate.artifacts.*`)。
|
||||
- **openclaw.svc.plus**: WebSocket 连接目标(生产网关)。
|
||||
|
||||
### 技术栈
|
||||
Go 1.25, gorilla/websocket, pion/webrtc v4, YAML 配置。无 gRPC。Docker 部署。
|
||||
|
||||
---
|
||||
|
||||
## 3. openclaw-multi-session-plugins (TypeScript)
|
||||
|
||||
### 核心职责
|
||||
OpenClaw 插件 — 工件管理平面。为每个 session/run 对创建隔离工件目录,扫描文件,生成 HMAC 签名引用,提供多 agent 编排能力。
|
||||
|
||||
### 入口模块
|
||||
| 入口 | 路径 | 作用 |
|
||||
|------|------|------|
|
||||
| plugin entry | `index.ts` | register() — 注册 5 个网关方法 + 2 个 agent 工具 |
|
||||
| artifacts | `src/exportArtifacts.ts` | 工件准备/导出/读取逻辑 |
|
||||
| bridge agents | `src/bridgeAgents.ts` | 通过 HTTP 调用 bridge 的 session.start |
|
||||
| manifest | `openclaw.plugin.json` | 声明网关方法和工具配置 |
|
||||
|
||||
### 关键目录
|
||||
```
|
||||
src/
|
||||
exportArtifacts.ts — 核心: prepare/export/list/read
|
||||
bridgeAgents.ts — bridge 调用: session.start (multi-agent)
|
||||
index.ts — 插件入口: register()
|
||||
openclaw.plugin.json — 插件清单: 网关方法 + 工具声明
|
||||
```
|
||||
|
||||
### 主要调用链
|
||||
```
|
||||
OpenClaw 网关 (WebSocket RPC)
|
||||
→ xworkmate.artifacts.prepare → 创建 tasks/<session>/<run>/ 目录
|
||||
→ xworkmate.artifacts.export → 扫描文件, 计算 HMAC 签名, 输出清单
|
||||
→ xworkmate.artifacts.read → 返回单个工件内容
|
||||
|
||||
Agent Tool Call (内部)
|
||||
→ openclaw_multi_session_agents
|
||||
→ bridgeAgents.run() → HTTP POST → bridge /acp/rpc (session.start, multiAgent: true)
|
||||
→ 结果写入 artifactDirectory → multi-agent-result.json
|
||||
```
|
||||
|
||||
### 与其他仓库的关系
|
||||
- **xworkmate-bridge**: 被 bridge 通过网关 RPC 调用(`xworkmate.artifacts.*`)。也主动调用 bridge 的 `/acp/rpc`(multi-agent 模式)。
|
||||
- **xworkmate-app**: 不直接通信。app 通过 bridge 的 `/artifacts/openclaw/download` 获取工件。
|
||||
|
||||
### 技术栈
|
||||
TypeScript (ESM), Node.js, OpenClaw Plugin SDK (2026.5.28)。无外部 HTTP 依赖。`bridgeAgents.ts` 中使用原生 `fetch`。
|
||||
284
docs/architecture/chain-map-artifact-lifecycle.md
Normal file
284
docs/architecture/chain-map-artifact-lifecycle.md
Normal file
@ -0,0 +1,284 @@
|
||||
# Chain Map: Artifact Lifecycle (Prepare → Export → Read → Download)
|
||||
|
||||
Repo chain: openclaw-multi-session-plugins ↔ xworkmate-bridge ↔ xworkmate-app
|
||||
|
||||
## Lifecycle States
|
||||
|
||||
```
|
||||
[prepare] → [execute] → [export] → [snapshot] → [download] → [sync]
|
||||
|
||||
prepare: mkdir tasks/<session>/<run>/ (multi-session-plugins)
|
||||
execute: tools write files (openclaw.svc.plus)
|
||||
export: scan + manifest + sign (multi-session-plugins)
|
||||
snapshot: assemble terminal result (xworkmate-bridge)
|
||||
download: signed URL proxy (xworkmate-bridge)
|
||||
sync: save to ~/.xworkmate/threads/<session>/ (xworkmate-app)
|
||||
```
|
||||
|
||||
## State 1: Prepare
|
||||
|
||||
```
|
||||
Caller: xworkmate-bridge → gateway.request('xworkmate.artifacts.prepare')
|
||||
Handler: openclaw-multi-session-plugins → prepareXWorkmateArtifacts()
|
||||
|
||||
Inputs:
|
||||
sessionKey: string // "agent:default:abc123"
|
||||
runId: string // "20260605-001"
|
||||
workspaceDir?: string // optional explicit path
|
||||
|
||||
Process:
|
||||
1. resolveWorkspaceDir({ sessionKey, params, pluginConfig, config })
|
||||
→ Falls back through: explicit → pluginConfig → agent config
|
||||
→ profile env → ~/.openclaw/workspace
|
||||
|
||||
2. safeScopeSegment(sessionKey)
|
||||
→ replace [/\\:*?"<>|] with "-", truncate to 96 chars
|
||||
|
||||
3. safeScopeSegment(runId) → same rules
|
||||
|
||||
4. scopeRoot = <workspace>/tasks/<safeSessionKey>/<safeRunId>/
|
||||
→ fs.mkdir(scopeRoot, { recursive: true })
|
||||
|
||||
5. Validate: isWithinRoot(workspaceRoot, scopeRoot)
|
||||
|
||||
Output:
|
||||
artifactScope: "tasks/<safeSessionKey>/<safeRunId>/"
|
||||
artifactDirectory: "<workspace>/tasks/<safeSessionKey>/<safeRunId>/"
|
||||
|
||||
Fragile:
|
||||
- workspace resolution chain has 5 fallback levels
|
||||
- session key format must match across bridge and plugin
|
||||
- no cleanup of old scope directories
|
||||
```
|
||||
|
||||
## State 2: Execute (GAP)
|
||||
|
||||
```
|
||||
During agent turn execution, OpenClaw tools write output files.
|
||||
These go to various locations depending on the tool:
|
||||
|
||||
Tool: browser (screenshot / download)
|
||||
→ screenshot: saveMediaBuffer(buffer, "image/png", "browser")
|
||||
→ ~/.openclaw/media/browser/<uuid>.png
|
||||
→ download: writeExternalFileWithinOutputRoot()
|
||||
→ requested download path (bounded by output root security)
|
||||
→ uploads: /tmp/openclaw/uploads/<file>
|
||||
|
||||
Tool: image-generation
|
||||
→ Returns GeneratedImageAsset { buffer, mimeType } (in-memory)
|
||||
→ Agent must explicitly write buffer to disk
|
||||
→ Usually saveMediaBuffer(buffer, mimeType, "browser")
|
||||
→ ~/.openclaw/media/browser/<uuid>.png
|
||||
|
||||
Tool: video-generation / rendering
|
||||
→ Output to media/ outbound or /tmp/ rendering workspace
|
||||
|
||||
Tool: file-write (agent writes files explicitly)
|
||||
→ May write to workingDirectory (pointed at tasks/<session>/<run>/)
|
||||
→ OR may write to ~/.openclaw/agents/<id>/workspace/
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
CRITICAL: None of these paths are inside tasks/<session>/<run>/
|
||||
unless the agent explicitly directs output there.
|
||||
|
||||
The artifact export below will NOT find these files.
|
||||
═══════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
## State 3: Export
|
||||
|
||||
```
|
||||
Caller: xworkmate-bridge → gateway.request('xworkmate.artifacts.export')
|
||||
Handler: openclaw-multi-session-plugins → exportXWorkmateArtifacts()
|
||||
|
||||
Inputs:
|
||||
artifactScope: "tasks/<session>/<run>/"
|
||||
workspaceDir?: string
|
||||
artifactRef?: string // alternative: read single artifact
|
||||
maxFiles?: number // default: 200
|
||||
maxInlineBytes?: number // default: 512KB, files larger are omitted
|
||||
|
||||
Process:
|
||||
1. resolveScopeRoot(workspaceRoot, artifactScope)
|
||||
→ <workspace>/tasks/<session>/<run>/
|
||||
→ Validate isWithinRoot()
|
||||
|
||||
2. collectCandidates(scopeRoot)
|
||||
→ Recursive walk of ONLY tasks/<session>/<run>/
|
||||
→ Skip: .git, .openclaw, .xworkmate, .pi, .dart_tool,
|
||||
.next, .turbo, node_modules
|
||||
→ Skip: symlinks (security measure)
|
||||
→ Apply: artifact-ignore.md rules
|
||||
|
||||
3. For each file under maxFiles limit:
|
||||
→ Read content (up to maxInlineBytes)
|
||||
→ Compute SHA-256 hash
|
||||
→ Determine content-type from extension
|
||||
|
||||
4. Build artifact manifest:
|
||||
{
|
||||
scope: "tasks/<session>/<run>/",
|
||||
sessionKey: "<sessionKey>",
|
||||
runId: "<runId>",
|
||||
totalCandidates: <N>,
|
||||
files: [
|
||||
{ relativePath, displayPath, size, contentType, sha256, inline }
|
||||
]
|
||||
}
|
||||
|
||||
5. Generate signed refs:
|
||||
→ signArtifactRef(sessionKey, runId, relativePath)
|
||||
→ HMAC-SHA256(signingSecret, "<key>::<run>::<path>")
|
||||
→ Valid for 24h
|
||||
|
||||
Output:
|
||||
manifest: { scope, sessionKey, runId, totalCandidates, files[] }
|
||||
Each file: { relativePath, displayPath, size, contentType, sha256, inline?, ref }
|
||||
|
||||
Fragile:
|
||||
- Only scans tasks/<session>/<run>/ — misses media/browser/ etc.
|
||||
- symlinks rejected even if pointing within workspace
|
||||
- maxFiles=200, maxInlineBytes=512KB — large files silently omitted
|
||||
- signing secret rotation invalidates all existing refs
|
||||
```
|
||||
|
||||
## State 4: Snapshot (Bridge)
|
||||
|
||||
```
|
||||
Caller: completeOpenClawTask() in openclaw_async_tasks.go
|
||||
triggered by probeOpenClawTask() detecting completion
|
||||
|
||||
Process:
|
||||
1. Call gateway.request('xworkmate.artifacts.export')
|
||||
→ Get manifest from plugin
|
||||
|
||||
2. openClawArtifactExport()
|
||||
→ Transform manifest files into stable result shape
|
||||
→ decorateOpenClawArtifactDownloadURLs()
|
||||
→ Replace each file.ref with signed download URL:
|
||||
/artifacts/openclaw/download?ref=<signed>&t=<expiry>
|
||||
|
||||
3. Build terminal snapshot:
|
||||
{
|
||||
success: true,
|
||||
status: "completed",
|
||||
sessionId: "<id>",
|
||||
threadId: "<id>",
|
||||
turnId: "<id>",
|
||||
runId: "<id>",
|
||||
text: "<agent final output>",
|
||||
artifacts: {
|
||||
items: [{ path, url, sha256, size, contentType }],
|
||||
scope: "tasks/<session>/<run>/"
|
||||
}
|
||||
}
|
||||
|
||||
4. Store snapshot for xworkmate.tasks.get queries
|
||||
|
||||
5. Send SSE session.update to app
|
||||
|
||||
Fragile:
|
||||
- If export returns empty manifest, snapshot has no artifacts
|
||||
- Artifact download URLs expire after 24h
|
||||
- Snapshot stored only in memory (lost on bridge restart)
|
||||
```
|
||||
|
||||
## State 5: Download (Bridge Proxy)
|
||||
|
||||
```
|
||||
Endpoint: GET /artifacts/openclaw/download?ref=<signed>&t=<expiry>
|
||||
|
||||
Handler: HandleOpenClawArtifactDownload() in openclaw_artifact_download.go
|
||||
|
||||
Process:
|
||||
1. Parse signed ref: <sessionKey>::<runId>::<relativePath>::<hmac>
|
||||
2. Verify HMAC signature against signing secret
|
||||
3. Check expiry (24h TTL, t=<unixSeconds>)
|
||||
4. Resolve artifact:
|
||||
→ Build artifactScope: tasks/<session>/<run>/
|
||||
→ Call gateway.request('xworkmate.artifacts.read',
|
||||
{ artifactScope, relativePath })
|
||||
→ Up to 3 retries on failure
|
||||
|
||||
5. Plugin's readXWorkmateArtifact():
|
||||
→ resolveScopeRoot(workspaceRoot, artifactScope)
|
||||
→ Verify isWithinRoot()
|
||||
→ Resolve file path: <workspace>/tasks/<session>/<run>/<relativePath>
|
||||
→ Check file exists and not a symlink (reject symlinks to outside)
|
||||
→ Read file content (up to 64MB max)
|
||||
→ Return { content, contentType, size, sha256 }
|
||||
|
||||
6. Bridge streams response:
|
||||
→ Content-Type from artifact metadata
|
||||
→ Content-Length header
|
||||
→ Supports Range header for partial content
|
||||
→ Validate SHA-256 of received content
|
||||
|
||||
Fragile:
|
||||
- Ref expiry: links stale after 24h
|
||||
- Secret rotation: all existing refs invalid
|
||||
- 64MB size limit
|
||||
- Symlink rejection: if agent created a symlink, download fails
|
||||
- 3 retry attempts only — persistent gateway failure = permanent 502
|
||||
```
|
||||
|
||||
## State 6: Sync (App)
|
||||
|
||||
```
|
||||
Location: xworkmate-app
|
||||
lib/runtime/desktop_thread_artifact_service.dart
|
||||
lib/app/app_controller_desktop_thread_actions.dart
|
||||
|
||||
Process:
|
||||
1. Receive terminal snapshot from bridge (SSE or task.get)
|
||||
2. Check success=true AND artifacts.items[] present
|
||||
3. For each artifact item:
|
||||
→ Download via bridge's artifact download URL
|
||||
→ Write to local workspace: ~/.xworkmate/threads/<session>/<artifact-relative>
|
||||
4. Update TaskThread state:
|
||||
→ lastArtifactSyncStatus = synced
|
||||
→ lastTaskArtifactRelativePaths = [downloaded paths]
|
||||
→ lastResultCode = success
|
||||
|
||||
Failure paths:
|
||||
- success=false → lastResultCode=failed
|
||||
- success=true, no artifacts → lastArtifactSyncStatus=no-exported-artifacts
|
||||
- download failed → lastArtifactSyncStatus=failed
|
||||
- artifact missing from source → OPENCLAW_REQUIRED_ARTIFACT_MISSING
|
||||
|
||||
Fragile:
|
||||
- Local workspace may have stale artifacts from previous runs
|
||||
- Concurrent writes to same thread directory (multiple turns)
|
||||
- Large artifact downloads block UI thread
|
||||
```
|
||||
|
||||
## Full State Machine
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Prepare: bridge calls prepare
|
||||
Prepare --> Execute: scope dir created
|
||||
Execute --> Execute: agent writes files
|
||||
Execute --> Export: task complete
|
||||
Export --> Snapshot: manifest returned
|
||||
Snapshot --> Download: app requests
|
||||
Download --> Sync: file saved locally
|
||||
Sync --> [*]: artifact lifecycle complete
|
||||
|
||||
Prepare --> PrepareFailed: gateway error / timeout
|
||||
Export --> NoArtifacts: scope dir empty or no matching files
|
||||
Download --> DownloadFailed: ref expired / file missing / secret rotated
|
||||
```
|
||||
|
||||
## Path Resolution Reference
|
||||
|
||||
| Layer | What | Default Path |
|
||||
|-------|------|-------------|
|
||||
| Plugin workspace | resolveWorkspaceDir() | `~/.openclaw/workspace` |
|
||||
| Plugin scope | tasks/<session>/<run>/ | `<workspace>/tasks/<s>/<r>/` |
|
||||
| Plugin export | exportXWorkmateArtifacts() | Scans only scope dir |
|
||||
| Bridge snapshot | completeOpenClawTask() | In-memory, 24h signed URLs |
|
||||
| Bridge download | /artifacts/openclaw/download | Proxied from plugin read |
|
||||
| App sync | syncArtifactsFromBridge() | `~/.xworkmate/threads/<s>/` |
|
||||
| OpenClaw media | saveMediaBuffer(subdir) | `~/.openclaw/media/<subdir>/` |
|
||||
| OpenClaw temp | resolvePreferredOpenClawTmpDir() | `/tmp/openclaw/` |
|
||||
226
docs/architecture/chain-map-bridge-distributed.md
Normal file
226
docs/architecture/chain-map-bridge-distributed.md
Normal file
@ -0,0 +1,226 @@
|
||||
# Chain Map: Bridge Distributed Forwarding
|
||||
|
||||
Repo chain: xworkmate-app → xworkmate-bridge (primary) → cn-xworkmate-bridge (edge) → OpenClaw
|
||||
|
||||
## Topology
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ xworkmate-app │
|
||||
│ POST https://xworkmate-bridge.svc.plus │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│ Internet
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ xworkmate-bridge (primary) │
|
||||
│ Node: jp-xhttp-contabo.svc.plus │
|
||||
│ IP: 172.29.10.1:8787 │
|
||||
│ Role: primary, zone: jp │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│ WireGuard VPN tunnel
|
||||
│ (wg-xwm interface)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ cn-xworkmate-bridge (edge) │
|
||||
│ Node: cn-xworkmate-bridge.svc.plus │
|
||||
│ IP: 172.29.10.2:8787 │
|
||||
│ Role: edge, zone: cn │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
OpenClaw Gateway
|
||||
```
|
||||
|
||||
## Config Structure
|
||||
|
||||
```yaml
|
||||
# xworkmate-bridge config.yaml
|
||||
distributed:
|
||||
strategy: dual-node # or star, mesh
|
||||
nodes:
|
||||
- id: xworkmate-bridge
|
||||
endpoint: https://172.29.10.1:8787
|
||||
roles: [primary]
|
||||
zone: jp
|
||||
capabilities: [gateway, acp-provider]
|
||||
|
||||
- id: cn-xworkmate-bridge
|
||||
endpoint: https://172.29.10.2:8787
|
||||
roles: [edge]
|
||||
zone: cn
|
||||
capabilities: [gateway]
|
||||
|
||||
forwarding:
|
||||
rules:
|
||||
- match:
|
||||
methods: ["session.start", "session.message"]
|
||||
providers: ["openclaw"]
|
||||
route: xworkmate-bridge
|
||||
|
||||
- match:
|
||||
methods: ["session.start", "session.message"]
|
||||
zones: ["cn"]
|
||||
route: cn-xworkmate-bridge
|
||||
|
||||
routes:
|
||||
- name: xworkmate-bridge
|
||||
strategy: direct
|
||||
target: xworkmate-bridge
|
||||
|
||||
- name: cn-xworkmate-bridge
|
||||
strategy: direct
|
||||
target: cn-xworkmate-bridge
|
||||
|
||||
session_stickiness:
|
||||
ttl: 24h # session.message follows session.start
|
||||
```
|
||||
|
||||
## Forward Flow
|
||||
|
||||
```
|
||||
App sends session.start with routing params
|
||||
└─ xworkmate-bridge primary: handleRequest()
|
||||
└─ distributedTaskRouter.shouldForward(request)
|
||||
├─ Check forwarding.rules
|
||||
├─ Match methods: ["session.start", "session.message"]
|
||||
├─ Match providers: ["openclaw"] → select target based on zone
|
||||
│
|
||||
├─ Primary (jp) target → handle locally
|
||||
│ └─ orchestrator.Process() → gateway.send(chat.send)
|
||||
│
|
||||
└─ Edge (cn) target → forward
|
||||
└─ HTTP POST https://172.29.10.2:8787/acp/rpc
|
||||
Headers:
|
||||
X-XWorkmate-Bridge-Forwarded: 1
|
||||
X-XWorkmate-Forward-Source: xworkmate-bridge
|
||||
X-XWorkmate-Forward-Target: cn-xworkmate-bridge
|
||||
X-XWorkmate-Forward-Trace: <id>
|
||||
X-XWorkmate-Forward-Hop: 1
|
||||
Authorization: Bearer <BRIDGE_TASK_FORWARD_TOKEN>
|
||||
Body: original JSON-RPC request
|
||||
|
||||
Edge bridge receives forwarded request:
|
||||
└─ handleRequest() (same handler)
|
||||
├─ Checks X-XWorkmate-Forward-Hop < 3 (hop limit)
|
||||
├─ Session route store:
|
||||
│ └─ For session.start:
|
||||
│ └─ Store mapping: sessionId → target node
|
||||
│ └─ For session.message:
|
||||
│ └─ Lookup sessionId → target node
|
||||
│ └─ Route to the node that handled session.start
|
||||
│
|
||||
└─ orchestrator.Process() → gateway.send(chat.send)
|
||||
|
||||
Response flows back through the same chain.
|
||||
```
|
||||
|
||||
## Session Stickiness
|
||||
|
||||
```
|
||||
session.start arrives at primary bridge:
|
||||
→ distributedTaskRouter stores:
|
||||
session_routes[sessionId] = {
|
||||
targetNode: "xworkmate-bridge",
|
||||
createdAt: <timestamp>,
|
||||
ttl: 24h
|
||||
}
|
||||
|
||||
session.message arrives (any bridge):
|
||||
→ distributedTaskRouter looks up sessionId:
|
||||
if found:
|
||||
→ forward to targetNode (the node that handled session.start)
|
||||
if not found (expired or new bridge instance):
|
||||
→ re-evaluate forwarding rules from scratch
|
||||
|
||||
This ensures all turns in a session go to the same node,
|
||||
which holds the in-memory session state.
|
||||
```
|
||||
|
||||
## Security Constraints
|
||||
|
||||
```
|
||||
Forwarding endpoints MUST be:
|
||||
- Loopback (127.0.0.0/8)
|
||||
- Private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
|
||||
- Link-local (169.254.0.0/16)
|
||||
|
||||
Public URLs are REJECTED for forwarding endpoints.
|
||||
|
||||
Auth:
|
||||
- XWORKMATE_BRIDGE_TASK_FORWARD_TOKEN (env var)
|
||||
- Falls back to BRIDGE_AUTH_TOKEN if not set
|
||||
|
||||
Hop limit:
|
||||
- X-XWorkmate-Forward-Hop: max 3
|
||||
- Exceeded → 502 Bad Gateway
|
||||
```
|
||||
|
||||
## Deployment (playbooks)
|
||||
|
||||
```
|
||||
roles/vhosts/xworkmate_bridge_distributed_vpn/
|
||||
├─ WireGuard tunnel setup (wg-xwm interface)
|
||||
├─ Xray + tproxy for VPN transport
|
||||
└─ systemd: wg-quick@wg-xwm, xray-wg-tproxy
|
||||
|
||||
group_vars/xworkmate_bridge_distributed.yml:
|
||||
├─ Node IDs and roles
|
||||
├─ WireGuard keys
|
||||
└─ Node endpoints
|
||||
|
||||
host_vars/cn-xworkmate-bridge.svc.plus.yml:
|
||||
└─ Host-specific: wg address, listen IP
|
||||
|
||||
host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml:
|
||||
└─ Host-specific: wg address, listen IP
|
||||
```
|
||||
|
||||
## Call Chain for Forwarded Request
|
||||
|
||||
```
|
||||
1. Primary bridge receives session.start
|
||||
→ distributedTaskRouter.SelectNode(params):
|
||||
├─ Match forwarding.rules against (method, provider, zone)
|
||||
├─ If exact match → use target node
|
||||
├─ If no match → handle locally
|
||||
└─ Log forwarding decision
|
||||
|
||||
2. Primary bridge forwards
|
||||
→ httpForwardTask(node, request):
|
||||
├─ Validate endpoint is private (no public URLs)
|
||||
├─ Add forwarding headers
|
||||
├─ HTTP POST to target node
|
||||
└─ Stream response back to caller
|
||||
|
||||
3. Edge bridge processes
|
||||
→ Same handler as primary
|
||||
→ Hop limit check
|
||||
→ Session route registration
|
||||
→ Normal task processing
|
||||
|
||||
4. Bridge identity per node
|
||||
→ Each node has its own Ed25519 identity
|
||||
→ Stored at ~/.xworkmate-bridge/openclaw-device.json
|
||||
→ Different device IDs for primary vs edge
|
||||
→ OpenClaw gateway sees them as separate runtimes
|
||||
```
|
||||
|
||||
## Fragile Points
|
||||
|
||||
1. **D1: Session stickiness breaks on node failure**
|
||||
Session routes TTL is 24h. If primary node goes down and edge takes over, existing sessions can't be re-routed to edge. The session.message will try primary, fail, and the session is lost (in-memory state on primary).
|
||||
|
||||
2. **D2: Forward token mismatch**
|
||||
`XWORKMATE_BRIDGE_TASK_FORWARD_TOKEN` must be the same on all nodes. If it differs, forwarded requests are rejected with 401.
|
||||
|
||||
3. **D3: Hop limit too low for future topology**
|
||||
Current max 3 hops. If topology expands beyond star/double-node (mesh, relay chains), 3 hops may be insufficient.
|
||||
|
||||
4. **D4: Forwarding endpoint IP changes**
|
||||
Endpoints are hardcoded IPs in config.yaml. If VPN IPs change (recreated WireGuard tunnels), forwarding breaks until config is updated and bridge restarted.
|
||||
|
||||
5. **D5: Config drift between nodes**
|
||||
Primary and edge bridge instances run the same binary but may have different config.yaml. If distributed section differs, forwarding decisions may be inconsistent.
|
||||
|
||||
6. **D6: All-in-memory state amplifies risk**
|
||||
Both session state and session route store are in-memory. If either node restarts, both session data and routing stickiness are lost simultaneously.
|
||||
210
docs/architecture/chain-map-session-recovery.md
Normal file
210
docs/architecture/chain-map-session-recovery.md
Normal file
@ -0,0 +1,210 @@
|
||||
# Chain Map: Session Recovery
|
||||
|
||||
Repo chain: xworkmate-app → xworkmate-bridge
|
||||
|
||||
## Recovery Scenarios
|
||||
|
||||
### S1: App restart with running task
|
||||
|
||||
```
|
||||
App starts
|
||||
└─ AppController.restoreTaskThreads()
|
||||
└─ ThreadStorage.loadAllThreads() → TaskThread[]
|
||||
└─ For each thread with lifecycleStatus=running:
|
||||
└─ resolveGatewayThreadConnectionState()
|
||||
├─ thread has pendingTurnId?
|
||||
│ ├─ Yes → pollBridgeTaskSnapshot(turnId)
|
||||
│ │ └─ xworkmate.tasks.get({ sessionId, threadId, turnId })
|
||||
│ │ ├─ Terminal snapshot found → apply result
|
||||
│ │ ├─ Session not found → mark failed
|
||||
│ │ └─ No response → mark unrecovered
|
||||
│ └─ No → mark ready (no pending turn)
|
||||
│
|
||||
└─ thread lifecycleStatus=queued?
|
||||
└─ drainOpenClawGatewayQueue() → re-send
|
||||
|
||||
Key files:
|
||||
lib/app/app_controller_desktop_thread_sessions.dart
|
||||
lib/runtime/external_code_agent_acp_desktop_transport.dart
|
||||
```
|
||||
|
||||
### S2: Bridge restart (all sessions lost)
|
||||
|
||||
```
|
||||
Bridge sends SSE close / WebSocket disconnects
|
||||
└─ App: ExternalCodeAgentAcpDesktopTransport detects disconnect
|
||||
└─ Enter recovery mode
|
||||
├─ Attempt reconnection to bridge /acp
|
||||
├─ If reconnected:
|
||||
│ └─ Call xworkmate.tasks.get for each pending task
|
||||
│ ├─ Task found → continue with snapshot
|
||||
│ └─ Session not found (all sessions lost)
|
||||
│ → Mark task as failed with ACP_BRIDGE_RESTART
|
||||
└─ If cannot reconnect:
|
||||
└─ Exponential backoff, max retries
|
||||
→ Eventually mark as ACP_UNREACHABLE
|
||||
|
||||
Key files:
|
||||
lib/runtime/gateway_runtime_core.dart (reconnection logic)
|
||||
lib/runtime/external_code_agent_acp_desktop_transport.dart (recovery)
|
||||
```
|
||||
|
||||
### S3: Network interruption mid-task
|
||||
|
||||
```
|
||||
SSE stream interrupted (network flap)
|
||||
└─ App: Transport detects stream close without terminal
|
||||
└─ Enter polling mode
|
||||
└─ Every N seconds: xworkmate.tasks.get({ sessionId, threadId, turnId })
|
||||
├─ Terminal snapshot → apply result, stop polling
|
||||
├─ Still running → continue polling
|
||||
├─ Session not found → mark failed
|
||||
└─ Max poll attempts reached → mark unrecovered
|
||||
|
||||
Critical parameters (check actual values in code):
|
||||
- poll interval: ? seconds
|
||||
- max poll attempts: ?
|
||||
- total poll timeout: ?
|
||||
|
||||
Key files:
|
||||
lib/runtime/external_code_agent_acp_desktop_transport.dart
|
||||
```
|
||||
|
||||
### S4: OpenClaw gateway unreachable
|
||||
|
||||
```
|
||||
Bridge side:
|
||||
└─ Gateway client: gatewayruntime/runtime.go
|
||||
└─ scheduleReconnect() with 2s delay
|
||||
└─ Suppressed for auth errors
|
||||
└─ openClawSilentFailureExceeded() → 10 min timeout
|
||||
└─ Mark task as OPENCLAW_GATEWAY_LOST
|
||||
|
||||
App side:
|
||||
└─ Receives SSE session.update with status=failed
|
||||
└─ applyGatewayChatResult() → lastResultCode=OPENCLAW_GATEWAY_LOST
|
||||
└─ TaskThread lifecycleStatus → ready
|
||||
```
|
||||
|
||||
### S5: App resend on OpenClaw lane busy
|
||||
|
||||
```
|
||||
App: sendChatMessage() with executionTarget=gateway
|
||||
└─ isOpenClawLaneIdle() → false (5 active tasks)
|
||||
└─ queueOpenClawGatewayWork()
|
||||
├─ lifecycleStatus = queued
|
||||
├─ Position in queue: N (max 20)
|
||||
├─ Queue timeout: 10 min
|
||||
└─ drainOpenClawGatewayQueue()
|
||||
├─ Poll for lane idle + position=0
|
||||
├─ Lane becomes idle:
|
||||
│ └─ Dequeue → send normally
|
||||
└─ Queue timeout:
|
||||
└─ lifecycleStatus = ready
|
||||
└─ lastResultCode = OPENCLAW_GATEWAY_BUSY
|
||||
|
||||
Note: The app-side queue is SEPARATE from bridge-side admission gate.
|
||||
Bridge also has its own 5/20/10min admission control.
|
||||
|
||||
Double queue scenario:
|
||||
App queue (5/20) → waits → sends to bridge
|
||||
Bridge queue (5/20) → waits → sends to OpenClaw
|
||||
|
||||
Potential issue: App queue drains after lane idle, but bridge gate
|
||||
might also be busy → further delay not visible to app UI.
|
||||
```
|
||||
|
||||
## Recovery State Machine
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Running: task submitted
|
||||
Running --> Running: SSE streaming
|
||||
Running --> Lost_Connection: socket close / network flap
|
||||
|
||||
Lost_Connection --> Polling: xworkmate.tasks.get
|
||||
Polling --> Recovered: terminal snapshot
|
||||
Polling --> Polling: still running
|
||||
Polling --> Session_Not_Found: bridge restarted
|
||||
Polling --> Max_Retries: exceeded
|
||||
|
||||
Recovered --> Ready: result applied
|
||||
Session_Not_Found --> Failed: ACP_BRIDGE_RESTART
|
||||
Max_Retries --> Failed: ACP_UNRECOVERABLE
|
||||
|
||||
Running --> Failed: task error / gateway lost
|
||||
Failed --> Ready: error recorded
|
||||
```
|
||||
|
||||
## Bridge Session Store (Memory-Only)
|
||||
|
||||
```
|
||||
xworkmate-bridge: internal/acp/types.go
|
||||
Server struct {
|
||||
sessions map[string]*session // ← IN-MEMORY ONLY
|
||||
}
|
||||
|
||||
session struct {
|
||||
id string
|
||||
threadId string
|
||||
turnId string
|
||||
runId string
|
||||
sessionKey string
|
||||
openclaw *OpenClawTaskRecord // nil for non-gateway sessions
|
||||
history []message
|
||||
...
|
||||
}
|
||||
|
||||
No persistence:
|
||||
- Bridge restart → all sessions lost
|
||||
- xworkmate.tasks.get returns "session not found"
|
||||
- App must detect and mark as failed
|
||||
```
|
||||
|
||||
## Key Bridge RPC Methods for Recovery
|
||||
|
||||
| Method | Params | Returns |
|
||||
|--------|--------|---------|
|
||||
| `xworkmate.tasks.get` | sessionId, threadId, turnId (optional) | Terminal snapshot or running status |
|
||||
| `xworkmate.tasks.cancel` | sessionId, threadId, turnId | Cancel confirmation |
|
||||
| `reassociateOpenClawTask` | taskHandle (from stored params) | Reconnected session |
|
||||
|
||||
## App Recovery Flow (Detailed)
|
||||
|
||||
```
|
||||
resolveGatewayThreadConnectionState(thread)
|
||||
├─ thread.lifecycleStatus == "queued"
|
||||
│ └─ drainOpenClawGatewayQueue()
|
||||
│
|
||||
├─ thread.lifecycleStatus == "running"
|
||||
│ ├─ thread.lastTurnId exists?
|
||||
│ │ ├─ Yes → transport.pollBridgeTaskSnapshot(turnId)
|
||||
│ │ │ └─ xworkmate.tasks.get:
|
||||
│ │ │ ├─ completed/failed → applyGatewayChatResult()
|
||||
│ │ │ ├─ running → leave as running, continue SSE
|
||||
│ │ │ └─ not found / error:
|
||||
│ │ │ ├─ isBridgeAvailable()
|
||||
│ │ │ │ ├─ Yes → bridge restarted, mark failed
|
||||
│ │ │ │ └─ No → network issue, retry later
|
||||
│ │ │ └─ set lifecycleStatus = ready
|
||||
│ │ │ set lastResultCode = ACP_SESSION_NOT_FOUND
|
||||
│ │ │
|
||||
│ │ └─ No → set lifecycleStatus = ready (no pending turn)
|
||||
│ │
|
||||
│ └─ no lastTurnId → ready
|
||||
│
|
||||
└─ thread.lifecycleStatus == "ready" || "archived"
|
||||
└─ No recovery needed
|
||||
```
|
||||
|
||||
## Fragile Points for Recovery
|
||||
|
||||
1. **R1: Bridge restart detection**: App must distinguish "bridge restarted, sessions lost" from "network temporarily down". Currently relies on `xworkmate.tasks.get` returning "not found" while bridge is reachable.
|
||||
|
||||
2. **R2: Double queuing**: App has its own queue, bridge has admission gate. If both are congested, total wait time can exceed user expectations.
|
||||
|
||||
3. **R3: Stale running state**: If app crashes mid-task, on restart the thread shows lifecycleStatus=running. The xworkmate.tasks.get probe is the only way to resolve.
|
||||
|
||||
4. **R4: Polling parameters**: Hardcoded poll interval/retry values in `ExternalCodeAgentAcpDesktopTransport` need to align with bridge's task deadlines (10/30/60 min). If polling stops before deadline, app marks failed while task is still running.
|
||||
|
||||
5. **R5: OpenClaw handle expiration**: The bridge's `OpenClawTaskRecord` has no persistent storage. If the app stores a runId and later queries it after bridge restart, the lookup fails silently.
|
||||
211
docs/architecture/chain-map-task-execution.md
Normal file
211
docs/architecture/chain-map-task-execution.md
Normal file
@ -0,0 +1,211 @@
|
||||
# Chain Map: Task Execution (App → Bridge → OpenClaw → Plugin)
|
||||
|
||||
Repo chain: xworkmate-app → xworkmate-bridge → openclaw.svc.plus → openclaw-multi-session-plugins
|
||||
|
||||
## Entry Points (xworkmate-app)
|
||||
|
||||
```
|
||||
1. AssistantPage.sendChatMessage()
|
||||
lib/features/assistant/assistant_page_state_actions.dart
|
||||
|
||||
2. AppController.resendChatMessage() (retry)
|
||||
lib/app/app_controller_desktop_thread_actions.dart
|
||||
|
||||
3. drainOpenClawGatewayQueue() (queue drain)
|
||||
lib/app/app_controller_openclaw_task_queue.dart
|
||||
```
|
||||
|
||||
## Call Flow
|
||||
|
||||
```
|
||||
xworkmate-app
|
||||
AssistantPage.sendChatMessage()
|
||||
└─ AppController.sendChatMessage()
|
||||
├─ Resolve/create TaskThread by sessionKey/threadId
|
||||
├─ Prepare local workspace: ~/.xworkmate/threads/<session>/
|
||||
├─ Build task context prompt (sessionKey, workspace, contract)
|
||||
├─ Attach metadata.xworkmateTaskArtifactContract
|
||||
└─ Select execution path:
|
||||
├─ Agent providers (codex/opencode/gemini/hermes)
|
||||
│ └─ DesktopGoTaskService.startSession()
|
||||
└─ OpenClaw gateway
|
||||
├─ Check admission: isOpenClawLaneIdle()
|
||||
│ ├─ Idle → send immediately
|
||||
│ └─ Busy → queueOpenClawGatewayWork()
|
||||
└─ DesktopGoTaskService.startSession()
|
||||
|
||||
DesktopGoTaskService
|
||||
└─ ExternalCodeAgentAcpDesktopTransport.executeTask()
|
||||
├─ acp.capabilities → verify provider availability
|
||||
├─ xworkmate.routing.resolve → pre-resolve route
|
||||
├─ session.start (WebSocket /acp)
|
||||
│ ├─ params: sessionId, threadId, prompt, workingDirectory
|
||||
│ ├─ routing: executionTarget=gateway, preferredGatewayProviderId=openclaw
|
||||
│ └─ metadata: xworkmateTaskArtifactContract
|
||||
└─ Listen for SSE session.update events
|
||||
├─ status=running → poll xworkmate.tasks.get
|
||||
├─ terminal snapshot → applyGatewayChatResult()
|
||||
└─ artifacts → sync to local workspace
|
||||
|
||||
───────────────────────────────────────────────────────────
|
||||
Protocol: ACP JSON-RPC 2.0 over WebSocket
|
||||
Path: wss://xworkmate-bridge.svc.plus/acp
|
||||
Auth: Bearer <BRIDGE_AUTH_TOKEN>
|
||||
───────────────────────────────────────────────────────────
|
||||
|
||||
xworkmate-bridge
|
||||
internal/acp/http_handler.go: WebSocket handler
|
||||
└─ handleRequest() (rpc_handler.go)
|
||||
├─ Validate routing params
|
||||
├─ forceOpenClawGatewayRequest() — ensure gateway routing
|
||||
└─ orchestrator.Process()
|
||||
├─ Routing engine: Resolve(params, prompt, memory)
|
||||
│ ├─ Heuristic: looksLocal() / looksOnline()
|
||||
│ ├─ Memory preferences
|
||||
│ └─ LLM classifier fallback
|
||||
│
|
||||
├─ openClawGatewayAdmissionGate.acquire()
|
||||
│ ├─ maxActive: 5, maxQueued: 20
|
||||
│ ├─ queueTimeout: 10 min
|
||||
│ └─ Returns: admission slot or OPENCLAW_GATEWAY_BUSY
|
||||
│
|
||||
└─ startOpenClawGatewayTask()
|
||||
├─ ensureProductionGatewayConnected()
|
||||
├─ openClawArtifactPrepare()
|
||||
│ └─ gateway.request('xworkmate.artifacts.prepare')
|
||||
│ └─ scope: tasks/<sessionKey>/<runId>/
|
||||
│
|
||||
├─ gateway.request('chat.send')
|
||||
│ └─ payload: message, attachments, artifactSince, workingDirectory
|
||||
│
|
||||
├─ Create OpenClawTaskRecord
|
||||
│ ├─ SessionID, ThreadID, TurnID, RunID
|
||||
│ ├─ SessionKey (from gateway response)
|
||||
│ ├─ TaskLoadClass (short_task/long_task/complex_chain_task)
|
||||
│ ├─ RuntimeBudgetMinutes (10/30/60)
|
||||
│ └─ PreparedArtifact scope ref
|
||||
│
|
||||
└─ startOpenClawTaskMonitor()
|
||||
└─ Every 1s: probeOpenClawTask()
|
||||
└─ gateway.request('agent.wait', timeout=1s)
|
||||
├─ completed → completeOpenClawTask()
|
||||
├─ failed → failOpenClawTask()
|
||||
├─ SLA expired → TASK_SLA_EXPIRED
|
||||
└─ silent failure >10min → cleanup
|
||||
|
||||
───────────────────────────────────────────────────────────
|
||||
Protocol: Custom JSON-RPC v4 over WebSocket
|
||||
Auth: Ed25519 device identity + shared token
|
||||
───────────────────────────────────────────────────────────
|
||||
|
||||
openclaw.svc.plus
|
||||
Gateway: receives 'chat.send'
|
||||
└─ Agent runner processes turn
|
||||
├─ Model inference (Claude/GPT/Gemini)
|
||||
├─ Tool execution loop
|
||||
│ ├─ browser → screenshot → saveMediaBuffer(browser)
|
||||
│ ├─ image-generation → GeneratedImageAsset (in-memory)
|
||||
│ ├─ file-write → write to working directory or arbitrary path
|
||||
│ └─ video-render → output to media dir or /tmp
|
||||
│
|
||||
└─ Tool output destinations:
|
||||
├─ ~/.openclaw/media/browser/<uuid>.png ← NOT in tasks/<session>/<run>/
|
||||
├─ ~/.openclaw/media/inbound/<uuid>.<ext> ← NOT in tasks/<session>/<run>/
|
||||
├─ /tmp/openclaw/downloads/ ← NOT in tasks/<session>/<run>/
|
||||
└─ tasks/<session>/<run>/output.md ← MAY be written here
|
||||
|
||||
openclaw-multi-session-plugins
|
||||
Receives gateway RPC: xworkmate.artifacts.prepare
|
||||
prepareXWorkmateArtifacts()
|
||||
├─ resolveWorkspaceDir() → workspace root
|
||||
├─ safeScopeSegment(sessionKey) → sanitize
|
||||
├─ safeScopeSegment(runId) → sanitize
|
||||
└─ mkdir <workspace>/tasks/<safeSessionKey>/<safeRunId>/
|
||||
|
||||
Receives gateway RPC: xworkmate.artifacts.export
|
||||
exportXWorkmateArtifacts()
|
||||
├─ resolveScopeRoot(workspaceRoot, artifactScope)
|
||||
├─ collectCandidates(scopeRoot)
|
||||
│ ├─ Walk tasks/<session>/<run>/ recursively ← ONLY THIS DIRECTORY
|
||||
│ ├─ Skip symlinks
|
||||
│ ├─ Skip .git, .openclaw, node_modules, etc.
|
||||
│ └─ Apply artifact-ignore.md rules
|
||||
│
|
||||
├─ Read each candidate file, compute SHA-256
|
||||
├─ signArtifactRef(sessionKey, runId, relativePath)
|
||||
└─ Return manifest + base64 file contents
|
||||
|
||||
───────────────────────────────────────────────────────────
|
||||
CRITICAL GAP: Tool outputs in ~/.openclaw/media/* or /tmp/*
|
||||
are NOT in tasks/<session>/<run>/ → NOT exported → NOT visible
|
||||
to bridge → NOT synced to app.
|
||||
───────────────────────────────────────────────────────────
|
||||
|
||||
Back to xworkmate-bridge:
|
||||
completeOpenClawTask()
|
||||
├─ Call xworkmate.artifacts.export via gateway
|
||||
├─ Collect artifact manifest
|
||||
├─ Build terminal snapshot with:
|
||||
│ ├─ status: completed/failed/cancelled
|
||||
│ ├─ artifacts: { items: [...], scope: "..." }
|
||||
│ └─ text: final output
|
||||
│
|
||||
├─ decorateOpenClawArtifactDownloadURLs()
|
||||
│ └─ Replace artifact refs with signed download URLs
|
||||
│ Format: /artifacts/openclaw/download?ref=<signed>&t=<expiry>
|
||||
│
|
||||
└─ Send SSE session.update to app
|
||||
|
||||
Back to xworkmate-app:
|
||||
ExternalCodeAgentAcpDesktopTransport
|
||||
├─ Receive terminal snapshot via SSE or xworkmate.tasks.get
|
||||
├─ applyGatewayChatResult()
|
||||
│ ├─ success=true && artifacts present → syncArtifactsFromBridge()
|
||||
│ ├─ success=false → lastResultCode=failed
|
||||
│ └─ no-exported-artifacts → lastArtifactSyncStatus=no-exported-artifacts
|
||||
│
|
||||
└─ syncArtifactsFromBridge()
|
||||
└─ Download each artifact via /artifacts/openclaw/download
|
||||
→ Save to ~/.xworkmate/threads/<session>/
|
||||
```
|
||||
|
||||
## Key Files by Repo
|
||||
|
||||
### xworkmate-app
|
||||
- `lib/runtime/external_code_agent_acp_desktop_transport.dart` — 核心 ACP transport
|
||||
- `lib/runtime/go_task_service_client.dart` — GoTaskService 数据模型
|
||||
- `lib/runtime/gateway_acp_client.dart` — ACP HTTP RPC client
|
||||
- `lib/app/app_controller_openclaw_task_queue.dart` — OpenClaw 并发队列
|
||||
- `lib/app/app_controller_desktop_thread_sessions.dart` — session 恢复逻辑
|
||||
- `lib/app/app_controller_desktop_thread_actions.dart` — 消息发送
|
||||
- `lib/runtime/agent_registry.dart` — agent registry
|
||||
|
||||
### xworkmate-bridge
|
||||
- `internal/acp/http_handler.go` — HTTP server + WebSocket handler
|
||||
- `internal/acp/rpc_handler.go` — JSON-RPC dispatcher
|
||||
- `internal/acp/orchestrator.go` — 会话编排 (2000+ lines)
|
||||
- `internal/acp/openclaw_gateway_admission.go` — 并发控制
|
||||
- `internal/acp/openclaw_async_tasks.go` — 异步任务管理
|
||||
- `internal/acp/openclaw_artifact_download.go` — artifact 下载代理
|
||||
- `internal/gatewayruntime/runtime.go` — gateway WebSocket client
|
||||
- `internal/router/router.go` — 路由引擎
|
||||
|
||||
### openclaw-multi-session-plugins
|
||||
- `src/exportArtifacts.ts` — artifact prepare/export/read (963 lines)
|
||||
- `index.ts` — plugin entry + gateway method registration
|
||||
- `src/bridgeAgents.ts` — bridge agent orchestration
|
||||
|
||||
### openclaw.svc.plus (reference)
|
||||
- `src/config/paths.ts` — ~/.openclaw/ state directory
|
||||
- `src/infra/tmp-openclaw-dir.ts` — /tmp/openclaw/
|
||||
- `dist/store-ezT1dexf.js` — saveMediaBuffer() → media/<subdir>/
|
||||
|
||||
## Fragile Points
|
||||
|
||||
1. **F1: Tool output path mismatch** — Tools save to media/, plugin exports from tasks/ → gap
|
||||
2. **F2: Session key mismatch** — App/Bridge/Plugin must agree on sessionKey format
|
||||
3. **F3: Prepare timing** — If prepare fails after send, no scope directory exists
|
||||
4. **F4: Admission gate rejection** — Queue full → OPENCLAW_GATEWAY_BUSY → app must handle
|
||||
5. **F5: Bridge restart** — In-memory sessions lost → app must detect and recover
|
||||
6. **F6: Artifact ref key rotation** — Secret change invalidates all signed refs
|
||||
7. **F7: SSE stream interruption** — Polling fallback must have correct timeout/retry
|
||||
359
docs/architecture/cross-repo-call-analysis-2026-06-05.md
Normal file
359
docs/architecture/cross-repo-call-analysis-2026-06-05.md
Normal file
@ -0,0 +1,359 @@
|
||||
# Cross-Repo Call Relation Analysis
|
||||
|
||||
Date: 2026-06-05
|
||||
Scope: xworkmate-app, xworkmate-bridge, openclaw-multi-session-plugins, openclaw.svc.plus (ref), playbooks (deploy)
|
||||
|
||||
---
|
||||
|
||||
## 1. 仓库角色与依赖方向
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ xworkmate-app (Flutter) │ Desktop UI + 本地状态
|
||||
│ 版本: 1.1.4+1 │ 用户线程、任务对话框、artifact 面板
|
||||
└──────────────┬───────────────┘
|
||||
│ JSON-RPC over WebSocket/HTTP
|
||||
│ Path: /acp, /acp/rpc
|
||||
│ Auth: Bearer <BRIDGE_AUTH_TOKEN>
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ xworkmate-bridge (Go) │ ACP 控制面 + 路由引擎
|
||||
│ 版本: v1.1.0 │ 任务编排、concurrency gate
|
||||
│ Port: 8787 │ artifact download proxy
|
||||
└──────────────┬───────────────┘
|
||||
│ Custom JSON-RPC over WebSocket v4
|
||||
│ Auth: Ed25519 device identity + shared token
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ openclaw.svc.plus (Node/TS)│ Agent 运行时 + 工具执行
|
||||
│ Port: 18789 │ browser、image-gen、video-gen 等工具
|
||||
│ State: ~/.openclaw/ │ 全局 media cache: media/browser/ 等
|
||||
└──────────────┬───────────────┘
|
||||
│ Plugin SDK (Gateway RPC)
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ openclaw-multi-session- │ 多会话 artifact 管理
|
||||
│ plugins (TS) │ 文件输出: tasks/<session>/<run>/
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
仓库 `playbooks` 是 Ansible 部署基础设施,管理 xworkmate-bridge 的 systemd、Caddy、VPN 拓扑。它不是运行时调用链的一部分,但控制 bridge 的 config.yaml 注入和环境变量。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三大调用链
|
||||
|
||||
### 链路 A: 任务执行链(主流程)
|
||||
|
||||
```
|
||||
1. xworkmate-app
|
||||
AssistantPage.sendChatMessage()
|
||||
→ AppController.sendChatMessage()
|
||||
→ ExternalCodeAgentAcpDesktopTransport.executeTask()
|
||||
→ GatewayAcpClient.request('session.start' | 'session.message')
|
||||
→ WebSocket POST /acp with routing params
|
||||
|
||||
2. xworkmate-bridge
|
||||
http_handler.Handler() → handleRequest()
|
||||
→ orchestrator.Process()
|
||||
→ routing engine: Resolve(params, prompt, memory)
|
||||
→ openClawGatewayAdmissionGate.acquire() // max 5 concurrent, 20 queued
|
||||
→ startOpenClawGatewayTask()
|
||||
→ openClawArtifactPrepare() // calls xworkmate.artifacts.prepare
|
||||
→ gatewayruntime.send('chat.send')
|
||||
→ startOpenClawTaskMonitor() // background probe every 1s
|
||||
|
||||
3. openclaw.svc.plus
|
||||
Gateway receives 'chat.send'
|
||||
→ Agent runner processes turn
|
||||
→ Tools (browser, image-gen, etc.) execute
|
||||
→ Tool outputs go to ~/.openclaw/media/* or /tmp/openclaw/* ← CRITICAL
|
||||
|
||||
4. openclaw-multi-session-plugins
|
||||
prepareXWorkmateArtifacts() → creates tasks/<session>/<run>/
|
||||
exportXWorkmateArtifacts() → scans ONLY tasks/<session>/<run>/
|
||||
→ returns manifest + base64 files to bridge
|
||||
|
||||
5. xworkmate-bridge
|
||||
openClawArtifactExport() → collects artifacts from plugin
|
||||
→ completeOpenClawTask() → builds terminal snapshot
|
||||
→ sends session.update (SSE) to app
|
||||
|
||||
6. xworkmate-app
|
||||
ExternalCodeAgentAcpDesktopTransport receives SSE
|
||||
→ applies gateway chat result
|
||||
→ syncArtifactsFromBridge() → writes to local ~/.xworkmate/threads/<session>/
|
||||
```
|
||||
|
||||
### 链路 B: Artifact 回传链
|
||||
|
||||
```
|
||||
openclaw.svc.plus (tool saves file to media/browser/)
|
||||
→ Agent declares output file path
|
||||
→ Multi-session plugin: detectPathIsOutsideTaskScope(path)
|
||||
→ FILE IS SKIPPED — not in tasks/<session>/<run>/
|
||||
→ exportXWorkmateArtifacts returns incomplete manifest
|
||||
→ xworkmate-bridge: terminal snapshot has no artifacts
|
||||
→ xworkmate-app: lastArtifactSyncStatus=no-exported-artifacts
|
||||
```
|
||||
|
||||
### 链路 C: Bridge Distributed Forwarding 链
|
||||
|
||||
```
|
||||
xworkmate-app → xworkmate-bridge (primary, 172.29.10.1:8787)
|
||||
→ distributedTaskRouter selects node
|
||||
→ HTTP POST → cn-xworkmate-bridge (edge, 172.29.10.2:8787)
|
||||
over WireGuard VPN tunnel
|
||||
→ Forward headers:
|
||||
X-XWorkmate-Forward-Source: <nodeId>
|
||||
X-XWorkmate-Forward-Target: <nodeId>
|
||||
X-XWorkmate-Forward-Hop: <N>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 协议边界
|
||||
|
||||
### ACP 协议(App ↔ Bridge)
|
||||
|
||||
- **Transport:** WebSocket GET `/acp` 或 HTTP POST `/acp/rpc`
|
||||
- **Format:** JSON-RPC 2.0
|
||||
- **Auth:** `Authorization: Bearer <BRIDGE_AUTH_TOKEN>`
|
||||
- **Methods:**
|
||||
| Method | 方向 | 说明 |
|
||||
|--------|------|------|
|
||||
| `acp.capabilities` | → | 获取 provider/gateway catalog |
|
||||
| `session.start` | → | 开始会话 |
|
||||
| `session.message` | → | 续写会话 |
|
||||
| `session.cancel` / `session.close` | → | 终止会话 |
|
||||
| `xworkmate.routing.resolve` | → | 路由预查询 |
|
||||
| `xworkmate.gateway.connect` | → | 建立 gateway 连接 |
|
||||
| `xworkmate.gateway.request` | → | gateway 代理请求 |
|
||||
| `xworkmate.tasks.get` | → | 查询异步任务快照 |
|
||||
| `xworkmate.tasks.cancel` | → | 取消异步任务 |
|
||||
| `xworkmate.tools.invoke` | → | 工具调用 |
|
||||
| `xworkmate.desktop.*` | ↔ | WebRTC 桌面流 |
|
||||
| `session.update` | ← | SSE 推送 (bridge→app) |
|
||||
|
||||
### Gateway 协议(Bridge ↔ OpenClaw)
|
||||
|
||||
- **Transport:** WebSocket to gateway endpoint
|
||||
- **Format:** Custom JSON-RPC v4
|
||||
- **Auth:** Ed25519 device identity signing + shared token
|
||||
- **Methods:**
|
||||
| Method | 方向 | 说明 |
|
||||
|--------|------|------|
|
||||
| `connect` | → | 建立 gateway 会话 |
|
||||
| `chat.send` | → | 提交 agent 任务 |
|
||||
| `agent.wait` | → | 轮询任务状态 (1s timeout) |
|
||||
| `agent.cancel` | → | 取消任务 |
|
||||
| `xworkmate.artifacts.prepare` | → | 分配 artifact 目录 |
|
||||
| `xworkmate.artifacts.export` | → | 导出 artifact |
|
||||
| `xworkmate.artifacts.read` | → | 读取单个 artifact |
|
||||
| `tools.invoke` | → | 调用 OpenClaw 工具 |
|
||||
|
||||
### Artifact 协议(Plugin ↔ File System)
|
||||
|
||||
- **Scope creation:** `tasks/<safeSessionKey>/<safeRunId>/` under workspace root
|
||||
- **Scope validation:** `isWithinRoot(workspaceRoot, scopeRoot)` — 防止路径穿越
|
||||
- **Security:** HMAC-SHA256 签名 artifact ref,24h TTL
|
||||
- **Bridge download proxy:** `/artifacts/openclaw/download?ref=<signed>&t=<expiry>`
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键数据结构
|
||||
|
||||
### OpenClawTaskRecord(xworkmate-bridge)
|
||||
|
||||
```go
|
||||
type OpenClawTaskRecord struct {
|
||||
SessionID, ThreadID, TurnID, RunID string // 四元组标识
|
||||
SessionKey string // 传递给 plugin 做 scope
|
||||
GatewayProviderID string // "openclaw"
|
||||
TaskLoadClass string // short_task/long_task/complex_chain_task
|
||||
ArtifactSinceUnixMs int64 // artifact 时间窗口起始
|
||||
RuntimeBudgetMinutes int // 10/30/60
|
||||
StartedAt, DeadlineAt, LastProbeAt time.Time
|
||||
ProgressStage, ProgressMessage string
|
||||
ProgressTerminal bool
|
||||
FirstSilentFailureAt time.Time // 静默失败计时
|
||||
PreparedArtifact *openClawPreparedArtifactScope
|
||||
ArtifactContract openClawArtifactContract
|
||||
AdmissionRelease func() // 释放并发槽位
|
||||
}
|
||||
```
|
||||
|
||||
### Session Key 格式
|
||||
|
||||
```
|
||||
agent:<agentId>:<sessionId>
|
||||
```
|
||||
|
||||
由 `agentIdFromSessionKey()` 解析(exportArtifacts.ts:730-736),用于查找 agent config 中的 workspace 路径。
|
||||
|
||||
### Artifact Ref
|
||||
|
||||
```
|
||||
<sessionKey>::<runId>::<relativePath>::<hmac>
|
||||
```
|
||||
|
||||
- HMAC-SHA256 with plugin-configured signing secret
|
||||
- 24h expiry
|
||||
- Bridge 验证后代理下载
|
||||
|
||||
### ArtifactScope(plugin)
|
||||
|
||||
```
|
||||
tasks/<safeSessionKey>/<safeRunId>/
|
||||
```
|
||||
|
||||
- `safeSessionKey` = sanitize(sessionKey): replace [/\\:*?"<>|] with -, truncate 96 chars
|
||||
- `safeRunId` = sanitize(runId): same rules
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用链入口(xworkmate-app 侧)
|
||||
|
||||
### 主入口:AssistantPage.sendChatMessage()
|
||||
|
||||
```
|
||||
lib/features/assistant/assistant_page.dart
|
||||
→ assistant_page_state_actions.dart: sendChatMessage()
|
||||
→ app_controller_openclaw_task_queue.dart: queueOpenClawGatewayWork()
|
||||
→ go_task_service_desktop_service.dart: DesktopGoTaskService.startSession()
|
||||
→ external_code_agent_acp_desktop_transport.dart: executeTask()
|
||||
→ GatewayAcpClient.request('session.start') via WebSocket /acp
|
||||
→ GatewayAcpClient.request('session.message') via WebSocket /acp
|
||||
→ pollBridgeTaskSnapshot() → 'xworkmate.tasks.get'
|
||||
```
|
||||
|
||||
### 恢复入口:AppController.resumeOpenClawRunningTask()
|
||||
|
||||
```
|
||||
app_controller_desktop_thread_sessions.dart: resolveGatewayThreadConnectionState()
|
||||
→ 检查 localLastRunId 是否对应一个未完成的 OpenClaw 任务
|
||||
→ external_code_agent_acp_desktop_transport.dart: pollBridgeTaskSnapshot()
|
||||
→ 'xworkmate.tasks.get' → 获取 terminal snapshot
|
||||
```
|
||||
|
||||
### 取消入口:AppController.cancelCurrentTask()
|
||||
|
||||
```
|
||||
assistant_page_state_actions.dart: onCancelTask()
|
||||
→ external_code_agent_acp_desktop_transport.dart: cancelAndCloseTask()
|
||||
→ 'session.cancel' 或 'xworkmate.tasks.cancel'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 最容易改坏的 10 个地方
|
||||
|
||||
### 🔴 CRITICAL: 路径断裂区
|
||||
|
||||
**6.1 OpenClaw 工具输出路径 ≠ plugin artifact scope**
|
||||
|
||||
OpenClaw 工具(browser screenshot、image generation、video render、file write)将输出写到:
|
||||
- `~/.openclaw/media/browser/<uuid>.png` — `saveMediaBuffer(buffer, contentType, "browser")`
|
||||
- `~/.openclaw/media/inbound/<uuid>.<ext>` — 上传/附件
|
||||
- `/tmp/openclaw/downloads/` — 浏览器下载
|
||||
- `/tmp/openclaw/traces/` — 工具 trace
|
||||
|
||||
但 `exportXWorkmateArtifacts()` 只扫描 `tasks/<session>/<run>/`。
|
||||
|
||||
**后果:** 工具生成的图片、文档、视频在 artifact 回传时被静默丢弃。用户收到 "no-exported-artifacts"。
|
||||
|
||||
**触发条件:** 任何使用 OpenClaw 内置工具(browser、image-gen、video-gen)的任务,如果工具输出没有显式复制到 `tasks/<session>/<run>/` 下。
|
||||
|
||||
**修复方向:**
|
||||
- 方案 A: 在 plugin 的 `exportXWorkmateArtifacts` 增加对 `~/.openclaw/media/browser/` 的扫描(按 session 过滤)
|
||||
- 方案 B: 修改 OpenClaw 工具,使其在 gateway session 上下文中使用 session-scoped 输出路径
|
||||
- 方案 C: 在 bridge 的 `openClawArtifactExport()` 中,额外从 gateway 查询 media cache 中的当前 session 产物
|
||||
|
||||
**6.2 Session Key 派生不一致**
|
||||
|
||||
Session key 由多个来源派生:
|
||||
- App: `ExternalCodeAgentAcpDesktopTransport._sessionKey` — 从 response params 获取
|
||||
- Bridge: `OpenClawTaskRecord.SessionKey` — 从 gateway response 获取
|
||||
- Plugin: `agentIdFromSessionKey(sessionKey)` — 解析 `agent:<id>:<session>` 格式
|
||||
|
||||
如果任一端对 session key 的生成或传递逻辑发生变化,artifact scope 将不匹配。
|
||||
|
||||
**验证方法:** 在 artifact prepare/export 两端打印 sessionKey,确保完全一致。
|
||||
|
||||
**6.3 Bridge 的 artifact prepare 时序**
|
||||
|
||||
`openClawArtifactPrepare()` 在 bridge 侧通过 gateway 调用 `xworkmate.artifacts.prepare`,但该调用发生在 `chat.send` 之前还是之后决定了 scope 目录是否存在。
|
||||
|
||||
当前代码: `startOpenClawGatewayTask()` 先 prepare 再 send — 正确。但如果在 send 之前 prepare 失败(超时、gateway 不可达),任务仍会提交但无 scope 目录。
|
||||
|
||||
**6.4 Artifact ref 签名密钥轮换**
|
||||
|
||||
HMAC-SHA256 签名密钥在 plugin config 中的 `artifactRefSigningSecret`。如果密钥变更,旧的 artifact ref 全部失效。Bridge 的 download proxy 会返回 403。
|
||||
|
||||
**6.5 Bridge admission gate 满时静默丢失**
|
||||
|
||||
`openClawGatewayAdmissionGate.acquire()`:
|
||||
- maxActive: 5
|
||||
- maxQueued: 20
|
||||
- queueTimeout: 10 min
|
||||
|
||||
当队列满且超时,返回 `OPENCLAW_GATEWAY_BUSY`。App 端是否有正确处理?检查 `app_controller_openclaw_task_queue.dart` 中的 `drainOpenClawGatewayQueue` 逻辑。
|
||||
|
||||
**6.6 Bridge 会话纯内存存储**
|
||||
|
||||
xworkmate-bridge 的 session 存储在 `Server.sessions map[string]*session`,不持久化。Bridge 重启 → 所有运行中的任务丢失 → App 端 `xworkmate.tasks.get` 返回 "session not found" → 任务静默失败。
|
||||
|
||||
### 🟠 HIGH: 状态同步区
|
||||
|
||||
**6.7 TaskSnapshot 字段不完整**
|
||||
|
||||
Bridge 返回的 terminal snapshot 依赖 `completeOpenClawTask()` 正确组装 artifact 列表。如果 plugin 返回的 artifact manifest 中有文件但内容为空、或相对路径超出 scope,bridge 的 snapshot 会缺少 artifact 条目。
|
||||
|
||||
**6.8 SSE 流中断后的轮询策略**
|
||||
|
||||
当 SSE stream 关闭(网络断开、bridge 重启),app 的 `ExternalCodeAgentAcpDesktopTransport` 进入 polling 模式调用 `xworkmate.tasks.get`。如果轮询间隔、重试次数、超时策略设置不当,可能导致:
|
||||
- 轮询过早结束(任务实际还在跑)
|
||||
- 轮询永不结束(bridge 已重启 session 丢失)
|
||||
|
||||
**6.9 Plugin 的 workspace 解析回退链过长**
|
||||
|
||||
`resolveWorkspaceDir()` 的回退链:
|
||||
1. 显式 params.workspaceDir
|
||||
2. pluginConfig.workspaceDir
|
||||
3. agent config (按 agentId 匹配 → default agent → 第一个 agent)
|
||||
4. OPENCLAW_PROFILE env → `~/.openclaw/workspace-<profile>`
|
||||
5. `~/.openclaw/workspace`
|
||||
|
||||
如果 agent config 变更(增删 agent、修改 default、调整 workspace),可能影响所有运行中 session 的 artifact 路径。
|
||||
|
||||
### 🟡 MEDIUM: 配置与部署区
|
||||
|
||||
**6.10 Distributed bridge forwarding topology 变更**
|
||||
|
||||
Bridge 的双节点拓扑(primary + edge)定义在 `config.yaml` 的 `distributed.nodes`。拓扑变更(增删节点、修改 endpoint、更新转发规则)会影响跨区域任务路由。session stickiness(24h TTL)意味着变更后已有 session 可能被路由到错误的节点。
|
||||
|
||||
---
|
||||
|
||||
## 7. tasks/<session>/<run> 状态矩阵
|
||||
|
||||
| 阶段 | 状态 | 文件位置 | 谁负责 |
|
||||
|------|------|----------|--------|
|
||||
| prepare | `xworkmate.artifacts.prepare` 调用 | plugin 创建空目录 | multi-session-plugins |
|
||||
| execute | Agent 工具运行 | 工具输出到 `~/.openclaw/media/` 或 `/tmp/` | openclaw.svc.plus |
|
||||
| — | **GAP: 工具输出不在 scope 内** | files in media/, NOT in tasks/ | 无人负责 |
|
||||
| export | `xworkmate.artifacts.export` 扫描 | 扫描 `tasks/<session>/<run>/` | multi-session-plugins |
|
||||
| snapshot | bridge 组装 terminal snapshot | 内存中, 通过 `xworkmate.tasks.get` 查询 | xworkmate-bridge |
|
||||
| sync | app 下载 artifact 到本地 | `~/.xworkmate/threads/<session>/` | xworkmate-app |
|
||||
|
||||
**核心 gap:** execute 阶段工具写文件到全局路径 → export 阶段只扫描 task scope → 文件不可见。
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议沉淀的 chain-map.md
|
||||
|
||||
| 链路 | 文件名 | 覆盖范围 |
|
||||
|------|--------|----------|
|
||||
| A | `chain-map-task-execution.md` | App→Bridge→OpenClaw→Plugin 完整任务执行 |
|
||||
| B | `chain-map-artifact-lifecycle.md` | Artifact prepare/export/read/download 全周期 |
|
||||
| C | `chain-map-bridge-distributed.md` | Bridge 分布式转发拓扑 |
|
||||
| D | `chain-map-session-recovery.md` | App 会话恢复、task.get 轮询、bridge 重启恢复 |
|
||||
Loading…
Reference in New Issue
Block a user