Plugin Manifest reference
每个插件在其根目录下都有一个 plugin.manifest.json 文件。 这是插件身份、所需权限、允许调用的网络目标、插件贡献的管理页面,以及为查看器 UI 添加的操作按钮的可信来源。
Plugins require Owncast 0.3.0 or later.
管理员在安装插件之前会审查清单。 主机在加载时解析它并强制执行每个声明。 编译后的插件中的任何内容都不能授予清单未请求的能力。
清单是描述插件的简单 JSON,与您编写代码的语言无关。 有关语言特定的细节,请参阅 JavaScript 或 Python SDK 参考。
最小清单
{
"api": "1",
"name": "My Plugin",
"version": "0.1.0",
"description": "Short description for admins",
"permissions": []
}
api、name 和 version 是必填项。 其他都是可选的,仅在您使用相应功能时需要。
顶级字段
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
api | 字符串 | 是 | 清单模式版本。 当前为 "1"。 |
name | 字符串 | 是 | 显示在管理员列表和注册卡中的人类可读名称。 示例:"了不起的回声机器人"。 |
slug | 字符串 | 否 | 规范标识符(URL 前缀、配置命名空间、文件名)。 如果省略,则自动由name派生。 见下文。 |
version | 字符串 | 是 | 您插件的版本。 SemVer recommended. Informational metadata for admins and the registry: the host doesn't gate loading on it. |
description | 字符串 | 否 | 管理员在插件列表中看到的单句总结,以及安装过程中。 |
category | 字符串 | 否 | Registry browse category. See category. |
permissions | 字符串[] | 否 | 您插件所需的功能列表。 查看 权限。 |
config | 对象 | 否 | 管理员可配置的设置,您的插件在运行时读取。 查看 配置。 |
bot | 对象 | 否 | 聊天机器人配置。 查看 bot。 |
network | 对象 | 否 | 出站 HTTP 允许列表,当授权 network.fetch 时必需。 见下文。 |
actions | 对象[] | 否 | 要添加到查看器 UI 的操作按钮。 查看 UI: 操作按钮。 |
admin | 对象 | 否 | 要添加到 Owncast 管理 UI 的管理页面。 查看 UI: 管理页面。 |
styles | 字符串[] | 否 | 嵌入到查看器页面中的 CSS 文件。 见 styles。 |
scripts | 字符串[] | 否 | 嵌入到查看器页面中的 JavaScript 文件。 见 scripts。 |
extraPageContent | 对象 | 否 | 声明一个 slug 和一个可选的 HTML 文件,插入到查看器的额外内容块之前。 见 extraPageContent。 |
tabs | object | no | Viewer-page tabs keyed by stable slug. 见 tabs。 |
name 和 slug
name 是人类可读的显示名称。 它可以包含任何字符,包括空格和标点符号,并且是管理员在插件列表中看到的内容,显示在注册浏览卡上,以及默认的聊天机器人的身份。
slug 是规范标识符。 它控制:
- 插件的 URL 前缀:
/plugins/<slug>/... - 配置(键值存储)命名空间
- 生成的构建产物的文件名(
<slug>.ocpkg) - 插件注册表中的主键
Slug 是小写字母、数字和连字符,以字母开头,最多 64 个字符。 SDK 自动从 name 派生一个 slug,当省略时:空格和标点符号合并为一个连字符,字母小写。 "了不起的回声机器人" 变为 awesome-echo-bot。 在自动派生不符合您的想法时,或当您的显示名称使用 ASCII 以外字符时,明确指定 slug(如 "咖啡馆助手" 将产生 caf-helper)。
避免在发布后更改 slug:重命名将使管理员看到不同的插件,带有新的配置存储。 更改 name(仅显示)是安全的。 它不会改变身份。
category: registry browse category
An optional label that places your plugin in a browse category on the registry and in the admin UI. The canonical values are chat-bots, chat-filters, moderation, authentication, themes, overlays, notifications, integrations, video, analytics, games, admin-utilities, examples, and other.
The SDK's packaging CLI warns when category isn't one of these, but nothing rejects it: the host and registry tolerate unknown categories, they just won't match any browse filter.
bot:聊天机器人身份
发布到聊天(使用 owncast.chat.send)的插件在聊天机器人用户下出现。 默认情况下,机器人以插件的显示 name 显示。 用 bot.displayName 重写:
{
"name": "Stream Sidekick",
"bot": {
"displayName": "Sidekick"
}
}
在聊天中,机器人以 "助手" 而不是 "流媒体助手" 的身份发帖。 插件第一次加载时, Owncast 提供了一个持久的聊天用户,其键为插件的 slug(以便机器人身份不会在重新安装和显示名称更改中丢失)。
bot.displayName 仅与具有 chat.send 权限的插件相关。 否则将被忽略。
config:管理员可配置设置
在这里声明类型设置,Owncast 将在管理员中呈现一个可编辑的表单,您的插件将在运行时通过 owncast.config.get 读取。 每个条目都有一个 type(string、number 或 boolean)、一个 default 和一个 description:
{
"config": {
"greeting": { "type": "string", "default": "welcome!", "description": "First-join message" },
"cooldownMs": { "type": "number", "default": 2000, "description": "Per-user command cooldown" },
"modOnly": { "type": "boolean", "default": false, "description": "Restrict to moderators" }
}
}
Config keys starting with __ are reserved: the host uses that prefix to inject per-instance state into the plugin runtime, and a manifest declaring one is rejected at load.
完整覆盖,包括表单呈现方式、凭证屏蔽、验证,以及覆盖存储位置,请参阅 配置。
permissions
每个条目都解锁一部分主机 API。 主机拒绝对未声明权限的方法的调用。
{
"permissions": ["chat.send", "storage.kv", "network.fetch"]
}
请参阅 权限参考 获取完整的标识符列表及其授予的内容。
network:出站 HTTP 允许列表
network.fetch 受主机名显式允许列表的限制。 如果您在 permissions 中声明 network.fetch,则还需要一个 network.allowedHosts 字段列出您将调用的主机:
{
"permissions": ["network.fetch"],
"network": {
"allowedHosts": ["api.discord.com", "*.weather.com"]
}
}
条目是主机名通配符。 像 api.discord.com 这样的裸名精确匹配。 * 是一个通配符段,因此 *.weather.com 匹配 api.weather.com 和 data.weather.com,但不匹配 weather.com 本身或 evil.com。
通配符 "*" 匹配任何主机,但您必须明确写出它:
{
"network": { "allowedHosts": ["*"] }
}
这是故意的。 审查清单的管理员会看到他们授予的范围。 大多数插件应列出它们调用的具体主机。
如果在没有 allowedHosts 条目的情况下授予 network.fetch,主机将拒绝加载。
actions:操作按钮
操作按钮是可点击条目,而 Owncast 在流下方展示。 当您的插件启用时,主机将其条目合并到 Owncast 已显示的列表中。
{
"permissions": ["ui.modify", "http.serve"],
"actions": [
{
"title": "Chat Overlay",
"description": "Open the live chat overlay",
"url": "/",
"icon": "/star.png",
"color": "#3b82f6"
},
{
"title": "Issue tracker",
"url": "https://github.com/example/my-plugin/issues",
"openExternally": true
},
{
"title": "About this stream",
"html": "<p>Live every weekday at 8pm UTC.</p>"
}
]
}
每个条目:
| 字段 | 类型 | 备注 |
|---|---|---|
title | string | 必需。 按钮标签。 |
url | string | 绝对的 https://... URL 或路径。 与 html 相互排斥。 |
html | 字符串 | 在内联模态中呈现的原始 HTML。 与 url 相互排斥。 |
icon | 字符串 | 在按钮上显示的可选图像 URL。 与 url 相同的路径规则。 |
color | 字符串 | 按钮背景的可选十六进制颜色。 |
description | 字符串 | 可选。 在为基于 URL 的操作打开的模态中显示。 |
openExternally | 布尔值 | 如果 true,则该 URL 在新标签页中打开,而不是在内联模态中打开。 |
主机在加载时强制的规则:
- 需要
ui.modify权限。 如果没有,清单将被拒绝。 - 每个条目必须正好有一个
url或html。 - 以
/开头的相对 URL(和图标)自动前缀为您的插件命名空间。"/"变为/plugins/my-plugin/。"/star.png"变为/plugins/my-plugin/star.png。 避免硬编码您的插件名称。 - 解析为您的命名空间的 URL(和图标)需要
http.serve权限,因为您是提供这些内容的。 - 指向另一个插件命名空间的 URL(和图标)将被拒绝。 捕捉拼写错误,防止一个插件宣传另一个插件的 UI。
在 UI: 动作按钮 中有完整覆盖。
admin: 管理页面
Plugins can register pages that appear in the Owncast admin UI under Plugins. The pages object is keyed by plugin-relative path glob:
{
"permissions": ["http.serve"],
"admin": {
"pages": {
"/admin": { "title": "Settings", "icon": "gear" }
}
}
}
每个条目有:
| Part | 类型 | 备注 |
|---|---|---|
| object key | 字符串 | Required path glob under the plugin's namespace, such as "/admin" or "/admin/*". |
title | 字符串 | 必填。 在管理 UI 中显示的选项卡标签。 |
icon | 字符串 | 可选。 一个简短的语义名称(gear, wrench, user,等等)。 |
The host derives each page path from its object key. A key of "/admin" maps to /plugins/<your-slug>/admin. Requests matching any key are auth-gated by the host, so unauthenticated requests get a 401 before your plugin code runs.
JSON object order is not significant. Owncast displays admin pages in lexicographic path order. pages must be an object. Do not add a path member to a page value. The host rejects arrays and page values containing the legacy path member.
在 UI: 管理页面 中有完整覆盖。
styles: CSS 注入
插件对查看者页面贡献的 CSS 文件列表。 每个文件的内容都内联到 Owncast 已经使用的同一个 <style> 块中,以便插件可以在页面上进行主题设置,而不需要每个贡献都有自己单独的 <link> 标签。
{
"permissions": ["ui.modify"],
"styles": ["theme.css", "overrides.css"]
}
路径规则匹配动作按钮 URL:
- 像
"theme.css"这样的裸路径自动前缀为您的插件命名空间。 - 像
"/theme.css"这样的单斜杠路径获得相同处理。 - 完全合格的
/plugins/<your-slug>/...路径直接通过。 - 另一个插件命名空间中的路径将被拒绝。
http://和https://URLs 将被拒绝。 将外部资产(字体、图片)捆绑在一起,并通过@font-face或url(...)在您的 CSS 中引用它们,以便管理审查清单时能看到每个将出现在他们页面上的文件。- 每个条目必须以
.css结尾。
仅需要 ui.modify (插件在 Owncast 的 chrome 内绘制)。 不需要 http.serve : 每个文件的字节从 assets/ 中读取并直接内联到 /api/config 中的 customStyles 中,而不是通过 URL 提供。 The host emits a /* plugin: <your-slug> ... */ comment in front of each contribution so a reader can attribute a rule back to whichever plugin shipped it.
对于依赖插件状态的 CSS,onPageStyles 处理程序在请求时返回它,没有清单字段。 其输出在这些静态文件之后追加到 customStyles。
在 UI: 查看器样式表 中有完整覆盖。
scripts: JavaScript 注入
插件对查看器页面贡献的 JavaScript 文件列表。 每个文件的内容被附加到管理员的自定义 JavaScript 已经来源的相同响应中(/customjavascript),因此插件可以扩展页面,而不需要每个贡献都有自己单独的 <script> 标签。
{
"permissions": ["ui.modify"],
"scripts": ["client.js"]
}
路径规则和所需权限与 styles 匹配,应用于 .js 文件(仅需要 ui.modify,主机从 assets/ 中读取并内联到 /customjavascript 中)。 将您的脚本包裹在 IIFE 中,以便顶级声明不会与管理员的 JavaScript 或其他插件发生冲突。 主机在每个贡献前发出 // plugin: <your-slug> ... 注释,并将每个贡献包裹在 try/catch 中,以便一个插件的运行时错误不会破坏其他插件。
对于依赖插件状态的 JavaScript,onPageScripts 处理程序在请求时返回它,没有清单字段。 其输出在这些静态文件之后追加到 /customjavascript。
在 UI: 查看器脚本 中有完整覆盖。
extraPageContent: HTML 块
一个对象,向查看器的额外内容区域贡献一个 HTML 块,在 /api/config 的管理员文字上方插入。
{
"permissions": ["ui.modify"],
"extraPageContent": { "slug": "banner", "content": "content.html" }
}
| 字段 | 类型 | 备注 |
|---|---|---|
slug | 字符串 | 仅在 content 被省略时必填(主机将其传递给 onPageContent)。 否则可选。 小写字母、数字和连字符,以字母开头。 |
content | string | 可选。 指向 assets/ 中静态 HTML 文件的相对路径。 存在时,该文件的字节会直接内联。 省略时,主机将调用 onPageContent。 |
静态(带有 content):主机在请求时读取该文件并内联字节。 与 styles 和 scripts 具有相同的路径规则,适用于单个 .html 条目。 插件 HTML 绕过 markdown 处理器,因此标签和属性按写入的形式传递。
Dynamic (without content): implement onPageContent({ slug, user? }) in your plugin to return HTML at request time. 当内容应根据查看者或获取实时数据而变化时使用(例如,个性化问候或当前流统计)。 user 是查看者的聊天身份,在经过身份验证时存在。
需要 ui.modify。 不需要 http.serve,因为 HTML 是内联到配置响应中,而不是作为 URL 提供。 Each contribution is wrapped with an <!-- plugin: <your-slug> ... --> comment so a reader can attribute the markup back.
在 UI: 额外页面内容 中有完整覆盖。
tabs: 查看器页面选项卡
The tabs object contributes tabs to the viewer page's tab row next to the built-in About and Followers tabs. Each object key is the tab's stable slug. Every value requires title, and content is optional.
{
"permissions": ["ui.modify"],
"tabs": {
"music": { "title": "Music", "content": "music.html" },
"schedule": { "title": "Schedule", "content": "schedule.html" }
}
}
Each entry has:
| Part | 备注 |
|---|---|
| object key | Required stable slug. 小写字母、数字和连字符,以字母开头。 The host passes this key to onTabContent when content is omitted. |
title | 必填。 选项卡上显示的标签。 必须在插件的选项卡中唯一。 |
content | 可选。 指向 assets/ 中 HTML 文件的相对路径。 与 extraPageContent 相同的路径规则(自动前缀为您的命名空间,跨插件路径和 http(s):// URLs 被拒绝,必须以 .html 结尾)。 When omitted, the host calls onTabContent. |
Within each plugin, Owncast displays tabs in lexicographic slug order. JSON object order is not significant. Ordering between tabs from different plugins is unspecified. tabs must be an object. Do not add a slug member to a tab value. The host rejects arrays and tab values containing the legacy slug member.
需要 ui.modify。 http.serve is not required: each static tab's HTML is read from assets/ and inlined into the pluginTabs[] array on /api/config. For a dynamic tab, the host passes the object key to onTabContent as slug and inlines the returned HTML.
在 UI: 查看器页面选项卡 中有完整覆盖。
清单与运行时的契约
当您的插件加载时,主机解析清单并请求运行时注册自身。 It compares the two and rejects the load when:
- the slugs don't match (
slugis the canonical identity on both sides) - the runtime uses a permission that wasn't declared in the manifest
version is intentionally not compared. It's informational metadata the host gates nothing on, and the SDK bakes it into the registration from the same manifest at build time anyway.
您不需要自己编写注册:SDK 根据您定义的处理程序生成它(请参阅您的 SDK 参考 了解如何在您的语言中声明处理程序)。 了解此契约的存在在调试时很有用。 "运行时请求的权限未在清单中声明" 错误意味着您添加了一个需要您忘记列出的权限的处理程序。
完整示例
一个非平凡的清单,演示大多数功能:
{
"api": "1",
"name": "Stream Sidekick",
"slug": "stream-sidekick",
"version": "0.2.0",
"description": "Posts to Discord on stream start, shows an overlay, and adds a Donate button.",
"permissions": [
"chat.send",
"chat.filter",
"storage.kv",
"http.serve",
"http.sse",
"network.fetch",
"notifications.send",
"ui.modify"
],
"bot": {
"displayName": "Sidekick"
},
"network": {
"allowedHosts": ["api.discord.com", "*.example.com"]
},
"actions": [
{
"title": "Donate",
"url": "https://example.com/donate",
"openExternally": true
}
],
"admin": {
"pages": {
"/admin": { "title": "Sidekick settings", "icon": "gear" }
}
},
"styles": ["sidekick.css"],
"scripts": ["sidekick.js"],
"extraPageContent": { "slug": "intro", "content": "intro.html" },
"tabs": {
"schedule": { "title": "Schedule", "content": "schedule.html" }
}
}
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Gabe Kangas