fix(arch): A1-A3 app layer anti-patterns cleanup

This commit is contained in:
Haitao Pan 2026-06-05 13:05:04 +08:00
parent b11df437ac
commit 3e20fcb504
8 changed files with 34 additions and 434 deletions

View File

@ -31,7 +31,7 @@ import '../runtime/assistant_artifacts.dart';
import '../runtime/desktop_thread_artifact_service.dart';
import '../runtime/external_code_agent_acp_desktop_transport.dart';
import '../runtime/go_task_service_client.dart';
import '../runtime/go_task_service_desktop_service.dart';
import '../runtime/go_runtime_dispatch_desktop_client.dart';
import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
@ -152,13 +152,10 @@ class AppController extends ChangeNotifier {
);
goTaskServiceClientInternal =
goTaskServiceClient ??
DesktopGoTaskService(
gateway: runtimeCoordinatorInternal.gateway,
acpTransport: ExternalCodeAgentAcpDesktopTransport(
client: gatewayAcpClientInternal,
endpointResolver: resolveExternalAcpEndpointForTargetInternal,
taskEndpointResolver: resolveExternalAcpEndpointForRequestInternal,
),
ExternalCodeAgentAcpDesktopTransport(
client: gatewayAcpClientInternal,
endpointResolver: resolveExternalAcpEndpointForTargetInternal,
taskEndpointResolver: resolveExternalAcpEndpointForRequestInternal,
);
bridgeAgentProviderCatalogInternal = normalizeSingleAgentProviderList(
initialBridgeProviderCatalog ?? const <SingleAgentProvider>[],
@ -267,8 +264,7 @@ class AppController extends ChangeNotifier {
final Map<String, OpenClawGatewayQueuedTurnInternal>
openClawGatewayActiveTurnsInternal =
<String, OpenClawGatewayQueuedTurnInternal>{};
int get openClawGatewayActiveTasksInternal =>
openClawGatewayActiveTurnsInternal.length;
int localMessageCounterInternal = 0;
int assistantDraftSessionCounterInternal = 0;

View File

@ -939,7 +939,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
final authorization =
await resolveBridgeArtifactAuthorizationHeaderInternal(uri);
if (authorization == null || authorization.trim().isEmpty) {
return const _ArtifactBytesResult.skipped();
return const _ArtifactBytesResult.failed();
}
final bytes = await _downloadBridgeArtifactBytesInternal(
uri,

View File

@ -1296,19 +1296,7 @@ extension AppControllerDesktopThreadActions on AppController {
notifyIfActiveInternal();
}
void clearGatewayTaskArtifactStateInternal(
String sessionKey, {
required double completedAtMs,
required String syncStatus,
}) {
upsertTaskThreadInternal(
sessionKey,
lastArtifactSyncAtMs: completedAtMs,
lastArtifactSyncStatus: syncStatus,
lastTaskArtifactRelativePaths: const <String>[],
updatedAtMs: completedAtMs,
);
}
Future<void> applyGatewayChatResultInternal({
required String sessionKey,
@ -1347,10 +1335,12 @@ extension AppControllerDesktopThreadActions on AppController {
return;
}
if (!result.success) {
clearGatewayTaskArtifactStateInternal(
upsertTaskThreadInternal(
sessionKey,
completedAtMs: completedAtMs,
syncStatus: 'failed',
lastArtifactSyncAtMs: completedAtMs,
lastArtifactSyncStatus: 'failed',
lastTaskArtifactRelativePaths: const <String>[],
updatedAtMs: completedAtMs,
);
appendLocalSessionMessageInternal(
sessionKey,
@ -1370,10 +1360,12 @@ extension AppControllerDesktopThreadActions on AppController {
return;
}
if (noDisplayableOutput) {
clearGatewayTaskArtifactStateInternal(
upsertTaskThreadInternal(
sessionKey,
completedAtMs: completedAtMs,
syncStatus: 'failed',
lastArtifactSyncAtMs: completedAtMs,
lastArtifactSyncStatus: 'failed',
lastTaskArtifactRelativePaths: const <String>[],
updatedAtMs: completedAtMs,
);
appendLocalSessionMessageInternal(
sessionKey,
@ -1554,7 +1546,10 @@ extension AppControllerDesktopThreadActions on AppController {
bool gatewayResultCodeRequiresNewSessionInternal(String code) {
final normalized = code.trim().toUpperCase();
if (normalized.isEmpty) {
if (normalized.isEmpty ||
normalized == 'SUCCESS' ||
normalized == 'COMPLETED' ||
normalized == 'READY') {
return false;
}
if (normalized == 'RUNNING' ||
@ -1566,6 +1561,7 @@ extension AppControllerDesktopThreadActions on AppController {
normalized == 'BRIDGE_NOT_CONNECTED' ||
normalized == 'ACP_HTTP_401' ||
normalized == 'ACP_HTTP_403' ||
normalized == 'OPENCLAW_GATEWAY_SOCKET_CLOSED' ||
normalized == 'OPENCLAW_GATEWAY_QUEUE_FULL' ||
normalized == 'OPENCLAW_AGENT_FAILED_BEFORE_REPLY' ||
normalized == 'OPENCLAW_NO_DISPLAYABLE_OUTPUT' ||
@ -1574,7 +1570,7 @@ extension AppControllerDesktopThreadActions on AppController {
normalized == 'ARTIFACT_MISSING') {
return true;
}
return false;
return true;
}
Future<void> abortRun() async {

View File

@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'acp_endpoint_paths.dart';
import 'gateway_acp_client.dart';
import 'go_task_service_client.dart';
import 'runtime_models.dart';
class ExternalCodeAgentAcpDesktopTransport
implements ExternalCodeAgentAcpTransport {
implements GoTaskServiceClient {
ExternalCodeAgentAcpDesktopTransport({
required GatewayAcpClient client,
required Uri? Function(AssistantExecutionTarget target) endpointResolver,
@ -28,72 +26,7 @@ class ExternalCodeAgentAcpDesktopTransport
final Duration _recoveryPollDelay;
final int? _recoveryMaxAttempts;
@visibleForTesting
GatewayAcpClient get clientForTest => _client;
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
}) async {
final response = await _client.request(
method: 'acp.capabilities',
params: const <String, dynamic>{},
endpointOverride: _endpointResolver(target),
);
final result = _castMap(response['result']);
final caps = _castMap(result['capabilities']);
final providerCatalog = _parseProviderCatalog(
result['providerCatalog'] ?? caps['providerCatalog'],
defaultTarget: AssistantExecutionTarget.agent,
);
final gatewayProviders = _parseProviderCatalog(
result['gatewayProviders'] ?? caps['gatewayProviders'],
defaultTarget: AssistantExecutionTarget.gateway,
);
return ExternalCodeAgentAcpCapabilities(
singleAgent:
_boolValue(result['singleAgent']) ??
_boolValue(caps['single_agent']) ??
providerCatalog.isNotEmpty,
multiAgent:
_boolValue(result['multiAgent']) ??
_boolValue(caps['multi_agent']) ??
true,
availableExecutionTargets: _parseAvailableExecutionTargets(
result['availableExecutionTargets'] ??
caps['availableExecutionTargets'],
singleAgent:
_boolValue(result['singleAgent']) ??
_boolValue(caps['single_agent']) ??
providerCatalog.isNotEmpty,
gatewayProviders: gatewayProviders,
),
providerCatalog: providerCatalog,
gatewayProviders: gatewayProviders,
raw: result,
);
}
@override
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
}) async {
final response = await _client.request(
method: 'xworkmate.routing.resolve',
params: <String, dynamic>{
'taskPrompt': taskPrompt,
'workingDirectory': workingDirectory.trim(),
'routing': routing.toJson(),
},
endpointOverride: _endpointResolver(AssistantExecutionTarget.gateway),
);
return ExternalCodeAgentAcpRoutingResolution(
raw: _castMap(response['result']),
);
}
@override
Future<GoTaskServiceResult> executeTask(
@ -330,6 +263,7 @@ class ExternalCodeAgentAcpDesktopTransport
@override
Future<void> cancelTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
@ -353,18 +287,7 @@ class ExternalCodeAgentAcpDesktopTransport
);
}
@override
Future<void> closeTask({
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) async {
await _client.closeSession(
sessionId: sessionId,
threadId: threadId,
endpointOverride: _endpointResolver(target),
);
}
@override
Future<void> dispose() => _client.dispose();
@ -531,130 +454,6 @@ class ExternalCodeAgentAcpDesktopTransport
return const <String, dynamic>{};
}
List<Object?> _asList(Object? raw) {
if (raw is List<Object?>) {
return raw;
}
if (raw is List) {
return raw.cast<Object?>();
}
return const <Object?>[];
}
bool? _boolValue(Object? raw) {
if (raw is bool) {
return raw;
}
if (raw is num) {
return raw != 0;
}
final text = raw?.toString().trim().toLowerCase();
if (text == null || text.isEmpty) {
return null;
}
if (text == 'true' || text == '1' || text == 'yes') {
return true;
}
if (text == 'false' || text == '0' || text == 'no') {
return false;
}
return null;
}
List<SingleAgentProvider> _parseProviderCatalog(
Object? raw, {
required AssistantExecutionTarget defaultTarget,
}) {
final providers = <SingleAgentProvider>[];
for (final item in _asList(raw)) {
final entry = _castMap(item);
final providerId = entry['providerId']?.toString().trim() ?? '';
if (providerId.isEmpty) {
continue;
}
final label = entry['label']?.toString().trim();
final providerDisplay = _castMap(entry['providerDisplay']);
final targets = _parseProviderTargets(
entry['targets'] ?? entry['executionTarget'],
defaultTarget: defaultTarget,
);
final provider = SingleAgentProviderCopy.fromJsonValue(
providerId,
label: label?.isNotEmpty == true ? label : null,
badge: entry['badge']?.toString().trim().isNotEmpty == true
? entry['badge']?.toString().trim()
: providerDisplay['badge']?.toString().trim(),
logoEmoji: entry['logoEmoji']?.toString().trim().isNotEmpty == true
? entry['logoEmoji']?.toString().trim()
: providerDisplay['logoEmoji']?.toString().trim(),
supportedTargets: targets,
enabled: _boolValue(entry['enabled']) ?? true,
unavailableReason:
entry['unavailableReason']?.toString().trim().isNotEmpty == true
? entry['unavailableReason']?.toString().trim()
: '',
);
if (!provider.isUnspecified) {
providers.add(provider);
}
}
return normalizeSingleAgentProviderList(providers);
}
List<AssistantExecutionTarget> _parseAvailableExecutionTargets(
Object? raw, {
required bool singleAgent,
required List<SingleAgentProvider> gatewayProviders,
}) {
final parsed = <AssistantExecutionTarget>[];
for (final item in _asList(raw)) {
final normalized = item?.toString().trim().toLowerCase() ?? '';
if (normalized == 'agent' || normalized == 'single-agent') {
if (!parsed.contains(AssistantExecutionTarget.agent)) {
parsed.add(AssistantExecutionTarget.agent);
}
} else if (normalized == 'gateway') {
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
parsed.add(AssistantExecutionTarget.gateway);
}
}
}
if (parsed.isNotEmpty) {
return parsed;
}
if (singleAgent) {
parsed.add(AssistantExecutionTarget.agent);
}
if (gatewayProviders.isNotEmpty) {
parsed.add(AssistantExecutionTarget.gateway);
}
return parsed;
}
List<AssistantExecutionTarget> _parseProviderTargets(
Object? raw, {
required AssistantExecutionTarget defaultTarget,
}) {
final parsed = <AssistantExecutionTarget>[];
final items = raw is List ? raw : <Object?>[raw];
for (final item in items) {
final normalized = item?.toString().trim().toLowerCase() ?? '';
if (normalized == 'agent' || normalized == 'single-agent') {
if (!parsed.contains(AssistantExecutionTarget.agent)) {
parsed.add(AssistantExecutionTarget.agent);
}
} else if (normalized == 'gateway') {
if (!parsed.contains(AssistantExecutionTarget.gateway)) {
parsed.add(AssistantExecutionTarget.gateway);
}
}
}
if (parsed.isNotEmpty) {
return parsed;
}
return <AssistantExecutionTarget>[defaultTarget];
}
bool _socketExceptionLooksLikeConnectTimeout(SocketException error) {
final lowered = error.toString().toLowerCase();
return lowered.contains('connection timed out') ||

View File

@ -88,21 +88,7 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
notifyListeners();
}
@visibleForTesting
void addRuntimeLogForTest({
required String level,
required String category,
required String message,
}) {
appendLogInternal(this, level, category, message);
}
@visibleForTesting
bool get usesSessionClient => sessionClientInternal != null;
@visibleForTesting
GatewayRuntimeSessionClient? get sessionClientForTest =>
sessionClientInternal;
bool get canConnectBridgeSession => sessionClientInternal != null;

View File

@ -680,57 +680,7 @@ String? goTaskServiceGatewayEntryState({
}
}
abstract class ExternalCodeAgentAcpTransport {
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
});
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
});
Future<GoTaskServiceResult> executeTask(
GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate,
});
Future<GoTaskServiceResult> getTask({
required AssistantExecutionTarget target,
required OpenClawTaskAssociation association,
required GoTaskServiceRoute route,
});
Future<void> cancelTask({
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
OpenClawTaskAssociation? association,
});
Future<void> closeTask({
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
});
Future<void> dispose();
}
abstract class GoTaskServiceClient {
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
});
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
});
Future<GoTaskServiceResult> executeTask(
GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate,
@ -750,13 +700,6 @@ abstract class GoTaskServiceClient {
OpenClawTaskAssociation? association,
});
Future<void> closeTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
});
Future<void> dispose();
}

View File

@ -1,84 +0,0 @@
import 'gateway_runtime.dart';
import 'go_task_service_client.dart';
import 'runtime_models.dart';
import 'package:flutter/foundation.dart';
class DesktopGoTaskService implements GoTaskServiceClient {
DesktopGoTaskService({
required GatewayRuntime gateway,
required ExternalCodeAgentAcpTransport acpTransport,
}) : _acpTransport = acpTransport;
final ExternalCodeAgentAcpTransport _acpTransport;
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
}) => _acpTransport.loadExternalAcpCapabilities(
target: target,
forceRefresh: forceRefresh,
);
@override
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
}) => _acpTransport.resolveExternalAcpRouting(
taskPrompt: taskPrompt,
workingDirectory: workingDirectory,
routing: routing,
);
@override
Future<GoTaskServiceResult> executeTask(
GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate,
}) => _acpTransport.executeTask(request, onUpdate: onUpdate);
@override
Future<GoTaskServiceResult> getTask({
required AssistantExecutionTarget target,
required OpenClawTaskAssociation association,
required GoTaskServiceRoute route,
}) => _acpTransport.getTask(
target: target,
association: association,
route: route,
);
@override
Future<void> cancelTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
OpenClawTaskAssociation? association,
}) => _acpTransport.cancelTask(
target: target,
sessionId: sessionId,
threadId: threadId,
association: association,
);
@override
Future<void> closeTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) => _acpTransport.closeTask(
target: target,
sessionId: sessionId,
threadId: threadId,
);
@visibleForTesting
ExternalCodeAgentAcpTransport get acpTransportForTest => _acpTransport;
@override
Future<void> dispose() async {
await _acpTransport.dispose();
}
}

View File

@ -3274,7 +3274,7 @@ void main() {
await fakeGoTaskService.waitForRequestCount(prompts.length);
expect(fakeGoTaskService.requests, hasLength(prompts.length));
expect(controller.openClawGatewayActiveTasksInternal, prompts.length);
expect(controller.openClawGatewayActiveTurnsInternal.length, prompts.length);
expect(controller.openClawGatewayQueuedTurnsInternal, isEmpty);
for (var index = 0; index < prompts.length; index += 1) {
final sessionKey = 'openclaw-e2e-$index';
@ -3332,7 +3332,7 @@ void main() {
_openClawE2ECanonicalPrompts.length,
);
expect(
controller.openClawGatewayActiveTasksInternal,
controller.openClawGatewayActiveTurnsInternal.length,
_openClawE2ECanonicalPrompts.length,
);
@ -3855,7 +3855,7 @@ void main() {
.status,
'running',
);
expect(controller.openClawGatewayActiveTasksInternal, 1);
expect(controller.openClawGatewayActiveTurnsInternal.length, 1);
},
);
@ -4602,12 +4602,12 @@ Future<void> _waitForOpenClawActiveTaskCount(
) async {
final deadline = DateTime.now().add(const Duration(seconds: 5));
while (DateTime.now().isBefore(deadline)) {
if (controller.openClawGatewayActiveTasksInternal == expected) {
if (controller.openClawGatewayActiveTurnsInternal.length == expected) {
return;
}
await Future<void>.delayed(const Duration(milliseconds: 10));
}
expect(controller.openClawGatewayActiveTasksInternal, expected);
expect(controller.openClawGatewayActiveTurnsInternal.length, expected);
}
Future<List<String>> _startOpenClawActiveTasks(
@ -4701,19 +4701,7 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
final List<Object> taskOutcomes = <Object>[];
Future<void> Function(GoTaskServiceRequest request)? onExecuteTask;
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
}) async => const ExternalCodeAgentAcpCapabilities.empty();
@override
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
}) async =>
const ExternalCodeAgentAcpRoutingResolution(raw: <String, dynamic>{});
@override
Future<GoTaskServiceResult> executeTask(
@ -4786,13 +4774,7 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
OpenClawTaskAssociation? association,
}) async {}
@override
Future<void> closeTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) async {}
@override
Future<void> dispose() async {}
@ -4809,19 +4791,7 @@ class _BlockingGoTaskServiceClient implements GoTaskServiceClient {
final Map<String, void Function(GoTaskServiceUpdate)> _updates =
<String, void Function(GoTaskServiceUpdate)>{};
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
}) async => const ExternalCodeAgentAcpCapabilities.empty();
@override
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
}) async =>
const ExternalCodeAgentAcpRoutingResolution(raw: <String, dynamic>{});
@override
Future<GoTaskServiceResult> executeTask(
@ -4927,13 +4897,7 @@ class _BlockingGoTaskServiceClient implements GoTaskServiceClient {
cancelledSessionIds.add(sessionId);
}
@override
Future<void> closeTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) async {}
@override
Future<void> dispose() async {}