From 11bc09d98564e893ce5a13b8eccb77fb3dc18970 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 27 Apr 2026 11:19:24 +0800 Subject: [PATCH] Fix ACP task response display parsing --- lib/runtime/go_task_service_client.dart | 111 ++++++++++++++++-- .../runtime/gateway_acp_client_auth_test.dart | 58 +++++++++ 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 36c843aa..77a14a46 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -714,15 +714,7 @@ GoTaskServiceResult goTaskServiceResultFromAcpResponse( } return errorText; }(); - final responseText = - (result['output']?.toString().trim().isNotEmpty == true - ? result['output'].toString().trim() - : result['summary']?.toString().trim().isNotEmpty == true - ? result['summary'].toString().trim() - : result['resultSummary']?.toString().trim().isNotEmpty == true - ? result['resultSummary'].toString().trim() - : result['message']?.toString().trim() ?? '') - .trim(); + final responseText = _extractGoTaskDisplayText(result); final primaryText = (completedMessage?.trim().isNotEmpty == true ? completedMessage!.trim() @@ -748,6 +740,100 @@ GoTaskServiceResult goTaskServiceResultFromAcpResponse( ); } +String _extractGoTaskDisplayText(Object? value, [Set? visited]) { + final seen = visited ?? {}; + if (value == null) { + return ''; + } + if (value is String) { + return value.trim(); + } + if (value is Map) { + if (!seen.add(value)) { + return ''; + } + final map = value.cast(); + for (final key in const [ + 'output', + 'summary', + 'resultSummary', + 'message', + 'content', + 'text', + 'output_text', + ]) { + final extracted = _extractGoTaskTextCandidate(map[key], seen); + if (extracted.isNotEmpty) { + return extracted; + } + } + final choices = _castList(map['choices']); + if (choices.isNotEmpty) { + for (final choice in choices) { + final extracted = _extractGoTaskDisplayText( + _castMap(choice)['message'], + seen, + ); + if (extracted.isNotEmpty) { + return extracted; + } + } + } + for (final key in const [ + 'result', + 'payload', + 'data', + 'response', + 'body', + ]) { + final extracted = _extractGoTaskDisplayText(map[key], seen); + if (extracted.isNotEmpty) { + return extracted; + } + } + return ''; + } + if (value is List) { + if (!seen.add(value)) { + return ''; + } + final parts = []; + for (final item in value) { + final extracted = _extractGoTaskDisplayText(item, seen); + if (extracted.isNotEmpty) { + parts.add(extracted); + } + } + return parts.join('\n').trim(); + } + return ''; +} + +String _extractGoTaskTextCandidate(Object? value, Set visited) { + if (value == null) { + return ''; + } + if (value is String) { + return value.trim(); + } + if (value is List) { + return _extractGoTaskDisplayText(value, visited); + } + if (value is Map) { + final map = value.cast(); + final type = map['type']?.toString().trim(); + if (type == 'output_text') { + final text = + map['text']?.toString().trim() ?? map['value']?.toString().trim(); + if (text != null && text.isNotEmpty) { + return text; + } + } + return _extractGoTaskDisplayText(map, visited); + } + return ''; +} + Map mergeGoTaskServiceResponseResult( Map response, Map overlay, @@ -818,3 +904,10 @@ List> _castMapList(Object? raw) { } return raw.map(_castMap).toList(growable: false); } + +List _castList(Object? raw) { + if (raw is List) { + return raw; + } + return const []; +} diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index f689f64d..9902e7dc 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -10,6 +10,64 @@ import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; void main() { + group('GoTaskService ACP response parsing', () { + test('uses direct bridge output text', () { + final result = goTaskServiceResultFromAcpResponse({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'output': 'direct response', + }, + }, route: GoTaskServiceRoute.externalAcpSingle); + + expect(result.success, isTrue); + expect(result.message, 'direct response'); + }); + + test('uses nested provider result output text', () { + final result = goTaskServiceResultFromAcpResponse({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'result': { + 'success': true, + 'output': 'nested provider response', + }, + }, + }, route: GoTaskServiceRoute.externalAcpSingle); + + expect(result.success, isTrue); + expect(result.message, 'nested provider response'); + }); + + test('uses output content list text', () { + final result = goTaskServiceResultFromAcpResponse({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'payload': { + 'output': >[ + { + 'content': >[ + { + 'type': 'output_text', + 'text': 'content list response', + }, + ], + }, + ], + }, + }, + }, route: GoTaskServiceRoute.externalAcpSingle); + + expect(result.success, isTrue); + expect(result.message, 'content list response'); + }); + }); + group('GatewayAcpClient authorization', () { test('normalizes raw resolver token into bearer header for HTTP', () async { final capture = await _startAcpHttpServer();