cleanup: remove ACP bypass fallbacks and stale architecture docs
This commit is contained in:
parent
a669453619
commit
792f7920e9
@ -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 版本。
|
||||
|
||||
|
||||
@ -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 文档口径已废止
|
||||
@ -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,只能视为待清理实现遗留
|
||||
@ -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` 组织。
|
||||
|
||||
本文在这些基础上进一步上升一层,定义跨设备会话的目标架构。
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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`(本文)
|
||||
|
||||
@ -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<br/>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`
|
||||
@ -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<TaskThread>"]
|
||||
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 只承载瞬时状态,不承载线程长期语义
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -691,39 +691,14 @@ extension AppControllerWebHelpers on AppController {
|
||||
Future<Map<String, dynamic>> requestAcpSessionMessageInternal({
|
||||
required Uri endpoint,
|
||||
required Map<String, dynamic> params,
|
||||
required bool hasInlineAttachments,
|
||||
void Function(Map<String, dynamic> 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<String, dynamic>.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<void> 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') {
|
||||
|
||||
@ -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<Map<String, dynamic>> _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: <String, dynamic>{
|
||||
...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 <String, dynamic>{};
|
||||
}
|
||||
|
||||
List<Uri> _resolveWebSocketRpcEndpoints([Uri? endpointOverride]) {
|
||||
final endpoint = endpointOverride ?? endpointResolver();
|
||||
if (endpoint == null || endpoint.host.trim().isEmpty) {
|
||||
return const <Uri>[];
|
||||
}
|
||||
final candidates = <Uri>[];
|
||||
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());
|
||||
}
|
||||
|
||||
@ -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<String> rpcMethods = <String>[];
|
||||
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<void> _listen() async {
|
||||
await for (final request in _server) {
|
||||
final wsPaths = <String>[
|
||||
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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user