Fix ACP task response display parsing

This commit is contained in:
Haitao Pan 2026-04-27 11:19:24 +08:00
parent e1e29b393f
commit 11bc09d985
2 changed files with 160 additions and 9 deletions

View File

@ -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<Object>? visited]) {
final seen = visited ?? <Object>{};
if (value == null) {
return '';
}
if (value is String) {
return value.trim();
}
if (value is Map) {
if (!seen.add(value)) {
return '';
}
final map = value.cast<String, dynamic>();
for (final key in const <String>[
'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 <String>[
'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 = <String>[];
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<Object> 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<String, dynamic>();
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<String, dynamic> mergeGoTaskServiceResponseResult(
Map<String, dynamic> response,
Map<String, dynamic> overlay,
@ -818,3 +904,10 @@ List<Map<String, dynamic>> _castMapList(Object? raw) {
}
return raw.map(_castMap).toList(growable: false);
}
List<dynamic> _castList(Object? raw) {
if (raw is List) {
return raw;
}
return const <dynamic>[];
}

View File

@ -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(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'request-id',
'result': <String, dynamic>{
'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(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'request-id',
'result': <String, dynamic>{
'success': true,
'result': <String, dynamic>{
'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(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'request-id',
'result': <String, dynamic>{
'success': true,
'payload': <String, dynamic>{
'output': <Map<String, dynamic>>[
<String, dynamic>{
'content': <Map<String, dynamic>>[
<String, dynamic>{
'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();