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:
Haitao Pan 2026-06-05 02:54:11 +00:00
parent f8449d42e7
commit 3522bb7b99
12 changed files with 2559 additions and 0 deletions

3
.gitignore vendored
View File

@ -67,3 +67,6 @@ app.*.map.json
# Gradle artifacts (including third_party)
**/.gradle/
# Repomix — dynamically generated, not committed
repomix-output.xml

View File

@ -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.

View 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 RPCEd25519 加密握手) |
| 传输 | 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": ""
}
}
```

View 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) | **高/循环** |

View 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) | **中** |

View 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 各有适用场景,不必强行统一 |

View 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`

View 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/` |

View 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.

View 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.

View 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

View 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 ref24h TTL
- **Bridge download proxy:** `/artifacts/openclaw/download?ref=<signed>&t=<expiry>`
---
## 4. 关键数据结构
### OpenClawTaskRecordxworkmate-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 验证后代理下载
### ArtifactScopeplugin
```
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 中有文件但内容为空、或相对路径超出 scopebridge 的 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 stickiness24h 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 重启恢复 |