xworkmate-app/lib/runtime/runtime_models_connection.dart
2026-04-20 17:59:33 +08:00

371 lines
11 KiB
Dart

// 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<AssistantExecutionTarget> compactAssistantExecutionTargets(
Iterable<AssistantExecutionTarget> targets,
) {
final ordered = <AssistantExecutionTarget>[];
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<AssistantExecutionTarget> 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 = <String, String>{
'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<String> kSupportedExternalAcpEndpointSchemes = <String>{
'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 <AssistantExecutionTarget>[],
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<AssistantExecutionTarget> 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<AssistantExecutionTarget>? 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<AssistantExecutionTarget>.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<AssistantExecutionTarget>? 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<AssistantExecutionTarget>? supportedTargets,
bool? enabled,
String? unavailableReason,
}) => SingleAgentProvider.fromJsonValue(
value,
label: label,
badge: badge,
logoEmoji: logoEmoji,
supportedTargets: supportedTargets,
enabled: enabled,
unavailableReason: unavailableReason,
);
}
enum SingleAgentProviderSource { externalExtension }
List<SingleAgentProvider> normalizeSingleAgentProviderList(
Iterable<SingleAgentProvider> providers,
) {
final normalized = <SingleAgentProvider>[];
final seen = <String>{};
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';
}