371 lines
11 KiB
Dart
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';
|
|
}
|