Files

12 KiB
Raw Permalink Blame History

插件开发参考

English

第一次开发请先阅读完整开发流程和发布指南。

Dock 右侧快捷入口为与主窗口同朝向的弧面临时浮层,常用/已安装/设置三个底部 tab;插件管理从设置打开独立的原生 Dock 大窗口。安装表单与插件源设置只在管理窗口中出现。

示例源码在 examples/showcase;React SDK 在 sdk/src/index.tsx。发行包 tools/source 提供完整 UI/SDK 示例构建工作区。插件页面需要自行打包为浏览器 JS,调用 registerPlugin({QuickPage, WindowPage}) 挂载 React,宿主先加载桥接 bootstrap。普通 bundle 插件 iframe 为 sandbox="allow-scripts" 的独立不透明来源;通过父窗口转发被允许的 SDK 请求,不能直接调用管理员 HTTP API。

Manifest

{
  "schemaVersion": 1,
  "apiVersion": 1,
  "id": "example.plugin",
  "name": "Example",
  "author": "Developer",
  "version": "1.0.0",
  "description": "Example plugin",
  "backend": {"entry": "backend.py", "runAs": "steamos", "autostart": false},
  "ui": {
    "quickPage": "page.js",
    "windows": {"main": {"entry": "page.js", "title": "Example", "dockIcon": true}}
  },
  "files": {}
}

backend 可省略。runAs 只支持 steamos 和 root,省略时为 steamos;autostart 默认 false。不再使用 permissions 字段,窗口、通知和联网无需权限声明。后端使用设备网络,普通插件页面允许 HTTP/HTTPS 请求及 WebSocket。

ID 只允许小写字母、数字、点、短横线、下划线;载荷路径限 ASCII 字母/数字、/._-+,禁止绝对路径、空段、.、..。入口必须在 files 中;pack 会自动计算 files,不必手写哈希。后端入口须可直接执行,脚本需要正确 shebang(示例 /usr/bin/python3)。Python/系统依赖不会由 Framely 自动下载。

版本不可覆盖,相同版本再次发布需要使用新版本号。运行身份变化会选择新的数据目录 /var/lib/framely/data/<id>/<identity>;旧身份数据保留,安装使用旧身份的版本可恢复访问,不自动跨身份迁移文件。

React API

import {framely, registerPlugin, Button, Section} from '@framely/sdk';

function QuickPage() {
  return <Section title="Example">
    <Button onClick={() => framely.windows.open('main')}>打开窗口</Button>
    <Button onClick={() => framely.call('save', {text: 'hello'})}>保存</Button>
    <Button onClick={() => framely.notifications.send({
      id: 'download', title: '完成', body: '下载已完成',
      durationMs: 8000, actions: [{id: 'open', label: '打开', icon: '↗'}]
    })}>发送通知</Button>
  </Section>;
}
registerPlugin({QuickPage});

窗口 key 必须在 manifest 中声明;framely.windows.close(key) 关闭。关闭页面不会停止后端。通知 ID 用于更新/撤回;framely.notifications.remove(id) 撤回。图片为 PNG/JPEG data URL(最多 1 MB)或 HTTPS URL。时长 1–60 秒,每插件每 10 秒最多 10 次发送。

framely.onEvent(callback) 返回取消订阅函数;接收 {type, data}。多个窗口分别按游标接收同一后台事件。文本输入框获得焦点时请求 SteamVR 虚拟键盘,完成后通过原生 setter 触发 React input/change;不使用网页 prompt。

后端协议

每行一个 JSON,stdin 请求、stdout 响应。stdout 只写协议,日志写 stderr(单文件最多约 2 MiB,保留一份轮转备份)。每条消息最多 64 KiB,输入写入超时 5 秒或响应超时 15 秒会停止后端,下次打开可重试。

{"id":1,"method":"save","params":{"text":"hello"}}
{"id":1,"result":{"saved":"hello"}}
{"id":2,"error":"Unknown method"}
{"event":"progress","data":{"percent":50}}

系统传入 FRAMELY_PLUGIN_ID、FRAMELY_DATA_DIR 和对应身份 HOME。通知按钮向后端调用 notification.action,参数 {id, action}。后端主动发送通知使用 {"event":"notification","data":{...通知...}},受同一 manifest 与频率限制。

打包与源发布

framely pack --manifest manifest.json --payload payload --output example.plugin-1.0.0.framely
framely verify example.plugin-1.0.0.framely
framely catalog --name 'My plugins' --base-url https://example.org/plugins --packages ./packages --output ./packages/catalog.json

包中包含 manifest.json 和文件载荷,files 记录每个载荷的 SHA256;不生成公钥或签名。只支持 manifest.json 与清单中声明的载荷文件,拒绝未声明的附加文件。

传统源可静态托管 catalog.json 及相对路径下的 plugins//versions.json;插件包可放在作者自己的 GitHub Release。在插件源管理中添加目录完整 URL;当前代码首次初始化加入社区 stable/testing 来源,实际内容以服务器为准。目录条目有版本、下载 URL、SHA256。插件库也支持 GitHub 自动发现及 release.json,同 ID 合并展示并按兼容语义版本推荐,历史版本保留不同来源及哈希。插件记录所选来源,切换来源仍经过安装确认。HTTP 必须用户明确允许;目录和包支持最多五次重定向,HTTPS 不允许降级为 HTTP。

悬停反馈

Framely 主页面和插件 iframe 自动为可交互控件提供轻微的手柄振动,插件无需自行触发。进入按钮、链接、输入控件、可操作 ARIA role、可聚焦控件或 cursor:pointer 区域时触发一次;同一控件的子元素之间移动不会重复触发,禁用或 inert 控件不触发。自定义 React 控件应采用正确的 ARIA role,也可以加 data-framely-interactive 标记。

振动由 OpenVR 的当前激光控制器执行(12 ms、120 Hz、幅度 0.15),宿主只接受当前可见且被激光命中的视图,限制相邻脉冲间隔为 80 ms。插件发出的反馈消息由 iframe 宿主绑定到实际视图,不能指定另一个窗口或手柄。

插件库资料与图标

Manifest 可声明 icon(载荷内 PNG,最多 1 MiB、1024×1024)、details、tags、screenshots(最多 8 个载荷内 PNG/JPEG 路径)、changelog。这些文件会和页面一样加入文件哈希清单。图标用于插件列表和插件独立窗口的原生 Dock 缩略图,省略时使用默认拼图图标。

清单还支持三个可选网页地址,未填写的入口不显示:authorUrl(作者主页)、documentationUrl(插件文档)、homepage(插件主页)。地址必须是 HTTP 或 HTTPS,可包含文档锚点,不接受带用户名密码的链接。这些字段会保留在安装后的清单,并由 framely catalog 导出到插件库目录;作者主页链接直接显示在作者名称上,未提供时作者名称为普通文本;插件库列表、详情、管理弹窗和安装确认使用相同行为。文档和插件主页保留独立入口,点击后由系统默认浏览器打开。

{
  "authorUrl": "https://github.com/author",
  "documentationUrl": "https://github.com/author/plugin#readme",
  "homepage": "https://github.com/author/plugin"
}

插件仓库可声明发布信息:

{
  "downloadUrl": "https://github.com/author/plugin/releases/download/v1.0.0/plugin.framely",
  "publish": {
    "icon": "https://github.com/author/plugin/releases/download/v1.0.0/icon.png",
    "screenshots": ["https://github.com/author/plugin/releases/download/v1.0.0/screen.png"]
  }
}

上述为显式地址示例,downloadUrl 并非源码必填字段:GitHub 社区登记可按仓库、v<version> 和 <id>-<version>.framely 自动生成。包内仍需最终地址,当前 pack 不会自动读取 Git remote;按发布指南生成临时清单即可。社区图标通常只需 icon,数据库从固定源码提交生成并校验 Raw 地址;无需额外 publish.icon。本地 catalog CLI 使用 publish 外部图片,省略时不会导出包内图标/截图,插件库采用默认图标。包内资源仍用于已安装插件。

插件数据库只登记 submodule 固定提交,读取该提交的清单,补全默认下载地址,先取得 Release digest 或源码 downloadSha256 中的预期整包哈希,再下载并比对 SHA256、清单声明和载荷。发布分支提供各渠道目录及历史版本 JSON,不保存插件包或图片,不需要 entries 登记。数据库 CI 自动展示版本和运行用户变化,发布时禁止同 ID、版本更换 SHA256;不执行作者代码,不要求逐插件人工源码审核,也不验证源码与二进制可复现关系。

framely catalog 也只生成 JSON:下载 URL 优先使用 downloadUrl,没有该字段时采用 --base-url 加包文件名。图标与截图来自 publish 的外部 URL,不复制资源。插件库支持关键词、标签、来源、已安装和可更新筛选;源暂时不可用时显示会话缓存。安装使用已经校验的同一份包,不在确认后重新下载。

后端默认按失败自动恢复,包括已经启动的按需插件;可用 backend.restart: "never" 关闭。默认第三次连续失败后停用,运行稳定一分钟后重置计数,详见生命周期。

脚手架与预览

node tools/plugin-dev.mjs init ./my-plugin example.my-plugin
cd my-plugin
npm install
npm run dev
npm run build

发行包中的工具位置为 tools/source/tools/plugin-dev.mjs。预览监听本机 127.0.0.1:5173,修改页面后自动重载;它不会启动后端或获得 Frame 系统权限。窗口和通知在浏览器里模拟,身份、键盘、振动及后台调用仍需安装到设备测试。

SDK 提供 Section、Button、Toggle、Slider、TextField、Select、Tabs、Notice、useBackend、usePluginEvent。Select 使用页面内弹层,兼容离屏 CEF。registerPlugin 还可传入 windows: {main: Component, other: Component} 为多个声明窗口选择不同 React 页面。

通知按钮向后端发送 notification.action,同时广播给当前打开的插件页面,事件为 {type:'notification.action',data:{id,action}};无后端插件可以通过 framely.onEvent 接收。

插件安装、更新、停止和异常恢复的约定见生命周期开发说明。

窗口可选声明 width、height(逻辑像素)和 widthMeters(VR 物理宽度,米)。默认 1600×900(16:9)、VR 宽度 3 米;像素范围为宽 640–2560、高 360–1440,物理宽度 0.4–4.0 米。示例:{"entry":"page.js","title":"温控","dockIcon":true,"width":1600,"height":900,"widthMeters":3.0}。尺寸在重新打开窗口时生效。

依赖、可选依赖、冲突和独占资源见插件关系,多个插件源聚合见源订阅。

插件只使用 tags 描述用途,不再使用分类;为兼容已发布的包,旧 category 字段读取时忽略,新的清单及目录不再导出它。插件数据库每次生成目录时同时生成 tags.json,格式为 { "schemaVersion": 1, "tags": ["工具", "温控"] },聚合当前目录全部插件的标签、精确去重并排序。stable/testing 分别生成自己的标签文件,不生成跨渠道并集;程序加载多个源后在本地聚合去重。标签随插件更新自动增删,无须手工维护。

上传的插件包、插件库下载及其依赖包暂存于 /tmp/framely-package-*,确认安装后才写入插件目录。取消、安装完成或失败后删除暂存包;未完成上传和待确认安装包闲置 15 分钟后过期,每 30 秒清理一次。

本机网页窗口在 ui.windows.<key> 设置 localWeb: true,宿主通过后端 window.get 获取窗口 URL;这是窗口加载方式,不是权限声明。URL 使用 http://localhost:<端口>/framely-window/<key>。普通窗口无需设置此项。

插件库主目录 catalog.json 中每个插件 ID 仅出现一次,代表最新版。历史条目存放在相对目录 plugins/<插件ID>/versions.json,格式为 { "schemaVersion": 1, "id": "作者.插件", "versions": [...] };版本条目沿用目录条目的字段,列表不包含主目录的最新版。插件库进入详情时按需读取历史,依赖解析仅在最新版不满足范围时读取历史。附加目录字段可扩展,读取端忽略未知字段,但校验已知字段;改变必需语义应升级 schemaVersion。插件安装包清单仍严格校验。