feat: update app controller, AI gateway page, and Codex FFI bindings
This commit is contained in:
parent
b247190ac0
commit
6765960757
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user