// ignore_for_file: unused_import, unnecessary_import import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'app_metadata.dart'; import 'app_capabilities.dart'; import 'app_store_policy.dart'; import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; import '../runtime/aris_bundle.dart'; import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; import '../runtime/runtime_controllers.dart'; import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; import '../runtime/code_agent_node_orchestrator.dart'; import '../runtime/assistant_artifacts.dart'; import '../runtime/desktop_thread_artifact_service.dart'; import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_gateway.dart'; import 'app_controller_desktop_settings.dart'; import 'app_controller_desktop_single_agent.dart'; import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_actions.dart'; import 'app_controller_desktop_workspace_execution.dart'; import 'app_controller_desktop_settings_runtime.dart'; import 'app_controller_desktop_thread_storage.dart'; import 'app_controller_desktop_skill_permissions.dart'; import 'app_controller_desktop_runtime_coordination_impl.dart'; // ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member extension AppControllerDesktopRuntimeHelpers on AppController { Future persistAssistantLastSessionKeyInternal(String sessionKey) async { if (disposedInternal) { return; } final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, ); if (normalizedSessionKey.isEmpty || settings.assistantLastSessionKey == normalizedSessionKey) { return; } try { await AppControllerDesktopSettings(this).saveSettings( settings.copyWith(assistantLastSessionKey: normalizedSessionKey), refreshAfterSave: false, ); } catch (_) { // Best effort only during teardown-sensitive transitions. } } void setAiGatewayStreamingTextInternal(String sessionKey, String text) { final key = normalizedAssistantSessionKeyInternal(sessionKey); if (text.trim().isEmpty) { aiGatewayStreamingTextBySessionInternal.remove(key); } else { aiGatewayStreamingTextBySessionInternal[key] = text; } notifyIfActiveInternal(); } void appendAiGatewayStreamingTextInternal(String sessionKey, String delta) { if (delta.isEmpty) { return; } final key = normalizedAssistantSessionKeyInternal(sessionKey); final current = aiGatewayStreamingTextBySessionInternal[key] ?? ''; aiGatewayStreamingTextBySessionInternal[key] = '$current$delta'; notifyIfActiveInternal(); } void clearAiGatewayStreamingTextInternal(String sessionKey) { final key = normalizedAssistantSessionKeyInternal(sessionKey); if (aiGatewayStreamingTextBySessionInternal.remove(key) != null) { notifyIfActiveInternal(); } } String nextLocalMessageIdInternal() { localMessageCounterInternal += 1; return 'local-${DateTime.now().microsecondsSinceEpoch}-$localMessageCounterInternal'; } Future enqueueThreadTurnInternal( String threadId, Future Function() task, ) { final normalizedThreadId = normalizedAssistantSessionKeyInternal(threadId); final previous = assistantThreadTurnQueuesInternal[normalizedThreadId] ?? Future.value(); final completer = Completer(); late final Future next; next = previous .catchError((_) {}) .then((_) async { try { completer.complete(await task()); } catch (error, stackTrace) { completer.completeError(error, stackTrace); } }) .whenComplete(() { if (identical( assistantThreadTurnQueuesInternal[normalizedThreadId], next, )) { assistantThreadTurnQueuesInternal.remove(normalizedThreadId); } }); assistantThreadTurnQueuesInternal[normalizedThreadId] = next; return completer.future; } Uri? normalizeAiGatewayBaseUrlInternal(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) { return null; } final candidate = trimmed.contains('://') ? trimmed : 'https://$trimmed'; final uri = Uri.tryParse(candidate); if (uri == null || uri.host.trim().isEmpty) { return null; } final pathSegments = uri.pathSegments.where((item) => item.isNotEmpty); return uri.replace( pathSegments: pathSegments.isEmpty ? const ['v1'] : pathSegments, query: null, fragment: null, ); } Uri aiGatewayChatUriInternal(Uri baseUrl) { final pathSegments = baseUrl.pathSegments .where((item) => item.isNotEmpty) .toList(growable: true); if (pathSegments.isEmpty) { pathSegments.add('v1'); } if (pathSegments.length >= 2 && pathSegments[pathSegments.length - 2] == 'chat' && pathSegments.last == 'completions') { return baseUrl.replace(query: null, fragment: null); } if (pathSegments.last == 'models') { pathSegments.removeLast(); } if (pathSegments.last != 'chat') { pathSegments.add('chat'); } pathSegments.add('completions'); return baseUrl.replace( pathSegments: pathSegments, query: null, fragment: null, ); } String aiGatewayHostLabelInternal(String raw) { final uri = normalizeAiGatewayBaseUrlInternal(raw); if (uri == null) { return ''; } if (uri.hasPort) { return '${uri.host}:${uri.port}'; } return uri.host; } String aiGatewayErrorLabelInternal(Object error) { if (error is AiGatewayChatExceptionInternal) { return error.message; } if (error is SocketException) { return appText('无法连接到 LLM API。', 'Unable to reach the LLM API.'); } if (error is HandshakeException) { return appText('LLM API TLS 握手失败。', 'LLM API TLS handshake failed.'); } if (error is TimeoutException) { return appText('LLM API 请求超时。', 'LLM API request timed out.'); } if (error is FormatException) { return appText( 'LLM API 返回了无法解析的响应。', 'LLM API returned an invalid response.', ); } return error.toString(); } String formatAiGatewayHttpErrorInternal(int statusCode, String detail) { final base = switch (statusCode) { 400 => appText( 'LLM API 请求无效 (400)', 'LLM API rejected the request (400)', ), 401 => appText( 'LLM API 鉴权失败 (401)', 'LLM API authentication failed (401)', ), 403 => appText('LLM API 拒绝访问 (403)', 'LLM API denied access (403)'), 404 => appText( 'LLM API chat 接口不存在 (404)', 'LLM API chat endpoint was not found (404)', ), 429 => appText( 'LLM API 限流 (429)', 'LLM API rate limited the request (429)', ), >= 500 => appText( 'LLM API 当前不可用 ($statusCode)', 'LLM API is unavailable right now ($statusCode)', ), _ => appText( 'LLM API 返回状态码 $statusCode', 'LLM API responded with status $statusCode', ), }; final trimmed = detail.trim(); return trimmed.isEmpty ? base : '$base · $trimmed'; } String extractAiGatewayErrorDetailInternal(String body) { if (body.trim().isEmpty) { return ''; } try { final decoded = jsonDecode(extractFirstJsonDocumentInternal(body)); final map = asMap(decoded); final error = asMap(map['error']); return (stringValue(error['message']) ?? stringValue(map['message']) ?? stringValue(map['detail']) ?? '') .trim(); } on FormatException { return ''; } } String extractAiGatewayAssistantTextInternal(Object? decoded) { final map = asMap(decoded); final choices = asList(map['choices']); if (choices.isNotEmpty) { final firstChoice = asMap(choices.first); final message = asMap(firstChoice['message']); final content = extractAiGatewayContentInternal(message['content']); if (content.isNotEmpty) { return content; } } final output = asList(map['output']); for (final item in output) { final entry = asMap(item); final content = extractAiGatewayContentInternal(entry['content']); if (content.isNotEmpty) { return content; } } final direct = extractAiGatewayContentInternal(map['content']); if (direct.isNotEmpty) { return direct; } return stringValue(map['output_text'])?.trim() ?? ''; } String extractAiGatewayContentInternal(Object? content) { if (content is String) { return content.trim(); } final parts = []; for (final item in asList(content)) { final map = asMap(item); final nestedText = stringValue(map['text']); if (nestedText != null && nestedText.trim().isNotEmpty) { parts.add(nestedText.trim()); continue; } final type = stringValue(map['type']) ?? ''; if (type == 'output_text') { final text = stringValue(map['text']) ?? stringValue(map['value']); if (text != null && text.trim().isNotEmpty) { parts.add(text.trim()); } } } return parts.join('\n').trim(); } String extractFirstJsonDocumentInternal(String body) { final trimmed = body.trimLeft(); if (trimmed.isEmpty) { throw const FormatException('Empty response body'); } final start = trimmed.indexOf(RegExp(r'[\{\[]')); if (start < 0) { throw const FormatException('Missing JSON document'); } var depth = 0; var inString = false; var escaped = false; for (var index = start; index < trimmed.length; index++) { final char = trimmed[index]; if (escaped) { escaped = false; continue; } if (char == r'\') { escaped = true; continue; } if (char == '"') { inString = !inString; continue; } if (inString) { continue; } if (char == '{' || char == '[') { depth += 1; } else if (char == '}' || char == ']') { depth -= 1; if (depth == 0) { return trimmed.substring(start, index + 1); } } } throw const FormatException('Unterminated JSON document'); } SettingsSnapshot sanitizeCodeAgentSettingsInternal( SettingsSnapshot snapshot, ) { final normalizedRuntimeMode = snapshot.codeAgentRuntimeMode == CodeAgentRuntimeMode.builtIn ? CodeAgentRuntimeMode.externalCli : snapshot.codeAgentRuntimeMode; codexRuntimeWarningInternal = snapshot.codeAgentRuntimeMode == CodeAgentRuntimeMode.builtIn ? appText( '内置 Codex 运行时当前仅保留为未来扩展位;已自动切换为 External Codex CLI。', 'Built-in Codex runtime is reserved for a future release; XWorkmate switched back to External Codex CLI automatically.', ) : null; final normalizedPath = snapshot.codexCliPath.trim(); if (normalizedPath == snapshot.codexCliPath && normalizedRuntimeMode == snapshot.codeAgentRuntimeMode) { return snapshot; } return snapshot.copyWith( codeAgentRuntimeMode: normalizedRuntimeMode, codexCliPath: normalizedPath, ); } Future refreshAcpCapabilitiesInternal({ bool forceRefresh = false, bool persistMountTargets = false, }) => refreshAcpCapabilitiesRuntimeInternal( this, forceRefresh: forceRefresh, persistMountTargets: persistMountTargets, ); Future refreshSingleAgentCapabilitiesInternal({ bool forceRefresh = false, }) => refreshSingleAgentCapabilitiesRuntimeInternal( this, forceRefresh: forceRefresh, ); Future refreshResolvedCodexCliPathInternal() async { if (effectiveCodeAgentRuntimeMode != CodeAgentRuntimeMode.externalCli) { resolvedCodexCliPathInternal = null; return; } if (shouldBlockEmbeddedAgentLaunch( isAppleHost: Platform.isIOS || Platform.isMacOS, )) { resolvedCodexCliPathInternal = null; return; } final configuredPath = configuredCodexCliPath; String? detectedPath; if (configuredPath.isNotEmpty) { try { if (await File(configuredPath).exists()) { detectedPath = configuredPath; } } catch (_) { detectedPath = null; } } detectedPath ??= await runtimeCoordinatorInternal.codex.findCodexBinary(); if (disposedInternal) { return; } resolvedCodexCliPathInternal = detectedPath; } List mergeAcpCapabilitiesIntoMountTargetsInternal( List current, GatewayAcpCapabilities capabilities, ) => mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal( this, current, capabilities, ); String? assistantWorkingDirectoryForSessionInternal(String sessionKey) => assistantWorkingDirectoryForSessionRuntimeInternal(this, sessionKey); String? resolveLocalAssistantWorkingDirectoryForSessionInternal( String sessionKey, { bool requireLocalExistence = true, }) => resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal( this, sessionKey, requireLocalExistence: requireLocalExistence, ); String? resolveSingleAgentWorkingDirectoryForSessionInternal( String sessionKey, { SingleAgentProvider? provider, }) => resolveSingleAgentWorkingDirectoryForSessionRuntimeInternal( this, sessionKey, provider: provider, ); bool singleAgentProviderRequiresLocalPathInternal( SingleAgentProvider provider, ) => singleAgentProviderRequiresLocalPathRuntimeInternal(this, provider); void registerCodexExternalProviderInternal() { runtimeCoordinatorInternal.registerExternalCodeAgent( ExternalCodeAgentProvider( id: 'codex', name: 'Codex ACP', command: 'xworkmate-agent-gateway', transport: ExternalAgentTransport.websocketJsonRpc, endpoint: '', defaultArgs: const [], capabilities: const [ 'chat', 'code-edit', 'gateway-bridge', 'memory-sync', 'single-agent', 'multi-agent', ], ), ); } CodeAgentNodeState buildCodeAgentNodeStateInternal() => buildCodeAgentNodeStateRuntimeInternal(this); GatewayMode bridgeGatewayModeInternal() => bridgeGatewayModeRuntimeInternal(this); Future ensureCodexGatewayRegistrationInternal() => ensureCodexGatewayRegistrationRuntimeInternal(this); void clearCodexGatewayRegistrationInternal() => clearCodexGatewayRegistrationRuntimeInternal(this); void recomputeTasksInternal() => recomputeTasksRuntimeInternal(this); void attachChildListenersInternal() { runtimeCoordinatorInternal.addListener(relayChildChangeInternal); settingsControllerInternal.addListener( handleSettingsControllerChangeInternal, ); agentsControllerInternal.addListener(relayChildChangeInternal); sessionsControllerInternal.addListener(relayChildChangeInternal); chatControllerInternal.addListener(relayChildChangeInternal); instancesControllerInternal.addListener(relayChildChangeInternal); skillsControllerInternal.addListener(relayChildChangeInternal); connectorsControllerInternal.addListener(relayChildChangeInternal); modelsControllerInternal.addListener(relayChildChangeInternal); cronJobsControllerInternal.addListener(relayChildChangeInternal); devicesControllerInternal.addListener(relayChildChangeInternal); tasksControllerInternal.addListener(relayChildChangeInternal); multiAgentOrchestratorInternal.addListener(relayChildChangeInternal); } void detachChildListenersInternal() { runtimeCoordinatorInternal.removeListener(relayChildChangeInternal); settingsControllerInternal.removeListener( handleSettingsControllerChangeInternal, ); agentsControllerInternal.removeListener(relayChildChangeInternal); sessionsControllerInternal.removeListener(relayChildChangeInternal); chatControllerInternal.removeListener(relayChildChangeInternal); instancesControllerInternal.removeListener(relayChildChangeInternal); skillsControllerInternal.removeListener(relayChildChangeInternal); connectorsControllerInternal.removeListener(relayChildChangeInternal); modelsControllerInternal.removeListener(relayChildChangeInternal); cronJobsControllerInternal.removeListener(relayChildChangeInternal); devicesControllerInternal.removeListener(relayChildChangeInternal); tasksControllerInternal.removeListener(relayChildChangeInternal); multiAgentOrchestratorInternal.removeListener(relayChildChangeInternal); } void handleSettingsControllerChangeInternal() { final previous = lastObservedSettingsSnapshotInternal; final current = settings; final previousJson = previous.toJsonString(); final currentJson = current.toJsonString(); if (currentJson == previousJson) { notifyIfActiveInternal(); return; } final hadDraftChanges = settingsDraftInitializedInternal && (settingsDraftInternal.toJsonString() != previousJson || draftSecretValuesInternal.isNotEmpty); if (!settingsDraftInitializedInternal || !hadDraftChanges) { settingsDraftInternal = current; settingsDraftInitializedInternal = true; settingsDraftStatusMessageInternal = ''; } lastObservedSettingsSnapshotInternal = current; settingsObservationQueueInternal = settingsObservationQueueInternal .then((_) async { await handleObservedSettingsChangeInternal( previous: previous, current: current, ); }) .catchError((_) {}); notifyIfActiveInternal(); } Future handleObservedSettingsChangeInternal({ required SettingsSnapshot previous, required SettingsSnapshot current, }) async { if (disposedInternal) { return; } setActiveAppLanguage(current.appLanguage); multiAgentOrchestratorInternal.updateConfig(current.multiAgent); if (previous.codexCliPath != current.codexCliPath || previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) { await refreshResolvedCodexCliPathInternal(); registerCodexExternalProviderInternal(); if (disposedInternal) { return; } } if (authorizedSkillDirectoriesChangedInternal(previous, current)) { await refreshSharedSingleAgentLocalSkillsCacheInternal(forceRescan: true); if (disposedInternal) { return; } if (assistantExecutionTargetForSession(currentSessionKey) == AssistantExecutionTarget.singleAgent) { await refreshSingleAgentSkillsForSession(currentSessionKey); } } notifyIfActiveInternal(); } void relayChildChangeInternal() { notifyIfActiveInternal(); } void notifyIfActiveInternal() { if (disposedInternal) { return; } notifyListeners(); } Uri? resolveSingleAgentEndpointInternal(SingleAgentProvider provider) { final endpoint = settings .externalAcpEndpointForProvider(provider) .endpoint .trim(); if (endpoint.isEmpty) { return null; } final normalizedInput = endpoint.contains('://') ? endpoint : 'ws://$endpoint'; final uri = Uri.tryParse(normalizedInput); if (uri == null || uri.host.trim().isEmpty) { return null; } final scheme = uri.scheme.trim().toLowerCase(); if (scheme != 'ws' && scheme != 'wss' && scheme != 'http' && scheme != 'https') { return null; } return uri; } Future resolveSingleAgentAuthorizationHeaderInternal( Uri endpoint, ) async { final normalizedEndpoint = _normalizeExternalAcpEndpointInternal( endpoint.toString(), ); if (normalizedEndpoint == null) { return ''; } for (final profile in settings.externalAcpEndpoints) { final profileEndpoint = _normalizeExternalAcpEndpointInternal( profile.endpoint, ); if (profileEndpoint == null || profileEndpoint != normalizedEndpoint) { continue; } final authRef = profile.authRef.trim(); if (authRef.isEmpty) { return ''; } return settingsControllerInternal.resolveSecretValueInternal( refName: authRef, ); } return ''; } Uri? resolveGatewayAcpEndpointInternal() { final target = assistantExecutionTargetForSession( sessionsControllerInternal.currentSessionKey, ); if (target == AssistantExecutionTarget.singleAgent) { final remote = gatewayProfileBaseUriInternal( settings.primaryRemoteGatewayProfile, ); if (remote != null) { return remote; } return gatewayProfileBaseUriInternal(settings.primaryLocalGatewayProfile); } return gatewayProfileBaseUriInternal( gatewayProfileForAssistantExecutionTargetInternal(target), ); } Uri? resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget target, ) { if (target == AssistantExecutionTarget.singleAgent) { return null; } return gatewayProfileBaseUriInternal( gatewayProfileForAssistantExecutionTargetInternal(target), ); } Uri? gatewayProfileBaseUriInternal(GatewayConnectionProfile profile) { final host = profile.host.trim(); if (host.isEmpty || profile.port <= 0) { return null; } return Uri( scheme: profile.tls ? 'https' : 'http', host: host, port: profile.port, ); } RuntimeConnectionMode modeFromHostInternal(String host) { final trimmed = host.trim().toLowerCase(); if (isLoopbackHostInternal(trimmed)) { return RuntimeConnectionMode.local; } return RuntimeConnectionMode.remote; } bool isLoopbackHostInternal(String host) { final trimmed = host.trim().toLowerCase(); return trimmed == '127.0.0.1' || trimmed == 'localhost'; } String? _normalizeExternalAcpEndpointInternal(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) { return null; } final candidate = trimmed.contains('://') ? trimmed : 'ws://$trimmed'; final uri = Uri.tryParse(candidate); if (uri == null || uri.host.trim().isEmpty) { return null; } final scheme = uri.scheme.trim().toLowerCase(); if (scheme != 'ws' && scheme != 'wss' && scheme != 'http' && scheme != 'https') { return null; } final defaultPort = switch (scheme) { 'https' || 'wss' => 443, _ => 80, }; final port = uri.hasPort ? uri.port : defaultPort; final path = uri.path.trim().isEmpty ? '/' : uri.path.trim(); return '$scheme://${uri.host.toLowerCase()}:$port$path'; } AssistantExecutionTarget assistantExecutionTargetForModeInternal( RuntimeConnectionMode mode, ) { return switch (mode) { RuntimeConnectionMode.unconfigured => AssistantExecutionTarget.singleAgent, RuntimeConnectionMode.local => AssistantExecutionTarget.local, RuntimeConnectionMode.remote => AssistantExecutionTarget.remote, }; } GatewayConnectionProfile gatewayProfileForAssistantExecutionTargetInternal( AssistantExecutionTarget target, ) { return switch (target) { AssistantExecutionTarget.local => settings.primaryLocalGatewayProfile, AssistantExecutionTarget.remote => settings.primaryRemoteGatewayProfile, AssistantExecutionTarget.singleAgent => throw StateError( 'Single Agent target has no OpenClaw gateway profile.', ), }; } int gatewayProfileIndexForExecutionTargetInternal( AssistantExecutionTarget target, ) { return switch (target) { AssistantExecutionTarget.local => kGatewayLocalProfileIndex, AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex, AssistantExecutionTarget.singleAgent => throw StateError( 'Single Agent target has no OpenClaw gateway profile index.', ), }; } } class AiGatewayChatExceptionInternal implements Exception { const AiGatewayChatExceptionInternal(this.message); final String message; @override String toString() => message; } class AiGatewayAbortExceptionInternal implements Exception { const AiGatewayAbortExceptionInternal(this.partialText); final String partialText; }