Code Review · 代码梳理

移动端会话管理:
操作清单缓存策略梳理

apps/mobile-expo/src/query/sessions.ts 已实现的会话操作、MMKV 缓存与 react-query 联动做一次盘点,并给出按优先级排序的改进建议。

sessions.ts react-query MMKV cache gateway-contract 2026-08-30
01

总览

读取侧 5 个函数 · 写入侧 7 个函数

文件以 gateway-contract 的路径构建与响应解析函数为契约层,配合 react-query 做服务端状态、MMKV 做冷启动占位缓存。核心思路:让抽屉和聊天屏在冷启动瞬间先渲染上次数据,实时请求在后台补全

UI / HooksSessionsScreen · use-chat-session
use-session-history
query/sessions.ts契约解析 · 动作函数
缓存写入条件
gateway-contractpath 构建 · Zod 解析
类型定义
Gateway APIREST /api/sessions
realtime WS
placeholderData
MMKV 会话列表 → 抽屉冷启动即渲染
scope: profileId
sessionHistory
历史消息头页缓存
scope: profileId + sessionKey
sessionDetail
详情缓存
scope: profileId + sessionKey
02

已实现的会话操作

全部基于 gateway-contract 的路径构建

R查询类(读)

  • fetchSessionsList — 分页列表,支持 search / channel / sortBy=updatedAt
  • readPlaceholderSessions — 读 MMKV 占位数据(react-query placeholderData)
  • fetchSession — 会话详情,404 → null
  • fetchSessionActiveRun — active run 状态,无 key / 404 → { active:false }
  • fetchSessionMessagePage — 历史消息分页(before 游标)

W动作类(写)

  • createSession — 创建 + 注入新会话偏好(model / thinkingLevel)
  • deleteSession — 删除
  • renameSession — 重命名
  • archiveSession / unarchiveSession — 归档 / 取消归档
  • pinSession / unpinSession — 置顶 / 取消置顶

↑ createSession 还会从 preferences-store 读取按 gateway 保存的偏好(选中 agent、模型、思考级别),必要时对路由到的 agent 二次 PATCH initialAgentConfig。

03

缓存策略

MMKV 持久化 + react-query 内存态

CMMKV 持久化缓存

  • 列表:仅 offset===0 && !search 时写入,按 profileId 分键
  • 历史 / 详情:按 profileId + sessionKey 分键(每会话独立 scope)
  • 统一 TTL 1 小时(query-cache.ts 默认)

Qreact-query 联动

  • 动作后 → refreshList() → resetQueries(sessionsAll) 重拉第一页
  • 聊天收尾 → mergeLatestSessionHistoryPage 就地合并 + invalidateSessionLists(仅标脏)
  • 冷启动 → placeholderData 立即渲染,实时请求后台补全
// 缓存写入条件(fetchSessionsList 内)
if (offset === 0 && !search) {
  writeCachedSessions(activeGatewayId, items);  // 仅无过滤的第一页
}
04

改进建议

按优先级排序 · 前两条影响正确性
  1. 动作函数校验响应体 ok / error

    delete / rename / archive 等只检查 res.ok(HTTP 状态)。但契约已提供 parseSessionActionResponse / parseSessionRenameResponse,协议可能返回 HTTP 200 但 body { ok:false, error:'…' },现在会被静默当成成功。

    const parsed = parseSessionRenameResponse(await res.json());
    if (parsed.ok === false || parsed.renamed === false) {
      throw new Error(parsed.error ?? 'Rename failed');
    }

    收益:消除静默失败;同一模式应用到全部 6 个动作函数。

  2. 删除 / 归档后清理该会话的详情与历史缓存

    列表缓存会在动作后经 refreshList 覆写,但 sessionDetail / sessionHistory 的 MMKV scope 不会同步清理。删除会话后,若用户冷启动并命中旧缓存,可能看到已删除会话的历史。

    // deleteSession 成功后
    clearCachedSessionDetail(profileId, key);
    // + 清除该 key 的 sessionHistory scope

    收益:避免删除/归档后冷启动渲染陈旧会话内容。

  3. 统一失效语义:invalidate vs refresh

    列表失效逻辑散落三处(infinite-list-sync / workspace-sync / use-chat-session),且语义不同:invalidateSessionListsrefetchType:'none' 只标脏不重拉。聊天发完一条消息后,抽屉的 updatedAt 排序可能不实时

    收益:明确“发消息后需强制刷新排序”,而不是仅标脏;收敛为单一入口。

  4. 缓存写入的 channel 语义统一

    fetchSessionsList 的 channel 默认是 undefined → 'webchat',而 SessionsScreen 传 null(全量)。缓存写入条件不区分 channel —— 若未来某调用方以 webchat 拉第一页,会覆盖全量缓存,抽屉数据范围漂移。

    // 建议:仅 channel === null(全量)时写入列表缓存
    if (offset === 0 && !search && channel === null) { … }

    收益:缓存数据范围与“抽屉应显示的内容”保持一致。

  5. 时效敏感数据不应共用 1 小时 TTL

    active-run、archived / pinned 状态变化快,1 小时 TTL 会让冷启动看到过期状态。建议为这类数据提供更短的 maxAgeMs,或状态变化时主动 clear。

    收益:冷启动展示的状态更接近真实。

  6. 健壮性小修

    createSession 里“路由后补 PATCH”逻辑可读性略绕(用 if (!body.initialAgentConfig) 判断);动作函数的 key 未统一 trim。可抽一个 resolveAgentConfig 辅助函数。

    收益:降低后续维护时的误解成本。

05

建议的行动顺序

  1. 1 先做 #1 响应体校验 — 改动小、收益确定,消除静默失败隐患。
  2. 2 再做 #2 缓存清理 — 删除/归档后同步清详情与历史缓存。
  3. 3 随后处理 #3 / #4 / #5 — 统一失效语义、channel 与 TTL 策略。
  4. 4 #6 随重构顺手做 — 不单独阻塞。