Skip to content

manifest 规范

每个挂件包必须在根目录提供一份 manifest.json,描述元信息、入口、配置 schema 与可选的主题 schema。客户端依据该清单完成校验、安装、配置面板生成与运行时初始化。

1. 包结构

<widget-id>/
├── manifest.json        ← 必需
├── index.html           ← 入口(manifest.entry 指定,默认 index.html)
├── thumbnail.png        ← 可选:预览图(建议 16:9,≤ 200 KB)
├── assets/              ← 任意自定义资源
│   ├── app.js
│   ├── style.css
│   └── ...
└── README.md            ← 可选

打包为 .zip 时,manifest.json 必须位于 zip 的根目录(不可多包一层文件夹)。

2. 顶层字段

字段类型必需说明
manifestVersion1清单格式版本号;当前固定为 1
idstring挂件全局唯一标识;命名规则见 §6
namestring显示名称(1–40 字符)
versionstringsemver 语义化版本(x.y.z[-prerelease]
authorstring作者名(1–40 字符)
sdkVersion1目标 SDK 主版本,主播酱据此做兼容校验
defaultSize{width,height}添加到画布时的初始尺寸(像素)
descriptionstring一句话简介(≤ 200 字符)
homepagestring作者主页或仓库 URL
thumbnailstring预览图路径(包内相对路径)
entrystring入口 HTML 文件,默认 "index.html"
minSize{width,height}允许缩放的最小尺寸
maxSize{width,height}允许缩放的最大尺寸
keepAspectRatioboolean缩放时锁定宽高比,默认 false
platformsLivePlatform[]限定可用的直播平台;省略表示全部
categoryWidgetCategory元素分类,决定在素材面板/挂件库/市场中归属哪一组;默认 "custom",见 §2.1
configWidgetConfigField[]功能配置 schema,见 §3
themeWidgetConfigField[]外观主题 schema,结构同 config,见 §3
permissionsWidgetPermission[]能力声明,见 §4

platforms 字段的合法取值为 平台事件 列出的五种:bilibilidouyinkuaishoushipinhaoxiaohongshu

2.1 元素分类 category

category 决定挂件在客户端中的分类归属,仅作元数据:挂件运行时始终以 CustomWidget 元素 + iframe 形式渲染,与该字段取值无关,不会替换内置元素的实现。它影响三处展示:

  • 素材面板:声明对应分类后,挂件会出现在该分类菜单下与同类内置元素并列。例如 category: "overtime-machine" 的挂件可在「素材 → 倒计时机」中直接选用,对终端用户像是「同类型的不同实现」。
  • 创意工坊「我的挂件」列表:按分类分组展示。
  • 市场上架(路线图第二阶段)category 即市场 SKU 的频道分类。

合法取值与主播酱内置元素类型的 PascalCase 名称一一对应,外加 custom 作为默认的「无对应」标记:

取值对应内置 ElementType 枚举键中文
custom (默认)自定义(新玩法,无可对应内置元素)
TextElementType.TEXT文本
ImageElementType.IMAGE图片
VideoElementType.VIDEO视频
GiftMenuElementType.GIFT_MENU礼物菜单
OvertimeMachineElementType.OVERTIME_MACHINE加班机
LikeRankElementType.LIKE_RANK点赞榜
WishListElementType.WISH_LIST心愿单
ChatRendererElementType.CHAT_RENDERER弹幕姬

category 字符串与主播酱内置元素类型名一一对应,无需中间映射。

选择规则:

  • 挂件功能上等价或扩展了某个内置元素 → 选对应分类,让用户在熟悉入口能找到;
  • 挂件实现全新玩法(无可对应的内置元素)→ 用 custom 或省略字段;
  • 拿不准时优先 custom:分类错位的体验损失(用户从「加班机」入口拖入却拿到一个非加班机的挂件)比放在自定义分类下更差。

字段省略时按 custom 处理。挂件作者无需因为声明了 category 而改变挂件实现——分类只是入口标签。

3. 配置字段 WidgetConfigField

configtheme 共用同一套字段结构。区别仅在语义与渲染位置:

  • config → 渲染在配置面板的「配置」Tab 主区;
  • theme → 渲染在「样式」Tab。
ts
interface WidgetConfigField {
  /** 配置键名;写入 element.config[key] 与 CSS 变量 --zbj-<key>;须为 CSS 标识符安全字符 */
  key: string;
  /** 控件类型,见 §3.1 */
  type: WidgetFieldType;
  /** 面板显示标签 */
  label: string;
  /** 默认值,类型须与 type 匹配 */
  default: unknown;
  /** 字段说明 */
  tip?: string;
  /** 分组名;同组字段在面板中归到一起 */
  group?: string;
  /** text / textarea */
  placeholder?: string;
  maxlength?: number;
  /** textarea */
  rows?: number;
  /** number / slider */
  min?: number;
  max?: number;
  step?: number;
  /** color */
  alpha?: boolean;
  /** select */
  options?: { label: string; value: string | number }[];
  /** list:子字段 schema(不可再嵌套 list) */
  fields?: WidgetConfigField[];
  /** 条件显隐:config[showIf.key] 的值 ∈ showIf.in 时该字段才显示 */
  showIf?: { key: string; in: unknown[] };
}

3.1 字段类型表

type渲染控件额外属性值类型
textn-inputplaceholdermaxlengthstring
textarean-input(多行)placeholdermaxlengthrowsstring
numbern-input-numberminmaxstepnumber
slidern-sliderminmaxstepnumber
colorn-color-pickeralphastring#rrggbbrgba()
switchn-switchboolean
selectn-selectoptionsstring | number
image选图按钮(调起主播酱的文件选择 + 本机图片服务)string(URL)
fontn-select(复用 useFontListstring(字体名)
giftPicker礼物选择器(弹窗式礼物挑选 UI){ id: string | number; name: string; icon: string }
list可增删的重复项卡片fields(子 schema)object[]

3.2 条件显隐 showIf

字段仅在 config[showIf.key] 的值 ∈ showIf.in 时显示,常用于让一组字段在「模式选择」下联动出现:

jsonc
{ "key": "mode", "type": "select", "label": "模式", "default": "simple",
  "options": [
    { "label": "简单", "value": "simple" },
    { "label": "高级", "value": "advanced" }
  ]
},
{ "key": "advancedColor", "type": "color", "label": "高级配色",
  "default": "#ffffff",
  "showIf": { "key": "mode", "in": ["advanced"] }
}

3.3 重复项 list

值是对象数组,每个对象按 fields 子 schema 渲染一张卡片:

jsonc
{
  "key": "rules",
  "type": "list",
  "label": "触发规则",
  "default": [],
  "fields": [
    { "key": "keyword", "type": "text", "label": "关键词", "default": "" },
    { "key": "count", "type": "number", "label": "触发次数", "default": 1 }
  ]
}

fields 内不得再出现 type: "list"(仅支持一层重复项)。list 字段不参与 CSS 变量注入(仅 JS 可读)。

4. 能力声明 permissions

枚举:"gifts" | "roomInfo" | "storage"

取值含义
gifts调用 ZBJ.invoke("getGifts")
roomInfo调用 ZBJ.invoke("getRoomInfo")
storage使用 ZBJ.storage.* 持久化实例数据

当前阶段为声明性字段,仅用于展示与文档生成,不做强制拦截;后续若开放公开市场再转为强制(详见 版本策略)。

5. 完整示例

jsonc
{
  "manifestVersion": 1,
  "id": "gift-roll",
  "name": "礼物滚动榜",
  "version": "1.0.0",
  "author": "主播酱官方",
  "description": "把礼物记录做成自动滚动的列表",
  "homepage": "https://example.com/widgets/gift-roll",
  "sdkVersion": 1,
  "entry": "index.html",
  "thumbnail": "thumbnail.png",
  "defaultSize": { "width": 380, "height": 560 },
  "minSize": { "width": 200, "height": 200 },
  "platforms": ["bilibili", "douyin", "kuaishou", "shipinhao", "xiaohongshu"],
  "category": "custom",
  "permissions": ["gifts"],

  "config": [
    { "key": "title", "type": "text", "label": "标题",
      "default": "礼物榜", "group": "基础" },
    { "key": "max", "type": "slider", "label": "最多条数",
      "default": 20, "min": 5, "max": 50, "step": 1, "group": "基础" },
    { "key": "showIcon", "type": "switch", "label": "显示礼物图标",
      "default": true, "group": "基础" },
    { "key": "onlyGift", "type": "giftPicker", "label": "只统计指定礼物",
      "default": null, "tip": "留空则统计全部礼物", "group": "进阶" }
  ],

  "theme": [
    { "key": "accent", "type": "color", "label": "强调色",
      "default": "#63e2b7", "alpha": true },
    { "key": "fontSize", "type": "number", "label": "字号",
      "default": 16, "min": 10, "max": 48 }
  ]
}

6. 校验规则

宿主端在安装 / 加载挂件时按以下顺序校验:

6.1 JSON Schema 校验

jsonc
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["manifestVersion", "id", "name", "version", "author", "sdkVersion", "defaultSize"],
  "additionalProperties": false,
  "properties": {
    "manifestVersion": { "const": 1 },
    "id": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9]*([-.][a-z0-9]+)*$",
      "maxLength": 64
    },
    "name":        { "type": "string", "minLength": 1, "maxLength": 40 },
    "version":     { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$" },
    "author":      { "type": "string", "minLength": 1, "maxLength": 40 },
    "description": { "type": "string", "maxLength": 200 },
    "homepage":    { "type": "string", "format": "uri" },
    "sdkVersion":  { "const": 1 },
    "entry":       { "type": "string" },
    "thumbnail":   { "type": "string" },
    "defaultSize": { "$ref": "#/$defs/size" },
    "minSize":     { "$ref": "#/$defs/size" },
    "maxSize":     { "$ref": "#/$defs/size" },
    "keepAspectRatio": { "type": "boolean" },
    "platforms": {
      "type": "array",
      "items": { "enum": ["bilibili", "douyin", "kuaishou", "shipinhao", "xiaohongshu"] }
    },
    "category": {
      "enum": [
        "custom",
        "Text", "Image", "Video",
        "GiftMenu", "OvertimeMachine", "LikeRank", "WishList", "ChatRenderer"
      ],
      "default": "custom"
    },
    "config":      { "type": "array", "items": { "$ref": "#/$defs/field" } },
    "theme":       { "type": "array", "items": { "$ref": "#/$defs/field" } },
    "permissions": {
      "type": "array",
      "items": { "enum": ["gifts", "roomInfo", "storage"] }
    }
  },
  "$defs": {
    "size": {
      "type": "object",
      "required": ["width", "height"],
      "additionalProperties": false,
      "properties": {
        "width":  { "type": "number", "exclusiveMinimum": 0 },
        "height": { "type": "number", "exclusiveMinimum": 0 }
      }
    },
    "field": {
      "type": "object",
      "required": ["key", "type", "label", "default"],
      "properties": {
        "key":  { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "maxLength": 40 },
        "type": {
          "enum": ["text", "textarea", "number", "slider", "color",
                   "switch", "select", "image", "font", "giftPicker", "list"]
        },
        "label":   { "type": "string", "minLength": 1 },
        "default": {},
        "tip":     { "type": "string" },
        "group":   { "type": "string" },
        "placeholder": { "type": "string" },
        "maxlength":   { "type": "number" },
        "rows":        { "type": "number" },
        "min":         { "type": "number" },
        "max":         { "type": "number" },
        "step":        { "type": "number" },
        "alpha":       { "type": "boolean" },
        "options": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["label", "value"],
            "properties": {
              "label": { "type": "string" },
              "value": { "type": ["string", "number"] }
            }
          }
        },
        "fields": { "type": "array", "items": { "$ref": "#/$defs/field" } },
        "showIf": {
          "type": "object",
          "required": ["key", "in"],
          "properties": {
            "key": { "type": "string" },
            "in":  { "type": "array" }
          }
        }
      }
    }
  }
}

6.2 语义校验

规则错误码说明
id 命名INVALID_ID小写字母 / 数字 / 连字符 / 反向域名段,长度 ≤ 64
id 唯一性ID_CONFLICT与已安装挂件冲突时拒绝或提示覆盖
sdkVersion 兼容SDK_VERSION_UNSUPPORTED超出宿主支持上限时拒绝并提示升级客户端
入口存在性ENTRY_MISSINGentry(默认 index.html)所指文件须存在于包内
config / theme 键唯一KEY_CONFLICT两个数组的所有 key 合并后不得重复(共享同名 CSS 变量)
select.default 合法SELECT_DEFAULT_INVALID必须 ∈ options 的某个 value
list.fields 合法NESTED_LISTlist 必须有 fields,且 fields 内不得再出现 type: "list"
showIf.key 有效SHOWIF_KEY_INVALID指向的 key 必须在同一挂件的 config / theme 中存在
min ≤ maxRANGE_INVALIDnumber / slider 字段、minSize ≤ maxSize
default 类型匹配DEFAULT_TYPE_MISMATCH与字段 type 对应的值类型一致
解压路径穿越ZIP_PATH_TRAVERSAL.zip 每个条目路径不得含 ..,不得为绝对路径
包体积上限PACKAGE_TOO_LARGE默认 50 MB
category 取值CATEGORY_INVALID必须为 §2.1 列举的九种之一;未声明等价于 custom

校验失败的返回结构(来自 IPC widget:install / widget:get-manifest):

ts
type ManifestValidationError = {
  ok: false;
  code: string;       // 上表错误码之一
  message: string;    // 面向用户的本地化消息
  field?: string;     // 可选:触发错误的字段 path(如 "config[2].options")
};