Busabase

模板格式

一个 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,带 .busabaseignore

content/ 底下一个字节都没变——包格式原样保留,这正是它是一种复用同一份目录的格式、而不是第二种格式的原因。同一个目录既是合法的 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.jsonname——它们是同一个应用
description凡是列出这个 skill 的地方都会显示
metadata.busabase.template就是那个 opt-in。 true 表示"把它作为模板发布"
metadata.busabase.resources手册里提到的 Base slug。每一个都必须在 content/ 里存在
metadata.busabase.folderSlugrisk声明性字段——给读者和将来的工具用;当前安装流程不读它们

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 有 primaryadminpublictool有且只能有一个 primary
schemaVersion1声明的资源形状变化时由作者递增。它是归属戳的一部分,所以两个安装器必须对它取得一致
vaultNamespace这个应用的 secret 放在哪个 Vault 命名空间
secrets[]{ key, description, required }[]——只声明,从不创建
requires.airapp设为 true 时,"content/ 里没有 AirApp"从警告升级为错误

包格式里没有存放 secret 值的位置,也不该长出来——这就是整个格式赖以成立的那条"格式表达不了的东西就泄漏不出去"的规则。secrets 只是告诉用户该往 Vault 里放哪些键;安装既不创建它们,当前也不会提示你去填。

哪个 AirApp 才是"这个应用"

只有 primary 有机械含义;其余角色是给人看的标签,也是给 agent 的提示("把管理后台起起来")。解析顺序:

  1. 声明了 template.airapp 就用它——它必须能在 content/ 里对上一个 AirApp。
  2. 否则用 template.airapps,其中必须恰好有一个 primary,且引用的 AirApp 都得存在。
  3. 否则,如果 content/ 里刚好只有一个 AirApp,就是它。
  4. 否则报错。歧义绝不会用"按字母序取第一个"来化解——一个悄悄打开错应用的模板,比一个拒绝发布的模板更糟。

.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.mdname ≠ 清单里的 name它们是同一个应用,必须一致
resources 里某项在 content/ 找不到对应的 Baseagent 会往错的表里写
主 AirApp 解析不出来安装不能靠猜来决定打开哪个应用
某个 AirApp 的 package.json 不是合法 JSON,或没有 dev 脚本它会装得好好的,然后永远起不来
requires.airapptruecontent/ 里没有 AirApp清单和内容自相矛盾
某张表带了超过 50 条示例记录模板播的是演示的种子,不是发数据集

警告——照样发布,只是卡片说实话

警告后果
没有 AirApp纯数据模板:用户看到的是表,不是应用
没有示例记录第一次打开时应用是空的
没有截图卡片用占位图
没有 agentPrompts详情页没有建议提示词

"当不成模板"从来不是拒绝一个合法包的理由。 没通过这些检查的包照样能装——按普通 busabase-package@1 安装,跟模板出现之前完全一样。


归属戳

这就是让"两扇门、一个应用"成立的机制。有两条互相独立的代码路径会创建同一批资源——busabase-package 里的安装器,以及 skill 自己经由 busabase-sdk 跑的 setup.mjs——它们认出对方成果的唯一依据,就是写进节点 metadata 的那枚戳的形状。

节点
每个资源(Base、Drive、AirApp……)appIdresourceKeyschemaVersion
应用的根 Folder同一个三元组,resourceKey 固定为 "app-root",另加 versionsourcerepo / ref / subdir)和 installedAt
从包根提上来的 Skill 节点appIdisTemplateSkill: true

有两处细节分量很重:

  • resourceKey 是包内原本的 slug,不是安装后的 slug。 作者叫 settings 的表装进来叫 busa-email-settings,而戳上写的仍然是 settings。应用正是靠这个稳定的 handle 找到自己的表,无论它被装进了哪个 Folder。
  • SDK 把没盖戳、或盖了别的戳的节点当成别人的,抛 SETUP_CONFLICT 而不去动它。所以两个写入方一旦漂移,一个从模板中心装完、又在终端里跑同一个 skill 的用户,就会在自己的数据上撞上硬冲突。这也是为什么这枚戳只在契约层定义一次、两边都不许各自重新声明;SDK 那份会在编译期与契约层对齐检查。

Skill 节点上的 isTemplateSkill 标记在两个方向上都有用:export --template 正是靠它把那个节点提回包根(而不是写进 content/ 造出这个格式本要消除的重复),而 agent 侧也正是把这些节点列为*"这个 space 里可用的 app 手册"*。


相关

On this page