Folo 移动端 v0.5.2 修复深度解读:登出鉴权残留、AI 摘要不可用状态与列表末尾已读边界
2026/9/17 8:05:20 网站建设 项目流程

Folo 移动端 v0.5.2 修复深度解读:登出鉴权残留、AI 摘要不可用状态与列表末尾已读边界

本文以 apps/mobile/changelog/0.5.2.md 为核心,结合 follow 仓库对应版本的实际提交记录与源码,逐条拆解 v0.5.2 的「No longer broken」三个修复项:登出后的鉴权存储清理、AI 摘要不可用时的状态收敛,以及条目列表末尾的已读页脚边界。读完你既能复现并验证这些 Bug,也能理解 Folo 移动端在鉴权、摘要生成与滚动已读机制上的实现链路。

Folo(follow)是一个开源的 AI RSS 阅读器项目,其移动端(Expo / React Native)采用 monorepo 管理,版本节奏紧凑:v0.5.0 完成了 Expo SDK 55 升级、流式 TTS 与后台 OTA 能力,v0.5.1 修复了 cookie 更新后的会话刷新,而 v0.5.2 则集中收敛了三个稳定性问题。本文所有结论均可对照仓库中的真实提交与源码验证。

一、版本定位与修复全景

从仓库 git 历史看,v0.5.2 的发布提交为3b336b926 release(mobile): release v0.5.2,其与 v0.5.1 发布提交之间的代码变更中,对应本次 changelog 的修复提交如下:

修复项涉及模块关键提交核心代码位置
登出后遗留鉴权存储移动端鉴权与安全存储969a0770b fix(mobile): clear auth storage on sign outapps/mobile/src/lib/auth.ts、apps/mobile/src/lib/secure-store.ts
AI 摘要不可用破坏摘要状态共享 store 的摘要同步服务174515b54 fix: handle unavailable ai summarypackages/internal/store/src/modules/summary/store.ts
列表末尾最终条目 / 已读页脚问题滚动已读(scroll-mark-read)边界算法2fa23362c(allow final entries to scroll past)、425e8610a(stabilize footer boundary)packages/internal/shared/src/scroll-mark-read.ts

需要说明的是,changelog 归并到移动端发布,但第二、三项修复落在 monorepo 的共享包内(store、shared),桌面端与移动端共同受益,这正体现了 follow 采用packages/internal/*抽取跨端逻辑的工程组织方式。

二、修复一:登出后不再残留移动端鉴权存储

问题现象

在 v0.5.2 之前,用户在移动端执行登出后,本地可能残留 cookie 与会话相关的鉴权数据;下次启动或重新登录时,可能出现会话状态错乱。结合本版本前后脉络(v0.5.1 刚刚修复过“cookie 更新后的 session refresh”),这类残留对登出→登录的完整闭环影响尤为明显。

旧实现的隐患

对比提交969a0770b的 diff,旧的signOut实现大致为:

export const signOut = async () => { await authClient.signOut() safeSecureStore.removeItem(cookieKey) safeSecureStore.removeItem(sessionTokenKey) safeSecureStore.removeItem(sessionDataKey) // ... __DEV__ 下额外删除带 env profile 后缀的 key // ... 删除数据库文件后 reload }

隐患有二:

  1. fire-and-forget 删除safeSecureStore.removeItem内部对SecureStore.deleteItemAsync只做了void ... .catch(...),并未等待删除完成;
  2. 删除与 reload 竞态:登出流程随后会调用FileSystem.deleteAsync(dbPath)expo.reloadAppAsync(...),删除操作尚未落盘应用便已重载,容易出现鉴权 key 被“留”在系统安全存储中的情况。

新实现的修复链路

新版本把“清理本地鉴权存储”抽成独立的、可等待完成的clearAuthStorage,并保证即使远端登出失败也继续执行本地清理

const clearAuthStorage = async () => { const keys = [cookieKey, sessionTokenKey, sessionDataKey] if (__DEV__) { keys.push(`${cookieKey}_${getEnvProfile()}`) } await Promise.all(keys.map((key) => safeSecureStore.removeItemAsync(key))) } export const signOut = async () => { try { await authClient.signOut() } catch (error) { console.warn(`[auth] Remote sign out failed: ${error.message}`) } await clearAuthStorage() await userActions.removeCurrentUser() Navigation.rootNavigation.popToRoot() bumpAuthStateRevision() await refreshSessionQueries() const dbPath = getDbPath() await FileSystem.deleteAsync(dbPath, { idempotent: true }) await expo.reloadAppAsync("User sign out") }

关键改进点(对应 apps/mobile/src/lib/auth.ts):

  • 本地清理前置于重载await clearAuthStorage()先完成,再执行后续 popToRoot、刷新查询、删除数据库、重载应用;
  • 远端失败不阻断authClient.signOut()包在try/catch中,仅告警不抛出,本地凭据必然被清理,避免“远端 4xx 导致本地登出卡住”;
  • 开发环境 key 一并清理__DEV__下额外删除带getEnvProfile()后缀的 cookie key(便于多环境调试切换);
  • 删除幂等化FileSystem.deleteAsync(dbPath, { idempotent: true })防止重复登出时因文件不存在而抛错。

配套的安全存储基础设施

apps/mobile/src/lib/secure-store.ts 同步新增了removeItemAsync与内部辅助deleteSecureStoreItem

const deleteSecureStoreItem = async (key: string) => { try { await SecureStore.deleteItemAsync(key) } catch (error) { if (isSecureStoreUnavailable(error)) { forceFallback = true warnFallback("removeItem", key, error) return } console.warn(`[auth-storage] SecureStore removeItem failed for ${key}: ...`) } }

该文件保留了forceFallback降级策略:当系统expo-secure-store不可用(例如设备不支持、权限异常)时,自动切换到普通Storage后端。removeItemAsync在降级场景下会同步先删Storage中的兜底值,再决定是否继续删除 SecureStore,保证两条路径都干净。

验证方法:开发环境(__DEV__)登出后检查 cookie/session key 是否全部消失;或在登出瞬间杀掉进程,确认重开应用不会恢复旧的登录态。

三、修复二:AI 摘要不可用时不再破坏摘要状态

问题现象

Folo 的 AI 摘要(AI summary)模块在服务端不可用、限流或返回空摘要时,会让摘要的“生成状态”停留在异常值,导致 UI 上摘要区域卡在生成中/错误态,或复用已失效的旧摘要数据。

根因:空数据被当作异常抛出

摘要同步服务位于共享 store 包:SummarySyncService.generateSummary(packages/internal/store/src/modules/summary/store.ts)。修复前的逻辑是:

.then((summary) => { if (!summary.data) { throw new FollowAPIError("AI summary limit exceeded. Please try again later.", 402) } // ... 写回 state.data、generatingStatus 等 return summary.data || "" })

也就是说:只要接口没有返回data,无论原因是“真的没生成出内容”还是“服务端限流”,都被统一抛出FollowAPIError(402)。抛错会走外层的.catch,把该条目的generatingStatus置为失败态,同时已生成的摘要状态(content / readabilitySummary 等)也没有被正确收敛。

修复:把“无内容”视为一次成功的空结果

提交174515b54将“返回空”从异常路径改为正常的终止状态

.then((summary) => { const generatedSummary = summary.data?.trim() ? summary.data : null if (!generatedSummary) { immerSet((state) => { state.generatingStatus[statusID] = SummaryGeneratingStatus.Success }) return null } immerSet((state) => { // 仅在确有摘要内容时写回 data / readabilitySummary / lastAccessed state.data[entryId][actionLanguage] = { summary: generatedSummary, ... } state.generatingStatus[statusID] = SummaryGeneratingStatus.Success }) return generatedSummary })

同时:

  • pendingPromises的类型从Promise<string>放宽为Promise<string | null>,允许“生成成功但无内容”的返回值;
  • 空的返回值不再触发FollowAPIErrorgeneratingStatus会置为SummaryGeneratingStatus.Success(终态),等待中的调用方拿到null后可自行决定 UI 表现(如隐藏摘要区),而不是让整条状态卡住或闪烁报错;
  • 真正来自 API 的错误(网络失败、超时等)仍走.catch置为失败态,两类情形得到区分。

同一提交还新增了 99 行单元测试:packages/internal/store/src/modules/summary/store.test.ts,覆盖“空摘要→状态收敛为 Success”“有内容→正确写回 data”“异常→失败态”等分支。该 store 位于packages/internal/store,移动端与桌面端共用,因此这条修复也同步惠及桌面端阅读体验。

关联阅读:v0.5.0 曾“Increased AI summary font size for better readability”,摘要 UI 的阅读优先级始终较高;本次则从数据层保证其状态机不会被不可用的 AI 服务打断。

四、修复三:列表末尾的最终条目与已读页脚边界

问题现象

Folo 在滚动条目列表时会触发“滚动即已读”(scroll mark-read)——滚出视口的条目自动标记已读。但接近列表末尾时存在两类问题:

  1. 最终条目滚不“过去”:最后几条内容被页脚/底部提示挤压,无法完整滚动出可视区;
  2. 已读页脚触发不稳:触底时的 mark-read footer(已读标记的边界指示)位置抖动或提前触发,导致最后若干条目未被标记已读。

共享算法层的边界修复

滚动已读的边界算法集中在共享包 packages/internal/shared/src/scroll-mark-read.ts,本版本由两个提交协同修复:

  • 2fa23362c fix: allow final entries to scroll past(允许最终条目滚过);
  • 425e8610a fix: stabilize scroll mark-read footer boundary(稳定页脚边界)。

算法层的关键改动包括引入末尾 padding 概念

export const MIN_SCROLL_MARK_READ_END_PADDING = 480 export const SCROLL_MARK_READ_END_INDICATOR_HEIGHT = 1 // 根据视口高度计算需要预留的末尾内边距,兜底为 480px export const getScrollMarkReadEndPadding = (viewportHeight) => { if (typeof viewportHeight !== "number" || !Number.isFinite(viewportHeight)) { return MIN_SCROLL_MARK_READ_END_PADDING } return Math.max(viewportHeight - SCROLL_MARK_READ_END_INDICATOR_HEIGHT, 0) } // 当列表有内容且没有下一页时,才渲染末尾 spacer export const shouldRenderScrollMarkReadEndSpacer = ({ entryCount, hasNextPage }) => entryCount > 0 && !hasNextPage

这里表达了修复思路的核心:“判断列表是否触底”不再只看内容是否到达容器底部,而是要考虑末尾 spacer/页脚的高度——只有为末尾内容预留足够滚动空间(视口高度或最少 480px),最后几条条目才能完整滚出并命中已读判定,已读页脚也才会在稳定位置出现。

同目录的测试 packages/internal/shared/src/scroll-mark-read.test.ts 在两个提交中各增加了数十行用例,分别验证“末尾 padding 计算”“无下一页时渲染 spacer”“最终条目可滚动越过边界”等场景。

渲染层同步适配

从提交统计看,桌面端渲染器的各列表形态(grid.tsxlist.tsxpicture-masonry.tsxEntryListFooter.tsxEntryListContent*系列)都针对新边界逻辑做了适配,并新增了EntryListEndScrollSpaceruseScrollMarkReadEndPaddinghook 等基础设施。移动端条目列表(apps/mobile/src/modules/entry-list/)与桌面端共享同一套scroll-mark-read算法与 store 层的已读批量写入,因此本次“列表末尾已读边界”修复对两端阅读列表同时生效——从仓库结构看,这也是 Folo 把滚动已读纯逻辑放入packages/internal/shared的主要收益:一个算法修复,桌面、移动、网页(ssr)的阅读列表共同受益。

五、如何在本仓库验证与复现

  • 单元测试回放:仓库使用 vitest 工作区(根目录 vitest.workspace.ts)。摘要状态机的新增用例位于 packages/internal/store/src/modules/summary/store.test.ts,滚动已读边界用例位于 packages/internal/shared/src/scroll-mark-read.test.ts,在对应包内执行 vitest 即可覆盖本次第二、三项修复的行为分支;
  • 源码比对:登出清理链路可在 apps/mobile/src/lib/auth.ts 与 apps/mobile/src/lib/secure-store.ts 中追踪signOut → clearAuthStorage → removeItemAsync → deleteSecureStoreItem的完整调用链;摘要在 packages/internal/store/src/modules/summary/store.ts 中查看SummaryGeneratingStatus的状态迁移;
  • 移动端手工验证
    1. 登出:连续执行“登出→重开应用”,确认不会恢复旧会话,也不会残留调试环境专属的 cookie key;
    2. AI 摘要:在 AI 服务不可用/返回空时打开条目摘要,确认摘要区收敛为“无内容”而非永久生成中或报错;
    3. 滚动已读:打开一个无后续分页的长列表,确认末尾最后若干条目可完整滚出并被标记已读,底部 footer 不再抖动。

六、小结与延伸阅读

v0.5.2 的三个修复分别体现了 Folo 移动端三个层面的工程质量取向:登出清理强调“本地永远能回到干净状态”(远端失败也不阻断);AI 摘要强调“外部能力不可用时应优雅收敛为终态而非污染状态机”;滚动已读强调“把跨端通用算法下沉到共享包,一处修复全端生效”。

如果想继续追踪后续演进,可对照本目录相邻版本的移动端 changelog(如 0.5.1、0.5.0),其中 cookie 会话刷新、SDK 55 升级等背景与本版本修复环环相扣;移动端发布元数据与文案的生成脚本位于 apps/mobile/scripts/apply-changelog.ts。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询