fix: route openclaw tasks via bridge gateway

This commit is contained in:
Haitao Pan 2026-05-03 11:22:09 +08:00
parent 6b542acc9a
commit 50c20eec16
5 changed files with 99 additions and 18 deletions

View File

@ -609,21 +609,19 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
var wroteArtifact = false;
for (final artifact in artifacts) {
if (!artifact.hasInlineContent) {
continue;
}
final relativePath = _sanitizeArtifactRelativePathInternal(
artifact.relativePath,
);
if (relativePath.isEmpty) {
continue;
}
final bytes = await artifactBytesInternal(artifact);
if (bytes == null) {
continue;
}
final target = await _nextArtifactTargetFileInternal(root, relativePath);
await target.parent.create(recursive: true);
await target.writeAsBytes(
_decodeArtifactContentInternal(artifact),
flush: true,
);
await target.writeAsBytes(bytes, flush: true);
wroteArtifact = true;
}
@ -635,6 +633,51 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
);
}
Future<List<int>?> artifactBytesInternal(
GoTaskServiceArtifact artifact,
) async {
if (artifact.hasInlineContent) {
return _decodeArtifactContentInternal(artifact);
}
final rawDownloadUrl = artifact.downloadUrl.trim();
if (rawDownloadUrl.isEmpty) {
return null;
}
final uri = Uri.tryParse(rawDownloadUrl);
if (uri == null || (uri.scheme != 'http' && uri.scheme != 'https')) {
return null;
}
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
final sameBridgeHost =
bridgeEndpoint != null &&
uri.host.trim().toLowerCase() ==
bridgeEndpoint.host.trim().toLowerCase();
if (!sameBridgeHost) {
return null;
}
final authorization = await resolveGatewayAcpAuthorizationHeaderInternal(
uri,
);
if (authorization == null || authorization.trim().isEmpty) {
return null;
}
final client = HttpClient();
try {
final request = await client.getUrl(uri);
request.headers.set(HttpHeaders.authorizationHeader, authorization);
final response = await request.close();
if (response.statusCode != HttpStatus.ok) {
return null;
}
return response.fold<List<int>>(
<int>[],
(buffer, chunk) => buffer..addAll(chunk),
);
} finally {
client.close(force: true);
}
}
Uri? resolveGatewayAcpEndpointInternal() {
return resolveBridgeAcpEndpointInternal();
}
@ -677,7 +720,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
Uri? resolveExternalAcpEndpointForRequestInternal(
GoTaskServiceRequest request,
) {
return resolveBridgeAcpEndpointInternal();
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
if (bridgeEndpoint == null) {
return null;
}
if (request.target.isGateway) {
return bridgeEndpoint.replace(path: '/gateway/openclaw');
}
return bridgeEndpoint;
}
Uri? gatewayProfileBaseUriInternal(GatewayConnectionProfile profile) {

View File

@ -94,6 +94,17 @@ Uri? resolveAcpHttpRpcEndpoint(Uri? endpoint) {
if (endpoint == null || endpoint.host.trim().isEmpty) {
return null;
}
if (_isGatewayOpenClawPath(endpoint.path)) {
final scheme = endpoint.scheme.trim().toLowerCase();
if (scheme != 'http' && scheme != 'https') {
return null;
}
return endpoint.replace(
path: '/gateway/openclaw',
query: null,
fragment: null,
);
}
if (AcpEndpointPaths.isProviderMappingPath(endpoint.path)) {
return null;
}
@ -104,3 +115,12 @@ Uri? resolveAcpHttpRpcEndpoint(Uri? endpoint) {
final paths = AcpEndpointPaths.fromBaseEndpoint(endpoint);
return endpoint.replace(path: paths.httpRpcPath, query: null, fragment: null);
}
bool _isGatewayOpenClawPath(String rawPath) {
var path = rawPath.trim();
if (!path.startsWith('/')) {
path = '/$path';
}
path = path.replaceFirst(RegExp(r'/+$'), '');
return path == '/gateway/openclaw';
}

View File

@ -11,16 +11,23 @@ void main() {
expect(endpoint.toString(), 'https://xworkmate-bridge.svc.plus/acp/rpc');
});
test('keeps OpenClaw gateway submit path as HTTP endpoint', () {
final endpoint = resolveAcpHttpRpcEndpoint(
Uri.parse('https://xworkmate-bridge.svc.plus/gateway/openclaw'),
);
expect(
endpoint.toString(),
'https://xworkmate-bridge.svc.plus/gateway/openclaw',
);
});
test('rejects provider mapping paths as app RPC bases', () {
final codexEndpoint = resolveAcpHttpRpcEndpoint(
Uri.parse('https://xworkmate-bridge.svc.plus/acp-server/codex'),
);
final gatewayEndpoint = resolveAcpHttpRpcEndpoint(
Uri.parse('https://xworkmate-bridge.svc.plus/gateway/openclaw'),
);
expect(codexEndpoint, isNull);
expect(gatewayEndpoint, isNull);
});
test('rejects provider mapping paths even when ACP suffix is present', () {

View File

@ -10,7 +10,9 @@ void main() {
test(
'keeps local workspace binding separate from remote execution workspace',
() {
final controller = AppController(environmentOverride: const <String, String>{});
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
final localWorkspace = Directory.systemTemp.createTempSync(
@ -62,7 +64,9 @@ void main() {
);
test('writes inline ACP artifacts into the local thread workspace', () async {
final controller = AppController(environmentOverride: const <String, String>{});
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
final localWorkspace = await Directory.systemTemp.createTemp(

View File

@ -583,7 +583,7 @@ void main() {
);
test(
'desktop task execution routes OpenClaw through bridge RPC with gateway params',
'desktop task execution routes OpenClaw through dedicated bridge gateway path',
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
@ -595,7 +595,8 @@ void main() {
final transport = ExternalCodeAgentAcpDesktopTransport(
client: client,
endpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) =>
capture.baseEndpoint.replace(path: '/gateway/openclaw'),
);
await transport.executeTask(
@ -607,10 +608,9 @@ void main() {
);
expect(capture.authorizationHeader, 'Bearer bridge-token');
expect(capture.requestPath, '/acp/rpc');
expect(capture.requestPath, '/gateway/openclaw');
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);