11 KiB
SDK API 文档
SDK 源码位于 sdk/src/index.tsx、sdk/src/lifecycle.ts 和 sdk/python/framely.py。页面从 @framely/sdk 导入,Python 协议库由构建工具复制到 payload/framely.py。
页面注册
import {registerPlugin, Section} from '@framely/sdk';
function QuickPage() {return <Section title="我的插件">快捷内容</Section>;}
function MainWindow() {return <Section title="主窗口">详细内容</Section>;}
registerPlugin({QuickPage, windows: {main: MainWindow}});
QuickPage 必填。WindowPage 是独立窗口的默认组件;windows 可按清单窗口 key 指定不同组件。没有对应组件时回退到 WindowPage,再回退到 QuickPage;quick 用于快捷页路由,避免将它用作窗口 key。页面运行在宿主的 sandbox iframe,宿主先建立桥接再加载你的 bundle。注册函数会挂载 React、基础样式和滚动条。
宿主接口 framely
| API | 参数与返回 | 用途 |
|---|---|---|
framely.call<T>(method, params = {}) |
Promise<T> |
调用本插件后端方法;错误会 reject |
framely.windows.open(key) |
窗口 key;异步结果 | 打开已在 Manifest 声明的窗口 |
framely.windows.close(key) |
窗口 key;异步结果 | 关闭窗口,不停用后端 |
framely.notifications.send(notification) |
通知对象;异步结果 | 发送或按 ID 更新通知 |
framely.notifications.remove(id) |
通知 ID;异步结果 | 关闭弹窗并移除收件箱副本 |
framely.notifications.dismiss(id) |
通知 ID;异步结果 | 仅关闭弹窗,保留收件箱副本 |
framely.dependencies() |
Promise<DependencyStatus[]> |
查询本插件声明的依赖状态 |
framely.language.get() |
Promise<{preference, language}> |
获取语言偏好和当前生效语言 |
framely.onEvent(callback) |
返回取消订阅函数 | 接收宿主转发的事件 |
所有调用只通过插件桥接,不提供管理员 API 或跨插件后端调用。用 try/catch 或 .catch 处理失败,异步操作期间禁用重复提交。
try {
const result = await framely.call<{text: string}>('settings.get');
console.log(result.text);
} catch (error) {console.error(error);}
通知
await framely.notifications.send({
id: 'task', title: '完成', body: '文件已处理', inbox: true, durationMs: 8000,
actions: [{id: 'open', label: '打开', icon: '↗'}],
});
id、title、body 必填。inbox 默认 false:浮窗启用时,只有明确设置 inbox: true 的通知才进入快捷面板的“通知”收件箱。收件箱消息在弹窗到期、关闭弹窗、插件停用或重启、本体重启后都会保留;按钮策略、主动移除或卸载插件会移除消息。重启后恢复的收件箱消息不会重新弹出。
| 字段 | 默认值 | 行为 |
|---|---|---|
inbox |
false |
保存到收件箱;用户浮窗设置也可要求保存 |
durationMs |
8000 |
弹窗自动关闭倒计时,单位毫秒;支持 1000-60000,0 表示持续显示直到主动关闭 |
actions[].closeOnClick |
true |
此按钮操作成功后关闭弹窗 |
actions[].removeFromInboxOnClick |
true |
此按钮操作成功后移除收件箱副本 |
用户的通知设置优先于发送方偏好。全局浮窗或此插件的浮窗开关关闭时,Framely 不展示弹窗,并将消息转存到收件箱,即使 inbox 是 false。查看收件箱会清除启动图标的未读角标,但保留消息及按钮。插件不能覆盖用户的浮窗和角标设置。
两个按钮策略独立配置。例如查看状态后仍要保留消息,可将两个字段均设为 false:
await framely.notifications.send({
id: 'capture', title: '录制中', body: '当前录制正在进行。',
inbox: true, durationMs: 0,
actions: [{id: 'status', label: '查看状态',
closeOnClick: false, removeFromInboxOnClick: false}],
});
弹窗显示剩余关闭时间,收件箱显示接收时间。关闭弹窗保留其收件箱副本。framely.notifications.remove(id) 同时撤回弹窗和收件箱消息;framely.notifications.dismiss(id) 仅关闭弹窗。重复发送同 ID 会替换消息并重新开始倒计时。按钮操作失败时保留消息供重试。按钮使用 label 文字显示,旧版 icon 字符串保持可选兼容。
image 可选,接受 PNG/JPEG data URL 或 HTTPS URL,字符串最大 1 MiB。actions 最多 3 项,每项必填 id、label。通知和按钮 ID 遵循本体 ID 规则,按钮 ID 在同一通知内不得重复;title 最大 160 字节、body 4096 字节、按钮 label 非空且最多 80 字节、可选 icon 最多 16 字节。每插件每 10 秒最多发送 10 次,全局最多 64 个同时显示的弹窗及 256 条收件箱消息。达到上限后,新消息发送会报错,请先移除旧消息;替换同 ID 不额外占用位置。
按钮调用后端 notification.action,参数为 {id, action};后端调用成功后转发同名页面事件。纯前端插件可以监听该事件。插件停用时,其收件箱消息仍可查看和移除,操作按钮需重新启用插件后使用。
React hooks
| Hook | 返回与行为 |
|---|---|
useBackend<T>(method, params = {}) |
{data, error, loading, call};不会自动发起请求,主动调用 call();该调用失败仍会抛出异常 |
usePluginEvent(callback) |
自动订阅并在页面卸载时取消订阅 |
useDependencies() |
{items, error};自动加载依赖并在 dependencies.changed 时刷新 |
手动订阅时通过 React effect 返回取消函数:
React.useEffect(() => framely.onEvent(event => console.log(event)), []);
DependencyStatus 包含 id、required、constraint、version、enabled、matches、available 和可选 state.phase。constraint 为版本范围字符串或 {version, source};未安装时 version 为 null。关系参考
宿主语言变化转发 {type: "language.changed", data: {preference, language}};依赖变化转发 {type: "dependencies.changed"}。onEvent 的类型为 unknown,访问字段前应做类型检查。Python emit("progress", {...}) 转发为 {type: "progress", data: {...}}。
UI 控件
| 组件 | 主要属性 |
|---|---|
Section |
title、children |
Button |
标准 React button 属性,包括 onClick、disabled |
Toggle |
label、checked、onChange(boolean);可选 description、disabled |
Slider |
label、value、onChange(number);默认 min=0、max=100、step=1 |
TextField |
label、value、onChange(string);可选 multiline、password、placeholder、disabled |
Select |
label、options: {value,label}[]、value、onChange、disabled |
Tabs |
tabs: {id,label}[]、value、onChange(id) |
Notice |
children;error=true 显示错误 |
Select 默认单选,value 为 string;设置 multiple 时 value 为 string[],回调返回数组,可使用 emptyLabel 和 clearLabel。下拉在页面内渲染,适配离屏 CEF。
标准输入控件在 Frame 中自动连接 SteamVR 键盘,标准可交互控件自动获得悬停反馈。自定义控件应使用正确 ARIA role,可增加 data-framely-interactive。网页不自行控制其他窗口、手柄或系统账号。
Python 后端 SDK
from framely import serve, emit
def dispatch(method, params):
if method == 'ping':
emit('progress', {'percent': 100})
return {'ok': True}
raise ValueError('Unknown method')
serve(dispatch)
serve(dispatch, lifecycle=None) 读取逐行请求、调用处理器,并把返回值或异常转换成 JSON 响应。emit(event, data) 输出事件,页面侧收到 {type, data};notification 事件可主动发送通知。日志写 stderr,stdout 专用于协议。生命周期字典写法见生命周期文档。
TypeScript 生命周期辅助 API
从 @framely/sdk/lifecycle 导入 registerLifecycle、LifecycleContext、LifecyclePhase、LifecycleCallbacks。registerLifecycle(callbacks) 返回异步 (method, context) dispatcher;把保留的生命周期方法交给它处理。它不实现 stdin/stdout 协议、不启动进程,也不赋予网页后台权限。
预览与权限边界
npm run dev 只模拟窗口、通知及部分桥接调用;后端、真实依赖状态、VR 输入、振动和身份行为需要实机测试。插件页面不能直接调用管理面板 API,也不能调用 framely.lifecycle.* 保留方法。SDK 本身不声明权限,运行用户由 Manifest 决定。
头显可见性与自动暂停
framely.ui.getVisibility() 返回 { known, captureObscured, pageVisible, sequence, sessionId }。framely.ui.onVisibilityChanged(callback, onError?) 订阅后立即查询当前状态,返回取消订阅函数;useVisibility() 提供同样状态及 error。captureObscured 为原生快捷面板、Framely 管理器和全部插件窗口的真实可见性并集,排除通知和 dock 图标。pageVisible 表示当前插件所在原生视图可见;远程浏览器始终为 false。浏览器不能上报或修改头显状态。
录像后端应在 manifest 中声明 backend.uiVisibilityEvents: true,处理宿主专用 JSON-line RPC framely.ui.visibility,正常回复请求 id。参数包含 known、captureObscured、sequence 和 views,不包含前端专属的 pageVisible。启动后首次通知包含当前状态,之后在状态变化时通知。原生宿主超过两秒没有心跳时,known 变为 false;录像应在 !known || captureObscured 时暂停。页面关闭不影响后端通知。旧插件默认不订阅。Node/TS 可从 @framely/sdk/visibility 使用 registerVisibility,Python serve(dispatch, visibility=callback) 支持相同通知。
const off = framely.ui.onVisibilityChanged(state => {
console.log(state.known, state.captureObscured, state.pageVisible);
});
// 页面卸载时调用 off()
framely.ui.close() 关闭当前原生界面:快捷页关闭 Framely 面板,插件窗口关闭自身;不会停止插件后端或当前录制。需要包含此 bridge 方法的新版宿主。
独立插件窗口直接展示插件页面,宿主不添加标题栏。插件应自行提供标题、导航和关闭按钮,可使用 framely.ui.close() 或 framely.windows.close(id)。快捷面板仍保留宿主导航。
启动台操作与本体兼容
本体页面滚动
原生 CEF 宿主在每个页面加载完成时安装主扳机纵向拖动滚动,覆盖现有插件页面、嵌套页面,以及不使用 SDK 的 localWeb 窗口。共用的页面标记防止本体桥接或 SDK 重复安装。激光指向的摇杆滚动也通过原生滚轮输入支持这些窗口。它选择指针下可滚动的祖先区域,并在边界继续滚动外层。输入框、文本域、可编辑内容和滑块保留自身交互。若插件需要自行处理纵向拖拽,请在对应元素或其祖先上设置 data-framely-no-scroll-drag;原生 draggable="true" 元素也会排除。无需新增清单字段或重新打包插件。