From 792f7920e9d9cec9ac03f94b4e1f8675b61451f8 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 8 Apr 2026 20:48:06 +0800 Subject: [PATCH] cleanup: remove ACP bypass fallbacks and stale architecture docs --- ...assistant-thread-working-directory-flow.md | 2 +- ...sistant-thread-information-architecture.md | 67 ------ .../assistant-thread-target-model-20260328.md | 81 ------- ...ce-multi-device-architecture-2026-03-30.md | 4 +- .../task-control-plane-unification.md | 18 ++ ...k-thread-session-key-isolation-20260329.md | 6 +- docs/architecture/xworkmate-integrations.md | 219 ------------------ .../xworkmate-internal-state-architecture.md | 96 -------- .../xworkmate-layered-architecture.md | 7 +- docs/plans/2026-03-21-Mobile.md | 4 +- lib/app/app_controller_web_helpers.dart | 49 +--- lib/runtime/gateway_acp_client.dart | 162 ++----------- test/runtime/gateway_acp_client_suite.dart | 58 +---- 13 files changed, 53 insertions(+), 720 deletions(-) delete mode 100644 docs/architecture/assistant-thread-information-architecture.md delete mode 100644 docs/architecture/assistant-thread-target-model-20260328.md delete mode 100644 docs/architecture/xworkmate-integrations.md delete mode 100644 docs/architecture/xworkmate-internal-state-architecture.md diff --git a/docs/architecture/archive/assistant-thread-working-directory-flow.md b/docs/architecture/archive/assistant-thread-working-directory-flow.md index f6c74db5..685e1309 100644 --- a/docs/architecture/archive/assistant-thread-working-directory-flow.md +++ b/docs/architecture/archive/assistant-thread-working-directory-flow.md @@ -5,7 +5,7 @@ > 已过时:本文记录的是 `workspaceRef / workspaceRefKind / cwd fallback` 主导时期的线程目录流转。 > > 当前实现请优先参考: -> [docs/architecture/assistant-thread-target-model-20260328.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/assistant-thread-target-model-20260328.md) +> [docs/architecture/task-control-plane-unification.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-control-plane-unification.md) > > 新文档已经把 TaskThread 的主流程图和状态图重画为基于 `workspaceBinding / executionBinding / lifecycleState` 的 Mermaid 版本。 diff --git a/docs/architecture/assistant-thread-information-architecture.md b/docs/architecture/assistant-thread-information-architecture.md deleted file mode 100644 index 0f7b3ef4..00000000 --- a/docs/architecture/assistant-thread-information-architecture.md +++ /dev/null @@ -1,67 +0,0 @@ -# Assistant TaskThread 信息架构 - -本文说明线程信息如何围绕 `TaskThread` 进入 UI、进入 controller/runtime 的执行请求构造,再通过统一任务入口回写到 UI。 - -统一目标规范以 -[任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) -为准。 - -## 主规则 - -1. UI 当前选择的是 `TaskThread.threadId` -2. UI 选中线程后读取完整 `TaskThread` -3. 主体区域显示、右栏显示、执行请求构造都围绕同一个 `TaskThread` -4. UI 保持现有结构,但不是线程信息的独立来源 - -## 信息流转图 - -```mermaid -flowchart LR - A["UI选择任务线程"] --> B["TaskThread.threadId"] - B --> C["读取 TaskThread"] - - C --> D1["ownerScope"] - C --> D2["workspaceBinding"] - C --> D3["executionBinding"] - C --> D4["contextState"] - - D1 --> E["构造执行请求"] - D2 --> E - D3 --> E - D4 --> E - - E --> F["GoTaskService.executeTask"] - F --> G["ACP Control Plane"] - G --> H{"resolvedExecutionTarget"} - H -->|single-agent| I["single-agent executor"] - H -->|multi-agent| J["multi-agent executor"] - H -->|gateway| K["gateway executor"] - - I --> L["执行结果"] - J --> L - K --> L - - L --> M["回写线程上下文"] - L --> N["显式更新 workspaceBinding"] - - M --> O["主体区域 / 右栏显示"] - N --> O -``` - -## Current implementation note - -- 当前实现中可能仍有 adapter 直连痕迹。 -- 这些痕迹不再作为信息架构规范的一部分。 - -## Target architecture rule - -- `读取 TaskThread` 是 UI 与执行层共享的唯一线程信息入口 -- `构造执行请求` 在 `GoTaskService / runtime` 协调层完成 -- 统一入口是 `GoTaskService.executeTask` -- `gateway` 是 ACP 解析出的执行器分支 -- `workspaceBinding` 只允许来自 create/load 显式绑定或结构化结果回写 - -## Compatibility route (temporary) - -- 不再定义新的 relay-only 执行协议 -- 旧的 direct gateway / direct collaboration 文档口径已废止 diff --git a/docs/architecture/assistant-thread-target-model-20260328.md b/docs/architecture/assistant-thread-target-model-20260328.md deleted file mode 100644 index f4074526..00000000 --- a/docs/architecture/assistant-thread-target-model-20260328.md +++ /dev/null @@ -1,81 +0,0 @@ -# Assistant TaskThread 当前模型(2026-03-28) - -本文保留 `TaskThread` 的当前模型说明,但不再把现有兼容分流描述为长期规范。 - -统一规范以 [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) 为准。 - -## 当前结论 - -1. `TaskThread` 是任务线程的唯一主对象。 -2. UI 保持现有结构不变,但线程选择的唯一键是 `TaskThread.threadId`。 -3. UI 选中线程后,系统必须读取完整 `TaskThread`。 -4. `workspaceBinding` 在 create/load 时必须完整;缺失 binding 的旧记录按非法数据处理并跳过加载。 -5. 执行请求由 controller / runtime 根据 `ownerScope / workspaceBinding / executionBinding / contextState` 构造。 -6. 当前实现曾存在多条执行链,但目标规范已经收敛到 `UI -> GoTaskService -> ACP -> resolved executor`。 -7. 执行结果先回写 `TaskThread.contextState`,主体区域同步显示。 -8. `contextState` 是线程上下文真相源;`lifecycleState` 只表达生命周期摘要。 - -## TaskThread 结构 - -```text -TaskThread -- threadId: String -- title: String -- ownerScope: ThreadOwnerScope -- workspaceBinding: WorkspaceBinding -- executionBinding: ExecutionBinding -- contextState: ThreadContextState -- lifecycleState: ThreadLifecycleState -- createdAtMs: double -- updatedAtMs: double? -``` - -## 生命周期主链 - -```mermaid -flowchart LR - A["UI选择任务线程"] --> B["TaskThread.threadId"] - B --> C["读取 TaskThread"] - - C --> D1["ownerScope"] - C --> D2["workspaceBinding"] - C --> D3["executionBinding"] - C --> D4["contextState"] - - D1 --> E["构造执行请求"] - D2 --> E - D3 --> E - D4 --> E - - E --> F["GoTaskService.executeTask"] - F --> G["ACP Control Plane"] - G --> H{"resolvedExecutionTarget"} - H -->|single-agent| I["single-agent executor"] - H -->|multi-agent| J["multi-agent executor"] - H -->|gateway| K["gateway executor"] - - I --> L["执行结果"] - J --> L - K --> L - - L --> M["回写线程上下文"] - L --> N["显式更新 workspaceBinding"] - - M --> O["主体区域 / 右栏显示"] - N --> O -``` - -## Current implementation note - -- 当前仓库仍能看到一些历史分流痕迹。 -- 这些实现痕迹不再作为长期规范文档的一部分。 - -## Target architecture rule - -- 目标规范是单一路径:`TaskThread -> GoTaskService -> ACP -> resolved executor` -- `gateway` 是解析后的执行器分支,不是 UI/controller 的规范旁路 - -## Compatibility route (temporary) - -- 历史文档中的 `OpenClaw lane`、`ACP lane` 并列口径已废止 -- 若代码中仍出现旧 route,只能视为待清理实现遗留 diff --git a/docs/architecture/cloud-session-service-multi-device-architecture-2026-03-30.md b/docs/architecture/cloud-session-service-multi-device-architecture-2026-03-30.md index 7009cec2..94c4867c 100644 --- a/docs/architecture/cloud-session-service-multi-device-architecture-2026-03-30.md +++ b/docs/architecture/cloud-session-service-multi-device-architecture-2026-03-30.md @@ -456,11 +456,11 @@ sequenceDiagram ## 12. 与现有文档的关系 -- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/assistant-thread-target-model-20260328.md`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/assistant-thread-target-model-20260328.md) +- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-control-plane-unification.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-control-plane-unification.md) 说明当前 `TaskThread` 主模型。 - [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-thread-session-key-isolation-20260329.md`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-thread-session-key-isolation-20260329.md) 说明 `sessionKey` 与线程身份隔离约束。 -- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-internal-state-architecture.md`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-internal-state-architecture.md) +- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-layered-architecture.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-layered-architecture.md) 说明当前内部状态如何围绕 `TaskThread` 组织。 本文在这些基础上进一步上升一层,定义跨设备会话的目标架构。 diff --git a/docs/architecture/task-control-plane-unification.md b/docs/architecture/task-control-plane-unification.md index 37b89ccd..35d09bd1 100644 --- a/docs/architecture/task-control-plane-unification.md +++ b/docs/architecture/task-control-plane-unification.md @@ -45,6 +45,20 @@ flowchart TD Q --> R["UI stream render"] ``` +## 端侧桥接规则 + +### Desktop App + +- Desktop App 直接桥接 Go 代码 +- Desktop 正常执行链路不以“先启动一个本地 HTTP server,再由 Desktop 自己回连”作为目标架构 +- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义,不是 Web server 回环语义 + +### Web / Mobile + +- Web / Mobile UI 连接的是 Go 代码启动出来的 server +- Web / Mobile 通过标准 ACP contract 与该 server 通信 +- 对 Web / Mobile 来说,`/acp` 与 `/acp/rpc` 是稳定的网络协议入口 + ## 协议约束 ### 传输协议 @@ -58,6 +72,8 @@ flowchart TD - websocket endpoint 规范路径:`/acp` - RPC endpoint 规范路径:`/acp/rpc` - base URL 派生时必须避免重复拼接 `/acp` +- 以上 endpoint contract 主要适用于 Web / Mobile 与外部 ACP server 的通信语义 +- Desktop 目标态不要求为自身 UI 再额外启动一层本地 HTTP ACP server ## 收敛原则 @@ -70,6 +86,8 @@ flowchart TD - 所有正常发送请求都先进入 `GoTaskService.executeTask` - 所有任务都先进入 ACP 控制面,再解析到 executor +- Desktop 采用直接桥接 Go 代码的控制面接入方式 +- Web / Mobile 采用连接 Go server 的控制面接入方式 ### Compatibility route (removed from target) diff --git a/docs/architecture/task-thread-session-key-isolation-20260329.md b/docs/architecture/task-thread-session-key-isolation-20260329.md index a3adfb61..631ece45 100644 --- a/docs/architecture/task-thread-session-key-isolation-20260329.md +++ b/docs/architecture/task-thread-session-key-isolation-20260329.md @@ -9,8 +9,7 @@ 本文是对现有 `TaskThread` 主模型的补充,不替代: -- `assistant-thread-target-model-20260328.md` -- `assistant-thread-information-architecture.md` +- `task-control-plane-unification.md` ## 1. 问题定义 @@ -315,6 +314,5 @@ single-agent 入口必须在执行前验证: 推荐阅读顺序: -1. `assistant-thread-target-model-20260328.md` -2. `assistant-thread-information-architecture.md` +1. `task-control-plane-unification.md` 3. `task-thread-session-key-isolation-20260329.md`(本文) diff --git a/docs/architecture/xworkmate-integrations.md b/docs/architecture/xworkmate-integrations.md deleted file mode 100644 index e18eef37..00000000 --- a/docs/architecture/xworkmate-integrations.md +++ /dev/null @@ -1,219 +0,0 @@ -# XWorkmate 集成架构 - -## 概述 - -XWorkmate 现阶段已经不只是“单一 Codex bridge”,但当前实现也不是一个单独的 -“Discovery / Distribution Catalog” 模块。 - -本文件只说明集成能力与 adapter 边界,不承担任务工作流主叙事。 - -任务工作流主叙事统一以 -[任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) -为准。 - -当前集成能力分散在几条明确的实现路径里: - -1. `GatewayRuntime` - - 负责 OpenClaw Gateway 的实时 RPC、会话、chat、pairing、cron -2. `MultiAgentBrokerServer` + `MultiAgentOrchestrator` - - 负责多 Agent 协作运行 -3. `MultiAgentMountManager` - - 负责按 adapter 做 CLI 能力探测、MCP reconcile、挂载状态汇总 -4. `CodexConfigBridge` / `OpencodeConfigBridge` - - 负责特定 CLI 的配置文件写入 -5. Assistant composer 与 feature flags - - 决定当前哪些集成入口真实对用户可见 - -也就是说,当前架构更接近“分布式集成面”,不是单一 catalog service。 -这些能力应被理解为控制面之下的 adapter / executor 能力,而不是 UI 规范直连入口。 - -## 当前架构基线 - -```mermaid -flowchart LR - X["XWorkmate App"] --> GR["GatewayRuntime"] - X --> BM["MultiAgentBrokerServer
WebSocket JSON-RPC"] - X --> MM["MultiAgentMountManager"] - X --> NO["CodeAgentNodeOrchestrator"] - X --> UI["Assistant composer / Settings / Feature flags"] - - BM --> O["MultiAgentOrchestrator"] - O --> C["Codex / Claude / Gemini / OpenCode"] - - MM --> MA["Codex / Claude / Gemini / OpenCode / OpenClaw adapters"] - MA --> CFG["Managed config writes / mcp list / local file discovery"] - - GR --> G["OpenClaw Gateway / Host"] - NO --> G - C --> A["AI Gateway or Ollama endpoint"] -``` - -关键点: - -- `MultiAgentBroker` 是多 CLI 协作的本地运行时入口。 -- `OpenClaw` 既是现有 Gateway 集成面,也是当前 app-mediated code-agent dispatch 的宿主控制面。 -- `AI Gateway` 既可以是 direct AI 对话入口,也可以是协作运行的注入式模型入口。 -- 当前没有一个单独命名为 `Discovery / Distribution Catalog` 的实现模块。 -- `GatewayRuntime`、relay、`GatewayAcpClient` 在统一收敛目标下都应视为 adapter/executor 能力。 - -## 1. OpenClaw Gateway / Host - -用途: - -- 运行时协同 -- 设备与信任边界 -- Agent / Session / Chat 通道 -- 宿主控制面发现 - -已使用能力: - -- `health` -- `status` -- `agents.list` -- `sessions.list` -- `chat.send` -- `device.pair.*` -- `cron.list` -- `agent/register` -- `memory/sync` - -当前定位: - -- 继续作为 Gateway RPC 面存在 -- 也是 app-mediated code-agent dispatch 的控制面目标 -- 在 mount 视角下,OpenClaw 目前更多是“本地发现 + 宿主控制面”,不是一个统一的 skills / plugins catalog service - -## 2. AI Gateway - -用途: - -- direct AI 对话入口 -- 协作运行时的模型注入入口 -- 对部分 CLI 的配置桥接入口 - -边界: - -- 不负责设备配对 -- 不负责 session / agent 生命周期 -- 不替换用户现有默认 provider / model - -当前策略: - -- `CodexConfigBridge` 可以写入受管 provider / MCP block -- `MultiAgentOrchestrator` 在协作运行中会通过环境变量或 `ollama launch` 传递模型入口 -- `Claude / Gemini` 的 mount reconcile 目前主要做 discovery,AI Gateway 仍保持 launch-scoped -- `OpenCode` 当前有受管 MCP config;AI Gateway 语义仍偏 launch-scoped / runtime injection - -换句话说,AI Gateway 能力是分散落地的,不是所有 CLI 都通过同一条托管 provider 路径接入。 - -## 3. Multi-Agent Runtime - -### 编排层 - -`MultiAgentOrchestrator` 负责: - -- Architect 任务分析 -- Engineer 实现 -- Tester / Doc 审阅 -- 迭代评分与回退 - -### Broker 层 - -`MultiAgentBroker` 负责: - -- 本地 `WebSocket JSON-RPC` -- run lifecycle -- worker CLI 启动 -- selected skills / MCP / Gateway 上下文注入 -- 结构化事件流回写当前会话 - -### UI 接线 - -- Assistant 继续复用现有 composer、附件、当前会话 -- 桌面端真正对用户可见的协作入口,当前主要是 Assistant composer 上的协作 toggle -- `SettingsPage` 里有 Multi-Agent 配置区块与 detail 页面代码,但桌面端 `settings.agents` 仍被 feature flag 关闭 -- 不新增独立任务页面 - -## 4. 发现与分发 - -当前实现里,`managed / external` 更像一套按 adapter 执行的操作规则,而不是单独的中心化状态目录。 - -XWorkmate 仍然区分两类对象: - -- `managed` - - 由 App 创建与维护的托管项 -- `external` - - 外部已有配置或 CLI 自带配置 - -统一规则: - -- 只更新 XWorkmate 托管项 -- 不删除外部已有项 -- 启动时与保存设置后自动 reconcile -- 这套规则当前由 `MultiAgentMountManager` 在各 adapter 上分别执行 - -## 5. 挂载入口矩阵 - -| 目标 | Skills 挂载入口 | MCP 挂载入口 | AI Gateway 挂载入口 | -| --- | --- | --- | --- | -| OpenClaw | 本地文件 / 目录发现 + Gateway 控制面 | 不作为 MCP 主挂载点 | app-mediated dispatch / gateway route | -| Codex | 当前线程 skills 上下文 +协作运行注入 | `~/.codex/config.toml` 受管 MCP block | 受管 provider bridge + runtime injection | -| Claude | 当前线程 skills 上下文 +协作运行注入 | `claude mcp list` 做 discovery | launch-scoped / env / `ollama launch` | -| Gemini | 当前线程 skills 上下文 +协作运行注入 | `gemini mcp list` 做 discovery | launch-scoped / env | -| OpenCode | 当前线程 skills 上下文 +协作运行注入 | `~/.opencode/config.toml` 受管 MCP block | runtime injection | - -## 6. 外部 Provider 与执行路径 - -保留现有统一 contract: - -- `ExternalCodeAgentProvider.id` -- `name` -- `command` -- `defaultArgs` -- `capabilities` -- `CodeAgentNodeOrchestrator.buildGatewayDispatch()` - -现状: - -- `codex` 仍是当前最完整 provider -- 其他 CLI 当前主要通过 `CliMountAdapter` discovery / reconcile 与 `MultiAgentOrchestrator` 运行时调用接入 -- 多 provider 调度 UI 不是当前交付目标 - -## 7. 安全边界 - -- `.env` 仅用于开发预填充,不自动连接,不作为持久化真值源 -- AI Gateway API Key 与 Gateway 凭证继续走 secure storage -- 新增协作路径不得把 secret 写入 `SharedPreferences` -- Launch-scoped 注入优先于全局配置改写 -- 远程 Gateway 不允许静默降级为非 TLS -- 协作事件与 metadata 不上传本地 secret 或本机绝对路径 - -## 8. 设置页统一动作语义(Gateway 家族) - -`OpenClaw Gateway`、`Vault`、`AI Gateway`(以及后续外部扩展)统一遵循同一操作语义: - -- `Test`:只使用当前草稿(含当前输入的临时 secret 覆盖)做连通性校验,不写入持久层。 -- `Save`:把草稿同步到本地持久存储(`SettingsStore` + `SecretStore`),不立即改变运行时会话行为。 -- `Apply`:在 `Save` 的基础上,立即让当前运行时按新配置生效。 - -实现约束: - -- Gateway 集成页不再重复显示顶层全局 `Save / Apply`,避免与卡片内动作语义冲突。 -- 桌面端 `settings.gateway_setup_code` 与 `settings.agents` 当前都被 feature flag 关闭。 -- 但桌面端 `assistant.multi_agent` 仍然开启,所以协作入口当前主要暴露在 Assistant composer,而不是设置页独立标签。 - -## 相关代码 - -- `lib/app/app_controller_desktop.dart` -- `lib/app/app_controller_web.dart` -- `lib/features/assistant/assistant_page.dart` -- `lib/features/settings/settings_page.dart` -- `lib/runtime/gateway_runtime.dart` -- `lib/runtime/runtime_models.dart` -- `lib/runtime/multi_agent_orchestrator.dart` -- `lib/runtime/multi_agent_broker.dart` -- `lib/runtime/multi_agent_mounts.dart` -- `lib/runtime/codex_config_bridge.dart` -- `lib/runtime/opencode_config_bridge.dart` -- `lib/runtime/code_agent_node_orchestrator.dart` -- `lib/runtime/runtime_coordinator.dart` diff --git a/docs/architecture/xworkmate-internal-state-architecture.md b/docs/architecture/xworkmate-internal-state-architecture.md deleted file mode 100644 index 83d4b64d..00000000 --- a/docs/architecture/xworkmate-internal-state-architecture.md +++ /dev/null @@ -1,96 +0,0 @@ -# XWorkmate App Internal State Architecture - -Last Updated: 2026-04-08 - -## Purpose - -本文定义当前 XWorkmate 的内部状态组织,重点说明以下对象之间的关系: - -- Settings 中心配置状态 -- `TaskThread` 线程状态 -- `GoTaskService / runtime` 协调状态 -- 派生 UI 状态 - -目标规范以 -[任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) -为准。 - -## Core Rule - -当前内部状态仍分为: - -- Layer A: Settings 中心配置状态 -- Layer B: `TaskThread` 线程状态 -- Layer C: 派生 UI 状态 - -最重要的规则是: - -- Settings 不是当前线程状态 -- `TaskThread` 负责当前线程真实使用的工作空间、执行通道、上下文和生命周期 -- UI 必须从解析后的 `TaskThread` 渲染 - -## Internal State Diagram - -```mermaid -graph TB - subgraph P["Persistence Layer"] - SettingsStore["SettingsStore"] - SecretStore["SecretStore"] - SecureConfigStore["SecureConfigStore"] - ThreadRepository["TaskThread Repository"] - end - - subgraph C["Controllers"] - settings["settings / settingsDraft"] - threadRecords["thread records"] - currentThreadId["current thread id"] - runtimeCaches["streaming / preview caches"] - end - - subgraph R["GoTaskService / Runtime Coordination"] - threadReader["read TaskThread by threadId"] - requestBuilder["build execution request"] - dispatcher["GoTaskService.executeTask"] - acp["ACP Control Plane"] - executors["resolved executors"] - resultWriter["write result back to TaskThread"] - end - - subgraph U["Derived UI State"] - assistantPage["AssistantPage"] - sidebar["right sidebar / preview"] - taskList["task list"] - settingsPage["SettingsPage"] - end - - SettingsStore --> settings - SecureConfigStore --> settings - SecretStore --> settings - ThreadRepository --> threadRecords - - currentThreadId --> threadReader - threadRecords --> threadReader - threadReader --> requestBuilder - requestBuilder --> dispatcher - dispatcher --> acp - acp --> executors - executors --> resultWriter - resultWriter --> threadRecords - - threadReader --> assistantPage - threadReader --> sidebar - threadRecords --> taskList - settings --> settingsPage -``` - -## Current implementation note - -- controller 侧可能仍有旧 dispatch 痕迹 -- 当前目标是让这些痕迹退出长期状态架构口径 - -## Target architecture rule - -- `GoTaskService.executeTask` 是唯一公开任务入口 -- ACP 是统一控制面 -- `gateway` 是解析出的 executor,不再作为 UI 规范旁路 -- runtime cache 只承载瞬时状态,不承载线程长期语义 diff --git a/docs/architecture/xworkmate-layered-architecture.md b/docs/architecture/xworkmate-layered-architecture.md index 903be1dc..de08fa0d 100644 --- a/docs/architecture/xworkmate-layered-architecture.md +++ b/docs/architecture/xworkmate-layered-architecture.md @@ -97,13 +97,12 @@ flowchart TB ### 当前实现观察 -- [Assistant TaskThread 当前模型(2026-03-28)](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/assistant-thread-target-model-20260328.md) -- [Assistant TaskThread 信息架构](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/assistant-thread-information-architecture.md) -- [XWorkmate App Internal State Architecture](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/xworkmate-internal-state-architecture.md) +- 当前实现观察不再保留独立主设计文档 +- 如需判断规范,以 [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) 为准 ### 边界与适配器说明 -- [XWorkmate 集成架构](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/xworkmate-integrations.md) +- 适配器边界统一收敛到本文件与主文档,不再保留旧的并列设计稿 ## Compatibility route (removed from target) diff --git a/docs/plans/2026-03-21-Mobile.md b/docs/plans/2026-03-21-Mobile.md index 9f2ce17f..6da80496 100644 --- a/docs/plans/2026-03-21-Mobile.md +++ b/docs/plans/2026-03-21-Mobile.md @@ -20,7 +20,7 @@ XWorkmate 的 iOS/Android 不应该被定义成“第二个 Code Agent 运行端 - 仓库现状: - XWorkmate 当前仍是 desktop-first。 - 移动端已存在统一壳层,但本质是 UI surface,不是独立执行 runtime。 - - 参考:[README.md](../../README.md)、[xworkmate-integrations.md](../architecture/xworkmate-integrations.md)、[mobile_shell.dart](../../lib/features/mobile/mobile_shell.dart) + - 参考:[README.md](../../README.md)、[task-control-plane-unification.md](../architecture/task-control-plane-unification.md)、[mobile_shell.dart](../../lib/features/mobile/mobile_shell.dart) - Codex 用户诉求: - 2025-08-27 的 `#2798` 请求 remote/headless sign-in。 - 2025-09-02 的 `#3052` 请求 approval/job completion 通知。 @@ -347,7 +347,7 @@ Mac Node Agent ### 本地架构与现状 - [README.md](../../README.md) -- [xworkmate-integrations.md](../architecture/xworkmate-integrations.md) +- [task-control-plane-unification.md](../architecture/task-control-plane-unification.md) - [gateway-dev-runbook.md](../runbooks/gateway-dev-runbook.md) - [mobile_shell.dart](../../lib/features/mobile/mobile_shell.dart) - [app_controller.dart](../../lib/app/app_controller.dart) diff --git a/lib/app/app_controller_web_helpers.dart b/lib/app/app_controller_web_helpers.dart index 8372dca7..dc265ef0 100644 --- a/lib/app/app_controller_web_helpers.dart +++ b/lib/app/app_controller_web_helpers.dart @@ -691,39 +691,14 @@ extension AppControllerWebHelpers on AppController { Future> requestAcpSessionMessageInternal({ required Uri endpoint, required Map params, - required bool hasInlineAttachments, void Function(Map notification)? onNotification, }) async { - try { - return await acpClientInternal.request( - endpoint: endpoint, - method: 'session.message', - params: params, - onNotification: onNotification, - ); - } on WebAcpException catch (error) { - if (!hasInlineAttachments || - !canFallbackInlineAttachmentsInternal(error)) { - rethrow; - } - final fallbackParams = Map.from(params) - ..remove('inlineAttachments'); - try { - return await acpClientInternal.request( - endpoint: endpoint, - method: 'session.message', - params: fallbackParams, - onNotification: onNotification, - ); - } on Object catch (fallbackError) { - throw Exception( - appText( - 'ACP 暂不支持 inline 附件,回退旧协议也失败:$fallbackError', - 'ACP does not support inline attachments, and fallback to legacy attachment payload failed: $fallbackError', - ), - ); - } - } + return acpClientInternal.request( + endpoint: endpoint, + method: 'session.message', + params: params, + onNotification: onNotification, + ); } Future refreshAcpCapabilitiesInternal(Uri endpoint) async { @@ -736,18 +711,6 @@ extension AppControllerWebHelpers on AppController { } } - bool canFallbackInlineAttachmentsInternal(WebAcpException error) { - final code = (error.code ?? '').trim(); - if (code == '-32602' || code == 'INVALID_PARAMS') { - return true; - } - final message = error.toString().toLowerCase(); - return message.contains('inlineattachment') || - message.contains('unexpected field') || - message.contains('unknown field') || - message.contains('invalid params'); - } - bool unsupportedAcpSkillsStatusInternal(WebAcpException error) { final code = (error.code ?? '').trim(); if (code == '-32601' || code == 'METHOD_NOT_FOUND') { diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index 10cf6f95..04170bb8 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -309,58 +309,8 @@ class GatewayAcpClient { }) async { final resolvedEndpoint = endpointOverride ?? endpointResolver(); final scheme = resolvedEndpoint?.scheme.trim().toLowerCase() ?? ''; - final canUseHttp = resolveAcpHttpRpcEndpoint(resolvedEndpoint) != null; if (scheme == 'http' || scheme == 'https') { - try { - return await _requestViaHttp( - request, - onNotification: onNotification, - endpointOverride: resolvedEndpoint, - authorizationOverride: authorizationOverride, - ); - } catch (error) { - if (error is GatewayAcpException) { - if (!_shouldRetryViaWebSocketAfterHttpFailure( - error, - endpoint: resolvedEndpoint, - )) { - rethrow; - } - try { - return await _requestViaWebSocket( - request, - onNotification: onNotification, - endpointOverride: resolvedEndpoint, - authorizationOverride: authorizationOverride, - ); - } catch (wsError) { - throw _toPreferredHttpFailureAfterWebSocketFallback( - httpError: error, - webSocketError: wsError, - ); - } - } - return _requestViaWebSocket( - request, - onNotification: onNotification, - endpointOverride: resolvedEndpoint, - authorizationOverride: authorizationOverride, - ); - } - } - - try { - return await _requestViaWebSocket( - request, - onNotification: onNotification, - endpointOverride: resolvedEndpoint, - authorizationOverride: authorizationOverride, - ); - } catch (_) { - if (!canUseHttp) { - rethrow; - } return _requestViaHttp( request, onNotification: onNotification, @@ -368,6 +318,13 @@ class GatewayAcpClient { authorizationOverride: authorizationOverride, ); } + + return _requestViaWebSocket( + request, + onNotification: onNotification, + endpointOverride: resolvedEndpoint, + authorizationOverride: authorizationOverride, + ); } Future> _requestViaWebSocket( @@ -376,35 +333,20 @@ class GatewayAcpClient { Uri? endpointOverride, String authorizationOverride = '', }) async { - final endpoints = _resolveWebSocketRpcEndpoints(endpointOverride); - if (endpoints.isEmpty) { + final endpoint = resolveAcpWebSocketEndpoint( + endpointOverride ?? endpointResolver(), + ); + if (endpoint == null) { throw const GatewayAcpException( 'Missing ACP endpoint', code: 'ACP_ENDPOINT_MISSING', ); } - - Object? lastError; - for (var index = 0; index < endpoints.length; index += 1) { - final endpoint = endpoints[index]; - try { - return await _requestViaWebSocketEndpoint( - request, - endpoint: endpoint, - onNotification: onNotification, - authorizationOverride: authorizationOverride, - ); - } catch (error) { - lastError = error; - if (index == endpoints.length - 1 || - !_shouldTryNextWebSocketCandidate(error)) { - rethrow; - } - } - } - throw GatewayAcpException( - lastError?.toString() ?? 'ACP websocket request failed', - code: 'ACP_WS_RUNTIME_ERROR', + return _requestViaWebSocketEndpoint( + request, + endpoint: endpoint, + onNotification: onNotification, + authorizationOverride: authorizationOverride, ); } @@ -592,54 +534,6 @@ class GatewayAcpClient { return base; } - bool _shouldRetryViaWebSocketAfterHttpFailure( - GatewayAcpException error, { - required Uri? endpoint, - }) { - if (resolveAcpWebSocketEndpoint(endpoint) == null) { - return false; - } - final details = asMap(error.details); - final statusCode = intValue(details['statusCode']); - final contentType = (stringValue(details['contentType']) ?? '') - .trim() - .toLowerCase(); - return statusCode == HttpStatus.notFound && - (contentType.isEmpty || contentType.contains('text/plain')); - } - - bool _shouldTryNextWebSocketCandidate(Object error) { - if (error is WebSocketException || - error is SocketException || - error is HandshakeException || - error is HttpException) { - return true; - } - if (error is! GatewayAcpException) { - return false; - } - return error.code == 'ACP_WS_EARLY_CLOSE' || - error.code == 'ACP_WS_RUNTIME_ERROR' || - error.code == 'ACP_WS_CONNECT_TIMEOUT'; - } - - GatewayAcpException _toPreferredHttpFailureAfterWebSocketFallback({ - required GatewayAcpException httpError, - required Object webSocketError, - }) { - final wsError = webSocketError is GatewayAcpException - ? webSocketError - : null; - return GatewayAcpException( - httpError.message, - code: httpError.code, - details: { - ...asMap(httpError.details), - if (wsError?.code != null) 'websocketFallbackCode': wsError!.code, - }, - ); - } - bool _contentTypeLooksJsonOrSse(String contentType) { return contentType.contains('application/json') || contentType.contains('application/problem+json') || @@ -879,30 +773,6 @@ class GatewayAcpClient { return const {}; } - List _resolveWebSocketRpcEndpoints([Uri? endpointOverride]) { - final endpoint = endpointOverride ?? endpointResolver(); - if (endpoint == null || endpoint.host.trim().isEmpty) { - return const []; - } - final candidates = []; - final derived = resolveAcpWebSocketEndpoint(endpoint); - if (derived != null) { - candidates.add(derived); - } - final scheme = switch (endpoint.scheme.trim().toLowerCase()) { - 'https' || 'wss' => 'wss', - _ => 'ws', - }; - final raw = endpoint.replace(scheme: scheme, query: null, fragment: null); - final duplicate = candidates.any( - (candidate) => candidate.toString() == raw.toString(), - ); - if (!duplicate) { - candidates.add(raw); - } - return candidates; - } - Uri? _resolveHttpRpcEndpoint([Uri? endpointOverride]) { return resolveAcpHttpRpcEndpoint(endpointOverride ?? endpointResolver()); } diff --git a/test/runtime/gateway_acp_client_suite.dart b/test/runtime/gateway_acp_client_suite.dart index 03928d5c..2b4af956 100644 --- a/test/runtime/gateway_acp_client_suite.dart +++ b/test/runtime/gateway_acp_client_suite.dart @@ -121,7 +121,7 @@ void main() { ); test( - 'falls back to websocket when HTTP bridge returns plain-text 404', + 'keeps HTTP 404 as the final error when HTTP ACP bridge is missing', () async { final server = await _AcpFakeServer.start( respondWithPlainTextNotFound: true, @@ -132,28 +132,6 @@ void main() { endpointResolver: () => server.baseHttpUri, ); - final capabilities = await client.loadCapabilities(forceRefresh: true); - - expect(capabilities.singleAgent, isTrue); - expect(server.lastHttpRequestPath, '/acp/rpc'); - expect(server.lastWebSocketRequestPath, '/acp'); - expect(server.rpcMethods, contains('acp.capabilities')); - }, - ); - - test( - 'keeps HTTP 404 as primary error when websocket fallback also fails', - () async { - final server = await _AcpFakeServer.start( - disableWebSocket: true, - respondWithPlainTextNotFound: true, - ); - addTearDown(server.close); - - final client = GatewayAcpClient( - endpointResolver: () => server.baseHttpUri, - ); - await expectLater( () => client.loadCapabilities(forceRefresh: true), throwsA( @@ -166,29 +144,7 @@ void main() { ), ), ); - }, - ); - - test( - 'falls back to raw websocket path when derived ACP path is unavailable', - () async { - final server = await _AcpFakeServer.start( - respondWithPlainTextNotFound: true, - pathPrefix: '/opencode', - useRawWebSocketPathOnly: true, - ); - addTearDown(server.close); - - final client = GatewayAcpClient( - endpointResolver: () => server.baseHttpUri, - ); - - final capabilities = await client.loadCapabilities(forceRefresh: true); - - expect(capabilities.singleAgent, isTrue); - expect(server.lastHttpRequestPath, '/opencode/acp/rpc'); - expect(server.lastWebSocketRequestPath, '/opencode'); - expect(server.rpcMethods, contains('acp.capabilities')); + expect(server.lastWebSocketRequestPath, isNull); }, ); @@ -310,7 +266,6 @@ class _AcpFakeServer { required this.disableWebSocket, required this.respondWithHtmlError, required this.respondWithPlainTextNotFound, - required this.useRawWebSocketPathOnly, required this.pathPrefix, }); @@ -318,7 +273,6 @@ class _AcpFakeServer { final bool disableWebSocket; final bool respondWithHtmlError; final bool respondWithPlainTextNotFound; - final bool useRawWebSocketPathOnly; final String pathPrefix; final List rpcMethods = []; String? lastWebSocketAuthorization; @@ -333,7 +287,6 @@ class _AcpFakeServer { bool disableWebSocket = false, bool respondWithHtmlError = false, bool respondWithPlainTextNotFound = false, - bool useRawWebSocketPathOnly = false, String pathPrefix = '', }) async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); @@ -342,7 +295,6 @@ class _AcpFakeServer { disableWebSocket: disableWebSocket, respondWithHtmlError: respondWithHtmlError, respondWithPlainTextNotFound: respondWithPlainTextNotFound, - useRawWebSocketPathOnly: useRawWebSocketPathOnly, pathPrefix: _normalizePathPrefix(pathPrefix), ); unawaited(fake._listen()); @@ -355,12 +307,8 @@ class _AcpFakeServer { Future _listen() async { await for (final request in _server) { - final wsPaths = [ - if (!useRawWebSocketPathOnly) '$pathPrefix/acp', - if (useRawWebSocketPathOnly) (pathPrefix.isEmpty ? '/' : pathPrefix), - ]; if (!disableWebSocket && - wsPaths.contains(request.uri.path) && + request.uri.path == '$pathPrefix/acp' && WebSocketTransformer.isUpgradeRequest(request)) { lastWebSocketRequestPath = request.uri.path; lastWebSocketAuthorization = request.headers.value(