From bfbe564cd6ea5a5cb9f35d2013635bd88cd6e7d1 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 22 Mar 2026 10:55:22 +0800 Subject: [PATCH] Harden web session persistence flow --- lib/app/app_controller_web.dart | 40 +++++------- lib/web/web_settings_page.dart | 7 ++- lib/web/web_store.dart | 11 ---- ...web_settings_persistence_browser_test.dart | 61 ++++++++++++++++++- 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/lib/app/app_controller_web.dart b/lib/app/app_controller_web.dart index e6e887f4..844b9a30 100644 --- a/lib/app/app_controller_web.dart +++ b/lib/app/app_controller_web.dart @@ -261,7 +261,6 @@ class AppController extends ChangeNotifier { _aiGatewayApiKeyCache = await _store.loadAiGatewayApiKey(); _relayTokenCache = await _store.loadRelayToken(); _relayPasswordCache = await _store.loadRelayPassword(); - _webSessionApiTokenCache = await _store.loadWebSessionApiToken(); _webSessionClientId = await _store.loadOrCreateWebSessionClientId(); final records = await _loadThreadRecords(); for (final record in records) { @@ -301,16 +300,6 @@ class AppController extends ChangeNotifier { final normalizedRemoteBaseUrl = RemoteWebSessionRepository.normalizeBaseUrl( trimmedRemoteBaseUrl, ); - _settings = _settings.copyWith( - webSessionPersistence: _settings.webSessionPersistence.copyWith( - mode: mode, - remoteBaseUrl: - normalizedRemoteBaseUrl?.toString() ?? trimmedRemoteBaseUrl, - ), - ); - _webSessionApiTokenCache = apiToken.trim(); - await _store.saveWebSessionApiToken(_webSessionApiTokenCache); - await _persistSettings(); if (mode == WebSessionPersistenceMode.remote && trimmedRemoteBaseUrl.isNotEmpty && normalizedRemoteBaseUrl == null) { @@ -321,6 +310,15 @@ class AppController extends ChangeNotifier { notifyListeners(); return; } + _settings = _settings.copyWith( + webSessionPersistence: _settings.webSessionPersistence.copyWith( + mode: mode, + remoteBaseUrl: + normalizedRemoteBaseUrl?.toString() ?? trimmedRemoteBaseUrl, + ), + ); + _webSessionApiTokenCache = apiToken.trim(); + await _persistSettings(); await _persistThreads(); notifyListeners(); } @@ -752,7 +750,7 @@ class AppController extends ChangeNotifier { RemoteWebSessionRepository.normalizeBaseUrl( snapshot.webSessionPersistence.remoteBaseUrl, )?.toString() ?? - snapshot.webSessionPersistence.remoteBaseUrl.trim(); + ''; return snapshot.copyWith( assistantExecutionTarget: target, gateway: snapshot.gateway.copyWith( @@ -917,19 +915,11 @@ class AppController extends ChangeNotifier { await _browserSessionRepository.saveThreadRecords(remoteRecords); return remoteRecords; } - if (browserRecords.isNotEmpty) { - await remoteRepository.saveThreadRecords(browserRecords); - _sessionPersistenceStatusMessage = appText( - '远端 Session API 为空,已使用当前浏览器缓存完成初始化。', - 'The remote session API was empty, so the current browser cache was used to seed it.', - ); - } else { - _sessionPersistenceStatusMessage = appText( - '远端 Session API 已启用,当前还没有可恢复的会话。', - 'The remote session API is active and there are no saved conversations yet.', - ); - } - return browserRecords; + _sessionPersistenceStatusMessage = appText( + '远端 Session API 已启用,但当前为空;浏览器缓存不会自动导入远端。', + 'The remote session API is active but empty, and the browser cache will not be imported automatically.', + ); + return const []; } catch (error) { _sessionPersistenceStatusMessage = _sessionPersistenceErrorLabel(error); return browserRecords; diff --git a/lib/web/web_settings_page.dart b/lib/web/web_settings_page.dart index 13354d99..27e45393 100644 --- a/lib/web/web_settings_page.dart +++ b/lib/web/web_settings_page.dart @@ -341,8 +341,11 @@ class _WebSettingsPageState extends State { decoration: InputDecoration( labelText: appText('Session API Token', 'Session API token'), helperText: controller.storedWebSessionApiTokenMask == null - ? null - : '${appText('已保存', 'Stored')}: ${controller.storedWebSessionApiTokenMask}', + ? appText( + '只保留在当前浏览器会话内存中;刷新页面后需要重新输入。', + 'Kept only in the current browser session memory; re-enter it after reload.', + ) + : '${appText('当前会话', 'This session')}: ${controller.storedWebSessionApiTokenMask} · ${appText('刷新后需重新输入', 'Re-enter after reload')}', ), ), ], diff --git a/lib/web/web_store.dart b/lib/web/web_store.dart index 4c2ef88c..182a0028 100644 --- a/lib/web/web_store.dart +++ b/lib/web/web_store.dart @@ -13,7 +13,6 @@ class WebStore { static const relayTokenKey = 'xworkmate.web.relay.token'; static const relayPasswordKey = 'xworkmate.web.relay.password'; static const relayDeviceIdentityKey = 'xworkmate.web.relay.device_identity'; - static const sessionApiTokenKey = 'xworkmate.web.session.api_token'; static const sessionClientIdKey = 'xworkmate.web.session.client_id'; static const themeModeKey = 'xworkmate.web.theme_mode'; @@ -93,16 +92,6 @@ class WebStore { await _prefs!.setString(relayPasswordKey, value.trim()); } - Future loadWebSessionApiToken() async { - await initialize(); - return (_prefs!.getString(sessionApiTokenKey) ?? '').trim(); - } - - Future saveWebSessionApiToken(String value) async { - await initialize(); - await _prefs!.setString(sessionApiTokenKey, value.trim()); - } - Future loadOrCreateWebSessionClientId() async { await initialize(); final existing = (_prefs!.getString(sessionClientIdKey) ?? '').trim(); diff --git a/test/web/web_settings_persistence_browser_test.dart b/test/web/web_settings_persistence_browser_test.dart index 92c7cf41..ebcb1fb8 100644 --- a/test/web/web_settings_persistence_browser_test.dart +++ b/test/web/web_settings_persistence_browser_test.dart @@ -74,7 +74,8 @@ void main() { ); expect(reloaded.storedAiGatewayApiKeyMask, isNotNull); expect(reloaded.storedRelayTokenMask, isNotNull); - expect(reloaded.storedWebSessionApiTokenMask, isNotNull); + expect(controller.storedWebSessionApiTokenMask, isNotNull); + expect(reloaded.storedWebSessionApiTokenMask, isNull); expect(remoteRecords, isNotEmpty); expect(reloaded.conversations, isNotEmpty); @@ -97,12 +98,66 @@ void main() { expect(controller.usesRemoteSessionPersistence, isFalse); expect(controller.sessionPersistenceStatusMessage, contains('HTTPS')); expect( - controller.settings.webSessionPersistence.remoteBaseUrl, - 'http://xworkmate.svc.plus/api/web-sessions', + controller.settings.webSessionPersistence.mode, + WebSessionPersistenceMode.browser, ); + expect(controller.settings.webSessionPersistence.remoteBaseUrl, isEmpty); + expect(controller.storedWebSessionApiTokenMask, isNull); controller.dispose(); }); + + test( + 'empty remote session api does not import stale browser cache', + () async { + SharedPreferences.setMockInitialValues({}); + final store = WebStore(); + final remoteRecords = []; + + await store.initialize(); + await store.saveSettingsSnapshot( + SettingsSnapshot.defaults().copyWith( + webSessionPersistence: const WebSessionPersistenceConfig( + mode: WebSessionPersistenceMode.remote, + remoteBaseUrl: 'https://xworkmate.svc.plus/api/web-sessions', + ), + ), + ); + await store.saveAssistantThreadRecords([ + AssistantThreadRecord( + sessionKey: 'direct:stale-browser-cache', + messages: const [], + updatedAtMs: 1, + title: 'stale browser cache', + archived: false, + executionTarget: AssistantExecutionTarget.aiGatewayOnly, + messageViewMode: AssistantMessageViewMode.rendered, + ), + ]); + + final controller = AppController( + store: store, + remoteSessionRepositoryBuilder: (config, clientId, accessToken) => + _MemoryRemoteSessionRepository(remoteRecords), + ); + await _waitForReady(controller); + + expect(remoteRecords, isEmpty); + expect( + controller.sessionPersistenceStatusMessage, + anyOf( + contains('不会自动导入远端'), + contains('will not be imported automatically'), + ), + ); + expect( + controller.conversations.single.title, + isNot('stale browser cache'), + ); + + controller.dispose(); + }, + ); } class _MemoryRemoteSessionRepository implements WebSessionRepository {