// ignore_for_file: unused_import, unnecessary_import import 'dart:convert'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import 'runtime_models_profiles.dart'; import 'runtime_models_configs.dart'; import 'runtime_models_settings_snapshot.dart'; import 'runtime_models_runtime_payloads.dart'; import 'runtime_models_gateway_entities.dart'; import 'runtime_models_multi_agent.dart'; enum RuntimeConnectionMode { unconfigured, remote } extension RuntimeConnectionModeCopy on RuntimeConnectionMode { String get label => switch (this) { RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'), RuntimeConnectionMode.remote => appText('远程', 'Remote'), }; static RuntimeConnectionMode fromJsonValue(String? value) { return RuntimeConnectionMode.values.firstWhere( (item) => item.name == value, orElse: () => RuntimeConnectionMode.unconfigured, ); } } enum RuntimeConnectionStatus { offline, connecting, connected, error } extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus { String get label => switch (this) { RuntimeConnectionStatus.offline => appText('离线', 'Offline'), RuntimeConnectionStatus.connecting => appText('连接中', 'Connecting'), RuntimeConnectionStatus.connected => appText('已连接', 'Connected'), RuntimeConnectionStatus.error => appText('错误', 'Error'), }; } bool isLegacyAutoAssistantExecutionTargetValue(String? value) { return value?.trim().toLowerCase() == 'auto'; } enum AssistantExecutionTarget { agent, gateway } extension AssistantExecutionTargetCopy on AssistantExecutionTarget { String get label => switch (this) { AssistantExecutionTarget.agent => appText('智能体', 'Agent'), AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'), }; String get promptValue => switch (this) { AssistantExecutionTarget.agent => 'agent', AssistantExecutionTarget.gateway => 'gateway', }; bool get isAgent => this == AssistantExecutionTarget.agent; bool get isGateway => this == AssistantExecutionTarget.gateway; String get compactLabel => switch (this) { AssistantExecutionTarget.agent => appText('智能体', 'Agent'), AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'), }; static AssistantExecutionTarget fromJsonValue(String? value) { return AssistantExecutionTarget.values.firstWhere( (item) => item.name == value?.trim() || item.promptValue == value?.trim(), orElse: () => AssistantExecutionTarget.agent, ); } } List compactAssistantExecutionTargets( Iterable targets, ) { final ordered = []; for (final candidate in AssistantExecutionTarget.values) { if (targets.contains(candidate)) { ordered.add(candidate); } } return ordered.isEmpty ? AssistantExecutionTarget.values : ordered; } AssistantExecutionTarget collapseAssistantExecutionTargetForDisplay( AssistantExecutionTarget target, ) => target; AssistantExecutionTarget resolveAssistantExecutionTargetFromVisibleTargets( Iterable visibleTargets, { AssistantExecutionTarget? currentTarget, }) { final visible = visibleTargets.toList(growable: false); if (currentTarget != null && visible.contains(currentTarget)) { return currentTarget; } if (visible.isNotEmpty) { return visible.first; } return AssistantExecutionTarget.agent; } String normalizeSingleAgentProviderId(String value) { final trimmed = value.trim().toLowerCase(); if (trimmed.isEmpty) { return ''; } final normalizedWhitespace = trimmed.replaceAll(RegExp(r'\s+'), '-'); final buffer = StringBuffer(); var previousWasSeparator = false; var hasOutput = false; for (final rune in normalizedWhitespace.runes) { final char = String.fromCharCode(rune); final isAlphaNumeric = (rune >= 97 && rune <= 122) || (rune >= 48 && rune <= 57); final isSeparator = char == '-' || char == '_' || char == '.'; if (isAlphaNumeric) { buffer.write(char); previousWasSeparator = false; hasOutput = true; continue; } if (isSeparator && !previousWasSeparator && hasOutput) { buffer.write('-'); previousWasSeparator = true; } } return buffer.toString().replaceAll(RegExp(r'^[-_.]+|[-_.]+$'), ''); } String providerFallbackLabelInternal(String providerId) { final normalized = normalizeSingleAgentProviderId(providerId); if (normalized.isEmpty) { return appText('Bridge Provider', 'Bridge Provider'); } return normalized .split(RegExp(r'[-_.]+')) .where((item) => item.isNotEmpty) .map((item) => '${item[0].toUpperCase()}${item.substring(1)}') .join(' '); } String providerFallbackBadgeInternal({ required String providerId, required String label, }) { final normalized = normalizeSingleAgentProviderId(providerId); final known = { 'codex': 'C', 'opencode': 'O', 'claude': 'Cl', 'gemini': 'G', }; final explicit = known[normalized]; if (explicit != null) { return explicit; } final stripped = label.replaceAll(RegExp(r'\s+'), ''); if (stripped.isEmpty) { return '?'; } final length = stripped.length >= 2 ? 2 : 1; return stripped.substring(0, length).toUpperCase(); } const Set kSupportedExternalAcpEndpointSchemes = { 'ws', 'wss', 'http', 'https', }; bool isSupportedExternalAcpEndpoint(String endpoint) { final trimmed = endpoint.trim(); if (trimmed.isEmpty) { return false; } final uri = Uri.tryParse(trimmed); final scheme = uri?.scheme.trim().toLowerCase() ?? ''; return kSupportedExternalAcpEndpointSchemes.contains(scheme); } class SingleAgentProvider { const SingleAgentProvider({ required this.providerId, required this.label, required this.badge, this.logoEmoji = '', this.supportedTargets = const [], this.enabled = true, this.unavailableReason = '', this.source = SingleAgentProviderSource.externalExtension, }); static const SingleAgentProvider unspecified = SingleAgentProvider( providerId: '', label: '', badge: '', ); static const SingleAgentProvider codex = SingleAgentProvider( providerId: 'codex', label: 'Codex', badge: 'C', ); static const SingleAgentProvider opencode = SingleAgentProvider( providerId: 'opencode', label: 'OpenCode', badge: 'O', ); static const SingleAgentProvider claude = SingleAgentProvider( providerId: 'claude', label: 'Claude', badge: 'Cl', ); static const SingleAgentProvider gemini = SingleAgentProvider( providerId: 'gemini', label: 'Gemini', badge: 'G', ); static const SingleAgentProvider openclaw = SingleAgentProvider( providerId: kCanonicalGatewayProviderId, label: kCanonicalGatewayProviderLabel, badge: 'OC', ); final String providerId; final String label; final String badge; final String logoEmoji; final List supportedTargets; final bool enabled; final String unavailableReason; final SingleAgentProviderSource source; bool get isUnspecified => providerId.trim().isEmpty; bool get isExternalExtension => source == SingleAgentProviderSource.externalExtension; SingleAgentProvider copyWith({ String? providerId, String? label, String? badge, String? logoEmoji, List? supportedTargets, bool? enabled, String? unavailableReason, SingleAgentProviderSource? source, }) { final resolvedProviderId = normalizeSingleAgentProviderId( providerId ?? this.providerId, ); final resolvedLabel = (label ?? this.label).trim(); final resolvedBadge = (badge ?? this.badge).trim(); return SingleAgentProvider( providerId: resolvedProviderId, label: resolvedLabel.isEmpty ? providerFallbackLabelInternal(resolvedProviderId) : resolvedLabel, badge: resolvedBadge.isEmpty ? providerFallbackBadgeInternal( providerId: resolvedProviderId, label: resolvedLabel, ) : resolvedBadge, logoEmoji: (logoEmoji ?? this.logoEmoji).trim(), supportedTargets: supportedTargets ?? List.from(this.supportedTargets), enabled: enabled ?? this.enabled, unavailableReason: (unavailableReason ?? this.unavailableReason).trim(), source: source ?? this.source, ); } static SingleAgentProvider fromJsonValue( String? value, { String? label, String? badge, String? logoEmoji, List? supportedTargets, bool? enabled, String? unavailableReason, }) { final normalized = normalizeSingleAgentProviderId(value ?? ''); final base = switch (normalized) { 'codex' => codex, 'opencode' => opencode, 'claude' => claude, 'gemini' => gemini, kCanonicalGatewayProviderId => openclaw, 'auto' || '' => unspecified, _ => SingleAgentProvider( providerId: normalized, label: providerFallbackLabelInternal(normalized), badge: providerFallbackBadgeInternal( providerId: normalized, label: providerFallbackLabelInternal(normalized), ), ), }; return base.copyWith( label: label, badge: badge, logoEmoji: logoEmoji, supportedTargets: supportedTargets, enabled: enabled, unavailableReason: unavailableReason, ); } @override bool operator ==(Object other) => identical(this, other) || (other is SingleAgentProvider && other.providerId == providerId); @override int get hashCode => providerId.hashCode; } extension SingleAgentProviderCopy on SingleAgentProvider { static SingleAgentProvider fromJsonValue( String? value, { String? label, String? badge, String? logoEmoji, List? supportedTargets, bool? enabled, String? unavailableReason, }) => SingleAgentProvider.fromJsonValue( value, label: label, badge: badge, logoEmoji: logoEmoji, supportedTargets: supportedTargets, enabled: enabled, unavailableReason: unavailableReason, ); } enum SingleAgentProviderSource { externalExtension } List normalizeSingleAgentProviderList( Iterable providers, ) { final normalized = []; final seen = {}; for (final provider in providers) { if (seen.add(provider.providerId)) { normalized.add(provider); } } return normalized; } const String kCanonicalGatewayProviderId = 'openclaw'; const String kCanonicalGatewayProviderLabel = 'OpenClaw'; bool isBridgeOwnedSingleAgentProviderId(String providerId) { final normalized = normalizeSingleAgentProviderId(providerId); return normalized == 'codex' || normalized == 'opencode' || normalized == 'gemini' || normalized == 'hermes'; }