fix(sessions): 刷新后校验当前会话是否还在,别一直指着已删会话

会话在**别处**被删掉(Web 面板 / 别的设备 / 直接调 API)时客户端无法感知:`refresh()`
成功后会整体替换 `sessions`,却**不校验 `activeSessionId` 是否还在新列表里**,
于是当前会话会一直指着一个已删会话——聊天页对着空会话,而且这个陈旧 id 会被
`prefs.setActive` 持久化下去,重启后依然是它。

本端自己的删除已经处理过(`deleteSession` 会主动切回主会话),漏的正是「别处删除」这一路。

- ui/sessions/SessionsViewModel.kt:新增纯函数 `resolveActiveSession(activeId, fetchedIds)`
  —— 不在列表里就回落主会话;**列表为空时不回落**(那是异常响应,不该拿它改用户的选择)。
  `refresh()` 成功分支调用它,值变化时同步持久化

测试:272 全绿(269 → +3):已删会话回落主会话 / 仍在列表则保留 / 空列表不覆盖用户选择。

实测:会话在别处删除后 force-stop 重开(重走 init 与刷新),
`cyrene_sessions.preferences_pb` 里 **`web:<sub>` 残留彻底消失**、`active_session_id` 回落 `web`;
界面回到主会话对话,不再出现已删会话的幽灵行。
This commit is contained in:
AskaEth committed 2026-10-04 17:10:26 +08:00
1 parent e642b0c18c
commit feeb49ebeb
2 files changed
+40

No files matched your search

@@ -89,9 +89,14 @@ class SessionsViewModel(private val container: AppContainer) : ViewModel() {
.onSuccess { list ->
prefs.saveSnapshot(list, fingerprint())
val cached = prefs.data.first()
// 会话可能在**别处**被删掉(Web 面板 / 别的设备 / 直接调 API),而别处删除无法感知——
// 不校验 `activeSessionId` 就会一直指着一个已删会话,且这个陈旧 id 会被持久化下去。
val active = resolveActiveSession(_state.value.activeSessionId, list.map { it.id })
if (active != _state.value.activeSessionId) prefs.setActive(active)
_state.update {
it.copy(
sessions = list.map { dto -> dto.toItem() },
activeSessionId = active,
loading = false,
fromCache = false,
syncedAt = cached.snapshotAt,
@@ -191,6 +196,23 @@ internal fun SessionSummaryDto.toItem(): SessionItem = SessionItem(
lastMessage = lastMessage?.content?.toPreviewText()?.takeIf { it.isNotBlank() },
)
/**
* 刷新成功后校验「当前会话」是否还在列表里;不在就回落到主会话。
*
* 什么时候会不在:会话在**别处**被删掉(Web 面板 / 别的设备 / 直接调 API)。
* 本端自己的删除已经处理过(`deleteSession` 会主动切回主会话),但**别处删除无法感知**——
* 不校验的话 `activeSessionId` 会一直指着一个已删会话:聊天页对着空会话,
* 而且这个陈旧 id 会被 `prefs.setActive` 持久化下去,重启后依然是它。
*
* 列表为空时**不回落**:那是异常响应,不该拿它去改用户的选择。
*/
internal fun resolveActiveSession(activeSessionId: String, fetchedIds: List<String>): String =
if (fetchedIds.isEmpty() || activeSessionId in fetchedIds) {
activeSessionId
} else {
SessionSummaryDto.MAIN_SESSION_ID
}
/**
* 列表行标题:优先用服务端的自动标题(首条用户消息生成),
* 会话还没有用户消息时 `title` 键整个缺席(或为空白),此时才兜底。