Remove OpenClaw direct ACP route

This commit is contained in:
Haitao Pan 2026-05-26 11:06:22 +08:00
parent 6f9f0c75c9
commit f861511a2f
10 changed files with 97 additions and 197 deletions

View File

@ -4,7 +4,7 @@ Last Updated: 2026-04-21
本文记录 `xworkmate-app` 当前对 `xworkmate-bridge` 的运行时路由合同。UI 不直接承载这些路径Assistant UI 仍由 `acp.capabilities` 返回的 `providerCatalog`、`gatewayProviders`、`availableExecutionTargets` 驱动。
App 侧任务发送默认调用 bridge 主入口 `/acp/rpc`,不再拼接 provider-specific 直连 URL。OpenClaw `session.start` 和同一任务的 `session.message` 是唯一例外,使用 bridge 公开的 task submit 专用路径 `/gateway/openclaw`。该路径不是全局 ACP base endpoint
App 侧任务发送统一调用 bridge 主入口 `/acp/rpc`,不再拼接 provider-specific 或 gateway-specific 直连 URL。OpenClaw `session.start` 和同一任务的 `session.message` 也走 `/acp/rpc`,通过 `routing.explicitExecutionTarget=gateway``routing.preferredGatewayProviderId=openclaw` 表达目标
## App Runtime Flow
@ -22,11 +22,8 @@ flowchart TD
D --> J["OpenClaw"]
A --> P["POST https://xworkmate-bridge.svc.plus/acp/rpc"]
A --> T["OpenClaw task POST /gateway/openclaw"]
P --> Q["Authorization: Bearer token"]
T --> Q
P --> R["provider / requestedExecutionTarget params"]
T --> R
P --> R["provider / routing / requestedExecutionTarget params"]
R --> S["bridge-owned routing"]
S --> K["Hermes internal runtime"]
@ -38,10 +35,10 @@ flowchart TD
## Routing Rules
- App runtime control-plane requests, agent tasks, multi-agent tasks, `session.cancel`, and `session.close` use `https://xworkmate-bridge.svc.plus/acp/rpc`.
- OpenClaw gateway `session.start` and follow-up `session.message` use `https://xworkmate-bridge.svc.plus/gateway/openclaw`.
- App runtime control-plane requests, agent tasks, multi-agent tasks, OpenClaw gateway tasks, `session.cancel`, and `session.close` use `https://xworkmate-bridge.svc.plus/acp/rpc`.
- OpenClaw gateway `session.start` and follow-up `session.message` are identified by routing metadata, not by a separate public path.
- Provider and gateway selection are passed as request params, including `provider`, `routing`, and `requestedExecutionTarget`.
- Bridge-owned internal routing is opaque to the App; it is not represented as public provider paths.
- The app must not route managed bridge tasks to local or LAN endpoints such as `127.0.0.1:*` or `192.168.*:*`.
- The app must not route managed bridge tasks by directly constructing `/acp-server/*` URLs.
- `/gateway/openclaw` is allowed only for OpenClaw task submit; it must not be reused for capabilities, routing, gateway control-plane, cancel, close, or as an ACP base endpoint.
- `/gateway/openclaw` must not participate in app runtime routing; it is neither a task submit path nor an ACP base endpoint.

View File

@ -17,7 +17,7 @@ The core ownership split is:
```mermaid
flowchart LR
U["User input / follow-up"] --> APP["xworkmate-app<br/>TaskThread + UI state"]
APP -->|session.start / session.message| BR["xworkmate-bridge<br/>/acp/rpc or /gateway/openclaw"]
APP -->|session.start / session.message| BR["xworkmate-bridge<br/>/acp/rpc"]
BR -->|xworkmate.routing.resolve| ROUTE{"Execution target"}
ROUTE -->|single-agent| AG["codex / opencode / gemini / hermes"]
ROUTE -->|gateway=openclaw| OC["OpenClaw Gateway Runtime"]
@ -108,7 +108,7 @@ flowchart TD
E --> G["drain queue"]
G --> C
C --> H["Bridge /gateway/openclaw"]
C --> H["Bridge /acp/rpc<br/>routing=gateway/openclaw"]
H --> I["OpenClaw execution"]
I --> J{"Result"}
J -->|success + output/files| K["APP ready<br/>lastResultCode=success<br/>sync artifacts"]
@ -118,13 +118,12 @@ flowchart TD
## Bridge Session And Routing Workflow
The bridge exposes one public session contract while keeping provider-specific behavior behind compatibility layers. `/gateway/openclaw` is a narrow OpenClaw task-submit lane, not a general ACP base endpoint.
The bridge exposes one public session contract while keeping provider-specific behavior behind bridge-owned routing. OpenClaw task submit uses `/acp/rpc` with explicit gateway routing metadata, not a separate app-facing path.
```mermaid
flowchart TD
REQ["APP request"] --> EP{"HTTP / WS entry"}
EP -->|/acp or /acp/rpc| RPC["General JSON-RPC"]
EP -->|/gateway/openclaw| GW["OpenClaw task-submit endpoint"]
RPC --> METHOD{"method"}
METHOD -->|acp.capabilities| CAP["Return agent providers + gatewayProviders=openclaw"]
@ -133,11 +132,6 @@ flowchart TD
METHOD -->|session.message| MSG["Continue existing session"]
METHOD -->|session.cancel / session.close| CTRL["Cancel / close session"]
GW --> GUARD{"method is session.start / session.message ?"}
GUARD -->|no| REJECT["Reject: not a global ACP base"]
GUARD -->|yes| FORCE["Force routing=gateway/openclaw<br/>Reject multiAgent"]
FORCE --> START
START --> ORCH["session_orchestrator"]
MSG --> ORCH
ORCH --> PROVIDER{"provider compat"}
@ -190,7 +184,7 @@ flowchart LR
## Boundary Rules
- The app does not store OpenClaw URLs. It only consumes bridge capabilities where `gatewayProviders` includes `openclaw`.
- `/gateway/openclaw` is only for OpenClaw `session.start` and `session.message`; it is not a global ACP endpoint.
- OpenClaw `session.start` and `session.message` use `/acp/rpc` with explicit OpenClaw gateway routing metadata; `/gateway/openclaw` is not an app-facing endpoint.
- Follow-up conversation uses the same `sessionKey` / `threadId`. Bridge `session.message` must continue the provider session state or return a structured continuation error.
- Artifact ownership is enforced by `openclaw-multi-session-plugins` with `tasks/<session>/<run>` scope. The app syncs only the current run's artifacts into the local thread workspace.
- Upgrade/install flows must preserve real local history. Cleanup must only remove explicitly known test-pollution session keys.

View File

@ -2,7 +2,7 @@
## 1. 架构概览 (Unified Routing Architecture)
当前系统采用 `xworkmate-bridge.svc.plus` 作为统一入口。App 侧通过 managed bridge ACP 主入口处理能力发现、路由解析、agent / multi-agent 任务和会话控制OpenClaw `session.start` / `session.message` 使用 bridge 暴露的 `/gateway/openclaw` task submit 专用入口。Provider runtime 地址仍由 bridge 后端内部拥有,不暴露为 App-facing public mapping。
当前系统采用 `xworkmate-bridge.svc.plus` 作为统一入口。App 侧通过 managed bridge ACP 主入口 `/acp/rpc` 处理能力发现、路由解析、agent / multi-agent 任务、OpenClaw gateway 任务和会话控制。Provider runtime 与 gateway runtime 地址仍由 bridge 后端内部拥有,不暴露为 App-facing public mapping。
```mermaid
graph TD
@ -16,7 +16,6 @@ graph TD
subgraph "Bridge-owned Routing"
ManagedBridge["Managed Bridge ACP<br/>/acp/rpc"]
OpenClawSubmit["OpenClaw task submit<br/>/gateway/openclaw"]
CodexProvider["Codex internal runtime"]
OpenCodeProvider["OpenCode internal runtime"]
GeminiAdapter["Gemini internal runtime"]
@ -27,11 +26,10 @@ graph TD
Client -->|HTTPS/WSS| Bridge_Domain
Bridge_Domain -->|/acp/rpc| ManagedBridge
Bridge_Domain -->|/gateway/openclaw| OpenClawSubmit
ManagedBridge -->|provider routing| CodexProvider
ManagedBridge -->|provider routing| OpenCodeProvider
ManagedBridge -->|provider routing| GeminiAdapter
OpenClawSubmit -->|forced openclaw routing| OpenClawGateway
ManagedBridge -->|gateway routing| OpenClawGateway
%% Service Connections
ManagedBridge -.->|Capabilities Discovery| Client
@ -41,15 +39,14 @@ graph TD
| Bridge-owned mapping | App 侧行为 | 备注 |
| :--- | :--- | :--- |
| `/acp/rpc` | 直接调用 | 能力发现、路由解析、agent / multi-agent 任务、cancel、close |
| `/gateway/openclaw` | 仅 OpenClaw task submit | 只用于 OpenClaw `session.start` / `session.message`,不是 ACP base endpoint |
| `/acp/rpc` | 直接调用 | 能力发现、路由解析、agent / multi-agent 任务、OpenClaw gateway 任务、cancel、close |
| provider runtime | 不直连 | Bridge 后端内部解析 provider |
| gateway runtime | 不直连 | Bridge 后端内部解析 gateway provider |
## 3. 运维配置优化
### 3.1 统一鉴权
App 发往 `xworkmate-bridge.svc.plus/acp/rpc` `xworkmate-bridge.svc.plus/gateway/openclaw` 的请求必须携带:
App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 的请求必须携带:
- **Header**: `Authorization: Bearer <bridge-auth-token>`
- **未授权响应**: `401 Unauthorized`
@ -66,5 +63,5 @@ App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 和 `xworkmate-bridge.svc.plus/ga
- App 不写入或拼接本地 provider endpoint。
- App 不直接调用 `/acp-server/*`
- App 仅可在 OpenClaw `session.start` / `session.message` task submit 中调用 `/gateway/openclaw`,不得把它作为全局 ACP base endpoint
- App 不调用 `/gateway/openclaw`OpenClaw `session.start` / `session.message` task submit 通过 `/acp/rpc` 的 routing metadata 表达
- `acp.capabilities` 是 provider catalog、gateway catalog、available execution targets 的唯一来源。

View File

@ -40,7 +40,7 @@ Last Updated: 2026-04-22
- 安全边界确认:
- `BRIDGE_AUTH_TOKEN` 是否只作为 Bearer token 使用
- `BRIDGE_SERVER_URL` 是否仅作为元数据
- 会话接口是否按当前 bridge contract 选择 `/acp/rpc` 或 OpenClaw task submit 专用入口 `/gateway/openclaw`
- 会话接口是否按当前 bridge contract 统一选择 `/acp/rpc`,并用 routing metadata 表达 OpenClaw gateway 目标
## 2. 统一测试前提
@ -76,8 +76,7 @@ Last Updated: 2026-04-22
| accounts | `/api/auth/login` | `POST` | 无 | 登录并拿 session token | Apple 审核只读账号使用 |
| accounts | `/api/auth/session` | `GET` | `Authorization: Bearer <session token>` | 获取当前会话和用户信息 | APP 端登录态校验 |
| accounts | `/api/auth/xworkmate/profile/sync` | `GET` | `Authorization: Bearer <session token>` | 拉取 bridge 同步元数据 | 返回 `BRIDGE_SERVER_URL` / `BRIDGE_AUTH_TOKEN` |
| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer <BRIDGE_AUTH_TOKEN>` | bridge JSON-RPC 主入口 | capabilities、routing、agent / multi-agent 任务、cancel、close |
| bridge | `/gateway/openclaw` | `POST` | `Authorization: Bearer <BRIDGE_AUTH_TOKEN>` | OpenClaw task submit 专用入口 | 仅 OpenClaw `session.start` / `session.message`,不是全局 ACP base endpoint |
| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer <BRIDGE_AUTH_TOKEN>` | bridge JSON-RPC 主入口 | capabilities、routing、agent / multi-agent 任务、OpenClaw gateway 任务、cancel、close |
| bridge | `acp.capabilities` | JSON-RPC method | 同上 | 拉取 provider catalog / target catalog | capability 只读快照 |
| bridge | `xworkmate.routing.resolve` | JSON-RPC method | 同上 | 解析 provider / gateway / skills 路由 | 返回 resolved / unavailable 信息 |
| bridge | `session.start` | JSON-RPC method | 同上 | 开启新会话 | session 生命周期起点 |
@ -581,10 +580,9 @@ Last Updated: 2026-04-22
### 8.3 建议断言
- `Authorization` 必须是 `Bearer <token>`
- capability、routing、agent / multi-agent 任务、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc`
- OpenClaw gateway `session.start` / `session.message` 请求路径必须是 `/gateway/openclaw`
- capability、routing、agent / multi-agent 任务、OpenClaw gateway `session.start` / `session.message`、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc`
- 不允许出现 `/acp-server/` 直连请求
- `/gateway/openclaw` 不得用于 capabilities、routing、gateway control-plane、cancel、close且不得作为全局 ACP base endpoint
- 不允许出现 `/gateway/openclaw` app-facing 请求OpenClaw gateway 目标必须通过 routing metadata 表达
- `session.cancel` / `session.close` 必须接受 `sessionId` + `threadId`
- `BRIDGE_SERVER_URL` 不可参与运行时路径拼接
@ -612,7 +610,7 @@ Last Updated: 2026-04-22
- 账户侧负责登录与同步元数据
- bridge 侧负责 capability、路由解析和会话生命周期
- capability、routing、agent / multi-agent 任务、cancel、close 经过 `/acp/rpc`
- OpenClaw `session.start` / `session.message` 经过 `/gateway/openclaw`
- capability、routing、agent / multi-agent 任务、OpenClaw gateway 任务、cancel、close 经过 `/acp/rpc`
- OpenClaw `session.start` / `session.message` 通过 `/acp/rpc` routing metadata 表达 gateway/openclaw 目标
- `session.cancel` / `session.close` 的协议入口已验证可用
- 当前剩余风险集中在桥接后端下游 provider 连接,而不是 APP 侧接口拼接

View File

@ -31,8 +31,8 @@
│ │ (SSE/HTTP) │ │ successfully via /acp/rpc │
│ /acp-server/opencode/ │ JSON-RPC │ PASS │ acp.capabilities returned │
│ │ (SSE/HTTP) │ │ successfully via /acp/rpc │
│ /gateway/openclaw/ │ WSS / RPC │ PASS │ WebSocket handshake successful at
│ │ │ /acp; received connect.challenge
│ /acp/rpc openclaw │ JSON-RPC │ PASS │ OpenClaw routing is expressed via
routing │ │ │ routing metadata on /acp/rpc │
### 2. Assistant 线程体验

View File

@ -1045,9 +1045,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
if (bridgeEndpoint == null) {
return null;
}
if (_usesOpenClawTaskSubmitEndpointInternal(request)) {
return bridgeEndpoint.replace(path: '/gateway/openclaw');
}
return resolveAcpHttpRpcEndpoint(bridgeEndpoint);
}
@ -1148,22 +1145,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
) => kGatewayRemoteProfileIndex;
}
bool _usesOpenClawTaskSubmitEndpointInternal(GoTaskServiceRequest request) {
if (request.isMultiAgentRequest || !request.target.isGateway) {
return false;
}
final providerId = normalizeSingleAgentProviderId(
request.provider.providerId,
);
if (providerId == kCanonicalGatewayProviderId) {
return true;
}
return normalizeSingleAgentProviderId(
request.effectiveRouting.preferredGatewayTarget,
) ==
kCanonicalGatewayProviderId;
}
String _normalizeAuthorizationHeaderInternal(String raw) {
final trimmed = raw.trim();
if (trimmed.isEmpty) {

View File

@ -276,16 +276,6 @@ class ExternalCodeAgentAcpDesktopTransport
if (controlEndpoint != null) {
return controlEndpoint;
}
final taskPath = taskEndpoint?.path.trim() ?? '';
if (taskEndpoint != null &&
(taskPath == '/gateway/openclaw' ||
taskPath.endsWith('/gateway/openclaw'))) {
return taskEndpoint.replace(
path: '/acp/rpc',
query: null,
fragment: null,
);
}
return resolveAcpHttpRpcEndpoint(taskEndpoint);
}

View File

@ -1311,14 +1311,6 @@ class GatewayAcpClient {
Uri? _resolveHttpRpcEndpoint([Uri? endpointOverride, String method = '']) {
final endpoint = endpointOverride ?? endpointResolver();
if (_isOpenClawTaskSubmitEndpoint(endpoint) &&
_isOpenClawTaskSubmitMethod(method)) {
return endpoint?.replace(
path: '/gateway/openclaw',
query: null,
fragment: null,
);
}
return resolveAcpHttpRpcEndpoint(endpoint);
}
@ -1470,15 +1462,6 @@ bool _isOpenClawTaskSubmitMethod(String method) {
return normalized == 'session.start' || normalized == 'session.message';
}
bool _isOpenClawTaskSubmitEndpoint(Uri? endpoint) {
var path = endpoint?.path.trim() ?? '';
if (!path.startsWith('/')) {
path = '/$path';
}
path = path.replaceFirst(RegExp(r'/+$'), '');
return path == '/gateway/openclaw';
}
Duration gatewayAcpHttpResponseTimeoutFor(
Uri endpoint,
String method, [
@ -1487,15 +1470,6 @@ Duration gatewayAcpHttpResponseTimeoutFor(
if (!_isOpenClawTaskSubmitMethod(method)) {
return const Duration(seconds: 120);
}
if (_isOpenClawTaskSubmitEndpoint(endpoint)) {
return Duration(
minutes: gatewayAcpTaskRuntimeBudgetMinutesForParams({
'requestedExecutionTarget':
AssistantExecutionTarget.gateway.promptValue,
...params,
}),
);
}
return Duration(minutes: gatewayAcpTaskRuntimeBudgetMinutesForParams(params));
}

View File

@ -28,8 +28,13 @@ void main() {
);
await tester.pumpAndSettle();
expect(find.text('助手'), findsOneWidget);
expect(find.text('设置'), findsOneWidget);
expect(find.byKey(const Key('mobile-assistant-page')), findsOneWidget);
expect(find.byKey(const Key('mobile-settings-page')), findsNothing);
controller.openSettings();
await tester.pump();
expect(find.byKey(const Key('mobile-settings-page')), findsOneWidget);
expect(find.text('Mobile-safe'), findsNothing);
expect(find.text('安全审批'), findsNothing);
expect(find.byKey(const Key('mobile-safe-strip')), findsNothing);

View File

@ -748,8 +748,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: GatewayAcpClient(endpointResolver: () => endpoint),
endpointResolver: (_) => endpoint,
taskEndpointResolver: (_) =>
endpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => endpoint,
);
addTearDown(transport.dispose);
@ -777,10 +776,7 @@ void main() {
expect(result.artifacts.single.relativePath, 'exports/snapshot.md');
expect(result.remoteWorkingDirectory, '/remote/openclaw/workspace');
expect(result.remoteWorkspaceRefKind, WorkspaceRefKind.remotePath);
expect(
requestPaths,
containsAll(<String>['/gateway/openclaw', '/acp/rpc']),
);
expect(requestPaths, everyElement('/acp/rpc'));
},
);
@ -842,8 +838,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: GatewayAcpClient(endpointResolver: () => endpoint),
endpointResolver: (_) => endpoint,
taskEndpointResolver: (_) =>
endpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => endpoint,
recoveryPollDelay: Duration.zero,
recoveryMaxAttempts: 1,
);
@ -871,10 +866,7 @@ void main() {
expect(result.success, isTrue);
expect(result.message, 'recovered after SSE no result');
expect(
requestPaths,
containsAll(<String>['/gateway/openclaw', '/acp/rpc']),
);
expect(requestPaths, everyElement('/acp/rpc'));
},
);
@ -942,8 +934,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: GatewayAcpClient(endpointResolver: () => endpoint),
endpointResolver: (_) => endpoint,
taskEndpointResolver: (_) =>
endpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => endpoint,
recoveryPollDelay: Duration.zero,
recoveryMaxAttempts: 4,
);
@ -1033,8 +1024,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: GatewayAcpClient(endpointResolver: () => endpoint),
endpointResolver: (_) => endpoint,
taskEndpointResolver: (_) =>
endpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => endpoint,
recoveryPollDelay: Duration.zero,
recoveryMaxAttempts: 1,
);
@ -1544,7 +1534,7 @@ void main() {
);
test(
'desktop task execution rejects OpenClaw gateway path for non-task methods',
'desktop task execution rejects OpenClaw gateway path as bridge RPC base',
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
@ -1573,7 +1563,7 @@ void main() {
);
test(
'desktop task execution routes OpenClaw through required task endpoint',
'desktop task execution routes OpenClaw through unified bridge RPC',
() async {
final capture = await _startAcpHttpServer(
streamResponse: true,
@ -1606,8 +1596,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: client,
endpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) =>
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => capture.baseEndpoint,
);
final result = await transport.executeTask(
@ -1620,9 +1609,9 @@ void main() {
expect(capture.authorizationHeader, 'Bearer bridge-token');
expect(capture.acceptHeader, 'text/event-stream, application/json');
expect(capture.requestPath, '/gateway/openclaw');
expect(capture.requestPath, '/acp/rpc');
expect(capture.requestPath, isNot(contains('/acp-server')));
expect(capture.requestPath, isNot(contains('/acp-server/gateway')));
expect(capture.requestPath, isNot(contains('/gateway/openclaw')));
final params = _lastRequestParams(capture);
final routing = params['routing'] as Map<String, dynamic>;
expect(params.containsKey('gatewayProvider'), isFalse);
@ -1706,18 +1695,17 @@ void main() {
'preferredGatewayProviderId': 'openclaw',
},
},
endpointOverride: capture.baseEndpoint.replace(
path: '/gateway/openclaw',
),
endpointOverride: capture.baseEndpoint,
onNotification: notifications.add,
);
final diagnostics = (response['_xworkmateDiagnostics'] as Map)
.cast<String, dynamic>();
expect(capture.requestPath, '/gateway/openclaw');
expect(capture.requestPath, '/acp/rpc');
expect((response['result'] as Map)['output'], 'done');
expect(diagnostics['transport'], 'http-sse');
expect(diagnostics['requestUrl'], contains('/gateway/openclaw'));
expect(diagnostics['requestUrl'], contains('/acp/rpc'));
expect(diagnostics['requestUrl'], isNot(contains('/gateway/openclaw')));
expect(diagnostics['bodyRead'], isTrue);
expect(diagnostics['sseKeepaliveReceived'], isTrue);
expect(diagnostics['sseLastEventAtMs'], isPositive);
@ -1734,7 +1722,7 @@ void main() {
);
test(
'desktop OpenClaw follow-up routes through required task endpoint',
'desktop OpenClaw follow-up routes through unified bridge RPC',
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
@ -1746,8 +1734,7 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: client,
endpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) =>
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
taskEndpointResolver: (_) => capture.baseEndpoint,
);
await transport.executeTask(
@ -1760,7 +1747,7 @@ void main() {
);
expect(capture.acceptHeader, 'text/event-stream, application/json');
expect(capture.requestPath, '/gateway/openclaw');
expect(capture.requestPath, '/acp/rpc');
expect(capture.requestBody, contains('"method":"session.message"'));
},
);
@ -1769,9 +1756,6 @@ void main() {
final openClawEndpoint = Uri.parse(
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
final openClawTaskEndpoint = Uri.parse(
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
);
final acpEndpoint = Uri.parse(
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
@ -1795,22 +1779,6 @@ void main() {
),
const Duration(minutes: 30),
);
expect(
gatewayAcpHttpResponseTimeoutFor(
openClawTaskEndpoint,
'session.start',
const <String, dynamic>{'taskPrompt': 'Reply after a long wait'},
),
const Duration(minutes: 10),
);
expect(
gatewayAcpHttpResponseTimeoutFor(
openClawTaskEndpoint,
'session.message',
const <String, dynamic>{'taskPrompt': '输出 PPTX 和 Markdown 文件'},
),
const Duration(minutes: 30),
);
expect(
gatewayAcpHttpResponseTimeoutFor(acpEndpoint, 'session.start'),
const Duration(minutes: 2),
@ -1821,62 +1789,58 @@ void main() {
);
});
test(
'desktop controller uses OpenClaw endpoint only for gateway task submit',
() {
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
test('desktop controller uses unified bridge RPC for all task submits', () {
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
final openClawStart = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
),
);
final openClawFollowUp = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
resumeSession: true,
),
);
final unspecifiedGateway = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.unspecified,
),
);
final multiAgentGateway = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
multiAgent: true,
),
);
final agentTask = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.agent,
provider: SingleAgentProvider.codex,
),
);
final openClawStart = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
),
);
final openClawFollowUp = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
resumeSession: true,
),
);
final unspecifiedGateway = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.unspecified,
),
);
final multiAgentGateway = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
multiAgent: true,
),
);
final agentTask = controller.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.agent,
provider: SingleAgentProvider.codex,
),
);
expect(openClawStart?.path, '/gateway/openclaw');
expect(openClawFollowUp?.path, '/gateway/openclaw');
expect(unspecifiedGateway?.path, '/gateway/openclaw');
expect(multiAgentGateway?.path, '/acp/rpc');
expect(agentTask?.path, '/acp/rpc');
},
);
expect(openClawStart?.path, '/acp/rpc');
expect(openClawFollowUp?.path, '/acp/rpc');
expect(unspecifiedGateway?.path, '/acp/rpc');
expect(multiAgentGateway?.path, '/acp/rpc');
expect(agentTask?.path, '/acp/rpc');
});
test(
'desktop controller resolves OpenClaw gateway submit to required task endpoint',
'desktop controller resolves OpenClaw gateway submit to unified bridge RPC',
() {
final controller = AppController(
environmentOverride: const <String, String>{},
@ -1893,10 +1857,10 @@ void main() {
expect(
endpoint.toString(),
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
expect(endpoint, isNotNull);
expect(endpoint!.path, isNot('/acp/rpc'));
expect(endpoint!.path, '/acp/rpc');
},
);