fix: recover openclaw interrupted results
This commit is contained in:
parent
68a1eeb1a0
commit
cbd8e478c2
@ -220,7 +220,7 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
final result = _castMap(snapshot['result']);
|
||||
final result = _recoveredResultFromSessionSnapshot(snapshot);
|
||||
if (result.isNotEmpty &&
|
||||
(status == 'completed' ||
|
||||
status == 'failed' ||
|
||||
@ -307,7 +307,7 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
snapshot.remove('result');
|
||||
snapshot['turnId'] = update.turnId;
|
||||
snapshot['success'] = !update.error;
|
||||
final text = _firstNonEmptyString(snapshot, const <String>[
|
||||
final text = _firstNonEmptyDisplayText(snapshot, const <String>[
|
||||
'output',
|
||||
'message',
|
||||
'summary',
|
||||
@ -322,9 +322,58 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
String _firstNonEmptyString(Map<String, dynamic> values, List<String> keys) {
|
||||
Map<String, dynamic> _recoveredResultFromSessionSnapshot(
|
||||
Map<String, dynamic> snapshot,
|
||||
) {
|
||||
final result = <String, dynamic>{..._castMap(snapshot['result'])};
|
||||
final artifactRecord = _castMap(snapshot['artifacts']);
|
||||
final artifactItems = _listValue(artifactRecord['items']);
|
||||
if (artifactItems.isNotEmpty && !_hasArtifactList(result)) {
|
||||
result['artifacts'] = artifactItems;
|
||||
}
|
||||
for (final entry in <String, String>{
|
||||
'remoteWorkingDirectory': 'remoteWorkingDirectory',
|
||||
'remoteWorkspaceRefKind': 'remoteWorkspaceRefKind',
|
||||
'resultSummary': 'summary',
|
||||
}.entries) {
|
||||
final value = artifactRecord[entry.key]?.toString().trim() ?? '';
|
||||
if (value.isNotEmpty &&
|
||||
(result[entry.value]?.toString().trim().isEmpty ?? true)) {
|
||||
result[entry.value] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _hasArtifactList(Map<String, dynamic> result) {
|
||||
for (final key in const <String>['artifacts', 'files', 'attachments']) {
|
||||
if (_listValue(result[key]).isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
final recordItems = _listValue(_castMap(result[key])['items']);
|
||||
if (recordItems.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (final key in const <String>['payload', 'result', 'data']) {
|
||||
final nested = _castMap(result[key]);
|
||||
if (nested.isNotEmpty && _hasArtifactList(nested)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Object?> _listValue(Object? value) {
|
||||
return value is List ? value : const <Object?>[];
|
||||
}
|
||||
|
||||
String _firstNonEmptyDisplayText(
|
||||
Map<String, dynamic> values,
|
||||
List<String> keys,
|
||||
) {
|
||||
for (final key in keys) {
|
||||
final value = values[key]?.toString().trim() ?? '';
|
||||
final value = _displayText(values[key]).trim();
|
||||
if (value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
@ -332,6 +381,55 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
return '';
|
||||
}
|
||||
|
||||
String _displayText(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',
|
||||
'delta',
|
||||
'output_text',
|
||||
]) {
|
||||
final extracted = _displayText(map[key], seen);
|
||||
if (extracted.isNotEmpty) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
for (final key in const <String>['result', 'payload', 'data']) {
|
||||
final extracted = _displayText(map[key], seen);
|
||||
if (extracted.isNotEmpty) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (value is List) {
|
||||
if (!seen.add(value)) {
|
||||
return '';
|
||||
}
|
||||
return value
|
||||
.map((item) => _displayText(item, seen))
|
||||
.where((item) => item.isNotEmpty)
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _castMap(Object? value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value;
|
||||
|
||||
@ -589,6 +589,11 @@ Object? _firstGoTaskArtifactList(Map<String, dynamic> result) {
|
||||
]) {
|
||||
if (candidate is List) {
|
||||
artifacts.addAll(candidate);
|
||||
} else if (candidate is Map) {
|
||||
final items = _castList(candidate.cast<String, dynamic>()['items']);
|
||||
if (items.isNotEmpty) {
|
||||
artifacts.addAll(items);
|
||||
}
|
||||
}
|
||||
}
|
||||
return artifacts.isEmpty ? null : artifacts;
|
||||
@ -723,7 +728,7 @@ GoTaskServiceUpdate? goTaskServiceUpdateFromAcpNotification(
|
||||
payload['text']?.toString() ??
|
||||
_castMap(payload['message'])['content']?.toString() ??
|
||||
'',
|
||||
message: payload['message']?.toString() ?? '',
|
||||
message: _extractGoTaskDisplayText(payload['message']),
|
||||
pending: _boolValue(payload['pending']) ?? false,
|
||||
error: _boolValue(payload['error']) ?? false,
|
||||
route:
|
||||
|
||||
@ -184,6 +184,31 @@ void main() {
|
||||
expect(result.artifacts.single.content, 'nested artifact body');
|
||||
});
|
||||
|
||||
test('uses bridge artifact record items as artifacts', () {
|
||||
final result = goTaskServiceResultFromAcpResponse(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'request-id',
|
||||
'result': <String, dynamic>{
|
||||
'success': true,
|
||||
'message': 'created from snapshot record',
|
||||
'artifacts': <String, dynamic>{
|
||||
'items': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'exports/report.md',
|
||||
'content': 'snapshot artifact body',
|
||||
'contentType': 'text/markdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, route: GoTaskServiceRoute.externalAcpSingle);
|
||||
|
||||
expect(result.message, 'created from snapshot record');
|
||||
expect(result.artifacts, hasLength(1));
|
||||
expect(result.artifacts.single.relativePath, 'exports/report.md');
|
||||
expect(result.artifacts.single.content, 'snapshot artifact body');
|
||||
});
|
||||
|
||||
test('uses bridge files and attachments aliases as artifacts', () {
|
||||
final result = goTaskServiceResultFromAcpResponse(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
@ -582,7 +607,9 @@ void main() {
|
||||
'event': 'completed',
|
||||
'pending': false,
|
||||
'error': false,
|
||||
'message': 'stable completed output',
|
||||
'message': <String, dynamic>{
|
||||
'content': 'stable completed output',
|
||||
},
|
||||
'result': <String, dynamic>{
|
||||
'success': true,
|
||||
'output': 'stable completed output',
|
||||
@ -692,7 +719,9 @@ void main() {
|
||||
'success': true,
|
||||
'output': 'recovered from bridge session snapshot',
|
||||
'turnId': 'turn-recovered',
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
},
|
||||
'artifacts': <String, dynamic>{
|
||||
'items': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'exports/snapshot.md',
|
||||
'downloadUrl':
|
||||
@ -702,6 +731,9 @@ void main() {
|
||||
'sizeBytes': 64,
|
||||
},
|
||||
],
|
||||
'remoteWorkingDirectory': '/remote/openclaw/workspace',
|
||||
'remoteWorkspaceRefKind': 'remotePath',
|
||||
'resultSummary': 'recovered from top-level artifacts',
|
||||
},
|
||||
},
|
||||
}),
|
||||
@ -743,6 +775,8 @@ void main() {
|
||||
expect(result.success, isTrue);
|
||||
expect(result.message, 'recovered from bridge session snapshot');
|
||||
expect(result.artifacts.single.relativePath, 'exports/snapshot.md');
|
||||
expect(result.remoteWorkingDirectory, '/remote/openclaw/workspace');
|
||||
expect(result.remoteWorkspaceRefKind, WorkspaceRefKind.remotePath);
|
||||
expect(
|
||||
requestPaths,
|
||||
containsAll(<String>['/gateway/openclaw', '/acp/rpc']),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user