@agentaily/presets
改名预告: 这个包正在升级成 app 市场的数据层,后续会改名为
@agentaily/market。本版不改包名;消费者迁移与改名留后续。
app 市场。 一个 *app 条目(AppEntry)*就是市场里一份完整、自包含的产物:一组多文件前端 VFS(files,零构建——沙箱 iframe 里原样 serve、index.html 是入口、用相对路径引同 app 内其它文件)+ 它所围绕的 AML 数据模型源码,外加市场元信息(预览图、标签、作者、fork 血缘)。没有垂直 / contract 概念了 —— 市场是一片扁平的 app 空间。无内置 app:每个条目都由大模型生成、再由用户 publish(或 fork)进市场。
interface AppEntry {
id: string;
name: string;
description: string;
aml: string; // 这个 app 围绕的 AML 数据模型源码
files?: Record<string, string>; // ★ 前端多文件 VFS:path → 内容;【必含 index.html 入口】(新规范)
indexHtml?: string; // @deprecated 旧的单文件渲染器,registry 与 files['index.html'] 保持一致
preview: string; // 预览图:PNG data URL(data:image/png;base64,…)或托管 URL
author?: string;
createdAt: number;
forkedFrom?: string; // 若由别的条目 fork 而来,记源条目 id
tags?: string[]; // 自由标签,供搜索 / 发现
}文件模型(REFACTOR §9 —— 零构建 VFS)
- 前端 = 一组静态文件(
files: path → content),沙箱 iframe 里直接 serve、运行时无构建步骤(LLM 写完即跑)。 index.html是唯一入口,必须存在;它用相对路径引同 app 内其它文件:.css(<link>)、.js含 ES module(<script type=module>+ 相对import)、.json、资源。引第三方库写 import map → CDN ESM。indexHtml仍保留(@deprecated) —— 只为让已有消费者(@agentaily/agent)继续编译。registry 把它与files['index.html']保持一致:publish时二者互相派生(files为准),读路径(list/get/search/fork)归一化,所以返回的条目两者永远相等。旧的「只有indexHtml、没有files」条目读出来时会被自动补出files['index.html'](向后兼容)。新代码请用files。
API
import { createAppRegistry, localStorageAppStorage, type AppEntry } from '@agentaily/presets';
const market = createAppRegistry({ storage: localStorageAppStorage() });
market.list(); // 市场里所有已发布 app(扁平,无垂直过滤)
market.get(id); // 按 id 取一个
market.publish(appEntry); // 加入市场(持久化;同 id 覆盖)
market.search('survey'); // 按 name / description / tags 大小写不敏感子串匹配;空串 = 全部
market.fork(id); // 复制一条为新草稿(新 id + forkedFrom + 新 createdAt),返回但【不】自动 publishpublish(app)—— 归一化后校验:条目必须有index.html入口(在files里,或经旧indexHtml补出);否则抛错。返回归一化后入库的条目。search(query)—— 在name/description/tags上做大小写不敏感的子串匹配;查询会先trim,空 / 纯空白查询返回全部。fork(id)—— 忠实复制一条已发布 app(含整组files,不只index.html,且是独立副本,改草稿不动源),生成新 id、把forkedFrom指向源、盖新createdAt,返回草稿但不持久化;由调用方决定何时(及是否)publish。源 id 不存在时抛错。
AppStorage 是 { read(): AppEntry[]; write(apps): void },可插拔,round-trip 完整 AppEntry(含多文件 files VFS)。默认的 localStorageAppStorage() 存在 agentaily:market:apps 键下;将来换服务端(D1/KV)店只改这一层。存储是哑持久化缝——files ↔ indexHtml 的归一化由 registry 负责,所以存储里可以躺着旧的 indexHtml-only 条目,读出来时被 registry 升级。
createAppRegistry 还可注入 newId / now(默认 crypto.randomUUID() / Date.now())给 fork 用,便于测试确定化。
现状
- 无内置 app。 渲染器 + AML 都由大模型生成;市场起初为空,靠用户
publish/fork填充。 - 当前用
localStorage落地;AppStorage这道缝就是将来接服务端店的地方(留后续 PR)。
已弃用:Preset* shim
旧的「绑定垂直(contract)的预设」模型 —— Preset / PresetStorage / PresetRegistry / createPresetRegistry / localStoragePresetStorage —— 仍原样导出(标 @deprecated),只为让现有消费者(apps/web)在迁移到 AppEntry 之前继续编译。它存在 agentaily:presets:published 键下,与新的市场层(agentaily:market:apps)互不影响。消费者迁移完成后整段删除。新代码请直接用 AppEntry / createAppRegistry。
测试:仓库根 pnpm test(vitest,全工作区)。