diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index e72f1a75..6f62f115 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -104,6 +104,11 @@ class AppController extends ChangeNotifier { _settingsController.secureRefs.containsKey('gateway_token'); String? get storedGatewayTokenMask => _settingsController.secureRefs['gateway_token']; + String get aiGatewayUrl => settings.aiGateway.baseUrl.trim(); + + Future loadAiGatewayApiKey() async { + return (await _store.loadAiGatewayApiKey())?.trim() ?? ''; + } List get aiGatewayModelChoices { final selected = settings.aiGateway.selectedModels .where(settings.aiGateway.availableModels.contains) diff --git a/lib/features/ai_gateway/ai_gateway_page.dart b/lib/features/ai_gateway/ai_gateway_page.dart index d1a19ee5..103a9712 100644 --- a/lib/features/ai_gateway/ai_gateway_page.dart +++ b/lib/features/ai_gateway/ai_gateway_page.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; @@ -96,8 +97,8 @@ class _AiGatewayPageState extends State { ); } - Widget _buildTabContent(BuildContext context, AiGatewayTab tab, AppController controller) { - final palette = context.palette; + Widget _buildTabContent(BuildContext context, AiGatewayTab tab, AppController controller) { + final palette = context.palette; switch (tab) { case AiGatewayTab.models: @@ -262,30 +263,29 @@ class _AiGatewayPageState extends State { ), ), ); - } - } + } + } - } - - StatusInfo? _connectionStatus(RuntimeConnectionStatus status) { - return switch (status) { - RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success), - RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent), - RuntimeConnectionStatus.offline => const StatusInfo('Offline', StatusTone.neutral), - RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger), - }; - } -} + StatusInfo? _connectionStatus(RuntimeConnectionStatus status) { + return switch (status) { + RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success), + RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent), + RuntimeConnectionStatus.offline => const StatusInfo('Offline', StatusTone.neutral), + RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger), + }; + } + } enum AiGatewayTab { models, agents, endpoints, tools } -extension AiGatewayTabCopy on AiGatewayTab { - String get label => switch (this) { - AiGatewayTab.models => appText('模型', 'Models'), - AiGatewayTab.agents => appText('代理', 'Agents'), - AiGatewayTab.endpoints => appText('端点', 'Endpoints'), - }; -} + extension AiGatewayTabCopy on AiGatewayTab { + String get label => switch (this) { + AiGatewayTab.models => appText('模型', 'Models'), + AiGatewayTab.agents => appText('代理', 'Agents'), + AiGatewayTab.endpoints => appText('端点', 'Endpoints'), + AiGatewayTab.tools => appText('工具', 'Tools'), + }; + } class _ModelCard extends StatelessWidget { const _ModelCard({required this.model, required this.onTap}); @@ -557,24 +557,24 @@ class _CodexIntegrationCardState extends State<_CodexIntegrationCard> { ); } - Future _exportConfig() async { - setState(() { - _isExporting = true; - _errorMessage = null; - }); + Future _exportConfig() async { + setState(() { + _isExporting = true; + _errorMessage = null; + }); try { final home = Platform.environment['HOME'] ?? ''; - final codexHome = Platform.environment['CODEX_HOME'] ?? '$home/.codex'; - final configPath = '$codexHome/config.toml'; + final codexHome = Platform.environment['CODEX_HOME'] ?? '$home/.codex'; + final configPath = '$codexHome/config.toml'; - // Get gateway URL and API key from controller - final gatewayUrl = widget.controller.aiGatewayUrl; - final apiKey = widget.controller.aiGatewayApiKey; + // Get gateway URL and API key from controller + final gatewayUrl = widget.controller.aiGatewayUrl; + final apiKey = await widget.controller.loadAiGatewayApiKey(); - if (gatewayUrl.isEmpty) { - throw Exception(appText('AI Gateway URL 未配置', 'AI Gateway URL not configured')); - } + if (gatewayUrl.isEmpty) { + throw Exception(appText('AI Gateway URL 未配置', 'AI Gateway URL not configured')); + } // Create config directory if needed final configDir = Directory(codexHome); diff --git a/lib/runtime/codex_ffi_bindings.dart b/lib/runtime/codex_ffi_bindings.dart index 54aeb0b9..24278395 100644 --- a/lib/runtime/codex_ffi_bindings.dart +++ b/lib/runtime/codex_ffi_bindings.dart @@ -198,12 +198,13 @@ class CodexFFIBindings { int sendMessage(int threadId, String message) { _ensureRuntime(); final messagePtr = message.toNativeUtf8(); + final handlePtr = calloc(); try { - final handle = ThreadHandleFFI(); - handle.id = threadId; - return _sendMessage(_runtime!, handle, messagePtr); + handlePtr.ref.id = threadId; + return _sendMessage(_runtime!, handlePtr.ref, messagePtr); } finally { calloc.free(messagePtr); + calloc.free(handlePtr); } } diff --git a/test/runtime/mode_switcher_test.dart b/test/runtime/mode_switcher_test.dart index 9b891f5d..0db1a4ea 100644 --- a/test/runtime/mode_switcher_test.dart +++ b/test/runtime/mode_switcher_test.dart @@ -3,26 +3,49 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/runtime/mode_switcher.dart'; import 'package:xworkmate/runtime/gateway_runtime.dart'; +import 'package:xworkmate/runtime/device_identity_store.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; +import 'package:xworkmate/runtime/secure_config_store.dart'; // Mock GatewayRuntime for testing -class MockGatewayRuntime extends ChangeNotifier implements GatewayRuntime { - final StreamController _events = StreamController.broadcast(); +class MockGatewayRuntime extends GatewayRuntime { + factory MockGatewayRuntime() { + final store = SecureConfigStore(); + return MockGatewayRuntime._(store); + } + + MockGatewayRuntime._(SecureConfigStore store) + : _storeForTest = store, + super( + store: store, + identityStore: DeviceIdentityStore(store), + ); + + final SecureConfigStore _storeForTest; + final StreamController _eventsController = + StreamController.broadcast(); GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial(); bool _isConnected = false; final List> _requests = []; void setConnected(bool connected) { _isConnected = connected; - _snapshot = GatewayConnectionSnapshot( - profile: GatewayConnectionProfile.defaults(), - status: connected ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline, + _snapshot = _snapshot.copyWith( + status: connected + ? RuntimeConnectionStatus.connected + : RuntimeConnectionStatus.offline, + statusText: connected ? 'Connected' : 'Offline', ); notifyListeners(); // Emit connection event if (connected) { - _events.add(GatewayPushEvent(event: 'gateway/connected', payload: {})); + _eventsController.add( + const GatewayPushEvent( + event: 'gateway/connected', + payload: {}, + ), + ); } } @@ -33,15 +56,15 @@ class MockGatewayRuntime extends ChangeNotifier implements GatewayRuntime { GatewayConnectionSnapshot get snapshot => _snapshot; @override - Stream get events => _events.stream; + Stream get events => _eventsController.stream; @override - Future> request( + Future request( String method, { - Map params = const {}, - Duration timeout = const Duration(seconds: 30), + Map? params, + Duration timeout = const Duration(seconds: 15), }) async { - _requests.add({'method': method, 'params': params}); + _requests.add({'method': method, 'params': params ?? const {}}); return {'success': true}; } @@ -55,39 +78,33 @@ class MockGatewayRuntime extends ChangeNotifier implements GatewayRuntime { String authPasswordOverride = '', }) async { _isConnected = true; - _snapshot = GatewayConnectionSnapshot( - profile: profile, + _snapshot = GatewayConnectionSnapshot.initial(mode: profile.mode).copyWith( status: RuntimeConnectionStatus.connected, + statusText: 'Connected', ); notifyListeners(); - _events.add(GatewayPushEvent(event: 'gateway/connected', payload: {})); + _eventsController.add( + const GatewayPushEvent( + event: 'gateway/connected', + payload: {}, + ), + ); } @override - Future disconnect() async { + Future disconnect({bool clearDesiredProfile = true}) async { _isConnected = false; - _snapshot = GatewayConnectionSnapshot( - profile: _snapshot.profile, - status: RuntimeConnectionStatus.offline, + _snapshot = GatewayConnectionSnapshot.initial(mode: _snapshot.mode).copyWith( + statusText: 'Offline', ); notifyListeners(); } @override - Future clearLogs() async {} - - @override - List get logs => []; - - @override - List get logsForTest => []; - - @override - void addRuntimeLogForTest({ - required String level, - required String category, - required String message, - }) {} + void dispose() { + _eventsController.close(); + super.dispose(); + } } void main() {