Fix bridge ACP provider endpoint normalization

This commit is contained in:
Haitao Pan 2026-04-23 09:30:36 +08:00
parent 4164c9bad0
commit ca34af24a7
4 changed files with 102 additions and 33 deletions

View File

@ -44,6 +44,12 @@ class AcpEndpointPaths {
}
path = path.replaceFirst(RegExp(r'/+$'), '');
if (path == '/acp-server' ||
path.startsWith('/acp-server/') ||
path == '/gateway' ||
path.startsWith('/gateway/')) {
return '';
}
return path == '/' ? '' : path;
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/acp_endpoint_paths.dart';
void main() {
group('ACP endpoint path resolution', () {
test('resolves managed bridge origin to ACP HTTP RPC path', () {
final endpoint = resolveAcpHttpRpcEndpoint(
Uri.parse('https://xworkmate-bridge.svc.plus'),
);
expect(endpoint.toString(), 'https://xworkmate-bridge.svc.plus/acp/rpc');
});
test('does not preserve 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.toString(),
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
expect(
gatewayEndpoint.toString(),
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
});
test(
'normalizes provider mapping paths even when ACP suffix is present',
() {
final endpoint = resolveAcpHttpRpcEndpoint(
Uri.parse(
'https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc',
),
);
expect(
endpoint.toString(),
'https://xworkmate-bridge.svc.plus/acp/rpc',
);
},
);
});
}

View File

@ -378,10 +378,7 @@ void main() {
);
expect(fakeGoTaskService.executeCount, 0);
expect(
controller.chatMessages.last.text,
contains('请先登录 svc.plus'),
);
expect(controller.chatMessages.last.text, contains('请先登录 svc.plus'));
},
);
@ -458,24 +455,24 @@ void main() {
'session-token';
controller.settingsControllerInternal.accountSessionInternal =
const AccountSessionSummary(
userId: 'user-1',
email: 'review@svc.plus',
name: 'Review User',
role: 'reviewer',
mfaEnabled: true,
);
userId: 'user-1',
email: 'review@svc.plus',
name: 'Review User',
role: 'reviewer',
mfaEnabled: true,
);
controller.settingsControllerInternal.accountSyncStateInternal =
AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: capture.baseEndpoint.toString(),
),
syncState: 'ready',
tokenConfigured: const AccountTokenConfigured(
bridge: true,
vault: false,
apisix: false,
),
);
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: capture.baseEndpoint.toString(),
),
syncState: 'ready',
tokenConfigured: const AccountTokenConfigured(
bridge: true,
vault: false,
apisix: false,
),
);
await controller.sessionsController.switchSession('session-1');
await controller.setAssistantExecutionTarget(
@ -502,19 +499,6 @@ void main() {
});
}
Future<void> _waitForRequest(
_CapabilityServerCapture capture, {
required int minimumCount,
}) async {
for (var index = 0; index < 20; index += 1) {
if (capture.requestCount >= minimumCount) {
return;
}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
fail('Timed out waiting for $minimumCount capability requests');
}
Future<_CapabilityServerCapture> _startCapabilityServer() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final capture = _CapabilityServerCapture._(

View File

@ -282,6 +282,37 @@ void main() {
},
);
test(
'desktop task execution normalizes provider endpoint paths back to 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.replace(path: '/acp-server/codex'),
);
await transport.executeTask(
_taskRequest(
target: AssistantExecutionTarget.agent,
provider: SingleAgentProvider.codex,
),
onUpdate: (_) {},
);
expect(capture.authorizationHeader, 'Bearer bridge-token');
expect(capture.requestPath, '/acp/rpc');
expect(capture.requestPath, isNot(contains('/acp-server')));
},
);
test(
'desktop task execution routes OpenClaw through bridge RPC with gateway params',
() async {