item API:读取、修改、删除素材

eagle.item 是插件最常用的模块。素材在官方文档里的原词是「项目」(Item),一个 Item 既可以是图片,也可以是视频、音频、PSD、SVG、书签,甚至一段网页剪藏。理解这一点很重要——很多「图片处理插件」的 API 写法,其实对所有素材类型都成立。

核心设计:读回来的对象是活的

这是 item API 最需要记住的一件事:从 API 取回的 Item 是内存中的对象引用,不是快照。你直接改属性,再调 save() 落盘:

let item = await eagle.item.getById('item_id');
item.name = 'New Name';
item.tags = ['tag_1', 'tag_2'];

await item.save();   // 必须 save 才生效

这个设计的理由很实际:Eagle 的资源库数据存在 metadata.json 里,由主应用统一管理并发。如果 API 要求「传入完整的修改后对象」,调用方就得先深拷贝、比对、再整体覆写,极其容易丢字段。改成「引用 + save」,主应用就能精确追踪哪些字段变了。

代价是:忘了 save() 改动就白改,而且不会有任何报错。

读取:按需选择粒度

get() 是最通用的读取方法,条件全部可选,组合起来就是筛选器:

// 只要 jpg
let jpgs = await eagle.item.get({ ext: "jpg" });

// 只要选了某个标签的素材,并且只取部分字段
let itemsWithFields = await eagle.item.get({
    tags: ["Design"],
    fields: ["id", "name", "tags", "modifiedAt"]
});

fields 参数是个性能利器。素材库里可能有十万条记录,全量返回会构造十万个完整对象。官方提示说,用它可以显著提升性能,特别是在处理大量文件时只需要部分信息的场景。

shape 参数的值域是:square、portrait、panoramic-portrait、landscape、panoramic-landscape——这几个是长宽比分类。

其余读取方法各有分工:

let one = await eagle.item.getById('item_id');
let many = await eagle.item.getByIds(['item_id_1', 'item_id_2']);
let selected = await eagle.item.getSelected();

// 只要数量时用 count,不要用 get().length
let jpgCount = await eagle.item.count({ ext: "jpg" });
let totalCount = await eagle.item.countAll();
let selectedCount = await eagle.item.countSelected();

增量同步专用

后台服务插件做增量同步时,用这个方法拿 ID 和修改时间,比全量 get() 快得多:

let idsWithTime = await eagle.item.getIdsWithModifiedAt();
// [{ id: "ITEM_ID_1", modifiedAt: 1625123456789 }, ...]

let changedItems = idsWithTime.filter(item => item.modifiedAt > lastSyncTime);

这是同步类插件的核心 API:每轮只处理上次之后变过的素材。

新增素材:四个 add 方法

// 从网络链接添加
const itemId = await eagle.item.addFromURL(imgURL, {
    name: 'Camping',
    website: 'https://dribbble.com/shots/19744134-Camping-2',
    tags: ["Dribbble", "Illustration"],
    folders: [],
    annotation: 'add from eagle api',
});

// 从 base64 添加
const itemId2 = await eagle.item.addFromBase64(base64, {
    name: 'Illustration Logo',
    tags: ["Adobe", "Logo"],
});

// 从本地路径添加
const itemId3 = await eagle.item.addFromPath(filePath, {
    name: 'Logo',
    tags: ["Adobe", "Logo"],
});

// 添加书签
const itemId4 = await eagle.item.addBookmark('https://www.google.com/', {
    name: 'Eagle',
    tags: ["Eagle", "Site"],
});

四个方法的 options 结构基本一致:name 文件名、website 来源网址、tags 标签数组、folders 文件夹 ID 数组、annotation 注释。区别在于 addBookmark 多一个 base64 字段用于自定义缩略图,因为它本身没有图像内容可抓。

addFromURL 支持 http、https、base64 三种形式的 URL,这是把网络图片收进素材库最直接的路径。

删除与打开

let item = await eagle.item.getById('item_id');

// 丢到垃圾桶(不是彻底删除)
await item.moveToTrash();

// 在全部列表显示
await item.open();
await item.open({ window: true });   // 在新窗口打开,需要 4.0 build12+

// 静态方法等价写法
await eagle.item.open('item_id');
await eagle.item.select(['ITEM_ID_1', 'ITEM_ID_2']);

moveToTrash() 把素材移到回收站,这是唯一官方提供的删除方式——没有「彻底删除且不可恢复」的 API。这个设计是刻意的:插件不应该有能力做不可逆删除。想做批量清理,就一批一批 moveToTrash()。

实例方法速查

let item = await eagle.item.getById('item_id');

// 替换文件本体(自动刷新缩略图)
await item.replaceFile('new_file_path');

// 重新刷新缩略图,同时重算尺寸、颜色、大小
await item.refreshThumbnail();

// 设置自定义缩略图
await item.setCustomThumbnail('thumbnail_path');

// 标注相关(需要 Eagle 4.0 build22+)
await item.addComment({ x: 350, y: 480, width: 380, height: 400, annotation: "脸部区域" });
await item.addComment({ duration: 65.5, annotation: "重要场景" });
await item.updateComment('comment_id', { annotation: "更新后的文字" });
await item.removeComment('comment_id');

// 只选中自己
await item.select();

replaceFile() 有个重要特性:会自动刷新缩略图,不需要再调 refreshThumbnail()。但官方给的建议值得照做——先把新文件保存到其他路径,确认无误后再替换。直接原地覆盖一旦中途出错,文件就损毁且无法复原。

实例属性:只读 vs 可写

这个区分很重要,因为给只读属性赋值不会报错,只是静默无效:

可写属性(改了记得 save()):name、url、annotation、tags、folders、star、width、height、importedAt。

只读属性(读了能用,赋值无效):id、ext、size、modifiedAt、isDeleted、palettes、comments、noThumbnail、noPreview、filePath、fileURL、thumbnailPath、thumbnailURL、metadataFilePath。

几个高频使用的说明:

  • star 是评分字段,取值 0 到 5。设成 0 相当于取消评分。
  • importedAt 可写但值必须是正整数时间戳,无效值会被忽略,需要 4.0 build18 以上。
  • thumbnailURL 返回 file:/// 链接,要在 HTML 里显示缩略图就直接用它。
  • metadataFilePath 存在,但官方反复强调不要直接改这个文件,要用 save()。

批量操作的正确写法

// 危险:一次处理十万条会卡死界面
const all = await eagle.item.getAll();
for (const item of all) {
    item.tags = ['processed'];
    await item.save();
}

// 安全:分批 + 让出主线程
const all = await eagle.item.get({ fields: ["id", "name", "tags"] });
const BATCH = 50;
for (let i = 0; i < all.length; i += BATCH) {
    const slice = all.slice(i, i + BATCH);
    for (const item of slice) {
        item.tags = [...new Set([...item.tags, 'processed'])];
        await item.save();
    }
    // 每批之间给 UI 一个呼吸的机会
    await new Promise(r => setTimeout(r, 0));
    const progress = Math.round((i + BATCH) / all.length * 100);
    console.log(`进度 ${progress}%`);
}

这里有两个必须记住的点。第一,用 fields 限制返回字段,因为批量修改只需要 id 和 tags。第二,getAll() 在资源库规模大(官方举例 20 万条以上)时不要无限制调用,会明显拖低应用性能。

开发流程

  1. 先用 eagle.item.getSelected() 确认能读到数据。
  2. 确定业务需要哪些字段,用 fields 参数把查询裁剪到最小。
  3. 改属性后立刻 await save(),再更新界面。
  4. 批量操作前用 count() 探规模,超过一千条就分批处理。
  5. 删除只用 moveToTrash(),并在 UI 上确认。
  6. 全程用 eagle.log 记录关键分支。

常见坑

改完属性不调 save()。 界面上看着「改不动」,重开窗口又变回去了。这是最常见的问题,而且不报错。养成「改完必 save」的习惯。

批量操作时忘了给事件循环让路。 几千个 await save() 连续执行会长时间占住主线程,用户看到的是 Eagle 卡死无响应。分批加 setTimeout 让出线程,界面才不会僵。

延伸注意: * 给只读属性赋值以为改成功了。 比如 item.size = 0、item.ext = 'png'、item.filePath = ...,都不会生效也不报错。要改尺寸用 width/height,要改扩展名就替换文件本体。 * 想找「彻底删除」方法。 没有这个 API。只能用 moveToTrash(),素材进回收站后由用户在 Eagle 里决定要不要永久清除。

整理人:WorkBuddy