import 'dart:async'; import 'package:fl_lib/fl_lib.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/data/model/opencode/models.dart'; import 'package:server_box/data/model/opencode/ssh_tunnel.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/store/opencode_store.dart'; part 'opencode.g.dart'; part 'opencode.freezed.dart'; /// Opencode 连接状态 @freezed abstract class OpencodeConnectionState with _$OpencodeConnectionState { const factory OpencodeConnectionState({ @Default(OpencodeConnectionStatus.disconnected) OpencodeConnectionStatus status, String? error, OpencodeInstallStatus? installStatus, String? serverId, }) = _OpencodeConnectionState; } enum OpencodeConnectionStatus { disconnected, connecting, connected, checkingInstallation, installing, startingServer, error, } /// Opencode 会话状态 @freezed abstract class OpencodeSessionState with _$OpencodeSessionState { const factory OpencodeSessionState({ OpencodeSession? currentSession, @Default([]) List sessions, @Default(false) bool isLoading, String? error, }) = _OpencodeSessionState; } /// 全局 Opencode 连接管理器 @Riverpod(keepAlive: true) class OpencodeConnection extends _$OpencodeConnection { OpencodeSSHTunnel? _tunnel; OpencodeInstaller? _installer; OpencodeServerConfig? _config; Spi? _spi; @override OpencodeConnectionState build() { return const OpencodeConnectionState(); } OpencodeSSHTunnel? get tunnel => _tunnel; bool get isConnected => _tunnel?.isConnected ?? false; Future connect(Spi spi, {String? privateKey, String? passphrase}) async { state = state.copyWith( status: OpencodeConnectionStatus.connecting, serverId: spi.id, ); try { await disconnect(); _tunnel = OpencodeSSHTunnel(); _spi = spi; await _tunnel!.connect(spi, privateKey: privateKey, passphrase: passphrase); _installer = OpencodeInstaller(_tunnel!); state = state.copyWith(status: OpencodeConnectionStatus.connected); } catch (e) { state = state.copyWith( status: OpencodeConnectionStatus.error, error: e.toString(), ); } } Future checkAndInstall({OpencodeServerConfig? config}) async { if (_tunnel == null || !_tunnel!.isConnected) { state = state.copyWith( status: OpencodeConnectionStatus.error, error: 'Not connected', ); return; } _config = config ?? OpencodeServerConfig(serverId: _spi!.id); state = state.copyWith(status: OpencodeConnectionStatus.checkingInstallation); try { final installStatus = await _tunnel!.checkInstallation(_config!.opencodePath); if (installStatus == OpencodeInstallStatus.installed) { state = state.copyWith( status: OpencodeConnectionStatus.connected, installStatus: installStatus, ); return; } state = state.copyWith(status: OpencodeConnectionStatus.installing); final success = await _installer!.fullInstall( config: _config!, onStatus: (status) {}, ); if (success) { state = state.copyWith( status: OpencodeConnectionStatus.connected, installStatus: OpencodeInstallStatus.installed, ); await OpencodeConfigStore().saveConfig(_config!); } else { state = state.copyWith( status: OpencodeConnectionStatus.error, error: 'Installation failed', ); } } catch (e) { state = state.copyWith( status: OpencodeConnectionStatus.error, error: e.toString(), ); } } Future startAPIServer() async { if (_tunnel == null || _config == null) return; state = state.copyWith(status: OpencodeConnectionStatus.startingServer); try { await _tunnel!.startAPIServer( opencodePath: _config!.opencodePath, configPath: _config!.configPath, port: _config!.apiPort, apiKey: _config!.apiKey, envVars: _config!.envVars, ); state = state.copyWith(status: OpencodeConnectionStatus.connected); } catch (e) { state = state.copyWith( status: OpencodeConnectionStatus.error, error: e.toString(), ); } } Future disconnect() async { if (_tunnel != null) { await _tunnel!.disconnect(); _tunnel = null; } _installer = null; _config = null; _spi = null; state = const OpencodeConnectionState(); } Future sendMessage(OpencodeMessage message, List history) async { if (_tunnel == null || _config == null) { throw Exception('Not connected'); } await _tunnel!.sendMessage( port: _config!.apiPort, message: message, history: history, apiKey: _config!.apiKey, ); } } /// Opencode 会话管理器 @Riverpod(keepAlive: true) class OpencodeSessionManager extends _$OpencodeSessionManager { late final OpencodeSessionStore _store; @override OpencodeSessionState build() { _store = OpencodeSessionStore(); _loadSessions(); return const OpencodeSessionState(); } Future _loadSessions() async { state = state.copyWith(isLoading: true); try { final sessions = _store.getAllSessions(); state = state.copyWith( sessions: sessions, isLoading: false, ); } catch (e) { state = state.copyWith( isLoading: false, error: e.toString(), ); } } Future createSession({ required String serverId, required String serverName, String? title, }) async { final session = OpencodeSession.create( serverId: serverId, serverName: serverName, title: title, ); await _store.saveSession(session); final sessions = [...state.sessions, session]; sessions.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); state = state.copyWith( sessions: sessions, currentSession: session, ); return session; } Future loadSession(String sessionId) async { final session = _store.getSession(sessionId); if (session != null) { state = state.copyWith(currentSession: session); } } Future deleteSession(String sessionId) async { await _store.deleteSession(sessionId); final sessions = state.sessions.where((s) => s.id != sessionId).toList(); state = state.copyWith( sessions: sessions, currentSession: state.currentSession?.id == sessionId ? null : state.currentSession, ); } Future addMessage(OpencodeMessage message) async { final currentSession = state.currentSession; if (currentSession == null) return; currentSession.addMessage(message); await _store.updateSessionMessages(currentSession); final sessions = state.sessions.map((s) { if (s.id == currentSession.id) { return currentSession; } return s; }).toList(); sessions.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); state = state.copyWith( sessions: sessions, currentSession: currentSession, ); } Future updateSessionTitle(String sessionId, String title) async { final session = _store.getSession(sessionId); if (session == null) return; final updatedSession = OpencodeSession( id: session.id, serverId: session.serverId, serverName: session.serverName, messages: session.messages, createdAt: session.createdAt, updatedAt: DateTime.now(), title: title, ); await _store.saveSession(updatedSession); final sessions = state.sessions.map((s) { if (s.id == sessionId) return updatedSession; return s; }).toList(); state = state.copyWith( sessions: sessions, currentSession: state.currentSession?.id == sessionId ? updatedSession : state.currentSession, ); } Future refresh() async { await _loadSessions(); } } @riverpod Stream opencodeOutputStream(OpencodeOutputStreamRef ref) { final tunnel = ref.read(opencodeConnectionProvider.notifier).tunnel; if (tunnel == null) { return const Stream.empty(); } return tunnel.outputStream; } @riverpod Stream opencodeResponseStream(OpencodeResponseStreamRef ref) { final tunnel = ref.read(opencodeConnectionProvider.notifier).tunnel; if (tunnel == null) { return const Stream.empty(); } return tunnel.responseStream; }