Merge branch 'codex/openclaw-skills-bridge-list'

This commit is contained in:
Haitao Pan 2026-05-22 21:27:54 +08:00
commit 659706a699
4 changed files with 260 additions and 9 deletions

View File

@ -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,

View File

@ -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';

View File

@ -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,

View 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);
},
);
}