HarmonyOS 7 PhotoAccessHelper + Core Vision Kit:相册权限变更后的失效资产剔除与增量重索引【鸿蒙心迹】
文搜图 Demo 跑久以后,我遇到过一个很容易被误判成“模型搜索不准”的问题:用户最初允许应用访问 62 张照片,索引也正常建立;后来把相册权限改成“部分照片”,应用能访问的资产只剩 54 张,但 Core Vision Kit 的本地索引里还残留着原来的路径。此时再搜“雪山 木屋”,结果列表里会混进已经不可访问的图片。
这不是搜索算法问题,而是媒体访问集合变化以后,业务索引没有跟着收敛。
这次我把 Demo 收缩成 PhotoScopeRepairLab,只做一件事:重新获取可访问资产快照,找出已经失效的索引项,删除它们;对内容发生变化的图片再做增量重索引,最后用一次真实查询验收结果。

一、先把“可访问资产”和“已建索引”分成两套账
本次固定数据为:
- Session:
scope_repair_20261001_13 - Permission:
LIMITED - Scope:
travel26 - Previous Indexed:
62 - Accessible:
54 - Stale Removed:
8 - Changed Reindexed:
4 - Query:
雪山 木屋 - TopK:
8 - Hits:
5 - State:
SYNCED - Last Cost:
214 ms
我之前的实现只保存“索引成功了多少张”,没有保存来源资产的指纹。这样一旦用户调整相册授权范围,业务侧根本不知道哪些 index 已经失效。
现在每个索引记录至少保留 assetId / sandboxPath / scope / modifiedTime / indexedAt。其中 assetId 用来做稳定身份,sandboxPath 给 textSearchImage 使用,modifiedTime 决定是否需要重建特征。
二、第一步不是重建全部索引,而是重新扫描当前可访问集合
页面重新回到前台,或者用户从系统权限页面返回后,我会执行一次轻量扫描。
interface AssetSnapshot {
assetId: string
path: string
modifiedTime: number
}
private async scanAccessibleAssets(): Promise<AssetSnapshot[]> {
const helper = this.photoHelper
const result = await this.assetScanner.queryAccessibleImages(helper)
return result.map(item => ({
assetId: item.assetId,
path: item.path,
modifiedTime: item.modifiedTime
}))
}
这里我把 PhotoAccessHelper 的具体查询封装到 AccessibleAssetScanner,页面只拿业务需要的字段。正式项目里还要及时关闭 FetchResult,避免把媒体查询对象长期挂在页面上。
这一步只解决“现在还能看到什么”。它不修改 Core Vision 索引。
三、用 assetId 做差集,不要拿路径字符串硬比
第二段代码解决“哪些应该删,哪些应该重新索引”。
private buildDiff(
current: AssetSnapshot[],
indexed: IndexedRecord[]
): RepairDiff {
const currentMap = new Map(
current.map(item => [item.assetId, item])
)
const indexedMap = new Map(
indexed.map(item => [item.assetId, item])
)
const toRemove = indexed.filter(
item => !currentMap.has(item.assetId)
)
const toReindex = current.filter(item => {
const old = indexedMap.get(item.assetId)
return old !== undefined &&
old.modifiedTime !== item.modifiedTime
})
return { toRemove, toReindex }
}
我没有用 sandbox path 作为主键,因为路径可能因为导入、编辑或业务缓存策略发生变化。assetId 负责身份判断,路径只作为当前一次 Core Vision 调用参数。
这次差集结果很明确:62 个已索引记录里,有 8 个已经不在可访问集合;另外 4 个资产虽然仍可访问,但修改时间变化,需要重建。
四、删除失效特征,再对变化项做增量重建
真正修改 Core Vision 索引时,我没有 clearData() 全量清库,而是按差集处理。
private async repairIndex(diff: RepairDiff): Promise<void> {
this.state = 'REINDEXING'
for (const item of diff.toRemove) {
await textSearchImage.deleteImage(
item.path,
'travel26'
)
await this.indexStore.remove(item.assetId)
}
for (const item of diff.toReindex) {
const old = await this.indexStore.get(item.assetId)
if (old) {
await textSearchImage.deleteImage(
old.path,
'travel26'
)
}
const ok = await textSearchImage.insertImage(
item.path,
'travel26'
)
if (ok) {
await this.indexStore.upsert(item)
}
}
}
这段代码最需要注意的是顺序:变化资产不是直接 insert 一次,而是先删除旧 path 对应的特征,再插入当前版本。
如果中途失败,正式项目不应该只靠内存继续跑,而要把本次 repair session 记录下来,下次从未完成项继续。Demo 为了看清主线,只保留了 generation token,避免用户连续触发两次修复时旧任务覆盖新结果。
五、修复完成后必须用搜索结果验收
索引层显示 SYNCED 还不够。我会立刻跑一次固定查询:
private async verifySearch(): Promise<void> {
const result = await textSearchImage.search(
'雪山 木屋',
'travel26',
8
)
this.hits = result.length
if (this.hits !== 5) {
throw new Error(
`unexpected hits: ${this.hits}`
)
}
this.state = 'SYNCED'
}
本次 TopK=8,最终命中 5 张。更重要的是,5 张结果全部来自当前授权范围,不再出现点击后无法读取的失效照片。
六、我把调试页做成了“权限变化后的对账面板”
项目结构保持得很小:
entry/src/main/ets/
├─ pages/ScopeRepairPage.ets
├─ media/AccessibleAssetScanner.ets
├─ index/VisionIndexRepairer.ets
└─ model/AssetSnapshot.ets
HiLog 固定输出:
permission=LIMITED
snapshot indexed=62 accessible=54
stale removed=8
changed reindexed=4
query=雪山 木屋 topK=8 hits=5
State: REINDEXING -> SYNCED

七、手机运行结果里我更关注“8”和“4”
最终页面并不是为了展示漂亮搜索卡片,而是为了确认修复链条真的执行过。
Stale Removed=8 说明旧索引确实被清掉;Changed Reindexed=4 说明仍然可访问但内容发生变化的资产被重新建索引;Accessible=54 与当前权限范围一致。
搜索区仍然用 雪山 木屋,最终 5 张结果都能正常打开。

八、几个真实项目里容易遗漏的边界
第一,权限缩小和媒体删除不是一回事。对当前应用来说结果都是“不可访问”,但日志里最好分开记录来源,便于解释为什么索引突然减少。
第二,PhotoAccessHelper 的资产变化通知适合触发轻量失效标记,但不要每来一次通知就立即全量扫描。图片批量导入时通知可能很密,可以做短时间合并。
第三,Core Vision Kit 的 deleteImage() 需要你仍然知道旧 path,所以索引元数据不能只存 assetId。
第四,模型能力更新出现需要 clearData() 的场景,应该与普通权限差集修复分开处理。前者是模型版本迁移,后者是业务资产集合变化,不能混成同一个按钮。
九、这次真正修掉的是“索引比权限活得更久”
文搜图工程里,搜索结果不只取决于 query 和模型,还取决于“索引里的图片是不是当前应用仍然有权访问”。
我最后把这条链路固定成:
SNAPSHOT → DIFF → CLEANING → REINDEXING → SYNCED
这样用户调整相册授权以后,不需要重建整个数据库,也不会继续保留已经失效的图片特征。
从实际开发角度看,这类问题比“怎么调用 search()”更值得提前设计,因为索引一旦脱离真实媒体权限集合,后面的排序再准,也只是对一份过期数据做准确计算。
更多推荐



所有评论(0)