# 角色头像本地优先

角色与历史消息保存稳定 `file_asset://` 引用。MediaService 现在先读进程内缓存与既有 SQLite 头像缓存，再按上传回执查找本机原图；只有本地不存在或文件不可用时才解析资产并下载。缓存身份包含环境、账号和资产 ID，签名 URL 过期不会淘汰文件；既有容量淘汰规则仍适用。

同资产的磁盘查找、解析及下载共用一个在途请求。不同缺失资产在 8 毫秒窗口内合并，按最多 50 个 UID 调用既有文件查询接口。上传回执继续负责原图与资产 ID 的对应关系，角色删除后仍可供冻结消息读取。缓存写失败不阻断展示；查询失败清除在途状态以便重试；切换账号或销毁服务后不返回旧账号结果。

| 消费面 | 路径与范围 |
|---|---|
| UI | 消息和角色选择器继续通过 `image.read` 读取，继承持久缓存；没有新增设置或加载操作 |
| SDK | 现有 `readImage` 使用同一 Host owner，签名、返回数据和能力版本不变 |
| Tools | 现有 `arkme_image_read` 使用同一 `MediaService.readImage`，不新增工具或授权 |
| Host | 缓存属于既有图片读取能力；复用头像表、角色上传回执与文件查询接口，无新业务 endpoint 或表 |

回归覆盖 SQLite 关闭/重开后的离线读取、上传原图复用、删除角色后的历史头像、同资产合并、不同资产分批、查询失败重试、环境/账号隔离、账号切换和损坏缓存恢复。头像资产沿用 8 MiB 头像上限，调用者指定的更小上限仍生效。

## 本次验证记录

- Node 24：完整测试 9,646 项通过、15 项跳过；typecheck、build、打包清单检查通过。
- 不可变 tgz 通过官方 CLI 安装到全新临时 Profile；目标官方 DSH 为 0.1.5-rc.2 / fb2c4b9e，源码保持干净。
- 已安装包在真实 Host 中通过外部 SDK `readImage`、真实会话 `arkme_image_read` 读取预置持久 PNG；资产查询接口刻意不可用，查询次数为 0。
- 通过浏览器在实际插件的“发给自己 → 发言角色”打开选择器；头像元素成功解码为图片，刷新页面后同样成功，查询次数仍为 0。该验收使用隔离账号和固定图片，无生产数据写入。
- 测试在 macOS 完成，未替换常驻插件，也未声称完成 Windows/Linux 实机验收。

## 分支审核补充

- 已复现：磁盘读取挂起期间 dispose，旧任务恢复后仍发起资产查询；结果虽最终被拒绝，但查询发生得太早。
- 修复：读取开始时捕获生命周期代数，异步账号检查完成后再次比对；查资产、逐批查询、取得下载配额后都验证当前代数。dispose 不影响之后的新读取，旧任务不能继续启动网络请求。
- 新增磁盘读取期间重置、第一批查询期间重置后不再查询第二批的回归。扩大验证：9,649 项通过、15 项跳过；typecheck 和 build 通过。
- 同一资产仍共用在途读取，单个调用者取消不会中断其他读者；账号/环境隔离、离线缓存读取与上传原图复用保持通过。

本轮审核安装包复验：新 tgz 在隔离 Profile 的官方 Host、SDK 和会话 Tool 中读取持久 PNG 成功，资产查询 0 次；未修改 DSH 源码，未重跑浏览器视觉验收。


## 最终审核修复：本地探测与损坏图片

- 浏览器持久缓存未命中时，先使用独立配额调用 `image.read { imageRef, cacheOnly: true }`。只探测当前账号的 Host 内存、SQLite 和上传原图，不加入正在下载的请求，也不发起资产查询；未命中才进入原有下载队列。不同头像的两个慢下载不再挡住 Host 已缓存的第三张头像。
- Host 对持久缓存、上传原图和新下载内容执行实际图片解码，拒绝只有文件头的截断图片。继续使用现有 8 MiB 字节上限，解码首帧额外限制为 64 MP；损坏缓存按未命中处理，下载成功后覆盖，损坏下载不持久化。
- 浏览器 CacheStorage 的读写也执行解码校验；旧的合法 base64、非法图片内容会被删除并回源。不可变文件仍无时间过期，容量淘汰、清理站点数据和损坏内容均视为缓存未命中。
- 保持现有账号、环境、生命周期隔离和同图在途去重。无需数据库迁移、后端部署、新增配置或依赖升级。

| 消费面 | 新增合同 |
|---|---|
| UI | 浏览器缓存 → 独立 Host 本地探测 → 原有下载；默认头像、角色选择与刷新继续使用原有界面 |
| SDK | `features.imageCacheRead` 能力探测；公开 `readCachedImage`，本地未命中返回 `undefined`，不支持的 Host 明确报错 |
| Tools | 既有 `arkme_image_read` 增加可选 `cache_only`，沿用原 grant；未命中返回 `image-cache-miss`，不偷偷下载 |
| Host | `MediaService.readImage({ cacheOnly: true })` 是共同 owner，探测仅支持稳定 `file_asset://`，普通读取的默认行为不变 |


本轮验证：完整测试 9,672 项通过、15 项跳过；随后补充解码清理与 Tool 错误标识测试，相关 44 项通过，typecheck/build 通过。最终 tgz 通过官方 CLI 安装至全新 Profile，仓外 SDK consumer 编译和调用通过。未修改的 DSH 0.1.5-rc.2 / fb2c4b9e 中，SDK 和真实会话 Tool 均验证本地命中及未命中，损坏 SQLite 记录被拒绝，上游资产查询为 0。实际 UI 首次只发出一次本地探测，刷新后图片仍成功解码，Host 读取计数保持为 1；切换默认角色无旧图残留。macOS 已验收，Windows/Linux 原生解码路径仍需对应平台验收。


## 最后一轮分支审查：存储限制降级

确认并修复一处 P2 问题：受限浏览器上下文访问 `caches` getter 本身可同步抛出 `SecurityError`。原实现仅捕获异步存储错误，写缓存时的同步异常会拒绝已经取得 Host 图片的加载任务。现在缓存可用性探测也受异常保护，持久存储不可用时继续显示 Host 返回的图片。

证据：修复前回归失败、修复后相关 55 项通过；真实浏览器的 sandbox iframe 中，旧代码 getter 被拒绝且加载失败，新代码在相同限制下正常解码显示图片。新 tgz 经官方 CLI 安装到隔离 DSH Profile，Host/SDK/真实会话 Tool 验收通过，资产查询为 0；实际 UI 刷新前后 Host 读取总数保持为 1。最新 `dev`（b630034b）与本分支合并模拟无冲突，录音与主题样式改动没有覆盖头像生命周期或能力声明。


本轮 typecheck/build 通过。两轮全量分别为 9,675 项通过、1 项失败、15 项跳过，失败均在本分支未修改的 `recording-presence-writer.test.ts`：一轮在固定等待 5 ms 后读取尚未创建的 timer，另一轮在初次请求落盘尚未结束时注入下一次落盘失败，导致请求计数断言波动。该文件单独复测 15 项通过；不能将本轮全量写成全部通过。未为本次头像任务修改录音业务或其测试。
