Sanity Functions + Agent Actions:媒体库多语言 Alt Text 自动生成实战指南
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
本文围绕 Sanity 官方示例media-library-auto-alt-text展开,讲解如何基于 Sanity Functions(服务端无服务器函数)与 Agent Actions,在 Media Library 资源创建或更新时自动为图片生成多语言无障碍 alt text(替代文本)。读完本文,你将掌握 Blueprint 函数的配置方法、Aspect 数据模型、Agent Action 的调用方式,以及 Asset Container 与 Image Asset 的底层数据解构与 GROQ 查询技巧,并能在自己的 Sanity 项目中直接复刻这套能力。
背景:为什么需要自动化的多语言 Alt Text
内容团队需要为图片提供无障碍 alt text,以支撑屏幕阅读器等辅助技术。然而在全球化场景下,手动为每一个资源逐一撰写多语言 alt text 既耗时又难以保持一致——不同成员、不同语言产出的描述质量参差不齐,最终造成可访问性缺口和糟糕的用户体验。
media-library-auto-alt-text正是为解决该痛点设计的示例函数:当资源在 Media Library 中被创建或更新时,函数会等待 Sanity 自动生成的关键词(keywords)就绪,然后调用 Agent Actions 为每个目标语言生成简洁、描述性的 alt text,最后将结果写入资源的aspects(资源自定义元数据)中。整套流程无需人工干预,即可实现 "Accessibility by default"。
该示例的核心收益包括:
- 默认可访问:每个图片自动获得描述性 alt text;
- 多语言支持:一次上传同时生成多种语言的 alt text;
- 提示词可定制:可调整 AI 提示词以匹配品牌语气与需求;
- 结果可编辑:生成的 alt text 可在 Media Library UI 中直接审阅修改;
- 节省时间:消除手动编写 alt text 的重复劳动,同时保持质量与一致性。
适用范围与前置概念
该函数面向任何启用了 Media Library 的 Sanity 项目,它直接作用于 Media Library 中的资源,不依赖特定的 Studio 模板,因此开箱即用。
在深入实现之前,需要先理解两个核心概念。
理解 Aspects(资源自定义元数据)
Aspects 是作用于资源(asset)的 schema 风格字段,用于为资源附加额外的元数据,帮助组织和标识资源。关键要点:
- Aspects 由开发者定义,由系统或用户应用到资源上;
- Aspects 存储在每个资源的
aspects对象中。
本函数包含一个 Aspect:altText,它定义了多语言 alt text 的结构与编辑 UI。其完整定义见 altText.ts:
import {defineAssetAspect, defineField, defineArrayMember} from 'sanity' const languages = [ {title: 'Dutch', value: 'nl'}, {title: 'English', value: 'en'}, {title: 'French', value: 'fr'}, {title: 'German', value: 'de'}, ] export default defineAssetAspect({ name: 'altText', title: 'Alternative text', description: 'Accessible alternative text for this asset, in one or more languages.', type: 'array', of: [ defineArrayMember({ name: 'altTextItem', type: 'object', fields: [ defineField({ name: 'language', type: 'string', description: 'The language that the alt text is written in', options: { list: languages, layout: 'radio', }, }), defineField({ name: 'value', title: 'Alternative text', type: 'string', description: 'Short description of the image, for screen readers (max ~100 characters).', }), ], preview: { select: { title: 'value', subtitle: 'language', }, }, }), ], })从结构上看,这是一个数组类型的 Aspect:数组元素altTextItem是一个对象,包含language(下拉/单选列表,限定四种语言)与value(alt text 内容,提示约 100 字符内)两个字段,并配置了以value为标题、language为副标题的预览。
关于defineAssetAspect的底层机制,可参考其类型定义源码 defineAssetAspect.ts:它会基于传入的定义生成一个_type为 Media Library asset aspect 类型的文档对象,_id取name,assetType会被规范化为数组,public字段透传。该文件注释明确指出Aspects 可通过sanity media deploy-aspectCLI 命令部署。仓库中还提供了另外两个 Aspect 示例可供参考:colourDetails.ts(颜色信息)与 productDetails.ts(价格与库存),它们展示了用相同 API 定义不同类型 Aspect 的写法。
前提要求
- Media Library:函数作用于 Media Library 中的资源;
- Media Library Aspect Setup:需要先完成 Aspect 的配置部署。
快速开始:初始化与添加函数
注意:以下命令必须从项目根目录运行(不要进入studio/子目录)。
初始化 Blueprint 示例
如果尚未初始化 blueprints,先执行:
npx sanity blueprints init执行时系统会提示选择你的组织(organization)和 Sanity Studio。
随后添加函数:
npx sanity blueprints add function --example media-library-auto-alt-text找到你的 Media Library ID
配置函数需要 Media Library ID。打开 Media Library,从 URL 中获取:
https://www.sanity.io/@<organizationId>/media/<mediaLibraryId>/assets即
<mediaLibraryId>这一段。在 Blueprint 中添加配置
编辑项目根目录的
sanity.blueprint.ts:// sanity.blueprint.ts import {defineBlueprint, defineMediaLibraryAssetFunction} from '@sanity/blueprints' export default defineBlueprint({ resources: [ defineMediaLibraryAssetFunction({ name: 'media-library-auto-alt-text', memory: 2, timeout: 30, src: './functions/media-library-auto-alt-text', event: { on: ['create', 'update'], filter: 'assetType == "sanity.imageAsset" && !defined(aspects.altText)', projection: '{ _id, currentVersion }', resource: { type: 'media-library', id: '<your-media-library-id>', // TODO: replace with your media library id }, }, }), ], })其中
id字段需要替换为上一步获取到的真实 Media Library ID。安装依赖
在项目根目录与函数目录分别安装依赖:
# Install in the functions directory cd functions/ pnpm install
配置参数详解
defineMediaLibraryAssetFunction的各个配置项与函数package.json中的blueprintResourceItem(见 package.json)一一对应:
| 参数 | 示例值 | 作用 |
|---|---|---|
name | media-library-auto-alt-text | 函数唯一标识 |
memory | 2 | 函数运行内存(GB) |
timeout | 30 | 单次执行超时时间(秒) |
src | ./functions/media-library-auto-alt-text | 函数代码目录 |
event.on | ['create', 'update'] | 触发时机:资源创建与更新时激活 |
event.filter | assetType == "sanity.imageAsset" && !defined(aspects.altText) | 事件过滤条件:仅处理图片资源,且尚未生成 altText(同时承担防循环职责) |
event.projection | { _id, currentVersion } | 事件载荷投影,最小化传入函数的数据 |
event.resource.type | media-library | 资源类型:Media Library |
event.resource.id | <your-media-library-id> | 目标 Media Library ID |
仓库根目录的 sanity.blueprint.ts 展示了另一种组织方式:通过遍历functions/目录读取各示例package.json中的blueprintResourceItem,按资源类型分发到defineMediaLibraryAssetFunction或defineDocumentFunction,实现函数资源的自动发现与注册——如果你有大量函数,可以采用这种"按包自描述"的注册模式。
函数核心实现解读
函数主逻辑位于 index.ts。它基于@sanity/functions的documentEventHandler与@sanity/client,核心依赖见 package.json:@sanity/client(^8.6.1)与@sanity/functions(^1.7.2)。
等待关键词就绪(waitForKeywords)
Media Library 会在图片上传后自动生成关键词,但该过程是异步的,因此函数必须轮询等待。源码中通过两个常量控制重试策略:
const MAX_KEYWORD_WAIT = 5 // How many times to retry (total attempts = MAX_KEYWORD_WAIT + 1) const KEYWORD_WAIT_MS = 1500 // Wait 1.5 seconds between checkswaitForKeywords最多执行 6 次检查(5 次重试 + 1 次初始尝试),每次间隔 1.5 秒;一旦拿到非空关键词数组立即返回,否则最终返回undefined并跳过本次执行。
创建 Media Library 客户端并拉取关键词
const mediaLibraryClient = createClient({ token: context.clientOptions.token, useCdn: false, apiVersion: '2025-05-08', resource: { type: 'media-library', id: mlId, }, }) const fetchKeywords = async () => { try { const result = await mediaLibraryClient.fetch<{keywords?: string[]}>( `*[_id == $assetId][0]{ "keywords": metadata.keywords }`, {assetId: detailedAssetId}, ) return result?.keywords || [] } catch (err) { console.error('Failed fetching keywords from asset', err) return [] } }注意这里的两个关键细节:
- 客户端通过
resource: {type: 'media-library', id: mlId}直接绑定 Media Library 资源,useCdn: false确保读到实时数据; detailedAssetId来自事件载荷中currentVersion._ref,即底层图片文档的引用。函数显式处理了!detailedAssetId的情况,直接跳过执行。
调用 Agent Actions 逐语言生成 alt text
拿到关键词后,函数为每个语言分别发起一次 Agent Action 调用——逐个语言、串行生成,以保证可靠性:
const agentClient = createClient({ ...context.clientOptions, dataset: 'production', apiVersion: 'vX', }) const altTextItemsArray: {_key: string; _type: string; language: string; value: string}[] = [] for (const lang of languages) { const altText = await agentClient.agent.action.prompt({ instruction: `Given the following keywords: [${keywords.join(', ')}], generate a short (max 100 chars) alt text in language: ${lang}. Respond with just the alt text string, no quotes or formatting.`, }) altTextItemsArray.push({ _key: crypto.randomUUID(), _type: 'altTextItem', language: lang, value: String(altText).trim(), }) }默认语言数组为const languages = ['nl', 'en', 'fr', 'de'](荷兰语、英语、法语、德语),与 Aspect 定义中的语言列表保持一致。Agent 提示词明确约束了输出格式:"不超过 100 字符、只返回 alt text 字符串、不加引号或格式",从而保证生成结果可直接入库。每个条目使用crypto.randomUUID()生成_key。
写回 Aspects
最后,使用 Media Library 客户端将结果写入资源的aspects对象:
const result = await mediaLibraryClient .patch(_id) .setIfMissing({aspects: {}}) .set({'aspects.altText': altTextItemsArray}) .commit() console.log('Mutation response:', JSON.stringify(result, null, 2))setIfMissing确保aspects对象存在,set则写入/覆盖altText数组。需要特别留意:如果资源已存在altText,事件过滤器(!defined(aspects.altText))会让函数直接跳过,这是防止"更新→再触发→再更新"死循环的关键机制。
数据模型:Asset Container 与 Image Asset
Media Library 区分两个相互关联的实体,理解这一点是编写与调试相关函数的基础:
- Asset Container:代表媒体条目的"主"文档,自定义 aspects 存储在这里;它包含对图片文档的引用;
- Image Asset:实际的图片文件,携带系统级元数据(尺寸、EXIF、关键词等)。
数据存放位置速查:
- 自定义 Aspects(
altText)→ Asset Container 的aspects对象; - 自动生成的关键词 → Image Asset 的
metadata.keywords数组(上传新图片时自动生成); - 图片尺寸、EXIF 数据 → Image Asset 的
metadata对象。
要读取位于图片文档上的关键词等数据,需要对currentVersion引用进行解引用(dereference):
*[_id == $assetContainerId][0]{ ..., "metadata": currentVersion->{ metadata } }Asset Container 结构
查询 Media Library 资源时(以下为脱敏示例),你将看到如下结构:
{ "_createdAt": "2025-01-01T12:00:00Z", "_id": "<asset-id>", "_rev": "<revision-id>", "_system": { "createdBy": "<user-id>" }, "_type": "sanity.asset", "_updatedAt": "2025-01-02T15:00:00Z", "aspects": { "altText": [ { "_key": "<key-1>", "_type": "altTextItem", "language": "nl", "value": "voorbeeld alt text" }, { "_key": "<key-2>", "_type": "altTextItem", "language": "en", "value": "Example alt text" } ] }, "assetType": "sanity.imageAsset", "cdnAccessPolicy": "public", "currentVersion": { "_ref": "<image-asset-version-ref>", "_type": "reference" }, "title": "example-image.png", "versions": [ { "_key": "<version-key-1>", "_type": "sanity.asset.version", "instance": { "_ref": "<image-asset-version-ref>", "_type": "reference" }, "title": "example-image.png" } ] }关键点:
aspects对象承载你的自定义元数据(完全可定制);currentVersion字段引用实际的图片文档;versions数组追踪资源的所有版本(支持资源版本化)。
Image Asset 结构与元数据查询
实际的图片元数据位于图片文档上,而非 Asset Container。通过解引用currentVersion引用即可获取完整图片数据:
*[_id == $currentVersion._ref][0]返回的底层图片结构(脱敏示例):
{ "_id": "<image-asset-version-id>", "_type": "sanity.imageAsset", "metadata": { "_type": "sanity.imageMetadata", "blurHash": "<blur hash string>", "dimensions": { "_type": "sanity.imageDimensions", "aspectRatio": 0.68, "height": 1200, "width": 810 }, "hasAlpha": false, "isOpaque": true, "keywords": ["movie poster", "character", "studio", "red cape"], "lqip": "<data-url>", "palette": { "_type": "sanity.imagePalette", "darkMuted": { "_type": "sanity.imagePaletteSwatch", "background": "#4c3134", "foreground": "#fff", "population": 0.36, "title": "#fff" } } }, "originalFilename": "<image-file-name>.jpg", "mimeType": "image/jpeg", "cdnAccessPolicy": "public" }重要提示:
- Media Library 生成的关键词位于图片文档的
metadata.keywords; - 为确保准确性,始终从解引用后的
currentVersion读取图片元数据。
这也解释了函数实现中的两层取数逻辑:事件载荷只携带{_id, currentVersion}投影,函数再用currentVersion._ref作为$assetId查询metadata.keywords——正是遵循了这一数据模型约定。
本地测试与部署
本地测试
在部署到生产环境之前,可以使用 Sanity CLI 在本地测试media-library-auto-alt-text函数的行为,确认触发逻辑、关键词等待与 Agent 生成链路均符合预期。
部署到生产
部署前置条件:项目已启用 Media Library。
1. 部署 Blueprint
从项目根目录运行:
npx sanity blueprints deploy该命令会完成以下工作:
- 打包你的函数代码;
- 上传至 Sanity 的基础设施;
- 配置资源创建/更新的事件触发器;
- 让函数在生产环境生效。
2. 验证部署
部署完成后,可通过以下方式验证函数是否正常运行:
- 向 Media Library 上传一个新资源;
- 等待 Media Library 的 alt text 生成(通常只需几秒钟);
- 检查资源的 aspects 中是否出现生成的 alt text;
- 在 Sanity CLI 中监控函数日志。
自定义语言
如需增删语言,修改函数代码 index.ts:
- 更新
languages数组,写入你需要的语言代码(如['en', 'ja', 'es']),并同步调整 Aspect 定义 altText.ts 中的语言选项列表,保持两端一致; - 重新部署函数。
编辑已生成的 Alt Text
生成的 alt text 可直接在 Media Library UI 中编辑:
- 在 Media Library 中打开资源;
- 进入 Aspects 面板;
- 编辑任意语言的 alt text;
- 修改会立即保存。
故障排查
常见问题
报错:"No Media Library keywords found"
- 原因:Sanity 的机器学习尚未生成关键词,或图片类型不受支持;
- 解决:上传后等待几秒,函数内置了重试逻辑;同时确认资源是 Media Library 可处理的图片类型。
函数陷入循环触发
- 原因:函数更新资源时又触发了自身;
- 解决:函数通过检查是否已存在
altText实现防循环逻辑(对应事件过滤器!defined(aspects.altText)),请确保不要移除这段逻辑。
生成的 alt text 不贴切
- 原因:Media Library 关键词未能准确描述图片,或 AI 提示词需要调整;
- 解决:在 Media Library UI 中手动编辑 alt text,或调整函数代码中的提示词以更贴合你的需求。
最佳实践
结合官方 README 与源码实现,可将以下实践沉淀为通用准则:
- 从图片资源(Image Asset)读取 Media Library 关键词,而不是从 Asset Container 读取;
- 将自定义元数据存放在 aspects(位于 Asset Container 上);
- 更新前检查是否已存在 Aspect 数据,防止更新循环(本示例通过事件过滤器与函数内双重防护实现);
- 对异步生成的数据保持耐心轮询:关键词由 Media Library 后台生成,函数需具备重试机制(
MAX_KEYWORD_WAIT+KEYWORD_WAIT_MS); - 逐语言串行调用 Agent Action:将多语言生成拆分为独立的、串行的 Agent 调用,比单次多语言请求更可靠;
- 严格约束 Agent 输出格式:在提示词中限定长度(如 max 100 chars)、禁止引号与多余格式,保证结果可直接入库渲染。
这一套"事件触发 → 等待异步数据 → Agent 生成 → 写回 Aspects"的链路,也可以平移到自动打标签、自动摘要、品牌语气校验等其他内容自动化场景——examples/functions 目录下的其他示例(如 auto-tag、auto-summary、brand-voice-validator)正是同一架构的不同应用,可作为扩展参考。
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考