fix: use openclaw task submit endpoint
This commit is contained in:
parent
3ee9404aa2
commit
0197894698
@ -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。Provider 与 gateway 的实际执行地址是 bridge 内部运行时事实,不属于 App contract。
|
||||
App 侧任务发送默认调用 bridge 主入口 `/acp/rpc`,不再拼接 provider-specific 直连 URL。OpenClaw `session.start` 和同一任务的 `session.message` 是唯一例外,使用 bridge 公开的 task submit 专用路径 `/gateway/openclaw`。该路径不是全局 ACP base endpoint。
|
||||
|
||||
## App Runtime Flow
|
||||
|
||||
@ -22,8 +22,11 @@ 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
|
||||
R --> S["bridge-owned routing"]
|
||||
|
||||
S --> K["Hermes internal runtime"]
|
||||
@ -35,9 +38,10 @@ flowchart TD
|
||||
|
||||
## Routing Rules
|
||||
|
||||
- App runtime requests use `https://xworkmate-bridge.svc.plus/acp/rpc`.
|
||||
- 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`.
|
||||
- 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/*` or `/gateway/*` URLs.
|
||||
- All App-side requests go through `https://xworkmate-bridge.svc.plus/acp/rpc`.
|
||||
- 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.
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 1. 架构概览 (Unified Routing Architecture)
|
||||
|
||||
当前系统采用 `xworkmate-bridge.svc.plus` 作为统一入口。App 侧只通过 managed bridge ACP 主入口发送任务,provider / gateway 的执行地址由 bridge 后端内部拥有,不暴露为 App-facing public mapping。
|
||||
当前系统采用 `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。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
@ -16,6 +16,7 @@ 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"]
|
||||
@ -26,10 +27,11 @@ 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
|
||||
ManagedBridge -->|gateway routing| OpenClawGateway
|
||||
OpenClawSubmit -->|forced openclaw routing| OpenClawGateway
|
||||
|
||||
%% Service Connections
|
||||
ManagedBridge -.->|Capabilities Discovery| Client
|
||||
@ -39,14 +41,15 @@ graph TD
|
||||
|
||||
| Bridge-owned mapping | App 侧行为 | 备注 |
|
||||
| :--- | :--- | :--- |
|
||||
| `/acp/rpc` | 直接调用 | Managed Bridge ACP 主入口,提供能力发现与任务发送 |
|
||||
| `/acp/rpc` | 直接调用 | 能力发现、路由解析、agent / multi-agent 任务、cancel、close |
|
||||
| `/gateway/openclaw` | 仅 OpenClaw task submit | 只用于 OpenClaw `session.start` / `session.message`,不是 ACP base endpoint |
|
||||
| provider runtime | 不直连 | Bridge 后端内部解析 provider |
|
||||
| gateway runtime | 不直连 | Bridge 后端内部解析 gateway provider |
|
||||
|
||||
## 3. 运维配置优化
|
||||
|
||||
### 3.1 统一鉴权
|
||||
App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 的请求必须携带:
|
||||
App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 和 `xworkmate-bridge.svc.plus/gateway/openclaw` 的请求必须携带:
|
||||
- **Header**: `Authorization: Bearer <bridge-auth-token>`
|
||||
- **未授权响应**: `401 Unauthorized`
|
||||
|
||||
@ -62,5 +65,6 @@ App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 的请求必须携带:
|
||||
## 4. App 侧不变量
|
||||
|
||||
- App 不写入或拼接本地 provider endpoint。
|
||||
- App 不直接调用 `/acp-server/*` 或 `/gateway/openclaw`。
|
||||
- App 不直接调用 `/acp-server/*`。
|
||||
- App 仅可在 OpenClaw `session.start` / `session.message` task submit 中调用 `/gateway/openclaw`,不得把它作为全局 ACP base endpoint。
|
||||
- `acp.capabilities` 是 provider catalog、gateway catalog、available execution targets 的唯一来源。
|
||||
|
||||
@ -40,7 +40,7 @@ Last Updated: 2026-04-22
|
||||
- 安全边界确认:
|
||||
- `BRIDGE_AUTH_TOKEN` 是否只作为 Bearer token 使用
|
||||
- `BRIDGE_SERVER_URL` 是否仅作为元数据
|
||||
- 会话接口是否仍通过统一 `/acp/rpc` 入口
|
||||
- 会话接口是否按当前 bridge contract 选择 `/acp/rpc` 或 OpenClaw task submit 专用入口 `/gateway/openclaw`
|
||||
|
||||
## 2. 统一测试前提
|
||||
|
||||
@ -76,7 +76,8 @@ 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 主入口 | 所有运行时任务统一走这里 |
|
||||
| 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.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 生命周期起点 |
|
||||
@ -580,8 +581,10 @@ Last Updated: 2026-04-22
|
||||
### 8.3 建议断言
|
||||
|
||||
- `Authorization` 必须是 `Bearer <token>`
|
||||
- 请求路径必须是 `/acp/rpc`
|
||||
- 不允许出现 `/acp-server/` 或 `/gateway/` 直连请求
|
||||
- capability、routing、agent / multi-agent 任务、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc`
|
||||
- OpenClaw gateway `session.start` / `session.message` 请求路径必须是 `/gateway/openclaw`
|
||||
- 不允许出现 `/acp-server/` 直连请求
|
||||
- `/gateway/openclaw` 不得用于 capabilities、routing、gateway control-plane、cancel、close,且不得作为全局 ACP base endpoint
|
||||
- `session.cancel` / `session.close` 必须接受 `sessionId` + `threadId`
|
||||
- `BRIDGE_SERVER_URL` 不可参与运行时路径拼接
|
||||
|
||||
@ -609,7 +612,7 @@ Last Updated: 2026-04-22
|
||||
|
||||
- 账户侧负责登录与同步元数据
|
||||
- bridge 侧负责 capability、路由解析和会话生命周期
|
||||
- 任务执行必须统一经过 `/acp/rpc`
|
||||
- `session.start` / `session.message` 的协议入口已验证通畅
|
||||
- capability、routing、agent / multi-agent 任务、cancel、close 经过 `/acp/rpc`
|
||||
- OpenClaw `session.start` / `session.message` 经过 `/gateway/openclaw`
|
||||
- `session.cancel` / `session.close` 的协议入口已验证可用
|
||||
- 当前剩余风险集中在桥接后端下游 provider 连接,而不是 APP 侧接口拼接
|
||||
|
||||
@ -867,6 +867,9 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
if (bridgeEndpoint == null) {
|
||||
return null;
|
||||
}
|
||||
if (_usesOpenClawTaskSubmitEndpointInternal(request)) {
|
||||
return bridgeEndpoint.replace(path: '/gateway/openclaw');
|
||||
}
|
||||
return bridgeEndpoint;
|
||||
}
|
||||
|
||||
@ -967,6 +970,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
) => kGatewayRemoteProfileIndex;
|
||||
}
|
||||
|
||||
bool _usesOpenClawTaskSubmitEndpointInternal(GoTaskServiceRequest request) {
|
||||
if (request.isMultiAgentRequest || !request.target.isGateway) {
|
||||
return false;
|
||||
}
|
||||
return normalizeSingleAgentProviderId(request.provider.providerId) ==
|
||||
kCanonicalGatewayProviderId;
|
||||
}
|
||||
|
||||
String _normalizeAuthorizationHeaderInternal(String raw) {
|
||||
final trimmed = raw.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
|
||||
@ -566,7 +566,7 @@ class GatewayAcpClient {
|
||||
);
|
||||
httpRequest.headers.set(
|
||||
HttpHeaders.acceptHeader,
|
||||
'text/event-stream, application/json',
|
||||
_httpAcceptHeaderFor(endpoint, request.method),
|
||||
);
|
||||
final authorization = await _resolveAuthorizationHeader(
|
||||
endpoint,
|
||||
@ -1049,6 +1049,14 @@ 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);
|
||||
}
|
||||
|
||||
@ -1107,7 +1115,33 @@ class GatewayAcpClient {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOpenClawTaskSubmitEndpoint(Uri? endpoint) {
|
||||
var path = endpoint?.path.trim() ?? '';
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/$path';
|
||||
}
|
||||
path = path.replaceFirst(RegExp(r'/+$'), '');
|
||||
return path == '/gateway/openclaw';
|
||||
}
|
||||
|
||||
bool _isOpenClawTaskSubmitMethod(String method) {
|
||||
final normalized = method.trim();
|
||||
return normalized == 'session.start' || normalized == 'session.message';
|
||||
}
|
||||
|
||||
String _httpAcceptHeaderFor(Uri endpoint, String method) {
|
||||
if (_isOpenClawTaskSubmitEndpoint(endpoint) &&
|
||||
_isOpenClawTaskSubmitMethod(method)) {
|
||||
return 'application/json';
|
||||
}
|
||||
return 'text/event-stream, application/json';
|
||||
}
|
||||
|
||||
Duration gatewayAcpHttpResponseTimeoutFor(Uri endpoint, String method) {
|
||||
if (_isOpenClawTaskSubmitEndpoint(endpoint) &&
|
||||
_isOpenClawTaskSubmitMethod(method)) {
|
||||
return const Duration(minutes: 10);
|
||||
}
|
||||
return const Duration(seconds: 120);
|
||||
}
|
||||
|
||||
|
||||
@ -687,160 +687,193 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('desktop task execution routes OpenClaw through bridge RPC', () async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => capture.baseEndpoint,
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: client,
|
||||
endpointResolver: (_) => capture.baseEndpoint,
|
||||
taskEndpointResolver: (_) => capture.baseEndpoint,
|
||||
);
|
||||
|
||||
await transport.executeTask(
|
||||
_taskRequest(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
provider: SingleAgentProvider.openclaw,
|
||||
),
|
||||
onUpdate: (_) {},
|
||||
);
|
||||
|
||||
expect(capture.authorizationHeader, 'Bearer bridge-token');
|
||||
expect(capture.acceptHeader, 'text/event-stream, application/json');
|
||||
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);
|
||||
expect(params.containsKey('gatewayProviderId'), isFalse);
|
||||
expect(params['executionTarget'], 'gateway');
|
||||
expect(params['requestedExecutionTarget'], 'gateway');
|
||||
expect(routing['preferredGatewayProviderId'], 'openclaw');
|
||||
expect(routing['explicitExecutionTarget'], 'gateway');
|
||||
expect(routing.containsKey('explicitProviderId'), isFalse);
|
||||
expect(capture.requestBody, contains('"method":"session.start"'));
|
||||
expect(capture.requestBody, isNot(contains('"method":"thread/start"')));
|
||||
});
|
||||
|
||||
test('desktop OpenClaw follow-up routes through bridge RPC', () async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => capture.baseEndpoint,
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: client,
|
||||
endpointResolver: (_) => capture.baseEndpoint,
|
||||
taskEndpointResolver: (_) => capture.baseEndpoint,
|
||||
);
|
||||
|
||||
await transport.executeTask(
|
||||
_taskRequest(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
provider: SingleAgentProvider.openclaw,
|
||||
resumeSession: true,
|
||||
),
|
||||
onUpdate: (_) {},
|
||||
);
|
||||
|
||||
expect(capture.acceptHeader, 'text/event-stream, application/json');
|
||||
expect(capture.requestPath, '/acp/rpc');
|
||||
expect(capture.requestPath, isNot(contains('/gateway/openclaw')));
|
||||
expect(capture.requestBody, contains('"method":"session.message"'));
|
||||
});
|
||||
|
||||
test(
|
||||
'bridge RPC session methods use the standard HTTP response timeout',
|
||||
() {
|
||||
final openClawEndpoint = Uri.parse(
|
||||
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
|
||||
);
|
||||
final acpEndpoint = Uri.parse(
|
||||
'https://xworkmate-bridge.svc.plus/acp/rpc',
|
||||
'desktop task execution rejects OpenClaw gateway path for non-task methods',
|
||||
() async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () =>
|
||||
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.start'),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.message'),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(acpEndpoint, 'session.start'),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(
|
||||
openClawEndpoint,
|
||||
'acp.capabilities',
|
||||
await expectLater(
|
||||
client.request(
|
||||
method: 'acp.capabilities',
|
||||
params: const <String, dynamic>{},
|
||||
),
|
||||
throwsA(
|
||||
isA<GatewayAcpException>().having(
|
||||
(error) => error.code,
|
||||
'code',
|
||||
'ACP_HTTP_ENDPOINT_MISSING',
|
||||
),
|
||||
),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
|
||||
expect(capture.requestBodies, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('desktop controller resolves task requests to the bridge origin', () {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
test(
|
||||
'desktop task execution routes OpenClaw through dedicated bridge gateway path',
|
||||
() async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => capture.baseEndpoint,
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
|
||||
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 transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: client,
|
||||
endpointResolver: (_) => capture.baseEndpoint,
|
||||
taskEndpointResolver: (_) =>
|
||||
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
|
||||
);
|
||||
|
||||
await transport.executeTask(
|
||||
_taskRequest(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
provider: SingleAgentProvider.openclaw,
|
||||
),
|
||||
onUpdate: (_) {},
|
||||
);
|
||||
|
||||
expect(capture.authorizationHeader, 'Bearer bridge-token');
|
||||
expect(capture.acceptHeader, 'application/json');
|
||||
expect(capture.requestPath, '/gateway/openclaw');
|
||||
expect(capture.requestPath, isNot(contains('/acp-server')));
|
||||
expect(capture.requestPath, isNot(contains('/acp-server/gateway')));
|
||||
final params = _lastRequestParams(capture);
|
||||
final routing = params['routing'] as Map<String, dynamic>;
|
||||
expect(params.containsKey('gatewayProvider'), isFalse);
|
||||
expect(params.containsKey('gatewayProviderId'), isFalse);
|
||||
expect(params['executionTarget'], 'gateway');
|
||||
expect(params['requestedExecutionTarget'], 'gateway');
|
||||
expect(routing['preferredGatewayProviderId'], 'openclaw');
|
||||
expect(routing['explicitExecutionTarget'], 'gateway');
|
||||
expect(routing.containsKey('explicitProviderId'), isFalse);
|
||||
expect(capture.requestBody, contains('"method":"session.start"'));
|
||||
expect(capture.requestBody, isNot(contains('"method":"thread/start"')));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'desktop OpenClaw follow-up routes through dedicated bridge gateway path',
|
||||
() async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => capture.baseEndpoint,
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: client,
|
||||
endpointResolver: (_) => capture.baseEndpoint,
|
||||
taskEndpointResolver: (_) =>
|
||||
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
|
||||
);
|
||||
|
||||
await transport.executeTask(
|
||||
_taskRequest(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
provider: SingleAgentProvider.openclaw,
|
||||
resumeSession: true,
|
||||
),
|
||||
onUpdate: (_) {},
|
||||
);
|
||||
|
||||
expect(capture.acceptHeader, 'application/json');
|
||||
expect(capture.requestPath, '/gateway/openclaw');
|
||||
expect(capture.requestBody, contains('"method":"session.message"'));
|
||||
},
|
||||
);
|
||||
|
||||
test('OpenClaw task submit uses extended HTTP response timeout', () {
|
||||
final openClawEndpoint = Uri.parse(
|
||||
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
|
||||
);
|
||||
final acpEndpoint = Uri.parse(
|
||||
'https://xworkmate-bridge.svc.plus/acp/rpc',
|
||||
);
|
||||
|
||||
expect(openClawStart?.path, '');
|
||||
expect(openClawFollowUp?.path, '');
|
||||
expect(unspecifiedGateway?.path, '');
|
||||
expect(multiAgentGateway?.path, '');
|
||||
expect(agentTask?.path, '');
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.start'),
|
||||
const Duration(minutes: 10),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.message'),
|
||||
const Duration(minutes: 10),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(acpEndpoint, 'session.start'),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
expect(
|
||||
gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'acp.capabilities'),
|
||||
const Duration(seconds: 120),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'desktop controller does not expose OpenClaw gateway path as task endpoint',
|
||||
'desktop controller only uses gateway path for OpenClaw task submit',
|
||||
() {
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
expect(openClawStart?.path, '/gateway/openclaw');
|
||||
expect(openClawFollowUp?.path, '/gateway/openclaw');
|
||||
expect(unspecifiedGateway?.path, '');
|
||||
expect(multiAgentGateway?.path, '');
|
||||
expect(agentTask?.path, '');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'desktop controller resolves OpenClaw gateway submit on managed bridge origin',
|
||||
() {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
@ -855,9 +888,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(endpoint.toString(), 'https://xworkmate-bridge.svc.plus');
|
||||
expect(
|
||||
endpoint.toString(),
|
||||
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
|
||||
);
|
||||
expect(endpoint, isNotNull);
|
||||
expect(endpoint!.path, isNot('/gateway/openclaw'));
|
||||
expect(endpoint!.path, isNot('/acp/rpc'));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user