Merge branch 'codex/openclaw-skills-bridge-list'
This commit is contained in:
commit
659706a699
@ -93,6 +93,14 @@ class AppController extends ChangeNotifier {
|
||||
hostUiFeaturePlatformInternal = Platform.isIOS || Platform.isAndroid
|
||||
? UiFeaturePlatform.mobile
|
||||
: UiFeaturePlatform.desktop;
|
||||
settingsControllerInternal = SettingsController(
|
||||
storeInternal,
|
||||
accountClientFactory: accountClientFactory,
|
||||
);
|
||||
gatewayAcpClientInternal = GatewayAcpClient(
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
authorizationResolver: resolveGatewayAcpAuthorizationHeaderInternal,
|
||||
);
|
||||
|
||||
final resolvedRuntimeCoordinator =
|
||||
runtimeCoordinator ??
|
||||
@ -100,6 +108,9 @@ class AppController extends ChangeNotifier {
|
||||
gateway: GatewayRuntime(
|
||||
store: storeInternal,
|
||||
identityStore: DeviceIdentityStore(storeInternal),
|
||||
sessionClient: GatewayAcpRuntimeSessionClient(
|
||||
client: gatewayAcpClientInternal,
|
||||
),
|
||||
),
|
||||
codex: CodexRuntime(),
|
||||
configBridge: CodexConfigBridge(),
|
||||
@ -112,10 +123,6 @@ class AppController extends ChangeNotifier {
|
||||
codeAgentBridgeRegistryInternal = AgentRegistry(
|
||||
runtimeCoordinatorInternal.gateway,
|
||||
);
|
||||
settingsControllerInternal = SettingsController(
|
||||
storeInternal,
|
||||
accountClientFactory: accountClientFactory,
|
||||
);
|
||||
agentsControllerInternal = GatewayAgentsController(
|
||||
runtimeCoordinatorInternal.gateway,
|
||||
);
|
||||
@ -142,10 +149,6 @@ class AppController extends ChangeNotifier {
|
||||
tasksControllerInternal = DerivedTasksController();
|
||||
desktopPlatformServiceInternal =
|
||||
desktopPlatformService ?? createDesktopPlatformService();
|
||||
gatewayAcpClientInternal = GatewayAcpClient(
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
authorizationResolver: resolveGatewayAcpAuthorizationHeaderInternal,
|
||||
);
|
||||
runtimeCoordinatorInternal.attachDispatchResolver(
|
||||
GoRuntimeDispatchDesktopClient(
|
||||
client: gatewayAcpClientInternal,
|
||||
|
||||
@ -3,6 +3,8 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'acp_endpoint_paths.dart';
|
||||
import 'gateway_runtime_errors.dart';
|
||||
import 'gateway_runtime_session_client.dart';
|
||||
import 'runtime_models.dart';
|
||||
|
||||
const int gatewayAcpHttpHandshakeInterruptedRetryCount = 5;
|
||||
@ -1375,6 +1377,94 @@ class GatewayAcpClient {
|
||||
}
|
||||
}
|
||||
|
||||
class GatewayAcpRuntimeSessionClient implements GatewayRuntimeSessionClient {
|
||||
GatewayAcpRuntimeSessionClient({required this.client});
|
||||
|
||||
final GatewayAcpClient client;
|
||||
final StreamController<GatewayRuntimeSessionUpdate> _updates =
|
||||
StreamController<GatewayRuntimeSessionUpdate>.broadcast();
|
||||
|
||||
@override
|
||||
Stream<GatewayRuntimeSessionUpdate> get updates => _updates.stream;
|
||||
|
||||
@override
|
||||
Future<GatewayRuntimeSessionConnectResult> connect(
|
||||
GatewayRuntimeSessionConnectRequest request,
|
||||
) async {
|
||||
final envelope = await client.request(
|
||||
method: 'xworkmate.gateway.connect',
|
||||
params: request.toJson(),
|
||||
onNotification: _handleNotification,
|
||||
);
|
||||
final result = client.asMap(envelope['result']);
|
||||
_throwGatewayResultIfNeeded(result, 'gateway connect failed');
|
||||
return GatewayRuntimeSessionConnectResult.fromJson(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> request({
|
||||
required String runtimeId,
|
||||
required String method,
|
||||
Map<String, dynamic>? params,
|
||||
Duration timeout = const Duration(seconds: 15),
|
||||
}) async {
|
||||
final envelope = await client.request(
|
||||
method: 'xworkmate.gateway.request',
|
||||
params: <String, dynamic>{
|
||||
'runtimeId': runtimeId,
|
||||
'method': method,
|
||||
'params': params ?? const <String, dynamic>{},
|
||||
'timeoutMs': timeout.inMilliseconds,
|
||||
},
|
||||
onNotification: _handleNotification,
|
||||
);
|
||||
final result = client.asMap(envelope['result']);
|
||||
_throwGatewayResultIfNeeded(result, '$method request failed');
|
||||
return result['payload'];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect({required String runtimeId}) async {
|
||||
await client.request(
|
||||
method: 'xworkmate.gateway.disconnect',
|
||||
params: <String, dynamic>{'runtimeId': runtimeId},
|
||||
onNotification: _handleNotification,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _updates.close();
|
||||
}
|
||||
|
||||
void _handleNotification(Map<String, dynamic> notification) {
|
||||
final method = notification['method']?.toString().trim() ?? '';
|
||||
if (!method.startsWith('xworkmate.gateway.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_updates.add(GatewayRuntimeSessionUpdate.fromNotification(notification));
|
||||
} on GatewayRuntimeException {
|
||||
// Other bridge notifications are intentionally ignored by this adapter.
|
||||
}
|
||||
}
|
||||
|
||||
void _throwGatewayResultIfNeeded(
|
||||
Map<String, dynamic> result,
|
||||
String fallbackMessage,
|
||||
) {
|
||||
if (client.boolValue(result['ok']) ?? false) {
|
||||
return;
|
||||
}
|
||||
final error = client.asMap(result['error']);
|
||||
throw GatewayRuntimeException(
|
||||
client.stringValue(error['message']) ?? fallbackMessage,
|
||||
code: client.stringValue(error['code']),
|
||||
details: error['details'] ?? error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOpenClawTaskSubmitMethod(String method) {
|
||||
final normalized = method.trim();
|
||||
return normalized == 'session.start' || normalized == 'session.message';
|
||||
|
||||
@ -507,7 +507,9 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
|
||||
}
|
||||
reconnectTimerInternal?.cancel();
|
||||
if (sessionClientInternal != null) {
|
||||
await sessionClientInternal!.disconnect(runtimeId: runtimeIdInternal);
|
||||
if (isConnected) {
|
||||
await sessionClientInternal!.disconnect(runtimeId: runtimeIdInternal);
|
||||
}
|
||||
snapshotInternal =
|
||||
GatewayConnectionSnapshot.initial(
|
||||
mode: snapshotInternal.mode,
|
||||
|
||||
156
test/runtime/gateway_runtime_bridge_skills_test.dart
Normal file
156
test/runtime/gateway_runtime_bridge_skills_test.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/runtime/device_identity_store.dart';
|
||||
import 'package:xworkmate/runtime/gateway_acp_client.dart';
|
||||
import 'package:xworkmate/runtime/gateway_runtime.dart';
|
||||
import 'package:xworkmate/runtime/runtime_controllers.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'SkillsController loads OpenClaw skills through bridge gateway request',
|
||||
() async {
|
||||
final observedMethods = <String>[];
|
||||
final observedGatewayRequests = <Map<String, dynamic>>[];
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final subscription = server.listen((request) async {
|
||||
final body = await utf8.decoder.bind(request).join();
|
||||
final rpc = jsonDecode(body) as Map<String, dynamic>;
|
||||
final method = rpc['method']?.toString().trim() ?? '';
|
||||
observedMethods.add(method);
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
|
||||
if (method == 'xworkmate.gateway.connect') {
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': true,
|
||||
'snapshot': <String, dynamic>{
|
||||
'status': 'connected',
|
||||
'mode': 'remote',
|
||||
'statusText': 'Connected',
|
||||
'mainSessionKey': 'main',
|
||||
},
|
||||
'auth': <String, dynamic>{
|
||||
'role': 'operator',
|
||||
'scopes': <String>['operator.read', 'operator.write'],
|
||||
},
|
||||
'returnedDeviceToken': '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == 'xworkmate.gateway.request') {
|
||||
final params = (rpc['params'] as Map).cast<String, dynamic>();
|
||||
observedGatewayRequests.add(params);
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': true,
|
||||
'payload': <String, dynamic>{
|
||||
'workspaceDir': '/home/ubuntu/.openclaw/workspace',
|
||||
'managedSkillsDir': '/home/ubuntu/.openclaw/skills',
|
||||
'skills': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'name': 'it-infra-continuous-png',
|
||||
'description': 'Generate infrastructure PNGs.',
|
||||
'source': 'openclaw-workspace',
|
||||
'skillKey': 'it-infra-continuous-png',
|
||||
'eligible': true,
|
||||
'disabled': false,
|
||||
'missing': <String, dynamic>{
|
||||
'bins': <String>[],
|
||||
'env': <String>[],
|
||||
'config': <String>[],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
request.response.statusCode = HttpStatus.badRequest;
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'error': <String, dynamic>{
|
||||
'code': -32601,
|
||||
'message': 'unexpected method: $method',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
});
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-bridge-skills-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${tempDir.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => tempDir.path,
|
||||
);
|
||||
final acpClient = GatewayAcpClient(
|
||||
endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'),
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
final runtime = GatewayRuntime(
|
||||
store: store,
|
||||
identityStore: DeviceIdentityStore(store),
|
||||
sessionClient: GatewayAcpRuntimeSessionClient(client: acpClient),
|
||||
);
|
||||
await runtime.initialize();
|
||||
addTearDown(() async {
|
||||
runtime.dispose();
|
||||
await subscription.cancel();
|
||||
await server.close(force: true);
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
await runtime.connectProfile(
|
||||
const GatewayConnectionProfile(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
host: 'xworkmate-bridge.svc.plus',
|
||||
port: 443,
|
||||
tls: true,
|
||||
tokenRef: '',
|
||||
passwordRef: '',
|
||||
selectedAgentId: 'main',
|
||||
),
|
||||
);
|
||||
|
||||
final controller = SkillsController(runtime);
|
||||
await controller.refresh(agentId: 'main');
|
||||
|
||||
expect(observedMethods, <String>[
|
||||
'xworkmate.gateway.connect',
|
||||
'xworkmate.gateway.request',
|
||||
]);
|
||||
expect(observedGatewayRequests.single['method'], 'skills.status');
|
||||
expect(
|
||||
(observedGatewayRequests.single['params'] as Map)['agentId'],
|
||||
'main',
|
||||
);
|
||||
expect(controller.items, hasLength(1));
|
||||
expect(controller.items.single.skillKey, 'it-infra-continuous-png');
|
||||
expect(controller.items.single.eligible, isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user