Entity/Work 系统移除 —— 交接(2026-09)
状态(2026-09-26 第二轮):审查 + 设备测试共修出 4 个会导致崩溃或数据丢失的缺陷与若干数据正确性问题(§6)。 验证:
compileDebugKotlin/compileDebugAndroidTestKotlin0 错误;testDebugUnitTest全绿; 设备(M332BF, API 37):本次涉及的 androidTest 全部通过(迁移、DAO、快照、备份恢复共 70 项), 真实用户库副本迁移结果与预期逐项一致(§6.5)。所有改动尚未提交。 本文档是继续这条工作线的唯一权威交接,替代会话内上下文。
1. 目标与已达成的架构决策
用户目标:彻底移除 Entity Graph / Work ownership,回归 projection-first / content-first, 并保证已有版本数据、备份、Google Drive 同步的合理迁移。
冻结的决策(不要重新讨论):
| 决策 | 内容 |
|---|---|
| 一次性移除 | 不做分阶段弃用、不留多版本兼容层;org.skepsun.kototoro.entitygraph 与 org.skepsun.kototoro.work 整体删除 |
| 状态所有者 | 全部用户状态(收藏 / 历史 / 统计 / 偏好 / 追踪 / scrobbling)直接挂在持久化的 manga 行(manga_id)上 |
entityId 命名残留 | 读取模型(FavouriteCardRow.entityId、HistoryCardRow.entityId、TrackerReadRows.entityId、DetailsOrigin.EntityGraph、DetailsNavKey.entityId、SpaceRouteSnapshot.WorkDetails.entityId)保留字段名,语义退化为「锚点投影 manga_id」。重命名会牵动 ~40 个 UI/导航点与已持久化的 space session JSON,收益低风险高。DAO 里一律写成 manga_id AS entity_id |
DetailsOrigin.EntityGraph | 保留为「打开这个 manga 的详情」的导航 origin;DetailsViewModel 已按 initialProjectionLocalMangaId ?: preferredLocalMangaId ?: entityId 取值 |
projectionCount 语义 | 退化为 1(localMangaIds 只剩自身),所有 "×N 投影" 徽标自然不再出现;字段不动 |
| 旧备份 / Drive 兼容 | BackupSection 保留全部 legacy key(WORK_*、ENTITY_GRAPH_*、PROJECTIONS);v1/v2/v3 旧备份的 WORK_* 段读取后按 anchor_manga_id 落到 favourites / history / stats,ENTITY_GRAPH_* 段 drain 后忽略。备份的 wire 字段 owner_id / entity_id 保留(保证旧 JSON 可反序列化),但不再参与身份判定 |
| DB 迁移 | DATABASE_VERSION = 84,Migration83To84 → ProjectionOwnershipMigrationResolver.migrate(db),物理 DROP 全部 8 张 entity/work 表 |
2. 已完成并自查通过的工作
2.1 数据库层
MangaDatabase(v84)不再注册任何 entity/work 实体,新增只读 DAO:HistoryLibraryReadDao、FavouriteLibraryReadDao、TrackerReadDao。Migration83To84+ProjectionOwnershipMigrationResolver(788 行,纯逻辑可单测)已完成: work_favourites→favourites、work_history→history(含 chapter 归属/reading-session 反歧义)、 entity_preferences→preferences、work_stats→stats、tracks/track_logs/scrobblings/tracking_site_links 重建去掉owner_id/entity_id。- 本轮修复的 KSP 阻塞:
TagsDao、MangaDao(4 条查询)、ChaptersDao中残留的work_*/entity_*SQL 已全部改写为history/favourites/stats/preferences上的投影查询。KSP 现在能过(可进入 Kotlin 编译阶段)。 core/db/Tables.kt新增TABLE_STATS;TABLE_WORK_*/TABLE_ENTITY_*常量故意保留——迁移解析器仍需用它们引用旧表名。
2.2 Scrobbling 层(本轮完成,无残留)
ScrobblingOwnership.kt 已重写为纯投影语义(findScrobblingByManga / deleteScrobblingByManga / upsertScrobbling / upsertScrobblingForManga / upsertScrobblingPreview / rebindScrobblingToManga / observeByMangaCandidates / findByMangaCandidates)。
本轮清掉了 7 个 Repository + 7 个 Scrobbler 子类 + ScrobblingModule + ScrobblerConfigViewModel 的 workResolver 依赖、旧函数名 import、以及 ScrobblingEntity 构造参数(entityId 已是只读计算属性)。
2.3 其它已完成
core/BaseApp.kt:删除EntityGraphMigrationWorker/EntityNameCollisionRepairWorker入队、requiresWorkMigrationNormalization归一化分支、EntryPoint 里的workResolver()。backups/:FavouriteBackup/HistoryBackup/StatisticBackup删除 Work*Entity 次构造;TrackBackup/TrackLogBackup/ScrobblingBackup的toEntity()改为投影直构(wire 字段保留);AppBackupAgent去掉workResolver注入。sync/domain/SyncHelper.kt:getHistory/getFavourites改读投影表(含 tombstone:findAllEntriesIncludingDeleted()), upsert 走HistoryDao.upsertSync/FavouritesDao.upsert;删除 4 个 entity 解析辅助函数。sync/domain/SyncGcCoordinator.kt、favourites/ui/container/FavouritesContainerViewModel.performDeduplication()改为投影语义(去重 = 删除重复 manga 行,FK CASCADE 带走其状态)。core/parser/ContentDataRepository.kt:删除 entity-prefs 层(getEntityMetadataSourceSelection(s)、setEntityMetadataSourceSelection、setEntityPreferredLocalMangaId、getOverridesForWorkItems), 统一到preferences上的get/setMetadataSourceSelection(s);invalidation tracker 改为监听preferences/favourites。favourites/data/FavouritesDao.kt:补齐投影方法findActiveNewest/findActive(categoryId)/findAllActiveByMangaIds/countCategories/observeCountActive;FavouritesRepository已切换到这些名字。history/data/HistoryDao.kt:补isActiveimport、updateFromEntity()(原来update(entity)解析到了 8 参数重载)。tracking/discovery/domain/EntityType.kt删除——TrackingDiscoveryModels.kt里已有同名枚举(重复声明)。details/domain/RelatedContentUseCase.kt:去掉EntityGraphRepository与boundProjectionKeys(投影优先下永远为空)。details/domain/DetailsProjectionFilter.kt:isWorkContentTypeCompatibleWith→core/model/ContentTypeHeuristics.kt新增的isSameContentFamilyAs(原测试保留并迁移为core/model/ContentTypeFamilyTest.kt)。space/data/DefaultSpaceSessionValidator.kt:去掉WorkResolver,WorkDetails路由按「不破坏性丢弃」策略原样保留;测试已重写。space/ui/MediaUniverseViewModel.kt:mergeMediaUniverseItems改为按Content.id合并;测试已重写。tracking/discovery/domain/TitleSimilarity.kt:新增normalizeStrictTitleKey(value, sourceNames)与stripTrailingSourceTitleSuffix。search/domain/ContentSearchRepository.kt、suggestions/ui/SuggestionsViewModel.kt、tracker/ui/feed/FeedViewModel.kt(metadata selection 改按 mangaId)、home/ui/HomeViewModel.kt: 去掉WorkResolver,聚合退化为「每条内容自成一项」。defaultSpaceSessionValidator/MediaUniverse/ContentSearch相关单测已同步重写。- 物理删除(本轮执行,
git status可见 91 个 D):entitygraph/(20 文件)、work/(11 文件)、favourites/ui/migration/(15 文件)、favourites/work/(2 文件)、 6 个 dead use case、WorkFavourite*/WorkHistory*/WorkStats*Entity+DAO+RestoreMerge、favourites/data/FavouriteLibraryRepresentative.kt(无引用)、 以及 test 侧entitygraph/、work/、favourites/ui/migration/、6 个对应测试文件。
2.4 与 Antigravity 会话的关系
本轮接续的是 Antigravity 会话 3f16a746-6bdf-407a-b69d-1f31fcc3d7d6(因配额 429 中断)。 Antigravity 留下了大量半迁移、不编译的文件(会话内自述「已完成」的部分实际不可编译), 本轮已修复其中系统性的部分并完成物理删除;错误数从约 250 降到 147。
3. 如何继续(构建循环)
cd E:\kototoro_demo\Kototoro
# JAVA_HOME 必须指向 JDK 17+(当前 D:\Java\jdk17\jdk-17.0.16+8)
.\gradlew :app:compileDebugKotlin --console=plain 2>&1 | Select-String -Pattern "^e: "
# 收敛后:
.\gradlew :app:testDebugUnitTest --no-daemon
.\gradlew :app:compileDebugAndroidTestKotlin # androidTest 尚未处理,见 §5本轮使用的辅助脚本保留在 .dsh_tmp_antigravity/(未跟踪,请勿提交)。
注意(第二轮):此前构建反复报 JVM 内存不足,根因是 C 盘被占满(页面文件无法扩展)。已删除过时的 C:\Users\chuxi\.gradle\caches\build-cache-1(24GB;构建缓存已改到项目根 .gradle/build-cache), 并把 Gradle 用户目录迁到 E:\gradle-home(用户环境变量 GRADLE_USER_HOME)。 androidTest 需要设备:本机目前没有连接设备,也没有 AVD。
4. 编译收敛(2026-09-26 完成)
原 147 个错误已全部清掉,要点(便于回溯行为变化):
HistoryDao新增投影查询findRecent/findRecentForSpace/findRecentForSpaceAndSources(按manga.content_type IN allowedTypes过滤,与TracksDao/SuggestionDao同口径;content_type为 NULL 的行不进入空间)、observeCountActive、findActiveMangaIds,update(entity)公开。HistoryRepository.findRecentContents:NSFW 过滤后若不够数,按 32→64→… 扩大窗口(恢复旧 "resume 跳过整批成人内容" 行为)。TrackingRepository补回mergeWith(ContentTracking)(RESULT_EXTERNAL_MODIFICATION)、clearReadUpdates、clearUpdates、clearCounters、clearLogs、getLogsCount;owner 一律为manga_id。TracksDao.findByOwnerId、TrackLogsDao.findDuplicate(ownerId,…)旧重载、repairWorkIdentities、resolveTrackOwnerId已删除。TrackingLogItemMapper未读判断改为manga.id in unreadOwnerIds(旧逻辑用-mangaId,投影优先下永远不匹配)。DetailsViewModel:删 entity 残留;bindReadingCandidateToTracking=storeContentAndReturn后切换到该 manga; 关系分区buildEntityRelationSections()不再依赖 entityId(只看当前 tracking 详情)。- 收藏「疑似重复」提示(
FavoriteDuplicatePrompt/DuplicateFavoritePromptDialog)已无触发源,端到端删除;favourite_duplicate_*字符串经 Weblate 管理,未动(可后续清理)。MainShellScene的organizeMessageseffect 删除。 - 外部备份导入:
ExternalBulkImportPlanner.kt重建为仅含BulkImportEntry(按manga_id合并),无 entity 分配。 GoogleDriveSyncMerger.compactSnapshot:content 按ProjectionIdentityKeys.contentCompactKey(同 source + url/publicUrl) 合并,保留最小 id,并把 history/favourites/stats/tracks/logs 重定向到该 id 再做 LWW。 旧的镜像站 / http↔https / url↔publicUrl 交叉 / syncId 合并属于 entity 推断,有意不再支持。RestoreSemanticContext.isAuthoritativeSchema(原isAuthoritativeWorkSchema,同定义)供 WebDAV 自动恢复判断是否回传合并快照。MigrateUseCase:迁移时整行preferences跟随到新 manga(投影优先下偏好即用户状态)。- 清理欠账已做:
AppSettings的isEntityGraphMigrated/isLegacyFavouriteProjectionMigrationCompleted/isLegacyEntityNameCollisionRepairCompleted/requiresWorkMigrationNormalization(及 KEY)、FavouritesRepository/HistoryRepository的normalizeWork*/ensureLegacy*no-op、SuggestionsViewModel死代码。 - 单测:
HistoryRepositoryResumeFilterTest、FavouritesFeedCategoryIdsTest、GoogleDriveSyncMergerTest、KotatsuBackupPayloadCompatTest、ScrobblingInfoIdentityTest、TrackingLogItemMapperTest等已按投影语义重写。
5. 尚未处理的区域
- androidTest 已迁移(第二轮),但尚未编译验证、未在设备上跑过:
- 删除(测的是已删除概念):
WorkPagingDaoTest、SpaceWorkDaoTest、FavouriteLibraryAggregateChainCharacterizationTest。 - 重写为投影语义:
FavouriteLibrarySeed(共享 fixture)、FavouriteLibraryReadDaoTest、FavouriteLibrarySnapshotStoreTest、HistoryLibrarySnapshotStoreTest、HistoryLibraryReadDaoScaleTest(语料种子)、TracksDaoTest、TrackerReadDaoTest、FeedSnapshotStoreTest、UpdatesSnapshotStoreTest、RestoreCheckpointTest、AppBackupAgentTest(旧 Kotatsu 备份历史数 5→6)。 RestoreCheckpointTest新增两条旧备份用例:v3WORK_*段按 anchor 落到投影;MERGE 时较新墓碑不被复活。Migration83To84Test开启 dropped-table 校验,新增边界用例(索引存在、遗留新行 LWW、entity-only 行按绑定回落)。MangaDatabaseTest中 65→66 / 74→75 等历史迁移用例操作的是当时真实存在的 entity 表,保留不动。
- 删除(测的是已删除概念):
- 清理欠账(剩余,低优先级):
HistoryLibraryDeriver等处提到WorkHistoryDao的注释;toUiGroupId在HistoryLibrarySnapshotStore/UpdatesSnapshotStore仍按 entityId 生成 UI id(语义已退化为 manga_id,行为正确,只是命名残留)。 - 行为回归需要人工验证的点:
- 旧备份(含
work_*段)恢复后,收藏分类 / 历史进度 / 阅读统计是否全部落到正确 manga; - Google Drive 同步在 v2↔v3 之间的合并;
- 详情页「换源」(
selectActiveLocalSource) 在投影优先下应只切 manga,不再有 entity 偏好持久化; HistoryListViewModel/ feed 的分组卡片不再出现 "×N 投影" 徽标(预期行为)。
- 旧备份(含
6. 第二轮:迁移审查结论(2026-09-26)
6.1 修复的缺陷
| 严重度 | 位置 | 问题 | 修复 |
|---|---|---|---|
| P0 升级必崩 | ProjectionOwnershipMigrationResolver 重建 track_logs / tracking_site_links | v83 已有同名索引(index_track_logs_manga_id、index_tracking_site_links_manga_id、index_tracking_site_links_service_remote_id)。在 *_new 上 CREATE INDEX IF NOT EXISTS 被静默跳过,DROP TABLE 又带走旧索引 → Room 校验失败,所有 v83 用户首次启动崩溃 | 索引改为在 DROP + RENAME 之后创建 |
| P1 数据 | 同上 migrateHistory / migrateFavourites | v83 旧 history/favourites 表可能残留未规范化的更新数据(恢复后 normalization 未跑、或收藏未解析),被 work 行 INSERT OR REPLACE 无条件覆盖 | 先用旧表行做种子,再按 LWW 合并 |
| P2 数据 | 同上 tracks / scrobblings / links / logs | manga_id 无效时(links 直接要求 != 0)整行丢弃 | 统一 ownerMangaIdSql():自身 manga 存在则用之,否则回落到 entity 最优本地绑定 |
| P2 数据 | 同上 stats | 只取 work_stats,丢弃旧 stats 表 | 两表 UNION 后按 (manga_id, started_at) 取 MAX |
| P2 数据 | BackupRepository 恢复 HISTORY / WORK_HISTORY | HistoryDao.upsert 走“活跃写入”语义,强制 deleted_at = 0;旧备份的删除墓碑在 MERGE 时被复活 | 改用 upsertSync(保留墓碑) |
| P0 数据丢失 | BackupRepository.restoreBackup SNAPSHOT_REPLACE | 上一轮把 WORK_HISTORY/WORK_FAVOURITES/WORK_STATS 的清表映射到投影表后,它们(新备份里为空数组)在 HISTORY/FAVOURITES/STATS 恢复之后再次清表 → 用当前格式备份做快照恢复会丢光历史和收藏(设备测试发现) | 按 restoreTarget 每张表每次恢复只清一次;断点续传时已完成节的目标表视为已清 |
| P0 升级必崩 | 迁移 EPUB / 阅读会话归属查询 | 列名写错(epub_chapter_mapping 无 manga_id;end_time 应为 end_at),真实数据走到该分支即崩(设备测试发现) | 见 §6.5 |
| P2 行为 | BackupPayloadGuard | 恢复旧 v3 备份时因“WORK 状态引用的 entity 缺失 / sync_id 重复”拒绝整份恢复,而投影优先只需要 anchor | 删除 entity/sync_id 校验;保留 identity-only、缺失分类、缺失 anchor 三项 |
6.2 随之删除的死代码
BackupOrphanReviewActivity(及 Manifest 声明)、ActiveWorkStateMissingEntityException、BackupOrphanInfo/BackupOrphanReport、 BackupPayloadGuard.WorkEntityMissingSyncIdException、BackupService 的孤儿复核流程与 allowDiscardingActiveWorkState 参数链。 backup_orphan_*、backup_guard_work_entity_missing_sync_id、favourite_duplicate_* 字符串由 Weblate 管理,未删除。
6.3 验证基础设施的坑
app/schemas/被.gitignore忽略;本地83.json曾被重构中途的构建覆盖(与 84 完全相同),导致上述 P0 无法被Migration83To84Test发现(该测试在污染的 schema 上根本跑不通)。已从 HEAD(v83 代码)用 KSP 重新生成真实83.json覆盖回去。app/schemas/已纳入版本控制(移出app/.gitignore)。已有版本:32–37、39、40、42、68–73、76–80、82–84。 缺失 1–31、38、41、43–67、74、75、81:这些版本的构建产物从未保留,最早可追溯到 2020 年 Kotatsu 时期,按历史提交重建 需要当年的工具链,不现实。因此MangaDatabaseTest中从 1 开始的migrateAll与 65→66、74→75 用例在本地无法运行。
6.4 对整体思路的判断
- 投影优先 + 一次性迁移 + 保留读模型字段名 + 保留备份 wire 字段,这套取舍是合理的;风险集中在 DB 迁移 与 旧备份/同步兼容, 这两处都已有自动化用例覆盖,但必须在真机/模拟器上跑一次
connectedDebugAndroidTest(至少Migration83To84Test、RestoreCheckpointTest、MangaDatabaseTest)后才能发版。 - 有意放弃的能力(需产品确认):Drive 同步不再合并镜像站 / http↔https / url↔publicUrl 交叉的“同一作品”; 详情页不再有“多投影聚合卡片”;收藏不再提示“疑似重复”。
BackupSection仍在新备份里写出空的WORK_*/ENTITY_GRAPH_*段(恢复时 drain)。无害;若确认不需要降级兼容可改为不写。
6.5 设备验证(2026-09-26,M332BF / API 37)
- 运行方式:
installDebug+installDebugAndroidTest(adb install -r保留数据)后用am instrument -e class ..., 不要用connectedDebugAndroidTest(结束时会卸载 debug 包)。RestoreCheckpointTest/AppBackupAgentTest会对 注入的真实kototoro-db执行clearAllTables(),只能在无重要数据的设备上跑。 - 设备测试又抓到两处迁移 SQL 写错列名(均会在真实数据上让升级崩溃),已修复:
- EPUB 归属查询:
epub_chapter_mapping没有manga_id,列名为驼峰internalChapterId/parentChapterId; 改为经映射取父章节,再在chapters中找拥有它的候选 manga。 - 阅读会话归属查询:
reading_sessions列名是end_at而非end_time。 - 已用脚本将解析器 SQL 中所有标识符对照真实 v83/v84 schema 核验。
- EPUB 归属查询:
Migration83To84Test原 fixture 缺 v83 必填列(entity.name_hash、favourite_categories.deleted_at、唯一sync_id), 在真实 v83 schema 上从未跑通,已补齐。- 新增
RealDataMigrationTest(opt-in):把 v83 库副本以realdata-83.db放进 debug 包databases/即运行, 否则跳过;走生产迁移链 + Room 校验并在 logcatRealDataMigration输出行数。真实用户库副本结果: history 1416(有效 1284)、favourites 317(有效 309 / 258 部)、stats 5、preferences 8、tracks 200、track_logs 104、 外键违规 0——与按规则离线计算的期望完全一致。丢弃的 6 条 work_favourites 均为无绑定的删除墓碑。 - 真实用户库备份:
E:\kototoro_demo\debug-db-backup-20260926-1640\(含 WAL)。 MangaDatabaseTest中versions(历史遗留Migration24To23导致不连续)与需要 1/65/74 等旧 schema JSON 的用例 是既有失败,与本次改动无关;根因同 §6.3(schemas 目录未纳入版本控制)。
6.6 旧备份恢复与详情页来源面板(2026-09-26 第三轮)
- 用真实 v3 备份(
kototoro_20260914-1126.bk.zip)在全新 debug 安装上走引导页“恢复备份”:- 修复:
WORK_*恢复与 Drive v2 转换用anchor > 0判断,丢掉负 id(本地/导入漫画)与 id 为 0 的漫画 (该备份中 825 条历史、171 条收藏、全部统计)。改为“anchor 漫画存在才恢复”,同时消除 143 个悬空外键。 - 结果:历史 1298(有效 1285)、收藏 311(有效 309)、外键违规 0,与离线期望一致。
- 修复:
- 详情页来源面板改为投影优先语义(
feat(details): switch reading source by migrating to the new manga):- 元数据来源:功能不变(按 manga 存
preferences.metadata_source_*),只改文案。 - 阅读来源:只显示当前漫画自身来源(修复扩展未安装时的“不可用”);搜索结果“换源”=
MigrateUseCase迁移收藏/历史/偏好/追踪后跳转;无本地漫画的追踪条目详情页则作为首次绑定。 - 删除多投影残留:
activeLocalSourceOptions、EntityChapterSourceInfo、激活/删除投影菜单。 - 设备验证:KOMIIC → 拷贝漫画 迁移后进度 2/17 跟随,旧历史写入墓碑。
- 元数据来源:功能不变(按 manga 存
- 遗留(未处理,需产品决定):旧备份
ENTITY_GRAPH_PREFS中的元数据来源选择(该备份 10 条)恢复时被丢弃; DB 迁移(§6.1)会保留同类数据,两者不一致。
6.7 备份导入导出简化(2026-09-26 第四轮)
- 统一恢复入口:设置里只剩一个“恢复备份”。
BackupPayloadGuard.detectRestoreFormat读 index 自动识别: Kototoro 且 semantic ≥ 3 →KOTOTORO_CURRENT(默认替换);其余(Kotatsu、旧版 Kototoro)→KOTATSU_OR_LEGACY_KOTOTORO(默认合并,只允许库相关分段)。两种模式都可在对话框里切换。 没有 index 的文件才报UnexpectedBackupFormatException。“从其他应用导入”只保留 Mihon / Aniyomi / Venera。 - 删除旧版恢复后的同步写阻断:
isWorkMigrationSyncWriteBlocked、isBackupWebDavAutoUploadBlockedByLegacyRestore及“需要 Work 归一化”提示全部移除。 旧 WORK_* 在恢复时已直接落到anchor_manga_id,不再有“待归一化”的中间态。 ExternalBackupWorkState→ExternalBackupLibrary,外部导出直接按 favourites 行取分类, 删除FavouriteCategoryMembership。- 修复 Mihon/Aniyomi 导出分类串号(以前就存在):Mihon 按
order关联分类,而 Kototoro 的sort_key可以重复(实测分类 1、2 都是 1),同时属于两个分类的漫画会被distinct()合并, 导入后会落进错误的分类。现在由MihonBackupExportMapper.assignCategoryOrders按 (sortKey, id) 分配连续的唯一 order。 - 设备验证:用
device-backup的 work_* 数据构造出 Kotatsu 结构的legacy-test.bk.zip,清空后恢复, 被识别为兼容格式、默认合并、只列出 5 个分段;结果 history 1285、favourites 309、categories 25、 stats 5、FK 0,与源数据一致。Mihon 导出 29 部漫画、34 条分类引用(修复前是 33),可从“从其他应用导入”导回。 - 已知限制(未改):Kotatsu 导出只保留设备上已加载为
KotatsuParserSource的源;未装 Kotatsu 解析器插件时, 导出只有分类。Mihon 历史依赖章节 URL,没抓过章节的漫画不会导出历史。
6.8 详情页导航去实体化(2026-09-26 第五轮)
- 删除
DetailsOrigin.EntityGraph、AppRouter.openEntityDetails、ContentListHost.resolveEntityIdForUiItemId/resolvePreferredLocalMangaIdForUiItemId、列表的onNavigateToEntityDetails回调,以及两处恒返回LocalMangaContent的resolveDetailsOriginForContent。 - 依据:收藏 / 历史 / 更新卡片的 content id 本来就是
displayMangaId ?: anchor,推荐的 entityId 恒为 null, 所以 entity 路径最后打开的一直都是 content.id。现在所有入口统一走 content(或LocalMangaId)。 DetailsNavKey只剩mangaId。空间会话快照的持久化格式WorkDetails(entityId, requestedProjectionId)保持不变:写入时两个字段都填 mangaId,读出时取requestedProjectionId ?: entityId。- 同时删除的死代码:从未被调用的
EntityTrackingOrigin、合成的 "Entity Graph" 内容判断、EntityRelationItem.entityId(没有任何生产者),以及 EntityGraph 打开时的二次doLoad。 - 行为变化:从列表进入详情时也会刷新 related sections,与
LocalMangaId入口一致。 - 设备验证:收藏网格、收藏 spotlight(
LocalMangaId)、历史、首页历史行都能进入详情,返回正常,无崩溃。 - 仍保留(命名残留,低优先级):feed / 列表模型里的
entityId/preferredLocalMangaId字段,已不再参与导航。