feat: update app controller, AI gateway page, and Codex FFI bindings

This commit is contained in:
Haitao Pan 2026-03-14 00:52:35 +08:00
parent b247190ac0
commit 6765960757
4 changed files with 93 additions and 70 deletions

View File

@ -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<String> loadAiGatewayApiKey() async {
return (await _store.loadAiGatewayApiKey())?.trim() ?? '';
}
List<String> get aiGatewayModelChoices {
final selected = settings.aiGateway.selectedModels
.where(settings.aiGateway.availableModels.contains)

View File

@ -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<AiGatewayPage> {
);
}
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<AiGatewayPage> {
),
),
);
}
}
}
}
}
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<void> _exportConfig() async {
setState(() {
_isExporting = true;
_errorMessage = null;
});
Future<void> _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);

View File

@ -198,12 +198,13 @@ class CodexFFIBindings {
int sendMessage(int threadId, String message) {
_ensureRuntime();
final messagePtr = message.toNativeUtf8();
final handlePtr = calloc<ThreadHandleFFI>();
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);
}
}

View File

@ -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<GatewayPushEvent> _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<GatewayPushEvent> _eventsController =
StreamController<GatewayPushEvent>.broadcast();
GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial();
bool _isConnected = false;
final List<Map<String, dynamic>> _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: <String, dynamic>{},
),
);
}
}
@ -33,15 +56,15 @@ class MockGatewayRuntime extends ChangeNotifier implements GatewayRuntime {
GatewayConnectionSnapshot get snapshot => _snapshot;
@override
Stream<GatewayPushEvent> get events => _events.stream;
Stream<GatewayPushEvent> get events => _eventsController.stream;
@override
Future<Map<String, dynamic>> request(
Future<dynamic> request(
String method, {
Map<String, dynamic> params = const {},
Duration timeout = const Duration(seconds: 30),
Map<String, dynamic>? 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: <String, dynamic>{},
),
);
}
@override
Future<void> disconnect() async {
Future<void> 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<void> clearLogs() async {}
@override
List<RuntimeLogEntry> get logs => [];
@override
List<RuntimeLogEntry> get logsForTest => [];
@override
void addRuntimeLogForTest({
required String level,
required String category,
required String message,
}) {}
void dispose() {
_eventsController.close();
super.dispose();
}
}
void main() {