视觉按最新原型,业务按已确认PRD。现有代码为满足设计而改造。执行见按设计实施与任务卡;当前代码与验证范围见消费者功能页。
App 注销与缓存归属独立复审
本页目录
结论已验证的 owner 和清理边界owner 归属和播放A/B 清理和失败删除URI 清理范围已关闭的旧发现Settings 摘要隔离同 ID 记录隔离长许可期限 timer当前 RN 注销复审(旧 P2 已关闭)A/B 收据隔离已关闭收据删除失败已变为可恢复终态条件性边界 / 未作为当前 P2account_terminal 响应契约O00/O01 接入前边界未确认的条件,不作为本轮已证实 P1/P2任务卡边界测试和检查日期:2026-10-03
范围:juku-mobile 当前工作树。
方式:只读源码复审、隔离内存 store 复现、已有测试与 TypeScript 检查;没有改业务源码、删除设备文件、连接真实 API、运行 Android/iOS 真机或发布。
本仓库没有发现适用于 juku-mobile 的 AGENTS.md。复审覆盖 offlineOwnership.ts、store/downloads.ts、files.ts、queries/playback.ts、auth/ConsumerAuthGate.tsx、auth/client.ts、新增 auth/deletion.ts 与 screens/DeleteAccountScreen.tsx,以及 Downloads、Drama、LocalPlayer、My、Account、Settings 页面;为检查全局旧 cache 读取,补看了 SettingsScreen.tsx、App.tsx、viewer scope 和 progress journal。
结论
没有发现当前可由现有 UI/下载入口触发的 P1。owner-scoped 基础对“已存在且带 owner/authorization/access 的记录”已经形成实际播放和删除门禁;A 的清理只按 A 的 origin + userId 处理,失败删除会先锁定记录,不能继续本地播放。旧 ownerless 记录在迁移后保留为 legacy_locked,只有单独的旧缓存确认入口会尝试清理。
本轮确认了三项旧报告发现已关闭:Settings 摘要已按 owner 过滤;记录 hydration、add 和 removeLocalDownload 已按 owner/URI 隔离;LocalPlayer 的长过期 timer 会按剩余期限重新排程。联合夹具现在会等待 Go 子进程并要求退出码为 0;修复测试 fixture 的完整剧集元数据后,最新读取到的 Go 日志以 ok juku/internal/app 11.693s 结束,联合结果为 29/29 通过。此前 GET hongguoduanju.com/detail 的失败是修复前 fixture 运行历史,不是当前联合结论。
新增 RN 注销流程本轮没有发现当前 P1/P2。它复用官方 Better Auth 1.7.7 Expo 客户端和官方 $fetch,要求 OTP 后的新同 owner session,deleteUser 未知结果只 GET 原 operation status,不重复 POST;MMKV 收据不含邮箱、OTP、cookie 或 bearer token。上轮报告的两个注销恢复 P2 已由当前代码和新测试关闭:收据现在按 origin + userId 保留多条记录,清理还精确匹配 operationId;收据删除异常会变成可重试的 deletion_recovery_unavailable,不会丢掉终态记录或重发删除 POST。
另有一个需真实 Go+PG+Better Auth 联调确认的条件边界:account_terminal 从 session GET 返回时,当前 RN 会在本机清理前报错;现有夹具只覆盖 null/same/foreign。它不作为本轮已确认 P2 或新发布 gate。
已验证的 owner 和清理边界
owner 归属和播放
src/playback/offlineOwnership.ts:6-13只接受规范 origin 和非空 user id;sameDownloadOwner在:15-16同时比较 origin 与 user id。downloadAccessible在:23-24同时要求access === 'ready'、owner 完全相同,以及permitId/orderId/assetId/deviceId均为非空字符串、expiresAt晚于当前时间。这里是元数据门禁,不是设备证明或真实离线许可证验证;文件首行和src/screens/LocalPlayerScreen.tsx:23-26都明确保留了 O00/O01 边界。src/store/downloads.ts:59-72在持久化恢复时确认文件仍存在;没有 owner/authorization/access 的旧记录被改为legacy_locked,不会猜当前账号。useDramaDownloads在:138-144、useDownloadedIndexes在src/queries/playback.ts:214-221都要求 owner、authorization、有效期和文件存在。src/screens/LocalPlayerScreen.tsx:28-48对 route record 和切集 sibling 都再次检查当前 owner、授权有效期和文件存在;失效或换账号时active变空并在:89-98返回锁定/登录状态,未把 URI 交给VideoStage。src/screens/DramaScreen.tsx:87-112已缓存分集只导航到本地播放器;联网续播先读取 owner 绑定的状态并在:107再核对当前 viewer。下载按钮在:114-135先要求 linked identity;src/queries/playback.ts:124-134再以官方getConsumerSession读取会话,随后无条件抛出offline_authorization_required,没有创建文件、任务、扣账或把旧 viewer 下载 URL 当许可发行器。
A/B 清理和失败删除
clearOwnerDownloads在src/store/downloads.ts:159-168对空 owner 不做全局清空;先lockOwner,再只枚举sameDownloadOwner的记录,最后只丢弃同 owner 的任务。removeLocalDownload在src/store/downloads.ts:147-155先把记录标成cleanup_pending,deleteMediaFile失败就返回 false、保留记录和锁定状态;成功后才移除记录和任务。因而失败删除不能继续通过downloadAccessible播放。src/auth/ConsumerAuthGate.tsx:13-24把旧 owner 传给本机下载清理、progress 清理和查询清理。只有当前 viewer 没有 owner 或仍是传入 owner 时,:17-21才forgetViewer并移除旧 viewer 查询;延迟 A 清理在 B 已成为当前 viewer 时不会清掉 B bridge/query。- 单独隔离复现(不写设备、不改源码)以 A/B/ownerless 三条记录运行
clearOwnerDownloads(A),结果只删除 A,B 和 ownerless 保留;把文件删除替代为失败后,A 记录变为cleanup_pending,downloadAccessible返回 false,随后显式重试成功才移除。仓库已有tests/download-owner.test.cjs的对应检查也覆盖这些事实。
URI 清理范围
src/files.ts:63-74 的 deleteMediaFile 只接受 mediaDirectory().uri + '/' 下的单层 child:先做 root 前缀检查,再 decode 一次并拒绝空值、./..、斜杠、反斜杠、百分号和控制字符;删除后再次确认 file.exists 为 false。隔离测试覆盖了外部目录、普通 ..、编码 traversal、双重编码、删除未生效和文件已不存在的情况。清理不会按 arbitrary URI 删除媒体目录外的文件;旧 ownerless 文件只有用户明确点 legacy 清理时才进入该函数。
已关闭的旧发现
Settings 摘要隔离
src/screens/SettingsScreen.tsx:32-40,80-84 现在通过 downloadOwner 与 sameDownloadOwner 只汇总当前 owner;旧 SettingsScreen 也不是四栏当前用户直接注册的页面,src/navigation/RootNavigator.tsx:115-119,139 实际注册 ConsumerSettingsScreen。原“B 可看到 A/ownerless 数量与空间”P2 已关闭。
同 ID 记录隔离
src/store/downloads.ts:66-70 的 hydration 去重包含 owner/id/URI,:81 的 add 只替换同 owner 同 id,:147-155 的 removeLocalDownload 同时匹配 id、URI、owner。tests/download-owner.test.cjs:88-92 以 A/B 同 id 复跑,清理 A 后只剩 B。原重复 id P2 已关闭;未使用的原始 remove(id) 仍是全局 API,当前没有调用者。
长许可期限 timer
src/screens/LocalPlayerScreen.tsx:46-53 用 clockTick 作为 effect 依赖,超过 JavaScript 单次 timer 上限时会按剩余期限再次排程。原约 24.85 天条件 P2 已关闭;真实 O00/O01 许可时长和真机行为仍未验收。
当前 RN 注销复审(旧 P2 已关闭)
A/B 收据隔离已关闭
src/auth/deletion.ts:22-55 兼容旧的单对象值,同时把新值保存为按 origin + userId 分组的动作数组;save 保留 A 的 submitted/deleting/deleted 收据,clear(value) 还会精确匹配 origin + userId + operationId。src/screens/DeleteAccountScreen.tsx:51-58 在 B 登录或启动时只选择 B 自己的记录,匿名完成状态继续保留原收据。tests/deletion-api.test.cjs:62-70 和 tests/deletion-screen.test.cjs:60-70 实际复现了 A/B 切换、同 owner 新 operation 和不同 origin,证明 B 的 intent/cancel 不会删除 A。
这是对上轮“切换到 B 丢 A 收据”P2 的关闭证据;它不意味着真实系统已经有 native 许可证或后台删除 worker。readDeletionRecoverySession 仍只接受 null 或原 owner,foreign session 会在本机清理前拒绝。
收据删除失败已变为可恢复终态
src/auth/deletion.ts:46-55 对 storage.remove 抛错或静默未删除都会抛 ConsumerAuthError(503, 'deletion_recovery_unavailable'),并保留原记录。DeleteAccountScreen.tsx:31-34 显示存储不可用而不清 React/持久状态,:41-43 取消未提交动作时吞掉清理失败并保留收据,:76-88 在服务端 deleted 后先完成本机 owner 清理,收据删除失败仍可重进并通过 status GET 重试;:90-96 的冷启动检查和重试没有重新调用 deleteUser。tests/deletion-api.test.cjs:72-78、tests/deletion-screen.test.cjs:72-78 对抛错和静默失败均通过。
这是对上轮“MMKV 收据删除异常卡在终态”P2 的关闭证据。系统 MMKV/Keychain 的真实写入失败、进程被杀和设备挂起仍未在真机验证,属于未确认条件而不是当前已复现缺陷。
条件性边界 / 未作为当前 P2
account_terminal 响应契约
src/auth/client.ts:141-152 收到 account_terminal 会先尝试删除官方凭据,再抛错;src/auth/deletion.ts:83-86 的 readDeletionRecoverySession 没把它视为 null;DeleteAccountScreen.tsx:76-88 因而会在本机清理前停止。后端实现 services/platform/src/auth.mjs:92-102 确实允许 session GET 返回该错误,但当前 RN/API 夹具只验证 null/same/foreign,真实删除路径是否在该时点已经删掉 session 尚未由本轮移动端测试证明。最新联合夹具已对实际 SDK→Go→Better Auth→隔离 PostgreSQL 的收据、单次删除 POST、null session 以及 Web 进度/推荐/删除路径通过 29 个检查;该条件仍未单独覆盖,不作为当前 gate。
O00/O01 接入前边界
当前 src/queries/playback.ts:124-134 在官方会话检查后固定拒绝 offline_authorization_required,没有创建文件、task、扣账或把 viewer 下载 URL 当许可发行器。没有真实 issuer、device binding、native asset、可恢复队列或持久许可时,不能把 OfflineAuthorization 元数据当 license,也不能把 raw file:// 播放当 O00/O01 已完成。
未确认的条件,不作为本轮已证实 P1/P2
clearConsumerProgress(owner)在src/auth/ConsumerAuthGate.tsx:14-16只传 owner;ProgressJournal.clear在src/playback/consumerProgress.ts:37-40按 owner 清理,而队列条目还带 origin。当前设置页固定DEFAULT_BASE_URL,没有用户可切换多 origin 的路径;若以后同一 owner 在多个 origin 共用 journal,需要补 origin 作用域,否则清一个 origin 会清另一个 origin 的 progress queue。- Better Auth Expo managed storage 会在值超过约 1800 字节时产生 cookie chunk keys;
src/auth/client.ts:112-123只直接删除官方 base_cookie与_session_data键。当前夹具和常规 session cookie 未证明会产生 chunk,且不能用静态替代声称 SecureStore 已完成真机擦除;接入/部署验收需检查 chunk 清理和系统 Keychain/Keystore 失败。 usePlaybackLease在src/queries/playback.ts:147-211以dramaId而非 owner/session 计租,hasRunningDownload也只看 drama;同剧快速换账号或未来同时恢复队列时可能延迟释放旧在线 playback session。这是服务端资源生命周期风险,当前下载 mutation 不创建任务、没有离线许可泄露证据,不列为本轮已确认 P2。- 媒体记录中的
deviceId仅做非空元数据检查;没有真实设备证明、许可证签发、Media3/AVAssetDownloadURLSession bridge、可恢复队列或 native 持久许可。不能把OfflineAuthorization当作 license,也不能把 LocalPlayer 的 rawfile://播放当作 O00/O01 通过。 - 系统文件删除失败、SecureStore 读写失败、后台挂起导致 JS timer 延迟、symlink/原生 File URI 语义均未在 Android/iOS 真机验证。源码在失败删除时选择保留并锁定记录,属于安全失败;不能从替代测试推出生产设备行为。
任务卡边界
任务卡把 O00 定义为实际系统离线 asset/密钥下载桥接与飞行模式/强退恢复,把 O01 定义为登录 App 缓存、队列、账号/设备绑定、许可 preview/order/license、断点与换号锁定,把 A03 定义为注销时资料、历史、片单和本机密钥的生命周期。证据:dramivio-model-task-cards.md:36,48-49;工程方案要求 Media3/AVAssetDownloadURLSession 桥接并用真机验收:dramivio-engineering-plan.md:28,156-169,190-196。
当前代码没有 native offline bridge、真实 issuer、device proof、可恢复下载队列、原生持久许可或密钥擦除。OfflineAuthorization 的 permit/order/asset/device 字段只是当前记录的元数据校验,不能称作 license;LocalPlayer 的 raw file:// 播放也不能称 O00/O01 已通过。
测试和检查
- 本轮
npm run typecheck:通过,退出码 0。 - 本轮
npm test:203/203 通过。 - 本轮定向串行
node --test --test-concurrency=1 tests/deletion-api.test.cjs tests/deletion-screen.test.cjs tests/download-owner.test.cjs:33/33 通过;其中当前注销 API 11/11、注销 screen 12/12、owner cleanup 10/10。新增的 A/B 收据和收据删除失败检查包含在这 33 项中。 193/193、41/41和198/198是上轮历史运行数量,不是本轮独立复跑结果,不能与当前 203/203 或 33/33 混称。- 当前联合夹具证据:
consumer-joint-checks.json为passed: true且包含 29 项;consumer-joint-server.log以ok juku/internal/app 11.693s结束。退出处理在consumer-joint-fixture.mjs:12-16,164-171会停止 Go、等待退出并要求child.exitCode === 0,因此这次结果不是仅由浏览器端计数得出。上游禁止请求仍保留在ui_viewer_test.go:24-29;触发失败的 fixture 已在consumer_platform_test.go:317-326补齐与当前 app 同源的 title/tags/status/date 元数据,cover repair、playback plan 和 portrait MP4 继续由浏览器 fixture 在consumer-joint-fixture.mjs:28-30,68-72提供,不放宽真实 provider 请求。 - 当前联合范围是 Chrome + repository Go routes + Better Auth + 隔离 PostgreSQL,邮件仅本地捕获、无外部 SMTP;它证明了本次列出的 29 个路径,不替代 Android/iOS 编译、真机离线、系统凭据、真实发行器、native bridge、持久许可证或生产发布证据。O00 原生离线 PoC、O01 登录 App 缓存和 A03 注销/本机许可擦除仍是未完成里程碑;本报告只确认 owner/本机清理基础和 RN 控制流边界。