模板格式
一个 Busabase 模板目录里有什么——根部的 SKILL.md、busabase.json 里的 template 对象、.busabaseignore、校验器的规则,以及让两个安装器认出同一个应用的归属戳。
模板就是 busabase-package@1 格式,加上一小块完全落在 content/ 之外的增量:
busa-email/
├── SKILL.md ← agent 读的手册
├── references/ ← 一起进入 Skill 节点
├── agents/
├── scripts/
├── assets/screenshots/ ← 模板中心的卡片图(永不安装)
├── busabase.json ← 清单 + 一个 `template` 对象
└── content/ ← 与普通包逐字节相同
├── settings/base.json
├── settings/records.ndjson
└── busa-email-app/ ← 一个 AirApp,带 .busabaseignorecontent/ 底下一个字节都没变——包格式原样保留,这正是它是一种复用同一份目录的格式、而不是第二种格式的原因。同一个目录既是合法的 Agent Skill(agent 可以用 npx skills add 装它),又是合法的 Busabase 包(busabase-cli install 能装它),而且两种视角都不需要知道对方存在。
根部的 SKILL.md
入口文件叫 SKILL.md 是有意为之:它在安装时会原样变成目标 Folder 里的一个 Skill 节点,于是手册跟着资源走,而不是留在发布者自己的硬盘上。
有三个附属目录会和它一起进入那个节点:
| 目录 | 为什么 |
|---|---|
references/ | 字段说明、schema、示例——SKILL.md 里链接过去的材料 |
agents/ | 这个 skill 自带的子 agent 定义 |
scripts/ | skill 自己的脚本,例如 setup.mjs |
scripts/ 会被存下来,但 Busabase 永远不会执行它。存它是为了让 agent 把 skill 从 space 里拉回去时拿到一份能用的副本——丢掉它,往返就是有损的。安全性来自 Busabase 不跑它,而不是来自拒绝存它。
Frontmatter
---
name: busa-email
description: 一个收件箱审批台——它起草回复,你来批准。
metadata:
busabase:
template: true
resources:
- settings
- reviews
---| 键 | 含义 |
|---|---|
name | 必填,且必须等于 busabase.json 的 name——它们是同一个应用 |
description | 凡是列出这个 skill 的地方都会显示 |
metadata.busabase.template | 就是那个 opt-in。 true 表示"把它作为模板发布" |
metadata.busabase.resources | 手册里提到的 Base slug。每一个都必须在 content/ 里存在 |
metadata.busabase.folderSlug、risk | 声明性字段——给读者和将来的工具用;当前安装流程不读它们 |
template: true 是显式的 opt-in,不是从目录形状推断出来的。发布一个模板意味着接受:装它的人会跑它的 AirApp 代码、会把它的 SKILL.md 喂给自己的 agent——这值得一个刻意的开关,而不是目录结构的副作用。
resources 是校验而非采信的:被告知了一张并不存在的表,agent 就会往错的表里写。
busabase.json 里的 template 对象
{
"format": "busabase-package@1",
"name": "busa-email",
"description": "一个收件箱审批台。",
"version": "1.2.0",
"template": {
"category": "email",
"tags": ["inbox", "triage"],
"screenshots": ["assets/screenshots/overview.webp"],
"agentPrompts": [
"把今天的收件箱过一遍,需要回的先起草一份回复。"
],
"airapp": "busa-email-app",
"schemaVersion": 1,
"secrets": [
{ "key": "BUSA_EMAIL_IMAP_PASSWORD_PRIMARY", "description": "邮箱密码", "required": true }
],
"requires": { "airapp": true }
}
}| 字段 | 默认值 | 作用 |
|---|---|---|
category | 必填 | 卡片上显示的模板中心分类 |
tags | [] | 画廊里可搜 |
screenshots | [] | 包内相对路径。由服务端解析成 URL,所以卡片不可能指向一个与它安装的 ref 不同的版本 |
agentPrompts | [] | 详情页上展示的预置提示词——"一堆表"和"能用的东西"之间的差别 |
airapp | — | 单 AirApp 的简写。与 airapps 互斥 |
airapps | — | { slug, role, label }[]。role 有 primary、admin、public、tool;有且只能有一个 primary |
schemaVersion | 1 | 声明的资源形状变化时由作者递增。它是归属戳的一部分,所以两个安装器必须对它取得一致 |
vaultNamespace | — | 这个应用的 secret 放在哪个 Vault 命名空间 |
secrets | [] | { key, description, required }[]——只声明,从不创建 |
requires.airapp | — | 设为 true 时,"content/ 里没有 AirApp"从警告升级为错误 |
包格式里没有存放 secret 值的位置,也不该长出来——这就是整个格式赖以成立的那条"格式表达不了的东西就泄漏不出去"的规则。secrets 只是告诉用户该往 Vault 里放哪些键;安装既不创建它们,当前也不会提示你去填。
哪个 AirApp 才是"这个应用"
只有 primary 有机械含义;其余角色是给人看的标签,也是给 agent 的提示("把管理后台起起来")。解析顺序:
- 声明了
template.airapp就用它——它必须能在content/里对上一个 AirApp。 - 否则用
template.airapps,其中必须恰好有一个primary,且引用的 AirApp 都得存在。 - 否则,如果
content/里刚好只有一个 AirApp,就是它。 - 否则报错。歧义绝不会用"按字母序取第一个"来化解——一个悄悄打开错应用的模板,比一个拒绝发布的模板更糟。
.busabaseignore
AirApp 节点就是那个工程——Busabase 是对节点里的内容原样跑 npm run dev 的——所以 content/<airapp>/ 既得是可部署产物,又得是开发者干活的目录。content/<airapp>/.busabaseignore 就是让一个目录同时是这两者的东西:仓库里留着 test/、锁文件和覆盖率报告,节点只收到必须跑起来的部分。
它是 gitignore 语法的一个刻意收窄的子集,在这里写清楚,免得作者被"差一点就匹配上"悄悄绊倒:
| 模式 | 含义 |
|---|---|
# 注释、空行 | 忽略 |
test/ | 该目录及其下所有内容 |
coverage | 任意层级下、叫这个名字的文件或目录 |
/build | 只锚定在 AirApp 根 |
*.log | * 只在一个路径段内匹配 |
docs/**/*.tmp | ** 跨路径段匹配 |
!keep.log | 取反——把前面规则排除掉的路径重新收回来 |
最后一条匹配的规则获胜,和 gitignore 一致。不支持: 字符类([a-z])和转义字面量(\#)——用到它们的模式会被按字面量匹配,而不是被悄悄重新解释。
package.json 以及它声明的入口文件永远不能被忽略。这是硬错误而不是警告,因为那样的 AirApp 会顺利装进来、然后起不来——这是最难归因的一类故障。
截图
assets/screenshots/ 是给模板中心用的卡片图。这些文件不是节点,也永远不会被安装——它们存在只是为了让画廊有东西可展示。在 template.screenshots 里用包内相对路径声明它们,服务端会按卡片所安装的同一个仓库与 ref 把它们解析成 URL。
文件缺失或改名会降级成占位图,而不是一个碎图标:没有它,卡片依然有用。
带必填关联的示例数据
records.ndjson 里的 relation 值是包内的 record key,按该 relation 字段的 targetBaseSlug
在目标 Base 内解析。安装器会先创建必填关联指向的那些记录,并在创建依赖它们的行之前把这些 key
替换成 canonical record id。可选关联仍然等到所有示例行都拿到 id 之后再统一补写。
这让 Company → Contact → Activity 这类无环依赖可以正常安装,而不需要削弱 Base 的 schema。 同时也让下面三类错误在预览阶段就明确失败,此时还没有创建任何工作区资源:
- 必填关联为空;
- 它的 key 在声明的目标 Base 里不存在;
- 必填关联形成了环,因此不存在合法的第一条记录。
Record key 的作用域是所属 Base。两个 Bases 都可以包含名为 default 的 key;让引用不产生歧义的是
relation 字段的 targetBaseSlug。
校验器都查什么
判定一个包是不是模板的只有一个函数,而且四个消费者共用它:目录构建、busabase-cli install、网页端的安装预览,以及自查产物的 agent。一个包在画廊里显示为模板、装进来却是普通包(或者反过来),那是信任缺陷,不是排版问题。
它是纯静态的——不启动 AirApp、不执行脚本、不联系服务器。
错误——命中任意一条就"不是模板"
| 检查 | 为什么是致命的 |
|---|---|
根部没有 SKILL.md | 那它就只是个普通包 |
| frontmatter 不是合法的 skill 声明 | 下游没有任何东西读得懂它 |
metadata.busabase.template 不是 true | 没有 opt-in 就不发布 |
busabase.json 没有 template 对象 | 至少 category 是必需的 |
SKILL.md 的 name ≠ 清单里的 name | 它们是同一个应用,必须一致 |
resources 里某项在 content/ 找不到对应的 Base | agent 会往错的表里写 |
| 主 AirApp 解析不出来 | 安装不能靠猜来决定打开哪个应用 |
某个 AirApp 的 package.json 不是合法 JSON,或没有 dev 脚本 | 它会装得好好的,然后永远起不来 |
requires.airapp 为 true 但 content/ 里没有 AirApp | 清单和内容自相矛盾 |
| 某张表带了超过 50 条示例记录 | 模板播的是演示的种子,不是发数据集 |
警告——照样发布,只是卡片说实话
| 警告 | 后果 |
|---|---|
| 没有 AirApp | 纯数据模板:用户看到的是表,不是应用 |
| 没有示例记录 | 第一次打开时应用是空的 |
| 没有截图 | 卡片用占位图 |
没有 agentPrompts | 详情页没有建议提示词 |
"当不成模板"从来不是拒绝一个合法包的理由。 没通过这些检查的包照样能装——按普通 busabase-package@1 安装,跟模板出现之前完全一样。
归属戳
这就是让"两扇门、一个应用"成立的机制。有两条互相独立的代码路径会创建同一批资源——busabase-package 里的安装器,以及 skill 自己经由 busabase-sdk 跑的 setup.mjs——它们认出对方成果的唯一依据,就是写进节点 metadata 的那枚戳的形状。
| 节点 | 戳 |
|---|---|
| 每个资源(Base、Drive、AirApp……) | appId、resourceKey、schemaVersion |
| 应用的根 Folder | 同一个三元组,resourceKey 固定为 "app-root",另加 version、source(repo / ref / subdir)和 installedAt |
| 从包根提上来的 Skill 节点 | appId 和 isTemplateSkill: true |
有两处细节分量很重:
resourceKey是包内原本的 slug,不是安装后的 slug。 作者叫settings的表装进来叫busa-email-settings,而戳上写的仍然是settings。应用正是靠这个稳定的 handle 找到自己的表,无论它被装进了哪个 Folder。- SDK 把没盖戳、或盖了别的戳的节点当成别人的,抛
SETUP_CONFLICT而不去动它。所以两个写入方一旦漂移,一个从模板中心装完、又在终端里跑同一个 skill 的用户,就会在自己的数据上撞上硬冲突。这也是为什么这枚戳只在契约层定义一次、两边都不许各自重新声明;SDK 那份会在编译期与契约层对齐检查。
Skill 节点上的 isTemplateSkill 标记在两个方向上都有用:export --template 正是靠它把那个节点提回包根(而不是写进 content/ 造出这个格式本要消除的重复),而 agent 侧也正是把这些节点列为*"这个 space 里可用的 app 手册"*。
相关
- 模板——怎么装、怎么用
- 发布模板——
export --template与目录 - 安装与导出(packages)——底层格式
- 节点类型——Skill 与 AirApp 节点