HarmonyOS Core Vision Kit textSearchImage.deleteImage:撤回素材时索引与文件的双阶段一致性【鸿蒙心迹】
一、照片被撤回,搜索结果为什么还会出现它
素材管理应用通常允许用户撤回某张图片:点击“撤回素材”,缩略图从相册列表消失。问题是文搜图并不只依赖这份 UI 列表。若之前已经把图片特征插入本地搜索索引,只删除界面中的记录,搜索服务可能仍然返回对应路径;反过来,如果先删除原文件,索引还保留了原路径,结果点击后会出现文件找不到。用户看到的是同一个动作,工程上却至少涉及搜索索引、业务元数据和沙箱文件三个相互独立的对象。
这篇示例叫 RecallShelf,任务编号 RC-101。预置一张演示素材 pic108.jpg,业务 ID 是 P-108,搜索 scope 使用 Album2026。界面最初显示“待撤回”,点击后期望显示“索引已移除 / 文件待清理”,在文件清理完成时才改为“已完成”。该状态与配图保持一致,但不代表已执行真实设备删除。

本篇不再重复上一篇文搜图的 scope 隔离和模型升级重建方案,而是专门处理素材撤回的单对象一致性。这里的“索引移除”指 textSearchImage.deleteImage 对一条图片索引的删除,不是 clearData。后者会清掉整个数据库,误用在撤回单图的按钮上,其他图片就可能一起失去可搜索能力。两者虽然都是清理行为,影响范围完全不同。
二、先把三个删除动作的边界说清楚
华为官方 Core Vision Kit 参考资料表明,textSearchImage 从 API 26.0.0 开始支持,在 Stage 模型下使用,属于视觉基础服务能力;deleteImage(imagePath, scope) 返回 Promise<boolean>,作用是从图片搜索数据库移除该项,不承担业务表记录删除、图片文件删除或系统相册管理。imagePath 为应用沙箱路径,并有输入长度约束;scope 需符合接口规则。对低版本设备或无相应系统能力的环境,业务层必须做兼容和降级说明。
最关键的顺序不是“先调哪个 API 比较快”,而是“任何一步失败时,页面如何表达真实状态”。本文的取舍是先标记待撤回、再删除搜索索引、再清理本应用持有的文件,最后才从可用列表中彻底移除。若索引删除失败,图片仍可保留以供重试;若索引删除成功但文件清理失败,记录应进入待清理队列,不能撒谎显示一切成功。
为了把状态机收敛到业务层,定义五种状态:READY、REMOVING_INDEX、FILE_PENDING、DONE、ERROR。这里的“业务状态”和后面展示的页面标签属于应用自定义字段,绝不是华为 SDK 自带枚举。第一段代码解决状态转换与重复点击拦截,数据字段用明确类型,不靠字符串提示词来判断。
type RemoveState = 'READY' | 'REMOVING_INDEX' |
'FILE_PENDING' | 'DONE' | 'ERROR';
interface AssetRow {
id: string;
path: string;
scope: string;
state: RemoveState;
}
class RecallModel {
private working: boolean = false;
row: AssetRow;
constructor(path: string) {
this.row = { id: 'P-108', path: path,
scope: 'Album2026', state: 'READY' };
}
acquire(): boolean {
if (this.working) return false;
this.working = true;
this.row.state = 'REMOVING_INDEX';
return true;
}
finish(state: RemoveState): void {
this.row.state = state;
this.working = false;
}
}
working 的作用是为一次撤回操作建立本地排他门禁,防止用户快速点击两次而产生两条同时运行的删除链。这里假设同一个页面只存在一个 RecallModel 实例;多窗口或跨进程需要把幂等保证上移到持久化业务层。path 在这里由 UIAbilityContext 的应用私有目录拼接得到,不能直接使用相册缩略图的展示 URI 冒充已受支持的沙箱路径。
三、检索接口的返回值不是文件删除凭据
撤回第一阶段必须调用已核对的正式 API。deleteImage 的返回值 true 表示搜索索引移除成功,返回 false 或抛出异常则说明还不能进入后续清理步骤。对错误码做日志登记时不要把路径和用户的真实搜索词一股脑输出;内部任务 ID、状态和经过脱敏的错误信息通常足以定位问题。
下面代码只实现已验证的索引阶段,依赖 RecallModel 管理 UI 自定义状态。注意 catch 的错误属于 SDK/运行时异常,示例不预设一定是哪一种错误码。
import { textSearchImage } from '@kit.CoreVisionKit';
async function removeFromIndex(model: RecallModel): Promise<boolean> {
if (!model.acquire()) return false;
try {
const ok: boolean = await textSearchImage.deleteImage(
model.row.path, model.row.scope);
model.finish(ok ? 'FILE_PENDING' : 'ERROR');
console.info(`RC-101 indexRemoved=${ok} state=${model.row.state}`);
return ok;
} catch (e) {
model.finish('ERROR');
console.error('RC-101 index removal failed');
return false;
}
}
这段代码完成后,即使结果为 true,也只能确认服务报告该索引项已删除,不能推论对应图片文件消失。只要没有完成持久化记录清理,应用重启时就需要读取 FILE_PENDING 再处理,否则一次进程退出就会把半完成的操作遗忘。出于示例可移植性考虑,持久化层和文件删除具体实现被单独抽象;不能把这里显示的“待清理”当作一个已核实的文件系统操作结果。

这张 DevEco Studio 风格图展示预期日志 RC-101 indexRemoved=true state=FILE_PENDING,对应操作之后应进入的中间阶段。日志、模拟器界面和代码属于人工制作的示意资料,不是 HiLog 真机采样。开发测试应准备三种独立场景:API 返回 false、API 抛错以及 API 返回 true 而文件删除失败,验证前两种不会误删文件,第三种仍能被恢复任务发现。
四、第二阶段故意不混淆平台接口与项目接口
图库中的“移除”往往涉及撤回展示权、移除本地副本和清理历史缓存。业务上也可能采用软删除:先移入隔离目录,经过一段确认期再真正回收。本篇只允许应用处理自身有权限管理的私有文件,不声称可以任意删除系统相册或其他应用文件。若图片来自系统照片选择器,首先应核对 URI 授权与复制策略;应用持有的副本和系统相册原图不是同一个资源。
下一段展示文件阶段的业务编排。LocalFileStore 是为示例定义的项目接口,并非系统 SDK;接入真实工程时应使用经官方文档确认、具备相应访问权限的文件模块实现其 removeOwnedCopy。saveRow 同样是项目持久化适配层。把接口抽象出来,反而能避免为了凑示例代码随意编造系统函数签名。
interface LocalFileStore {
removeOwnedCopy(path: string): Promise<void>;
}
interface RowStore {
saveRow(row: AssetRow): Promise<void>;
}
async function cleanupPending(model: RecallModel,
files: LocalFileStore, rows: RowStore): Promise<void> {
if (model.row.state !== 'FILE_PENDING') return;
try {
await files.removeOwnedCopy(model.row.path);
model.row.state = 'DONE';
await rows.saveRow(model.row);
console.info('RC-101 state=DONE');
} catch (e) {
model.row.state = 'FILE_PENDING';
await rows.saveRow(model.row);
console.error('RC-101 cleanup deferred');
}
}
这里采用“先确认本地副本清理,再记录完成”的顺序。如果 saveRow 失败,下一轮重试必须能容忍文件已经不存在的情况;因此具体 removeOwnedCopy 实现应做到重复调用安全,或在读取后先检查文件是否仍存在。这个需求不是 deleteImage 的功能,而是撤回事务自身必须具备的幂等性。若保存待清理记录也失败,应由更高一级的恢复队列或数据库事务承担保护,不能只依赖页面内存字段。
五、把失败状态写进演示,而不是全部画成成功
图里的手机页面显示任务 RC-101、素材 P-108、scope Album2026,当前状态是 FILE_PENDING,即“索引已移除,文件待清理”。选择中间状态而非“全部完成”是有意的,因为它能够逼着实现者关注第二阶段失败后的恢复。若示意图直接写“已完成”,读者会误解删除索引就是删除文件。搜索列表应在这一状态下暂时隐藏该条素材,避免用户再次点击到正在清理的路径,但业务存储不能立刻丢弃其待办记录。

实际验收不需要杜撰耗时。应该建立一张故障矩阵:索引失败时状态进入 ERROR,应用仍持有原副本;索引成功而文件失败时进入 FILE_PENDING,重启后继续清理;全部成功后为 DONE,该业务 ID 不再进入可搜索结果。还要单独检查 UI 更新发生在异步 await 之后的情况,页面退出后不要再对已经释放的界面实例写状态。
搜索能力升级后的错误码 1013100003 对应模型能力变更,官方给出了 clearData 后重新处理数据的建议;这属于全量索引维护,与本文单素材撤回不同。不要在 deleteImage 异常分支里不加区分地调用 clearData。如果未来接入多 scope 管理,撤回记录也必须保留原 scope,不要硬编码一个全局默认值。
六、留给真正工程接入的四个检查点
第一,创建索引前确认图片文件确实存在并在应用可访问沙箱内;错误路径会影响后续的撤回动作。第二,建立稳定 ID 到 sandbox path、scope 的映射,业务 ID 可以变化显示标题,但索引删除参数必须匹配插入时的身份。第三,恢复队列需要在应用重新启动后继续执行,防止单次网络或服务异常造成永久半完成。第四,在发布版本中,调试日志与 UI 不应暴露个人图片完整路径。
本文示例没有把系统相册图片原件当成应用私有文件,也没有声称真实运行过 SDK 调用。已确认的是 deleteImage(imagePath, scope) 的签名与作用范围;尚需在目标 API 26 系统、实际文件存储实现和应用生命周期下完成编译与故障注入。把索引与文件分阶段撤回,并清楚表达 FILE_PENDING,比单靠一个“删除成功”的 Toast 更容易让工程保持一致。
参考:华为官方《textSearchImage(文搜图)》https://developer.huawei.com/consumer/en/doc/harmonyos-references/core-vision-text-search-image-api 。
更多推荐


所有评论(0)