尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Animeko(animation-garden)MediaSelector 重构全解析:从响应式竞速到纯函数决策核

Animeko(animation-garden)MediaSelector 重构全解析:从响应式竞速到纯函数决策核 Animekoanimation-gardenMediaSelector 重构全解析从响应式竞速到纯函数决策核【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden导读本文围绕docs/dev/media/media-selector-refactor.md这份重构方案定稿展开系统拆解 Animeko开源弹幕追番平台100% Kotlin/Compose Multiplatform资源选择器MediaSelector的一次大型架构重构它如何把「响应式管道 并发竞速」的决策逻辑收敛为「纯函数决策核 单一执行循环」如何把三条互不知晓的「上次播放」持久化通道统一为一条 per-subject 记录以及如何为 BT/WEB 双模式拆分铺路。读完本文你将理解 Animeko 中 128 条可测行为断言、四阶段决策管线过滤 → 排序 → 偏好 → 选择、MediaAutoSelector两段截止时间策略与 Room 迁移方案的完整设计并可结合仓库源码逐一核对实现。1. 重构背景一个 913 行选择器里的四类问题1.1 要解决的问题按优先级方案原文docs/dev/media/media-selector-refactor.md§0列出了三个层次的动机决策逻辑与响应式管道纠缠DefaultMediaSelector当时约 913 行把 flow 编排、三层偏好合并、DFS 选择算法、事件广播全部搅在一起select {}四 clause 竞速配合CompletableDeferred门控让「优先级」依赖并发完成时序而非结构。代码里已经存在三处为对抗自身架构而生的 workaroundFILT-05filterMediaList只能收到savedDefaultPreference全局默认用户按条目的偏好无法覆盖生肉过滤——原因是若依赖 merged 偏好会产生「mediaList → mediaPreferenceItem → newPreferences → mediaList」的循环依赖行为清单 FILT-05 定位在MediaSelector.kt:339-348PREF-04trySelectPreferredWebSource的「快照-判空-等待-重试」四步补偿专门对抗候选流 combine shareIn 异步派生带来的传播延迟竞态avoid-suspension 注释竞速模型里优先级由完成时刻决定任何额外挂起都可能让低优先 clause 反超。「上次播放」三条互不知晓的持久化通道per-subject 的 MediaPreference JSONDataStoreRoom 表preferred_web_media_sourceautoEnableLastSelected合并链。三者天然失步并阻碍 channel 级记忆功能。历史堆积死代码、KDoc 与实现不符、MediaPreferenceItem.available提取 bugITEM-04/ITEM-05等默认钉住现状修复单列待批。1.2 硬约束128 条行为一条都不能破坏重构的铁律是「不破坏任何一个现有功能细节」全部 128 条行为要么已有测试锁定要么在动代码前补齐Phase 0已知 bug / 涌现行为默认钉住现状characterization test // PINNED: ID注释任何修复进「故意行为变更清单」方案 §6逐条签字MediaSelector接口、事件流语义、公开方法签名、构造入口保持不变desktopTest mock 须继续编译。执行顺序被硬性规定为Phase 0测试补齐→ 方向 A决策核纯函数化→ 方向 B持久化统一→ Phase CBT/WEB 双模式拆分串行执行以保证每一步都有完整测试网。2. Phase 0先于一切重构的测试补齐方案 §1 指出128 条行为中约 75 条已覆盖53 条有缺口P0×17、P1×24、P2×16且三大盲区恰好都是本次重构的落点偏好持久化保存链完全无测试——onChangePreference载荷合成默认值渗入、debounce 1s、SaveMediaPreferenceExtension、Repository round-trip、Room dao播放失败换源决策fastSelectWebSources(overrideUserSelectiontrue)的唯一生产调用方仅黑名单一条路径被测决策核若干关键规则无测试removePreferencesUntilFirstCandidate、FIND-04「不为 4K 换语言」、FIND-05「字幕组未命中不降级语言」、FILT-05 workaround、EMG-01 BT 死局、enableCachingtrue生产路径零覆盖。P0 清单共 18 项含对抗验证后升级的 4 项 ★关键条目包括#ID一句话备注1SAVE-03debounce 1s 保存窗口内取消丢写钉住新建 MediaSelectorEventHandlersTest虚拟时间4SEL-05onChangePreference 载荷默认值渗入 per-subject 记录方向 B 最关键行为8FILT-05用户按条目偏好被过滤阶段忽略钉住 workaround断言需 exclusionReason DSL10ORCH-06clause④ 以 null 结束编排、取消容忍窗内的快速选择竞速终止语义13SAVE-06Repository round-trip 坏 JSON 回退真实 DataStore—15★ROOM-03Room dao 真实读写 FK CASCADE 语义升级自 P1FK 级联是破坏性语义17★新增跨会话真实存储闭环会话 A 手动 select → debounce 落库 → 重建 session → 会话 B 读回并驱动自动选择生产接线点CreateMediaFetchSelectBundleFlowUseCase.kt:136/MediaSelectorFactory.kt:80从未与真实仓库在测试中相连18★新增Room 迁移测试基建 迁移测试老用户三通道同时有值且互相矛盾→ 迁移后统一记录取谁仓库现有 21 个 AutoMigration、全仓零迁移测试2.1 黄金样本 harness纯函数化的主回归网方案设计了「黄金样本」§1.2只覆盖纯决策过滤排序偏好筛选自动查找不覆盖竞速/时序快照口径用公开 APIfilteredCandidates.first()、preferredCandidatesMedia.first()、trySelectDefault()返回的 mediaId不绑定旧实现。几个值得注意的设计决策种子按场景独立派生如hash(维度组合)不用单一Random(42)顺序流——否则任何维度增删都重排全部采样核心规则用显式枚举网格兜底FIND-04/05 跨维度权衡、FILT-11 归因顺序、SORT 全序随机采样只负责组合爆炸区legacyDeprecated测试框架在整个重构期间必须保持存活且全绿约 75 条已覆盖行为在它里面重生成仅 JVM-Dani.golden.regeneratetrue显式触发diff 逐条 review。2.2 落地状态方案状态栏显示 Phase 0 已基本完成app-data desktopTest 共 967 个测试全绿其中 Phase 0 新增 17 个测试类约 90 个测试7 个关键判别点经变异验证改坏生产代码→测试变红→还原→变绿包括 SAVE-02 早返回、SEL-02 selected 写入时机、Room 偏好通道切断、ERR-05 两个时长常数、FIND-10 匹配池来源、生产接线点 savedUserPreference。10 项测试基建exclusionReason DSL、collectEvents、cachingScope 注入、DataStore/Room 夹具、startInBackgroundfetchCount、room-testing 迁移基建全部就绪。3. 方向 A决策核纯函数化已落地3.1 目标分层方案的核心是把「决策」从「响应式管道」中剥离五层结构如下L4 编排层: AutoSelectDriver — 唯一的 collect 循环, flow触发器 L3 适配层: DefaultMediaSelector — MediaSelector 接口不变, 公开 flow快照投影 L2 快照组装: SelectorSnapshot 组装子 — 唯一 shareIn 点, 每次 emission 内同步算全套派生数据 L1 纯决策核 (无 flow、无 suspend、无副作用): MediaSelectorFilterSortAlgorithm (已是纯函数, 一行不动) MediaSelectionDecider (DFS 查找 两阶段 缓存 松弛计划) AutoSelectPolicy (显式优先级规则序列, 替代 select{} 竞速) SelectionExecutor CommitMode (副作用与决策分离)新代码放在app-data/.../domain/media/selector/engine/internal不进稳定 API 面。3.2 核心数据类型一次内部一致的世界视图方案定义了输入快照SourceResultSnapshot/SelectorSnapshot其关键不变式是merged、filtered、preferred、availables 在同一次 emission 内同步纯计算——不存在「filtered 已更新而 preferred 未更新」或「state 是 Succeed 而候选流还没吸收结果」的中间态。在仓库中这一设计已落地为MediaAutoSelectSnapshot.ktdata class MediaAutoSelectSnapshot( val sources: ListMediaSourceSelectionSnapshot, val candidates: ListMaybeExcludedMedia.Included, val preferred: ListMaybeExcludedMedia.Included, val preference: MediaPreference, val settings: MediaSelectorSettings, val context: MediaSelectorContext, ) { internal val availableAlliances get() candidates.map { it.result.properties.alliance }.distinct().sortedBy { it } }方案中特别标注了一个 blockerV-A1并被实现采纳per-source 快照必须用combine(state, results)保持对 results 的单一稳定订阅禁止state.flatMapLatest { results.map {...} }——否则 state 在 Idle→Working 转换时会对 resultsshareIn WhileSubscribed(0ms)做取消-重订阅订阅数瞬间 1→0→1STOP 竞态赢时取消进行中的查询源会被打成 Abandoned 且按 FETCH-03 永久不重试若该源恰是偏好 Web 源还会连带删除用户偏好。对应实现见MediaAutoSelector.kt:114-132private fun sourceSnapshots(session: MediaFetchSession): FlowListMediaSourceSelectionSnapshot { if (session.mediaSourceResults.isEmpty()) return flowOf(emptyList()) return combine(session.mediaSourceResults.map { source - // Keep a stable subscription to results: these subscriptions also drive lazy source queries. combine(source.state, source.results) { state, results - val currentResults if (state.isFinal) source.results.first() else results ... } }) { it.toList() } }一致性论证在 combine 下依然成立state 置 Succeed 发生在 results 上游正常完结的 onCompletionMediaFetcher.kt:240-247此刻 replay1 缓存已含最终列表combine 的下一次 emission 必然是 (Succeed, 最终列表)。由此MISC-03 从「隐式依赖」变成「结构保证」编排在跑查询就在跑。3.3 纯决策核MediaSelectionDecider方案要求findUsingPreferenceFromCandidates旧MediaSelector.kt:545-715逐字迁入纯函数findByPreference(candidates, merged, availableAlliances, mediaSourcePrecedence, shouldPreferSeasons, preferKind)两处 suspend 读取提升为参数。仓库中MediaSelectionDecider.kt正是这一产物DFS 的全部特殊规则逐字保留嵌套顺序为 分辨率 字幕语言 字幕组 数据源注释// 选择顺序 1. 分辨率 2. 字幕语言 3. 字幕组 4. 数据源不为 4K 换语言FIND-04某分辨率层若存在资源但没有任何想要的字幕语言则跳过该分辨率换下一个宁选 1080P 简中不选 4K 生肉字幕组未命中不降级语言FIND-05某语言层所有偏好字幕组都无匹配时不换语言而是在同语言集合上再按 mediaSources 顺序选一轮仅当该语言完全无资源才换下一语言WEB similarity80 前置#1521preferKindWEB时先在相似度 80 的 WEB 子集内跑一轮防止快速选择选中高优先数据源里的错误资源季度分支shouldPreferSeasonssubjectFinishedtrue preferSeasons时先在hasSeason()true子集跑 DFS未果再全量selectAny 兜底照抄钉住FIND-08episodeRange?.hasSeason() null的谓词疑似手误应为true但已被测试do not prefer season if not matched锁定方案要求原样保留。3.4 编排MediaAutoSelector 的单一执行循环方案 §2.3 设计的显式优先级规则序列R1 PreferredWeb → R2 FastSelect → R3 Cache → R4 Fallback在仓库中落地为MediaAutoSelector.kt的select()执行循环combine(snapshotFlow, stage, selected)作为唯一触发器decide(snapshot, config, stage)是纯决策决策结果只有四种Wait / StartFallback / Exhausted / SelectCommit 走mediaSelector.selectAutomatically(decision.media, expectedSelection)。值得注意的当前实现细节对应方案中 Web 自动选择部分本地缓存最高优先selectCache默认开启preferred或candidates中任一 LocalCache 立即返回Decision.Select(local cache)缓存未终态时返回Decision.Wait期间不会选 Web/BT、不启动两段计时记忆 Web 源优先且阻塞preferredSourceId对应的源isFinal且 context 加载完成时在preferred中按该源过滤后findWebCandidate命中即提交两段截止时间Web.exactMatchAfter 5s、fuzzyMatchAfter 15s由launch { delay(...); stage.value ... }推进 StagePreferredSource → Instant → Exact → FuzzyInstant 只允许「有效 tier 0 的精确匹配」Exact 放开 tierFuzzy 允许模糊匹配精确匹配用SubjectMatchKind.EXACT而非相似度阈值findWebCandidate对subjectMatchKind EXACT的候选按「匹配等级 × 有效 tier」分组只在最优组内应用偏好relaxtrue时四项偏好全放开为ANY_FILTER对应 TRY-03 两阶段语义播放失败换源web.fastSelect与截止时间均取 1 秒exactMatchAfter fuzzyMatchAfter 1sfallbackToOtherKindsfalse保持 WEB-only提交即 CASselectAutomatically(candidate, expectedSelection)先检查selected.value ! expectedSelection || candidate expectedSelection立即返回 null随后 emitonBeforeSelect→compareAndSet(expectedSelection, candidate)→ emitonSelect全程不写偏好对应 SAVE-01「自动选择不写偏好」与 SEL-07 CAS 语义。这一执行循环同时解释了方案开头的两段提示2026-09-06 更新旧的「Web 四分支竞速、超时放开模糊匹配、ORCH-06 与 MIG-DUAL-02」已是历史基线WEB 与 BT 均不再使用分支协程竞速收拢为MediaAutoSelector单一执行循环MediaSelectorAutoSelect及.autoSelect已删除播放与换源统一调用MediaAutoSelector.select下载缓存调用方直接session.awaitCompletion()后调用trySelectDefault()。3.5 三处竞态 workaround 的消亡方案 §2.4 论证了三个 workaround 如何被构造性消灭FILT-05所谓循环是 flow 对象图的伪循环available 引用 filteredCandidatesMedia不是语义循环——finalSelected合并链完全不依赖 media list。组装子内按 DAG 分阶段同步计算merged不需要 media list→ filterMediaList仍用 savedDefault钉住现状→ availables → filterByPreferencePREF-04快照内 (stateSucceed) 与 (结果已在 filtered 中) 必然同时成立「结果还没传播过来」的第三种状态不存在四步补偿全部删除avoid-suspension 注释新模型优先级是结构性的R1R2R3R4 固定求值「多挂起一拍输掉竞争」在模型层面不可能发生。3.6 微观时序差异决策结果不变owner 知情重构也如实列出了少量决策结果不变、但时序有差异的点§2.5FAST-01 终态后停止跟踪改为持续跟踪6 处独立cached()收敛为单点 shareInselect{} 同刻 clause 偏置改为固定求值顺序R4 重评触发面扩大会话中改 preferKind 会立即触发兜底availableAlliances 从独立 cached 流改为同快照派生。4. 方向 B统一 last-played 持久化4.1 现状三条互不知晓的通道方案 §0 与行为清单共同确认了现状的三条「上次播放」通道per-subject MediaPreference JSONEpisodePreferencesRepositoryImplEpisodePreferencesRepository.kt:44-73以stringPreferencesKey(subjectId.toString())为 key将整份MediaPreference序列化为 JSON 写入 DataStore读侧mediaPreferenceFlow(subjectId)在无记录、空白串或反序列化失败时一律回退全局默认defaultMediaPreferenceSAVE-06不抛错、不清理坏数据Room 表preferred_web_media_sourcePreferredWebMediaSourceDao以 subjectId 为主键ObserveWebMediaSourcePreferenceExtension处理PreferWebSourceEvent时先读现值、不同才写入失败自动删除ROOM-02偏好源 state 为 Failed 或 Abandoned 时删除记录且该表对SubjectCollectionEntity有FK(CASCADE)ROOM-03取消收藏会级联删除 Web 源偏好未收藏的 subjectId upsert 会抛外键异常autoEnableLastSelected 合并链mediaSourceId.finalSelected合并顺序为「会话内覆盖 MediaPreference.mediaSourceIdDataStore JSON 全局默认」该通道还负责在会话启动时enable()上次使用的源#355。4.2 新记录media_selector_last_selection方向 B 的目标是「三条通道 → 一条显式 per-subject 记录」。方案设计了一张新表schema 21→22AutoMigration先例playback_history_recordCREATE TABLE media_selector_last_selection ( subjectId INTEGER NOT NULL, sourceKind TEXT NOT NULL, -- WEB|BitTorrent|LocalCache|UNKNOWN(backfill 时源未安装) mediaSourceId TEXT, -- 上次手动选择的源实例 id alliance TEXT, -- WEB: channel 名; BT: 字幕组名 (行内语义由 sourceKind 决定) resolution TEXT, subtitleLanguageId TEXT, webSourcePreferred INTEGER NOT NULL DEFAULT 1, -- 仅 WEB 行有意义; 失败即删改为置 0 selectionOrigin TEXT NOT NULL DEFAULT MANUAL, -- D1 预留 updatedAtMillis INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(subjectId, sourceKind) );两个关键设计决策复合主键(subjectId, sourceKind)按 kind 分行D112026-08-02 定单行 PK 即使在不拆模式的世界里也有两个跨 kind 互踩——(a) 手动选 BT 会把行覆盖成sourceKindBitTorrentclause①/R1 读kindWEB永落空等于 BT 选择清掉了 WEB 的 preferred-source 记忆(b) alliance 列 WEB 写 channel 名、BT 写字幕组名单行互相污染过滤。分行后二者天然消失无 FK 无级联消灭「未收藏写入抛 SQLiteConstraintException」C5与「收藏 REFRESH 级联清偏好」C4两个隐蔽语义取消收藏等显式删除按 subjectId 删全部 kind 行。写入时机保持现状仅手动路径——selectImpl(updatePreferencetrue)从 candidate 派生chip prefer/removePreference 单列部分更新所有自动选择路径不写SAVE-01。且 writer 禁止无条件整行 upsertV-B1 blockersubtitleLanguageId列仅当 candidate 字幕语言singleOrNull()非 null 时更新否则保留原列值——否则手动选多语言/零语言资源BT 常态会把已存字幕偏好清成 NULL破坏 SEL-03。4.3 迁移策略eager backfill 兼容期双写 读侧分步切换迁移分四步走§3.2Room migration22 版 AutoMigration纯加表空 spec提交22.json应用层 backfillAutoMigrationSpec 拿不到 DataStore故放应用层启动后后台一次经 DI 取 store绝不拼路径——三端文件名不同遍历 per-subject key 联表 Room 行按合并矩阵合并旧数据原样保留到 S6合并矩阵关键分支V-B2 修订后按 kind 分行JSON 通道①Room 通道②新表行按 kind 分行无/空白/坏 JSON无不建行有无一行落在 JSON.mediaSourceId 实例 kind 的行未安装→UNKNOWN 行无/坏有一行 (subjectId,WEB)mediaSourceIdRoom 行flag1其余 NULL有, JSON 源 kindWEB 且 Room 行有一行 WEB四维按 JSONflag1有且 JSON.mediaSourceId 为 null有兼容态非分歧一行 WEBmediaSourceId 取 Room其余维度按 JSON有, JSON 源是 BT/非 WEB kind有两行共存BT(UNKNOWN)行按 JSONWEB 行按 Room有, JSON 源 kindWEB 但 ≠ Room 行有唯一真冲突JSON 胜出落 WEB 行Room 行丢弃C1幂等合并V-B4 修订已存在的行按列合并仅填 NULL 列不整行跳过——backfill 是后台任务用户可能在其完成前就 select/点 chip 创建部分行整行跳过会让该 subject 的历史偏好永不迁入。双写期还有一个易被忽略的坑V-B3S3 双写必须镜像「删」——旧删除路径失败即删、取消收藏级联、收藏 REFRESH deleteAll 级联只作用于旧表的话S4b 切读后已删偏好会成批「复活」。规定失败即删镜像为markPreferredWebSourceFailedflag0级联/显式删除镜像为对应新表语义。4.4 旧语义映射要点方向 B 带来的故意行为变更§6包括C2 保存从「尽力而为」变「必达」最终态 S5 改为选择提交点的显式挂起调用SAVE-02/SAVE-03/EVT-02 三个丢失窗口消失C4 取消收藏不再清记录用户可感知取消再收藏后 clause① 仍等上次的源C5 不再抛 FK 异常C6 默认值不再渗入 per-subject 记录只写显式来源的值未设维度保持 NULL全局默认后续变更即时生效。而autoEnableLastSelected/ clause① 快照语义 / SEL-03 字段集则原封不动只是数据源换成新表投影。5. Phase CBT/WEB 双模式拆分方向已批5.1 依据现状已是两个算法被编织在一起专项分析3 调查 1 对抗审视得出三个结论kind 分叉量化41 个行为单元中真正 kind 无关仅约 27%编排层今天就按 kind 分叉clause①② 纯 WEB、③ 纯 LocalCache、④ 含 kind 条件播放失败换源整条 WEB-onlyBT 用户没有该功能9 处 WEB-only 逻辑#1521 similarity80 等被迫写成通用代码里的条件分支WEB media 属性近常量PROP-01Selector 源 resolution 恒等于源配置常量网页不解析、字幕语言兜底恒 CHS生肉过滤对 Selector/Jellyfin/Emby 结构性不可能触发、publishedTime 恒 0、alliancechannel 名且主路径已用 ANY_FILTER 绕开——保留三维偏好对 WEB 只剩污染通道BT 4K/CHT/字幕组写入偏好后灭掉 WEB 候选channel 名跨源误命中「硬拆失去 WEB→BT 软兜底」不成立现状兜底是残缺的时序彩票EMG-02——preferKindWEB 时 clause④ 只等 WEB kind 完成Failed 也算完成「WEB 全灭」恰是完成条件最快成立的时刻BT 结果通常未到trySelectDefault返回 null 即永久终结编排且选过 WEB 后 channel 名写入 allianceBT 候选早被硬过滤。显式 R5 是行为改进而非拆分代价。5.2 选定形态L2模式二态 策略表裁剪 数据分行 UI 三 tab方案否决了 L3「完全两个 selector」LocalCache 四处特权要复制两份、缓存页矛盾尖锐、无受益方选定 L2设置层模式二态WEB/BT取代 preferKind 三态自动选择按设置模式走null→WEBD8 已定连带设置页删除「无偏好」选项、iOS 隐藏 BT 模式无 BT 源SESS-02、PikPak 弹窗从「改偏好」升格为「切整套算法」策略表按模式裁剪建立在方向 A 的 R1–R4 结构上落地成本≈裁剪一张表WEB 模式 R1上次 Web 源 R2fastSelect R3缓存 R4_WEBtier 上次 (源, channel) similarity跳过 resolution/字幕语言/字幕组三维R5 跨模式兜底WEB 穷尽 → 等 allCompleted → 在 BT 候选上跑 BT 侧算法iOS 恒空退化BT 模式 R3 R4_BT老四维 DFS 逐字保留 R5对称BT 穷尽 → WEB 候选任一模式不得裁掉 R4查询驱动方MISC-03数据层按 kind 分行§3.1 复合主键已折入方向 BWEB 行记 (sourceId, channel)BT 行记四维偏好互不污染UI 三 tabD9/D10 已定简单模式WEB/ 详细模式仅 WEB 源/ BT单独展示 BT 源结果。tab 是纯展示选项切 tab 不影响正在播放的内容、不改设置模式BT tab 手动选择是一等操作写 BT 行记忆、模式不变经 R5 或用户切设置模式后生效两条共享边界定调LocalCache 是两模式共享的前置特权层过滤/偏好豁免、置顶、独立竞速全部 kind 无关拆分不得触碰缓存页不属于模式模式只管播放场景的自动选择缓存页保持全量双 kind、手动为主。同时记录了两处必须列入变更清单的用户可见变化C15、C16现「详细模式」是全 kind 混排改为仅 WEBBT 模式需要自己的 AutoSelecting banner现状 banner 只支持 WEB 源图标BT 自动选择运行期恒显示「需手动选择」MediaSelectorSummaryStateProducer.kt:56有 FIXME。5.3 排期硬约束Phase C 必须在 Phase 0 → A → B 全部完成后作为独立阶段执行提前或与 A 并行会作废 Phase 0 基线口径与 Step 6 双实现对拍。方案同时明确Phase C 启动前需要一轮细化设计R5 精确语义、三 tab 交互稿、C12–C16 逐条复核该细化不在方案文档范围内。6. 故意行为变更清单与 Owner 决策点6.1 方向 B 附带变更批准方案 批准这些C1 失步态消失D11 后大部分退化为两行共存/ C2 保存必达 / C3 EVT-02 消失 / C4 取消收藏不清记录 / C5 不再抛 FK 异常 / C6 默认值不渗入 / C7 坏 JSON 路径消失 / C8 兼容期后降级丢 last-played / C9 迁移裁决 / C10 旧表旧 API 删除。6.2 Phase C 附带变更C12 preferKind 三态→二态模式null/「无偏好」删除并迁 WEBD8/ C13 WEB 模式移除三维偏好参与跨 kind/跨源污染就此消灭/ C14 生肉过滤降为 BT-only对主流 WEB 源零实际影响属性上不可能触发/ C15 「详细模式」tab 从全 kind 混排改为仅 WEB / C16 新增 R5/R5 跨模式兜底。6.3 独立修复候选可单独批其中用户可感知的重点项#项建议用户可感知?F7FIND-08selectAnyhasSeason()null疑手误应为 true?若批准改兜底层真正优先季度全集是完结番空偏好的兜底选择变化F10EMG-01BT 偏好字幕组消失致自动选择死局移交方向 B 后评估为 BT 补偿放宽 alliance是F12★BT resolution displayName/id 失配RES-01BT media 存 displayName4K/2K偏好 fallback 列表用 id2160P/1440P裸字符串比较永不相等Phase C 的 BT 模式细化设计时一并定夺是F3/F4/F5/F6死代码/KDoc 不符清理建议批零风险否6.4 决策点汇总已定2026-08-02D8preferKindnull → WEB、D9UI 三 tab、D10BT tab 手动选择写 BT 行记忆、模式不变、D11复合主键 UNKNOWN sentinel。待批D1自动选择是否回写 last-selection默认关selectionOrigin列已预留、D2backfill flag 取值保守 0、D3失败即删→置 flag、D4双写窗口 ≥1 个 minor release、D5旧表删除时机、D6F1–F12 逐项批复、D7FILT-05 是否让用户按条目偏好覆盖生肉过滤本次不改列观察。7. 对抗验证记录两轮独立验证如何折入正文方案 §8 记录了两轮独立验证全部结论经源码逐行核实后才采纳破坏性猎手对两份设计逐条过 128 条行为清单找「会被无声破坏」项2 blockerV-A1 snapshotFlow 自取消竞态、V-B1 整行 upsert 清字幕偏好 7 majorV-A2 autoEnable 取消域、V-A3 迷你 policy 门控/终止、V-A4 缓存链路完成条件死锁、V-A5 R4 双分支、V-A6 载荷即时重算、V-B2 合并矩阵漏行、V-B3 双写不镜像删 4 minor测试计划批评家2 blockerRoom 迁移测试整类遗漏、MISC-03 假安全网 4 major跨会话闭环缺失、FIND-10 测试入口错误、ITEM-02/ROOM-03 应升 P0、黄金样本种子/口径/legacy 保活 3 minor第三轮BT/WEB 拆分专项3 调查 1 对抗审视产出 L2 形态推荐与 L3 否决、R5 兜底论证「现状软兜底是残缺时序彩票」的动态时序分析、D11 复合主键的独立佐证、F12 新 bugBT resolution displayName/id 失配、null 迁移三案评估。这些验证发现的 blocker 与 major全部已折入正文文中 ⚠ 标记处而非停留在验证记录里。8. 与当前源码的对应关系读者导航重构方案与其姊妹文档行为清单128 条可测行为断言中的 IDFILT-01、ORCH-06、FIND-04 等可直接在以下源码文件中核对主题源码位置过滤与排序纯函数MediaSelectorFilterSortAlgorithm.ktDFS 纯决策核MediaSelectionDecider.kt单一执行循环两段截止时间MediaAutoSelector.kt决策快照类型MediaAutoSelectSnapshot.ktMediaSelector 接口与 selectAutomaticallyMediaSelector.kt偏好三态模型OptionalPreference.ktdebounce 1s 保存链MediaSelectorEventHandlers.ktDataStore 偏好仓库JSON 通道EpisodePreferencesRepository.kt当前规则的权威描述media-selector.md需要说明的是方案正文中的部分行号如MediaSelector.kt:545-715与部分旧类型MediaSelectorAutoSelect标注为「历史基线」因为重构已推进——公开方法签名保持不变的约束让trySelectDefault/trySelectCached/trySelectFromMediaSources依旧存在但自动选择入口已收拢为MediaAutoSelector.select。读者在对照阅读时应以「行为 ID」为锚点而非行号行为语义在当前实现中依然成立例如 FIND-04/FIND-05 的跨维度权衡在MediaSelectionDecider.selectImpl中逐字可见。9. 总结这套方案的可复用方法论抛开 Animeko 的具体业务这份重构方案本身提供了三条可迁移的方法论重构前先建「不可破坏行为基线」把现状全部行为含已知 bug、workaround、涌现行为编号成可测断言清单未知行为先钉住characterization test修复走「故意变更清单」逐条签字——这保证了 128 条行为在 913 行代码被重写后一条不丢决策与执行分离把最难测的并发选择逻辑抽成「无 flow、无 suspend、无副作用」的纯函数让测试不再依赖虚拟时间与竞速时序「等待条件全部建模为可测状态」deadline token 直接放进 fired 集合零真实时间破坏性语义必须先有测试基线再动Room FK 级联、跨会话存储闭环、迁移裁决这类破坏性/数据级行为全部在 Phase 0 升级为 P0 测试再用「双写 读侧分步切换 双实现对拍」完成无感迁移。对于任何「老代码不敢动」的长期项目这套「行为清单 → 测试补齐 → 纯函数化 → 存储收敛 → 模式拆分」的推进顺序与两轮对抗验证流程都值得作为模板复用。【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表