From 3522bb7b994a304186779d4433cae4e2213bc47c Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 5 Jun 2026 02:54:11 +0000 Subject: [PATCH] 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/// --- .gitignore | 3 + AGENTS.md | 16 + docs/ai-context/architecture-map.md | 201 ++++++++++ docs/ai-context/chain-map.md | 271 +++++++++++++ docs/ai-context/module-boundary.md | 327 ++++++++++++++++ docs/ai-context/refactor-notes.md | 290 ++++++++++++++ docs/ai-context/repo-summary.md | 161 ++++++++ .../chain-map-artifact-lifecycle.md | 284 ++++++++++++++ .../chain-map-bridge-distributed.md | 226 +++++++++++ .../chain-map-session-recovery.md | 210 ++++++++++ docs/architecture/chain-map-task-execution.md | 211 ++++++++++ .../cross-repo-call-analysis-2026-06-05.md | 359 ++++++++++++++++++ 12 files changed, 2559 insertions(+) create mode 100644 docs/ai-context/architecture-map.md create mode 100644 docs/ai-context/chain-map.md create mode 100644 docs/ai-context/module-boundary.md create mode 100644 docs/ai-context/refactor-notes.md create mode 100644 docs/ai-context/repo-summary.md create mode 100644 docs/architecture/chain-map-artifact-lifecycle.md create mode 100644 docs/architecture/chain-map-bridge-distributed.md create mode 100644 docs/architecture/chain-map-session-recovery.md create mode 100644 docs/architecture/chain-map-task-execution.md create mode 100644 docs/architecture/cross-repo-call-analysis-2026-06-05.md diff --git a/.gitignore b/.gitignore index b7918e9c..e2320cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ app.*.map.json # Gradle artifacts (including third_party) **/.gradle/ + +# Repomix — dynamically generated, not committed +repomix-output.xml diff --git a/AGENTS.md b/AGENTS.md index 55310932..6a0a5477 100644 --- a/AGENTS.md +++ b/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 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///`. 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. diff --git a/docs/ai-context/architecture-map.md b/docs/ai-context/architecture-map.md new file mode 100644 index 00000000..f70f3d4d --- /dev/null +++ b/docs/ai-context/architecture-map.md @@ -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= + ├──────────────────────────────────► xworkmate-bridge + │ │ + │ └──► xworkmate.artifacts.read + │ │ + │ ▼ + │ openclaw-multi-session-plugins + │ + ▼ +本地文件: ~/.xworkmate/threads// +``` + +--- + +## 关键配置项 + +### 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": "" + } +} +``` diff --git a/docs/ai-context/chain-map.md b/docs/ai-context/chain-map.md new file mode 100644 index 00000000..7c12eea6 --- /dev/null +++ b/docs/ai-context/chain-map.md @@ -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=&t= 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:", + 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) | **高/循环** | diff --git a/docs/ai-context/module-boundary.md b/docs/ai-context/module-boundary.md new file mode 100644 index 00000000..c69042ac --- /dev/null +++ b/docs/ai-context/module-boundary.md @@ -0,0 +1,327 @@ +# Module Boundary — 模块边界与接口契约 + +> 生成日期: 2026-06-05 | 跨仓库接口定义、协议规范、安全边界 + +--- + +## 1. App ↔ Bridge: ACP 协议边界 + +### 端点定义 + +``` +# WebSocket (主通道) +ws://:8787/acp + +# HTTP RPC (后备通道) +POST https:///acp/rpc +Content-Type: application/json +Authorization: Bearer +``` + +### JSON-RPC 2.0 请求格式 + +```json +{ + "jsonrpc": "2.0", + "id": "", + "method": "", + "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 ` 头 +- 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 +``` + +### 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 + Content-Type: application/json + Body: + { + "jsonrpc": "2.0", + "id": "openclaw-", + "method": "session.start", + "params": { + "sessionId": "openclaw:", + "threadId": "", + "taskPrompt": "...", + "workingDirectory": "", + "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) | **中** | diff --git a/docs/ai-context/refactor-notes.md b/docs/ai-context/refactor-notes.md new file mode 100644 index 00000000..c4d439d6 --- /dev/null +++ b/docs/ai-context/refactor-notes.md @@ -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< 生成日期: 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 ) +``` + +### 与其他仓库的关系 +- **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/// 目录 + → 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`。 diff --git a/docs/architecture/chain-map-artifact-lifecycle.md b/docs/architecture/chain-map-artifact-lifecycle.md new file mode 100644 index 00000000..988abb7c --- /dev/null +++ b/docs/architecture/chain-map-artifact-lifecycle.md @@ -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/// (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// (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 = /tasks/// + → fs.mkdir(scopeRoot, { recursive: true }) + + 5. Validate: isWithinRoot(workspaceRoot, scopeRoot) + +Output: + artifactScope: "tasks///" + artifactDirectory: "/tasks///" + +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/.png + → download: writeExternalFileWithinOutputRoot() + → requested download path (bounded by output root security) + → uploads: /tmp/openclaw/uploads/ + +Tool: image-generation + → Returns GeneratedImageAsset { buffer, mimeType } (in-memory) + → Agent must explicitly write buffer to disk + → Usually saveMediaBuffer(buffer, mimeType, "browser") + → ~/.openclaw/media/browser/.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///) + → OR may write to ~/.openclaw/agents//workspace/ + +═══════════════════════════════════════════════════════════ +CRITICAL: None of these paths are inside tasks/// +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///" + 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) + → /tasks/// + → Validate isWithinRoot() + + 2. collectCandidates(scopeRoot) + → Recursive walk of ONLY tasks/// + → 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///", + sessionKey: "", + runId: "", + totalCandidates: , + files: [ + { relativePath, displayPath, size, contentType, sha256, inline } + ] + } + + 5. Generate signed refs: + → signArtifactRef(sessionKey, runId, relativePath) + → HMAC-SHA256(signingSecret, "::::") + → Valid for 24h + +Output: + manifest: { scope, sessionKey, runId, totalCandidates, files[] } + Each file: { relativePath, displayPath, size, contentType, sha256, inline?, ref } + +Fragile: + - Only scans tasks/// — 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=&t= + + 3. Build terminal snapshot: + { + success: true, + status: "completed", + sessionId: "", + threadId: "", + turnId: "", + runId: "", + text: "", + artifacts: { + items: [{ path, url, sha256, size, contentType }], + scope: "tasks///" + } + } + + 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=&t= + +Handler: HandleOpenClawArtifactDownload() in openclaw_artifact_download.go + +Process: + 1. Parse signed ref: :::::: + 2. Verify HMAC signature against signing secret + 3. Check expiry (24h TTL, t=) + 4. Resolve artifact: + → Build artifactScope: tasks/// + → 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: /tasks/// + → 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// + 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/// | `/tasks///` | +| 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//` | +| OpenClaw media | saveMediaBuffer(subdir) | `~/.openclaw/media//` | +| OpenClaw temp | resolvePreferredOpenClawTmpDir() | `/tmp/openclaw/` | diff --git a/docs/architecture/chain-map-bridge-distributed.md b/docs/architecture/chain-map-bridge-distributed.md new file mode 100644 index 00000000..af130051 --- /dev/null +++ b/docs/architecture/chain-map-bridge-distributed.md @@ -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: + X-XWorkmate-Forward-Hop: 1 + Authorization: Bearer + 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: , + 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. diff --git a/docs/architecture/chain-map-session-recovery.md b/docs/architecture/chain-map-session-recovery.md new file mode 100644 index 00000000..f9beb2b9 --- /dev/null +++ b/docs/architecture/chain-map-session-recovery.md @@ -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. diff --git a/docs/architecture/chain-map-task-execution.md b/docs/architecture/chain-map-task-execution.md new file mode 100644 index 00000000..bbf748ba --- /dev/null +++ b/docs/architecture/chain-map-task-execution.md @@ -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// + ├─ 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 +─────────────────────────────────────────────────────────── + +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/// + │ + ├─ 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/.png ← NOT in tasks/// + ├─ ~/.openclaw/media/inbound/. ← NOT in tasks/// + ├─ /tmp/openclaw/downloads/ ← NOT in tasks/// + └─ tasks///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 /tasks/// + + Receives gateway RPC: xworkmate.artifacts.export + exportXWorkmateArtifacts() + ├─ resolveScopeRoot(workspaceRoot, artifactScope) + ├─ collectCandidates(scopeRoot) + │ ├─ Walk tasks/// 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/// → 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=&t= + │ + └─ 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// +``` + +## 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// + +## 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 diff --git a/docs/architecture/cross-repo-call-analysis-2026-06-05.md b/docs/architecture/cross-repo-call-analysis-2026-06-05.md new file mode 100644 index 00000000..75963ed3 --- /dev/null +++ b/docs/architecture/cross-repo-call-analysis-2026-06-05.md @@ -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 + ▼ +┌──────────────────────────────┐ +│ 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/// +└──────────────────────────────┘ +``` + +仓库 `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/// + exportXWorkmateArtifacts() → scans ONLY tasks/// + → 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// +``` + +### 链路 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/// + → 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: + X-XWorkmate-Forward-Target: + X-XWorkmate-Forward-Hop: +``` + +--- + +## 3. 协议边界 + +### ACP 协议(App ↔ Bridge) + +- **Transport:** WebSocket GET `/acp` 或 HTTP POST `/acp/rpc` +- **Format:** JSON-RPC 2.0 +- **Auth:** `Authorization: Bearer ` +- **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///` under workspace root +- **Scope validation:** `isWithinRoot(workspaceRoot, scopeRoot)` — 防止路径穿越 +- **Security:** HMAC-SHA256 签名 artifact ref,24h TTL +- **Bridge download proxy:** `/artifacts/openclaw/download?ref=&t=` + +--- + +## 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:: +``` + +由 `agentIdFromSessionKey()` 解析(exportArtifacts.ts:730-736),用于查找 agent config 中的 workspace 路径。 + +### Artifact Ref + +``` +:::::: +``` + +- HMAC-SHA256 with plugin-configured signing secret +- 24h expiry +- Bridge 验证后代理下载 + +### ArtifactScope(plugin) + +``` +tasks/// +``` + +- `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/.png` — `saveMediaBuffer(buffer, contentType, "browser")` +- `~/.openclaw/media/inbound/.` — 上传/附件 +- `/tmp/openclaw/downloads/` — 浏览器下载 +- `/tmp/openclaw/traces/` — 工具 trace + +但 `exportXWorkmateArtifacts()` 只扫描 `tasks///`。 + +**后果:** 工具生成的图片、文档、视频在 artifact 回传时被静默丢弃。用户收到 "no-exported-artifacts"。 + +**触发条件:** 任何使用 OpenClaw 内置工具(browser、image-gen、video-gen)的任务,如果工具输出没有显式复制到 `tasks///` 下。 + +**修复方向:** +- 方案 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::` 格式 + +如果任一端对 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-` +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// 状态矩阵 + +| 阶段 | 状态 | 文件位置 | 谁负责 | +|------|------|----------|--------| +| 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///` | multi-session-plugins | +| snapshot | bridge 组装 terminal snapshot | 内存中, 通过 `xworkmate.tasks.get` 查询 | xworkmate-bridge | +| sync | app 下载 artifact 到本地 | `~/.xworkmate/threads//` | 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 重启恢复 |