主题
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. 顶层字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
manifestVersion | 1 | 是 | 清单格式版本号;当前固定为 1 |
id | string | 是 | 挂件全局唯一标识;命名规则见 §6 |
name | string | 是 | 显示名称(1–40 字符) |
version | string | 是 | semver 语义化版本(x.y.z[-prerelease]) |
author | string | 是 | 作者名(1–40 字符) |
sdkVersion | 1 | 是 | 目标 SDK 主版本,主播酱据此做兼容校验 |
defaultSize | {width,height} | 是 | 添加到画布时的初始尺寸(像素) |
description | string | 否 | 一句话简介(≤ 200 字符) |
homepage | string | 否 | 作者主页或仓库 URL |
thumbnail | string | 否 | 预览图路径(包内相对路径) |
entry | string | 否 | 入口 HTML 文件,默认 "index.html" |
minSize | {width,height} | 否 | 允许缩放的最小尺寸 |
maxSize | {width,height} | 否 | 允许缩放的最大尺寸 |
keepAspectRatio | boolean | 否 | 缩放时锁定宽高比,默认 false |
platforms | LivePlatform[] | 否 | 限定可用的直播平台;省略表示全部 |
category | WidgetCategory | 否 | 元素分类,决定在素材面板/挂件库/市场中归属哪一组;默认 "custom",见 §2.1 |
config | WidgetConfigField[] | 否 | 功能配置 schema,见 §3 |
theme | WidgetConfigField[] | 否 | 外观主题 schema,结构同 config,见 §3 |
permissions | WidgetPermission[] | 否 | 能力声明,见 §4 |
platforms 字段的合法取值为 平台事件 列出的五种:bilibili、douyin、kuaishou、shipinhao、xiaohongshu。
2.1 元素分类 category
category 决定挂件在客户端中的分类归属,仅作元数据:挂件运行时始终以 CustomWidget 元素 + iframe 形式渲染,与该字段取值无关,不会替换内置元素的实现。它影响三处展示:
- 素材面板:声明对应分类后,挂件会出现在该分类菜单下与同类内置元素并列。例如
category: "overtime-machine"的挂件可在「素材 → 倒计时机」中直接选用,对终端用户像是「同类型的不同实现」。 - 创意工坊「我的挂件」列表:按分类分组展示。
- 市场上架(路线图第二阶段):
category即市场 SKU 的频道分类。
合法取值与主播酱内置元素类型的 PascalCase 名称一一对应,外加 custom 作为默认的「无对应」标记:
| 取值 | 对应内置 ElementType 枚举键 | 中文 |
|---|---|---|
custom (默认) | — | 自定义(新玩法,无可对应内置元素) |
Text | ElementType.TEXT | 文本 |
Image | ElementType.IMAGE | 图片 |
Video | ElementType.VIDEO | 视频 |
GiftMenu | ElementType.GIFT_MENU | 礼物菜单 |
OvertimeMachine | ElementType.OVERTIME_MACHINE | 加班机 |
LikeRank | ElementType.LIKE_RANK | 点赞榜 |
WishList | ElementType.WISH_LIST | 心愿单 |
ChatRenderer | ElementType.CHAT_RENDERER | 弹幕姬 |
category 字符串与主播酱内置元素类型名一一对应,无需中间映射。
选择规则:
- 挂件功能上等价或扩展了某个内置元素 → 选对应分类,让用户在熟悉入口能找到;
- 挂件实现全新玩法(无可对应的内置元素)→ 用
custom或省略字段; - 拿不准时优先
custom:分类错位的体验损失(用户从「加班机」入口拖入却拿到一个非加班机的挂件)比放在自定义分类下更差。
字段省略时按 custom 处理。挂件作者无需因为声明了 category 而改变挂件实现——分类只是入口标签。
3. 配置字段 WidgetConfigField
config 与 theme 共用同一套字段结构。区别仅在语义与渲染位置:
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 | 渲染控件 | 额外属性 | 值类型 |
|---|---|---|---|
text | n-input | placeholder、maxlength | string |
textarea | n-input(多行) | placeholder、maxlength、rows | string |
number | n-input-number | min、max、step | number |
slider | n-slider | min、max、step | number |
color | n-color-picker | alpha | string(#rrggbb 或 rgba()) |
switch | n-switch | — | boolean |
select | n-select | options | string | number |
image | 选图按钮(调起主播酱的文件选择 + 本机图片服务) | — | string(URL) |
font | n-select(复用 useFontList) | — | string(字体名) |
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_MISSING | entry(默认 index.html)所指文件须存在于包内 |
config / theme 键唯一 | KEY_CONFLICT | 两个数组的所有 key 合并后不得重复(共享同名 CSS 变量) |
select.default 合法 | SELECT_DEFAULT_INVALID | 必须 ∈ options 的某个 value |
list.fields 合法 | NESTED_LIST | list 必须有 fields,且 fields 内不得再出现 type: "list" |
showIf.key 有效 | SHOWIF_KEY_INVALID | 指向的 key 必须在同一挂件的 config / theme 中存在 |
min ≤ max | RANGE_INVALID | number / 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")
};