Harden web session persistence flow
This commit is contained in:
parent
ce9049d54c
commit
bfbe564cd6
@ -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 <AssistantThreadRecord>[];
|
||||
} catch (error) {
|
||||
_sessionPersistenceStatusMessage = _sessionPersistenceErrorLabel(error);
|
||||
return browserRecords;
|
||||
|
||||
@ -341,8 +341,11 @@ class _WebSettingsPageState extends State<WebSettingsPage> {
|
||||
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')}',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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<String> loadWebSessionApiToken() async {
|
||||
await initialize();
|
||||
return (_prefs!.getString(sessionApiTokenKey) ?? '').trim();
|
||||
}
|
||||
|
||||
Future<void> saveWebSessionApiToken(String value) async {
|
||||
await initialize();
|
||||
await _prefs!.setString(sessionApiTokenKey, value.trim());
|
||||
}
|
||||
|
||||
Future<String> loadOrCreateWebSessionClientId() async {
|
||||
await initialize();
|
||||
final existing = (_prefs!.getString(sessionClientIdKey) ?? '').trim();
|
||||
|
||||
@ -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(<String, Object>{});
|
||||
final store = WebStore();
|
||||
final remoteRecords = <AssistantThreadRecord>[];
|
||||
|
||||
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>[
|
||||
AssistantThreadRecord(
|
||||
sessionKey: 'direct:stale-browser-cache',
|
||||
messages: const <GatewayChatMessage>[],
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user