xworkmate-app/lib/runtime/runtime_models_configs.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,
);
}
}