xworkmate-app/lib/runtime/runtime_models_connection.dart
2026-04-11 18:25:26 +08:00

348 lines
10 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, local, remote }
extension RuntimeConnectionModeCopy on RuntimeConnectionMode {
String get label => switch (this) {
RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'),
RuntimeConnectionMode.local => appText('本地', 'Local'),
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 { singleAgent, gateway }
extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
String get label => switch (this) {
AssistantExecutionTarget.singleAgent => appText('Agent', 'Agent'),
AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'),
};
String get promptValue => switch (this) {
AssistantExecutionTarget.singleAgent => 'single-agent',
AssistantExecutionTarget.gateway => 'gateway',
};
bool get isGateway => this == AssistantExecutionTarget.gateway;
String get compactLabel => switch (this) {
AssistantExecutionTarget.singleAgent => appText('智能体', 'Agent'),
AssistantExecutionTarget.gateway => appText('Gateway', 'Gateway'),
};
static AssistantExecutionTarget fromJsonValue(String? value) {
final normalized = value?.trim() ?? '';
switch (normalized) {
case 'singleAgent':
case 'single-agent':
case 'agent':
return AssistantExecutionTarget.singleAgent;
case 'gateway':
return AssistantExecutionTarget.gateway;
default:
return AssistantExecutionTarget.singleAgent;
}
}
}
List<AssistantExecutionTarget> compactAssistantExecutionTargets(
Iterable<AssistantExecutionTarget> targets,
) {
final ordered = <AssistantExecutionTarget>[];
var addedGateway = false;
for (final target in targets) {
if (target == AssistantExecutionTarget.singleAgent) {
if (!ordered.contains(AssistantExecutionTarget.singleAgent)) {
ordered.add(AssistantExecutionTarget.singleAgent);
}
continue;
}
if (!addedGateway) {
ordered.add(AssistantExecutionTarget.gateway);
addedGateway = true;
}
}
return List<AssistantExecutionTarget>.unmodifiable(ordered);
}
AssistantExecutionTarget collapseAssistantExecutionTargetForDisplay(
AssistantExecutionTarget target,
) => target;
AssistantExecutionTarget resolveGatewayExecutionTargetFromVisibleTargets(
Iterable<AssistantExecutionTarget> visibleTargets, {
AssistantExecutionTarget? currentTarget,
}) {
final visible = visibleTargets.toList(growable: false);
if (currentTarget != null && currentTarget.isGateway) {
if (visible.contains(AssistantExecutionTarget.gateway)) {
return AssistantExecutionTarget.gateway;
}
}
if (visible.contains(AssistantExecutionTarget.gateway)) {
return AssistantExecutionTarget.gateway;
}
return AssistantExecutionTarget.gateway;
}
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 singleAgentProviderFallbackLabelInternal(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 singleAgentProviderFallbackBadgeInternal({
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.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',
);
final String providerId;
final String label;
final String badge;
final SingleAgentProviderSource source;
bool get isUnspecified => providerId.trim().isEmpty;
bool get isExternalExtension =>
source == SingleAgentProviderSource.externalExtension;
SingleAgentProvider copyWith({
String? providerId,
String? label,
String? badge,
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
? singleAgentProviderFallbackLabelInternal(resolvedProviderId)
: resolvedLabel,
badge: resolvedBadge.isEmpty
? singleAgentProviderFallbackBadgeInternal(
providerId: resolvedProviderId,
label: resolvedLabel,
)
: resolvedBadge,
source: source ?? this.source,
);
}
static SingleAgentProvider fromJsonValue(
String? value, {
String? label,
String? badge,
}) {
final normalized = normalizeSingleAgentProviderId(value ?? '');
final base = switch (normalized) {
'codex' => codex,
'opencode' => opencode,
'claude' => claude,
'gemini' => gemini,
'auto' || '' => unspecified,
_ => SingleAgentProvider(
providerId: normalized,
label: singleAgentProviderFallbackLabelInternal(normalized),
badge: singleAgentProviderFallbackBadgeInternal(
providerId: normalized,
label: singleAgentProviderFallbackLabelInternal(normalized),
),
),
};
return base.copyWith(label: label, badge: badge);
}
@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,
}) => SingleAgentProvider.fromJsonValue(value, label: label, badge: badge);
}
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 List<SingleAgentProvider> kPresetExternalAcpProviders =
<SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
SingleAgentProvider.gemini,
];
const String kCanonicalGatewayProviderId = 'openclaw';
const String kCanonicalGatewayProviderLabel = 'OpenClaw';
const List<SingleAgentProvider> kKnownSingleAgentProviders =
<SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
SingleAgentProvider.claude,
SingleAgentProvider.gemini,
];
const Set<String> kLegacyExternalAcpProviderIds = <String>{'claude'};