552 lines
15 KiB
Dart
552 lines
15 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_connection.dart';
|
|
import 'runtime_models_profiles.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';
|
|
|
|
class GatewayConnectionProfile {
|
|
const GatewayConnectionProfile({
|
|
required this.mode,
|
|
required this.useSetupCode,
|
|
required this.setupCode,
|
|
required this.host,
|
|
required this.port,
|
|
required this.tls,
|
|
required this.tokenRef,
|
|
required this.passwordRef,
|
|
required this.selectedAgentId,
|
|
});
|
|
|
|
final RuntimeConnectionMode mode;
|
|
final bool useSetupCode;
|
|
final String setupCode;
|
|
final String host;
|
|
final int port;
|
|
final bool tls;
|
|
final String tokenRef;
|
|
final String passwordRef;
|
|
final String selectedAgentId;
|
|
|
|
factory GatewayConnectionProfile.defaults() {
|
|
return GatewayConnectionProfile.defaultsGateway();
|
|
}
|
|
|
|
factory GatewayConnectionProfile.defaultsGateway() {
|
|
return const GatewayConnectionProfile(
|
|
mode: RuntimeConnectionMode.unconfigured,
|
|
useSetupCode: false,
|
|
setupCode: '',
|
|
host: '',
|
|
port: 443,
|
|
tls: true,
|
|
tokenRef: 'gateway_token_0',
|
|
passwordRef: 'gateway_password_0',
|
|
selectedAgentId: '',
|
|
);
|
|
}
|
|
|
|
GatewayConnectionProfile copyWith({
|
|
RuntimeConnectionMode? mode,
|
|
bool? useSetupCode,
|
|
String? setupCode,
|
|
String? host,
|
|
int? port,
|
|
bool? tls,
|
|
String? tokenRef,
|
|
String? passwordRef,
|
|
String? selectedAgentId,
|
|
}) {
|
|
final normalized = normalizeGatewayManualEndpointInternal(
|
|
host: host ?? this.host,
|
|
port: port ?? this.port,
|
|
tls: tls ?? this.tls,
|
|
);
|
|
return GatewayConnectionProfile(
|
|
mode: mode ?? this.mode,
|
|
useSetupCode: useSetupCode ?? this.useSetupCode,
|
|
setupCode: setupCode ?? this.setupCode,
|
|
host: normalized.host,
|
|
port: normalized.port,
|
|
tls: normalized.tls,
|
|
tokenRef: tokenRef ?? this.tokenRef,
|
|
passwordRef: passwordRef ?? this.passwordRef,
|
|
selectedAgentId: selectedAgentId ?? this.selectedAgentId,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'mode': mode.name,
|
|
'useSetupCode': useSetupCode,
|
|
'setupCode': setupCode,
|
|
'host': host,
|
|
'port': port,
|
|
'tls': tls,
|
|
'tokenRef': tokenRef,
|
|
'passwordRef': passwordRef,
|
|
'selectedAgentId': selectedAgentId,
|
|
};
|
|
}
|
|
|
|
factory GatewayConnectionProfile.fromJson(Map<String, dynamic> json) {
|
|
final defaults = GatewayConnectionProfile.defaults();
|
|
final normalized = normalizeGatewayManualEndpointInternal(
|
|
host: json['host'] as String? ?? defaults.host,
|
|
port: json['port'] as int? ?? defaults.port,
|
|
tls: json['tls'] as bool? ?? defaults.tls,
|
|
);
|
|
return GatewayConnectionProfile(
|
|
mode: RuntimeConnectionModeCopy.fromJsonValue(json['mode'] as String?),
|
|
useSetupCode: json['useSetupCode'] as bool? ?? false,
|
|
setupCode: json['setupCode'] as String? ?? '',
|
|
host: normalized.host,
|
|
port: normalized.port,
|
|
tls: normalized.tls,
|
|
tokenRef: json['tokenRef'] as String? ?? '',
|
|
passwordRef: json['passwordRef'] as String? ?? '',
|
|
selectedAgentId: json['selectedAgentId'] as String? ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
const int kGatewayProfileListLength = 1;
|
|
const int kGatewayRemoteProfileIndex = 0;
|
|
|
|
List<GatewayConnectionProfile> normalizeGatewayProfiles({
|
|
Iterable<GatewayConnectionProfile>? profiles,
|
|
}) {
|
|
final fallback = GatewayConnectionProfile.defaultsGateway();
|
|
final incoming =
|
|
profiles?.toList(growable: false) ?? const <GatewayConnectionProfile>[];
|
|
final current = incoming.isNotEmpty ? incoming.first : fallback;
|
|
final hasEndpoint =
|
|
current.host.trim().isNotEmpty &&
|
|
current.port > 0 &&
|
|
!_isGatewayLoopbackHost(current.host);
|
|
final slotMode = switch (current.mode) {
|
|
RuntimeConnectionMode.remote => RuntimeConnectionMode.remote,
|
|
RuntimeConnectionMode.unconfigured =>
|
|
hasEndpoint
|
|
? RuntimeConnectionMode.remote
|
|
: RuntimeConnectionMode.unconfigured,
|
|
};
|
|
return List<GatewayConnectionProfile>.unmodifiable(<GatewayConnectionProfile>[
|
|
current.copyWith(
|
|
mode: slotMode,
|
|
useSetupCode: current.useSetupCode,
|
|
setupCode: current.setupCode,
|
|
host: hasEndpoint ? current.host : fallback.host,
|
|
port: current.port > 0 ? current.port : fallback.port,
|
|
tls: hasEndpoint ? current.tls : fallback.tls,
|
|
tokenRef: current.tokenRef.trim().isEmpty
|
|
? fallback.tokenRef
|
|
: current.tokenRef,
|
|
passwordRef: current.passwordRef.trim().isEmpty
|
|
? fallback.passwordRef
|
|
: current.passwordRef,
|
|
),
|
|
]);
|
|
}
|
|
|
|
bool _isGatewayLoopbackHost(String host) {
|
|
final normalized = host.trim().toLowerCase();
|
|
return normalized == '127.0.0.1' || normalized == 'localhost';
|
|
}
|
|
|
|
List<GatewayConnectionProfile> replaceGatewayProfileAt(
|
|
List<GatewayConnectionProfile> profiles,
|
|
int _,
|
|
GatewayConnectionProfile profile,
|
|
) {
|
|
return normalizeGatewayProfiles(
|
|
profiles: <GatewayConnectionProfile>[profile],
|
|
);
|
|
}
|
|
|
|
({String host, int port, bool tls}) normalizeGatewayManualEndpointInternal({
|
|
required String host,
|
|
required int port,
|
|
required bool tls,
|
|
}) {
|
|
final trimmedHost = host.trim();
|
|
if (trimmedHost.isEmpty) {
|
|
return (host: trimmedHost, port: port, tls: tls);
|
|
}
|
|
final normalizedInput = trimmedHost.contains('://')
|
|
? trimmedHost
|
|
: '${tls ? 'https' : 'http'}://$trimmedHost:${port > 0 ? port : (tls ? 443 : 18789)}';
|
|
final uri = Uri.tryParse(normalizedInput);
|
|
final normalizedHost = uri?.host.trim() ?? trimmedHost;
|
|
if (normalizedHost.isEmpty) {
|
|
return (host: trimmedHost, port: port, tls: tls);
|
|
}
|
|
final scheme = uri?.scheme.trim().toLowerCase() ?? (tls ? 'https' : 'http');
|
|
final normalizedTls = switch (scheme) {
|
|
'ws' || 'http' => false,
|
|
_ => true,
|
|
};
|
|
final normalizedPort = uri?.hasPort == true
|
|
? uri!.port
|
|
: normalizedTls
|
|
? 443
|
|
: 18789;
|
|
return (
|
|
host: normalizedHost,
|
|
port: normalizedPort > 0 ? normalizedPort : port,
|
|
tls: normalizedTls,
|
|
);
|
|
}
|
|
|
|
class OllamaLocalConfig {
|
|
const OllamaLocalConfig({
|
|
required this.endpoint,
|
|
required this.defaultModel,
|
|
required this.autoDiscover,
|
|
});
|
|
|
|
final String endpoint;
|
|
final String defaultModel;
|
|
final bool autoDiscover;
|
|
|
|
factory OllamaLocalConfig.defaults() {
|
|
return const OllamaLocalConfig(
|
|
endpoint: 'http://127.0.0.1:11434',
|
|
defaultModel: 'qwen2.5-coder:latest',
|
|
autoDiscover: true,
|
|
);
|
|
}
|
|
|
|
OllamaLocalConfig copyWith({
|
|
String? endpoint,
|
|
String? defaultModel,
|
|
bool? autoDiscover,
|
|
}) {
|
|
return OllamaLocalConfig(
|
|
endpoint: endpoint ?? this.endpoint,
|
|
defaultModel: defaultModel ?? this.defaultModel,
|
|
autoDiscover: autoDiscover ?? this.autoDiscover,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'endpoint': endpoint,
|
|
'defaultModel': defaultModel,
|
|
'autoDiscover': autoDiscover,
|
|
};
|
|
}
|
|
|
|
factory OllamaLocalConfig.fromJson(Map<String, dynamic> json) {
|
|
return OllamaLocalConfig(
|
|
endpoint:
|
|
json['endpoint'] as String? ?? OllamaLocalConfig.defaults().endpoint,
|
|
defaultModel:
|
|
json['defaultModel'] as String? ??
|
|
OllamaLocalConfig.defaults().defaultModel,
|
|
autoDiscover: json['autoDiscover'] as bool? ?? true,
|
|
);
|
|
}
|
|
}
|
|
|
|
class OllamaCloudConfig {
|
|
const OllamaCloudConfig({
|
|
required this.baseUrl,
|
|
required this.organization,
|
|
required this.workspace,
|
|
required this.defaultModel,
|
|
required this.apiKeyRef,
|
|
});
|
|
|
|
final String baseUrl;
|
|
final String organization;
|
|
final String workspace;
|
|
final String defaultModel;
|
|
final String apiKeyRef;
|
|
|
|
factory OllamaCloudConfig.defaults() {
|
|
return const OllamaCloudConfig(
|
|
baseUrl: 'https://ollama.com',
|
|
organization: '',
|
|
workspace: '',
|
|
defaultModel: 'gpt-oss:120b',
|
|
apiKeyRef: 'ollama_cloud_api_key',
|
|
);
|
|
}
|
|
|
|
OllamaCloudConfig copyWith({
|
|
String? baseUrl,
|
|
String? organization,
|
|
String? workspace,
|
|
String? defaultModel,
|
|
String? apiKeyRef,
|
|
}) {
|
|
return OllamaCloudConfig(
|
|
baseUrl: baseUrl ?? this.baseUrl,
|
|
organization: organization ?? this.organization,
|
|
workspace: workspace ?? this.workspace,
|
|
defaultModel: defaultModel ?? this.defaultModel,
|
|
apiKeyRef: apiKeyRef ?? this.apiKeyRef,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'baseUrl': baseUrl,
|
|
'organization': organization,
|
|
'workspace': workspace,
|
|
'defaultModel': defaultModel,
|
|
'apiKeyRef': apiKeyRef,
|
|
};
|
|
}
|
|
|
|
factory OllamaCloudConfig.fromJson(Map<String, dynamic> json) {
|
|
return OllamaCloudConfig(
|
|
baseUrl:
|
|
json['baseUrl'] as String? ?? OllamaCloudConfig.defaults().baseUrl,
|
|
organization: json['organization'] as String? ?? '',
|
|
workspace: json['workspace'] as String? ?? '',
|
|
defaultModel:
|
|
json['defaultModel'] as String? ??
|
|
OllamaCloudConfig.defaults().defaultModel,
|
|
apiKeyRef:
|
|
json['apiKeyRef'] as String? ??
|
|
OllamaCloudConfig.defaults().apiKeyRef,
|
|
);
|
|
}
|
|
}
|
|
|
|
class VaultConfig {
|
|
const VaultConfig({
|
|
required this.address,
|
|
required this.namespace,
|
|
required this.authMode,
|
|
required this.tokenRef,
|
|
});
|
|
|
|
final String address;
|
|
final String namespace;
|
|
final String authMode;
|
|
final String tokenRef;
|
|
|
|
factory VaultConfig.defaults() {
|
|
return const VaultConfig(
|
|
address: 'http://127.0.0.1:8200',
|
|
namespace: '',
|
|
authMode: 'token',
|
|
tokenRef: 'vault_token',
|
|
);
|
|
}
|
|
|
|
VaultConfig copyWith({
|
|
String? address,
|
|
String? namespace,
|
|
String? authMode,
|
|
String? tokenRef,
|
|
}) {
|
|
return VaultConfig(
|
|
address: address ?? this.address,
|
|
namespace: namespace ?? this.namespace,
|
|
authMode: authMode ?? this.authMode,
|
|
tokenRef: tokenRef ?? this.tokenRef,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'address': address,
|
|
'namespace': namespace,
|
|
'authMode': authMode,
|
|
'tokenRef': tokenRef,
|
|
};
|
|
}
|
|
|
|
factory VaultConfig.fromJson(Map<String, dynamic> json) {
|
|
return VaultConfig(
|
|
address: json['address'] as String? ?? VaultConfig.defaults().address,
|
|
namespace:
|
|
json['namespace'] as String? ?? VaultConfig.defaults().namespace,
|
|
authMode: json['authMode'] as String? ?? VaultConfig.defaults().authMode,
|
|
tokenRef: json['tokenRef'] as String? ?? VaultConfig.defaults().tokenRef,
|
|
);
|
|
}
|
|
}
|
|
|
|
class AiGatewayProfile {
|
|
const AiGatewayProfile({
|
|
required this.name,
|
|
required this.baseUrl,
|
|
required this.apiKeyRef,
|
|
required this.availableModels,
|
|
required this.selectedModels,
|
|
required this.syncState,
|
|
required this.syncMessage,
|
|
});
|
|
|
|
final String name;
|
|
final String baseUrl;
|
|
final String apiKeyRef;
|
|
final List<String> availableModels;
|
|
final List<String> selectedModels;
|
|
final String syncState;
|
|
final String syncMessage;
|
|
|
|
factory AiGatewayProfile.defaults() {
|
|
return const AiGatewayProfile(
|
|
name: 'LLM API',
|
|
baseUrl: '',
|
|
apiKeyRef: 'ai_gateway_api_key',
|
|
availableModels: <String>[],
|
|
selectedModels: <String>[],
|
|
syncState: 'idle',
|
|
syncMessage: 'Ready to sync models',
|
|
);
|
|
}
|
|
|
|
AiGatewayProfile copyWith({
|
|
String? name,
|
|
String? baseUrl,
|
|
String? apiKeyRef,
|
|
List<String>? availableModels,
|
|
List<String>? selectedModels,
|
|
String? syncState,
|
|
String? syncMessage,
|
|
}) {
|
|
return AiGatewayProfile(
|
|
name: name ?? this.name,
|
|
baseUrl: baseUrl ?? this.baseUrl,
|
|
apiKeyRef: apiKeyRef ?? this.apiKeyRef,
|
|
availableModels: availableModels ?? this.availableModels,
|
|
selectedModels: selectedModels ?? this.selectedModels,
|
|
syncState: syncState ?? this.syncState,
|
|
syncMessage: syncMessage ?? this.syncMessage,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'name': name,
|
|
'baseUrl': baseUrl,
|
|
'apiKeyRef': apiKeyRef,
|
|
'availableModels': availableModels,
|
|
'selectedModels': selectedModels,
|
|
'syncState': syncState,
|
|
'syncMessage': syncMessage,
|
|
};
|
|
}
|
|
|
|
factory AiGatewayProfile.fromJson(Map<String, dynamic> json) {
|
|
List<String> normalizeList(Object? value) {
|
|
if (value is! List) {
|
|
return const <String>[];
|
|
}
|
|
return value
|
|
.map((item) => item.toString().trim())
|
|
.where((item) => item.isNotEmpty)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
final defaults = AiGatewayProfile.defaults();
|
|
final availableModels = normalizeList(json['availableModels']);
|
|
final selectedModels = normalizeList(json['selectedModels'])
|
|
.where(
|
|
(item) => availableModels.isEmpty || availableModels.contains(item),
|
|
)
|
|
.toList(growable: false);
|
|
return AiGatewayProfile(
|
|
name: json['name'] as String? ?? defaults.name,
|
|
baseUrl: json['baseUrl'] as String? ?? defaults.baseUrl,
|
|
apiKeyRef: json['apiKeyRef'] as String? ?? defaults.apiKeyRef,
|
|
availableModels: availableModels,
|
|
selectedModels: selectedModels,
|
|
syncState: json['syncState'] as String? ?? defaults.syncState,
|
|
syncMessage: json['syncMessage'] as String? ?? defaults.syncMessage,
|
|
);
|
|
}
|
|
}
|
|
|
|
class AiGatewayConnectionCheck {
|
|
const AiGatewayConnectionCheck({
|
|
required this.state,
|
|
required this.message,
|
|
required this.endpoint,
|
|
required this.modelCount,
|
|
});
|
|
|
|
final String state;
|
|
final String message;
|
|
final String endpoint;
|
|
final int modelCount;
|
|
|
|
bool get success => state == 'ready' || state == 'empty';
|
|
}
|
|
|
|
enum WebSessionPersistenceMode { browser, remote }
|
|
|
|
extension WebSessionPersistenceModeCopy on WebSessionPersistenceMode {
|
|
String get label => switch (this) {
|
|
WebSessionPersistenceMode.browser => appText('浏览器本地缓存', 'Browser cache'),
|
|
WebSessionPersistenceMode.remote => appText(
|
|
'远端 Session API',
|
|
'Remote session API',
|
|
),
|
|
};
|
|
|
|
static WebSessionPersistenceMode fromJsonValue(String? value) {
|
|
return WebSessionPersistenceMode.values.firstWhere(
|
|
(item) => item.name == value,
|
|
orElse: () => WebSessionPersistenceMode.browser,
|
|
);
|
|
}
|
|
}
|
|
|
|
class WebSessionPersistenceConfig {
|
|
const WebSessionPersistenceConfig({
|
|
required this.mode,
|
|
required this.remoteBaseUrl,
|
|
});
|
|
|
|
final WebSessionPersistenceMode mode;
|
|
final String remoteBaseUrl;
|
|
|
|
factory WebSessionPersistenceConfig.defaults() {
|
|
return const WebSessionPersistenceConfig(
|
|
mode: WebSessionPersistenceMode.browser,
|
|
remoteBaseUrl: '',
|
|
);
|
|
}
|
|
|
|
bool get usesRemoteApi =>
|
|
mode == WebSessionPersistenceMode.remote &&
|
|
remoteBaseUrl.trim().isNotEmpty;
|
|
|
|
WebSessionPersistenceConfig copyWith({
|
|
WebSessionPersistenceMode? mode,
|
|
String? remoteBaseUrl,
|
|
}) {
|
|
return WebSessionPersistenceConfig(
|
|
mode: mode ?? this.mode,
|
|
remoteBaseUrl: remoteBaseUrl ?? this.remoteBaseUrl,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {'mode': mode.name, 'remoteBaseUrl': remoteBaseUrl};
|
|
}
|
|
|
|
factory WebSessionPersistenceConfig.fromJson(Map<String, dynamic> json) {
|
|
final defaults = WebSessionPersistenceConfig.defaults();
|
|
return WebSessionPersistenceConfig(
|
|
mode: WebSessionPersistenceModeCopy.fromJsonValue(
|
|
json['mode'] as String?,
|
|
),
|
|
remoteBaseUrl: json['remoteBaseUrl'] as String? ?? defaults.remoteBaseUrl,
|
|
);
|
|
}
|
|
}
|