diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index ec4beccf..b5109a2e 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -228,6 +228,24 @@ extension AppControllerDesktopRuntimeHelpers on AppController { }) { final raw = error.toString().trim(); final lowered = raw.toLowerCase(); + final detailCode = error is GatewayAcpException + ? error.detailCode?.trim().toUpperCase() + : null; + final primaryCode = error is GatewayAcpException + ? error.code?.trim().toUpperCase() + : null; + final openClawSocketClosed = + target.isGateway && + (detailCode == 'OPENCLAW_GATEWAY_SOCKET_CLOSED' || + primaryCode == 'OPENCLAW_GATEWAY_SOCKET_CLOSED' || + raw.contains('OPENCLAW_GATEWAY_SOCKET_CLOSED') || + lowered.contains('openclaw') && lowered.contains('socket closed')); + if (openClawSocketClosed) { + return appText( + 'OpenClaw Gateway 连接在任务执行中断开,请稍后重试;若持续出现,请检查 xworkmate-bridge 主机到 127.0.0.1:18789 的 OpenClaw runtime 连接。', + 'The OpenClaw Gateway connection closed during task execution. Try again later; if it keeps happening, check the OpenClaw runtime connection from the xworkmate-bridge host to 127.0.0.1:18789.', + ); + } if (lowered.contains('gateway not connected') || lowered.contains('code: offline') || lowered.contains('offlin') && lowered.contains('gateway')) { @@ -974,7 +992,9 @@ bool _usesOpenClawTaskSubmitEndpointInternal(GoTaskServiceRequest request) { if (request.isMultiAgentRequest || !request.target.isGateway) { return false; } - final providerId = normalizeSingleAgentProviderId(request.provider.providerId); + final providerId = normalizeSingleAgentProviderId( + request.provider.providerId, + ); if (providerId == kCanonicalGatewayProviderId) { return true; } diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index ef095815..79dfcc2c 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -6,10 +6,16 @@ import 'acp_endpoint_paths.dart'; import 'runtime_models.dart'; class GatewayAcpException implements Exception { - const GatewayAcpException(this.message, {this.code, this.details}); + const GatewayAcpException( + this.message, { + this.code, + this.detailCode, + this.details, + }); final String message; final String? code; + final String? detailCode; final Object? details; @override @@ -1022,13 +1028,22 @@ class GatewayAcpClient { if (error.isEmpty) { return; } + final details = error['data'] ?? error['details']; throw GatewayAcpException( stringValue(error['message']) ?? 'ACP JSON-RPC request failed', code: stringValue(error['code']), - details: error['data'], + detailCode: _jsonRpcErrorDetailCode(details), + details: details, ); } + String? _jsonRpcErrorDetailCode(Object? details) { + final data = asMap(details); + return stringValue(data['code']) ?? + stringValue(data['detailCode']) ?? + stringValue(data['errorCode']); + } + Map _decodeMap(dynamic raw) { if (raw is Map) { return raw; diff --git a/test/runtime/assistant_connection_state_test.dart b/test/runtime/assistant_connection_state_test.dart index 82994394..08149e2b 100644 --- a/test/runtime/assistant_connection_state_test.dart +++ b/test/runtime/assistant_connection_state_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/runtime/gateway_acp_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; @@ -138,6 +139,27 @@ void main() { }, ); + test( + 'labels OpenClaw socket close without exposing raw JSON-RPC error', + () async { + final controller = await _isolatedController(); + addTearDown(controller.dispose); + + final label = controller.gatewayExecutionErrorLabelInternal( + const GatewayAcpException( + 'OPENCLAW_GATEWAY_SOCKET_CLOSED: OpenClaw gateway connection closed during task execution', + code: '-32002', + detailCode: 'OPENCLAW_GATEWAY_SOCKET_CLOSED', + ), + target: AssistantExecutionTarget.gateway, + ); + + expect(label, contains('OpenClaw Gateway 连接在任务执行中断开')); + expect(label, isNot(contains('-32002'))); + expect(label, isNot(contains('socket closed'))); + }, + ); + test('keeps signed-out generic runtime failures disconnected', () async { final controller = await _isolatedController(); addTearDown(controller.dispose); diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 7fe195ee..b3e2e66e 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -1014,6 +1014,56 @@ void main() { }, ); + test('preserves OpenClaw gateway socket close detail code', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(() => server.close(force: true)); + server.listen((request) async { + await utf8.decoder.bind(request).join(); + request.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.json + ..write( + jsonEncode({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'error': { + 'code': -32002, + 'message': + 'OPENCLAW_GATEWAY_SOCKET_CLOSED: OpenClaw gateway connection closed during task execution', + 'data': { + 'code': 'OPENCLAW_GATEWAY_SOCKET_CLOSED', + 'originalCode': 'SOCKET_CLOSED', + }, + }, + }), + ); + await request.response.close(); + }); + final endpoint = Uri.parse('http://127.0.0.1:${server.port}'); + final client = GatewayAcpClient(endpointResolver: () => endpoint); + + await expectLater( + client.request( + method: 'session.start', + params: const {}, + ), + throwsA( + isA() + .having((error) => error.code, 'code', '-32002') + .having( + (error) => error.detailCode, + 'detailCode', + 'OPENCLAW_GATEWAY_SOCKET_CLOSED', + ) + .having( + (error) => error.details, + 'details', + containsPair('originalCode', 'SOCKET_CLOSED'), + ), + ), + ); + }); + test('desktop follow-up execution uses session.message', () async { final capture = await _startAcpHttpServer(); addTearDown(capture.close);