codex_runtime.dart (-290 lines): - Remove 17 dead methods behind UnsupportedError guard (findCodexBinary, startStdio, request, startThread, resumeThread, sendMessage, interrupt, getAccount, listModels, listSkills, stop, dispose, _resolveLaunchConfiguration + 3 @visibleForTesting wrappers) - Remove 10 dead fields (_process, _state, _pendingRequests, _events, etc.) - Remove ChangeNotifier mixin (nothing to notify) - Keep only model types, enums, and standalone helper functions AGENTS.md (+21 lines): - Add Fallback and Dead Code Elimination Policy section - Forbidden: cascading fallbacks, lingering DEPRECATED code, dead code behind guards, silent catch blocks, redundant indirection, excessive JSON key probing - Required: inline WHY comments on every retained fallback chain Additional cleanup: - gateway_acp_client.dart: remove unused _GatewayAcpSessionUpdate class - runtime_controllers_entities.dart: replace _canRefreshThroughRuntime with runtimeInternal.isConnected - runtime_models_gateway_entities.dart: relocate CollaborationAttachment
329 lines
8.9 KiB
Dart
329 lines
8.9 KiB
Dart
// ignore_for_file: unused_import, unnecessary_import
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'gateway_runtime.dart';
|
|
import 'runtime_models.dart';
|
|
import 'secure_config_store.dart';
|
|
import 'runtime_controllers_settings.dart';
|
|
import 'runtime_controllers_gateway.dart';
|
|
import 'runtime_controllers_derived_tasks.dart';
|
|
|
|
class SkillsController extends ChangeNotifier {
|
|
SkillsController(this.runtimeInternal) {
|
|
_runtimeListener = () {
|
|
if (!runtimeInternal.isConnected) {
|
|
// Reset auto-refresh flag on disconnect so a subsequent reconnect
|
|
// will trigger a fresh load.
|
|
_hasAutoRefreshed = false;
|
|
return;
|
|
}
|
|
// Auto-refresh on first gateway connect only when skills are empty,
|
|
// not already loading, and haven't auto-refreshed this session.
|
|
if (loadingInternal || itemsInternal.isNotEmpty || _hasAutoRefreshed) {
|
|
return;
|
|
}
|
|
_hasAutoRefreshed = true;
|
|
refresh();
|
|
};
|
|
runtimeInternal.addListener(_runtimeListener!);
|
|
}
|
|
|
|
final GatewayRuntime runtimeInternal;
|
|
|
|
List<GatewaySkillSummary> itemsInternal = const <GatewaySkillSummary>[];
|
|
bool loadingInternal = false;
|
|
String? errorInternal;
|
|
int _retryCount = 0;
|
|
static const int _maxRetries = 2;
|
|
VoidCallback? _runtimeListener;
|
|
bool _hasAutoRefreshed = false;
|
|
|
|
List<GatewaySkillSummary> get items => itemsInternal;
|
|
bool get loading => loadingInternal;
|
|
String? get error => errorInternal;
|
|
|
|
/// Whether the user can manually retry (non-empty error + not loading).
|
|
bool get canRetry => (errorInternal?.isNotEmpty ?? false) && !loadingInternal;
|
|
|
|
bool get _canRefreshThroughRuntime =>
|
|
runtimeInternal.isConnected || runtimeInternal.canConnectBridgeSession;
|
|
|
|
Future<void> refresh({String? agentId}) async {
|
|
if (!_canRefreshThroughRuntime) {
|
|
errorInternal = 'Gateway 未连接,无法加载技能列表。';
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
loadingInternal = true;
|
|
errorInternal = null;
|
|
_retryCount = 0;
|
|
notifyListeners();
|
|
await _doRefresh(agentId: agentId);
|
|
}
|
|
|
|
Future<void> _doRefresh({String? agentId}) async {
|
|
try {
|
|
itemsInternal = await runtimeInternal.listSkills(agentId: agentId);
|
|
errorInternal = null;
|
|
_retryCount = 0;
|
|
} catch (error) {
|
|
if (_retryCount < _maxRetries && runtimeInternal.isConnected) {
|
|
_retryCount++;
|
|
final delay = Duration(seconds: _retryCount * 2);
|
|
await Future<void>.delayed(delay);
|
|
if (runtimeInternal.isConnected) {
|
|
await _doRefresh(agentId: agentId);
|
|
return;
|
|
}
|
|
}
|
|
errorInternal = error.toString();
|
|
} finally {
|
|
loadingInternal = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (_runtimeListener != null) {
|
|
runtimeInternal.removeListener(_runtimeListener!);
|
|
_runtimeListener = null;
|
|
}
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class ModelsController extends ChangeNotifier {
|
|
ModelsController(this.runtimeInternal, this.settingsControllerInternal);
|
|
|
|
final GatewayRuntime runtimeInternal;
|
|
final SettingsController settingsControllerInternal;
|
|
|
|
List<GatewayModelSummary> itemsInternal = const <GatewayModelSummary>[];
|
|
bool loadingInternal = false;
|
|
String? errorInternal;
|
|
|
|
List<GatewayModelSummary> get items => itemsInternal;
|
|
bool get loading => loadingInternal;
|
|
String? get error => errorInternal;
|
|
|
|
void restoreFromSettings(AiGatewayProfile profile) {
|
|
final models = modelsFromProfileInternal(profile);
|
|
if (models.length == itemsInternal.length &&
|
|
models.every(
|
|
(item) => itemsInternal.any((current) => current.id == item.id),
|
|
)) {
|
|
return;
|
|
}
|
|
itemsInternal = models;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
loadingInternal = true;
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
final profile = settingsControllerInternal.snapshot.aiGateway;
|
|
if (profile.baseUrl.trim().isNotEmpty) {
|
|
final synced = await settingsControllerInternal.syncAiGatewayCatalog(
|
|
profile,
|
|
);
|
|
itemsInternal = modelsFromProfileInternal(synced);
|
|
} else if (runtimeInternal.isConnected) {
|
|
itemsInternal = await runtimeInternal.listModels();
|
|
} else {
|
|
itemsInternal = modelsFromProfileInternal(profile);
|
|
}
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
} finally {
|
|
loadingInternal = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
List<GatewayModelSummary> modelsFromProfileInternal(
|
|
AiGatewayProfile profile,
|
|
) {
|
|
final selected = profile.selectedModels
|
|
.where(profile.availableModels.contains)
|
|
.toList(growable: false);
|
|
final candidates = selected.isNotEmpty
|
|
? selected
|
|
: profile.availableModels.take(5).toList(growable: false);
|
|
return candidates
|
|
.map(
|
|
(item) => GatewayModelSummary(
|
|
id: item,
|
|
name: item,
|
|
provider: 'LLM API',
|
|
contextWindow: null,
|
|
maxOutputTokens: null,
|
|
),
|
|
)
|
|
.toList(growable: false);
|
|
}
|
|
}
|
|
|
|
class CronJobsController extends ChangeNotifier {
|
|
CronJobsController(this.runtimeInternal);
|
|
|
|
final GatewayRuntime runtimeInternal;
|
|
|
|
List<GatewayCronJobSummary> itemsInternal = const <GatewayCronJobSummary>[];
|
|
bool loadingInternal = false;
|
|
String? errorInternal;
|
|
|
|
List<GatewayCronJobSummary> get items => itemsInternal;
|
|
bool get loading => loadingInternal;
|
|
String? get error => errorInternal;
|
|
|
|
Future<void> refresh() async {
|
|
if (!runtimeInternal.isConnected) {
|
|
itemsInternal = const <GatewayCronJobSummary>[];
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
loadingInternal = true;
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
itemsInternal = await runtimeInternal.listCronJobs();
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
} finally {
|
|
loadingInternal = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
class DevicesController extends ChangeNotifier {
|
|
DevicesController(this.runtimeInternal);
|
|
|
|
final GatewayRuntime runtimeInternal;
|
|
|
|
GatewayDevicePairingList itemsInternal =
|
|
const GatewayDevicePairingList.empty();
|
|
bool loadingInternal = false;
|
|
String? errorInternal;
|
|
|
|
GatewayDevicePairingList get items => itemsInternal;
|
|
bool get loading => loadingInternal;
|
|
String? get error => errorInternal;
|
|
|
|
Future<void> refresh({bool quiet = false}) async {
|
|
if (!runtimeInternal.isConnected) {
|
|
itemsInternal = const GatewayDevicePairingList.empty();
|
|
if (!quiet) {
|
|
errorInternal = null;
|
|
}
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
if (loadingInternal) {
|
|
return;
|
|
}
|
|
loadingInternal = true;
|
|
if (!quiet) {
|
|
errorInternal = null;
|
|
}
|
|
notifyListeners();
|
|
try {
|
|
itemsInternal = await runtimeInternal.listDevicePairing();
|
|
} catch (error) {
|
|
if (!quiet) {
|
|
errorInternal = error.toString();
|
|
}
|
|
} finally {
|
|
loadingInternal = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> approve(String requestId) async {
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
await runtimeInternal.approveDevicePairing(requestId);
|
|
await refresh(quiet: true);
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> reject(String requestId) async {
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
await runtimeInternal.rejectDevicePairing(requestId);
|
|
await refresh(quiet: true);
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> remove(String deviceId) async {
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
await runtimeInternal.removePairedDevice(deviceId);
|
|
await refresh(quiet: true);
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<String?> rotateToken({
|
|
required String deviceId,
|
|
required String role,
|
|
List<String> scopes = const <String>[],
|
|
}) async {
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
final token = await runtimeInternal.rotateDeviceToken(
|
|
deviceId: deviceId,
|
|
role: role,
|
|
scopes: scopes,
|
|
);
|
|
await refresh(quiet: true);
|
|
return token;
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
notifyListeners();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> revokeToken({
|
|
required String deviceId,
|
|
required String role,
|
|
}) async {
|
|
errorInternal = null;
|
|
notifyListeners();
|
|
try {
|
|
await runtimeInternal.revokeDeviceToken(deviceId: deviceId, role: role);
|
|
await refresh(quiet: true);
|
|
} catch (error) {
|
|
errorInternal = error.toString();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
itemsInternal = const GatewayDevicePairingList.empty();
|
|
errorInternal = null;
|
|
loadingInternal = false;
|
|
notifyListeners();
|
|
}
|
|
}
|