Make English the default repository documentation

Keep Simplified Chinese translations in README.zh-CN.md and docs/zh-CN, update language navigation, and align generated plugin and release-package documentation.
This commit is contained in:
toorux committed 2026-10-04 21:16:07 +08:00
1 parent cdbe974e13
commit 14a15f8a2c
60 files changed
+1848 -1848

No files matched your search

+35 -35
View File
@@ -1,12 +1,12 @@
# 插件开发到发布:完整流程
# Plugin development to publication
[English](../en/developer-guide/README.md)
[简体中文](../zh-CN/developer-guide/README.md)
[文档首页](../README.md) · [发布指南](publishing.md)
[Documentation](../README.md) · [Publishing](publishing.md)
## 1. 准备开发环境
## 1. Prepare the development environment
开发机准备 Git、Node.js 22、npm;发布指南中的临时清单脚本还需要 Python 3。本体 CLI 使用 Linux 专用接口,源码打包命令需在安装 Rust 的 Linux/WSL 环境执行,不能直接在 Windows/macOS 原生编译本体。也可将清单和 payload 拷到 Frame,使用已安装的 CLI 打包。设备端无需 Node.js/Rust,Python 后端依赖设备的 Python 3。开发机可以用于界面预览和打包,VR 交互必须在 Frame 验证。
Install Git, Node.js 22 and npm. The publishing Manifest script also needs Python 3. The core CLI uses Linux-specific interfaces, so source packaging requires Rust on Linux/WSL, not native Windows/macOS compilation. Alternatively copy Manifest/payload to Frame and use its installed CLI. Device users do not need Node.js/Rust; Python backends use device Python 3. Preview and packaging work on the development computer; VR interaction requires Frame.
```bash
git clone https://github.com/SteamFramelyHomebrew/framely.git
@@ -17,42 +17,42 @@ npm install
npm run dev
```
这里的 `yourname.my-plugin` 是示例 ID,发布前换成你自己的稳定命名空间。脚手架拒绝覆盖已有目录,复制 SDK 和构建工具,生成后不依赖原 Framely 仓库的位置。将生成项目放入自己的 Git 仓库,并提交 `package-lock.json` 和 vendored SDK;不要提交 `node_modules/`、`payload/`、发行包和密码。
Replace the example ID with your own stable namespace. The scaffold refuses to overwrite directories and copies the SDK and tools, so the resulting project does not depend on the original repository location. Create your plugin Git repository and commit `package-lock.json` and the vendored SDK. Do not commit `node_modules/`, `payload/`, archives or passwords.
## 2. 认识模板
## 2. Understand the template
| 文件 | 用途 |
| File | Purpose |
| --- | --- |
| `manifest.json` | ID、名称、版本、运行用户、页面、生命周期、依赖 |
| `page.tsx` | React 快捷页和独立窗口 |
| `backend.py` | Python 后端及数据保存 |
| `vendor/framely-sdk/` | UI、桥接接口和 Python 协议辅助库 |
| `dev.mjs` | 构建和本地预览工具 |
| `LICENSE` | AGPLv3 许可证全文 |
| `manifest.json` | Identity, version, runtime user, pages, lifecycle and dependencies |
| `page.tsx` | React quick page and independent window |
| `backend.py` | Python backend and persistent settings |
| `vendor/framely-sdk/` | UI, bridge API and Python protocol helper |
| `dev.mjs` | Build and local preview |
| `LICENSE` | Full AGPLv3 text |
模板的“读取/保存”使用 `settings.get` / `settings.set`,内容保存在后端的 `FRAMELY_DATA_DIR/settings.json`。它还演示打开 `main` 窗口、发送通知、启动/停止和安装/更新/卸载回调。默认身份 `steamos`,默认按需启动。
Read/save uses `settings.get` / `settings.set` with `FRAMELY_DATA_DIR/settings.json`. The template opens the `main` window, sends notifications and demonstrates install/update/start/stop/uninstall callbacks. It defaults to `steamos` and on-demand startup.
## 3. 修改清单与界面
## 3. Edit Manifest and UI
先修改 `manifest.json` 中的 ID、作者、名称、描述、版本、标签和 `changelog`。使用完整 SemVer,例如 `0.1.0`、`0.2.0-preview.1`;已发布版本不覆盖。
Set the ID, author, name, description, version, tags and changelog. Use full SemVer such as `0.1.0` or `0.2.0-preview.1`; never overwrite a published version.
`ui.quickPage` 和 `ui.windows.main.entry` 指向构建后的 `page.js`。页面通过 `registerPlugin({QuickPage, WindowPage})` 注册。窗口 key 必须与清单一致。`files` 留空,打包时自动计算哈希。
`ui.quickPage` and `ui.windows.main.entry` refer to built `page.js`. Register components with `registerPlugin({QuickPage, WindowPage})`, matching declared keys. Leave `files` empty; packaging computes hashes.
在浏览器打开 `http://127.0.0.1:5173`;修改页面后自动重载。预览只模拟窗口和通知,不启动 Python 后端,不拥有 Frame 权限;读取/保存按钮在预览中报出后端不可用是正常情况。不要据此声称设备功能已验收。
Preview at `http://127.0.0.1:5173`, with automatic reload. Preview simulates windows and notifications, without Python backends or device privileges. Read/save therefore reports backend unavailability; this is expected and is not device validation.
UI 与后端 API 见 [SDK 文档](sdk.md),字段、默认值和范围见 [Manifest 配置](manifest.md);窗口与商店资料补充见[插件开发参考](../plugin-development.md)。
See [SDK API](sdk.md), [Manifest configuration](manifest.md) and the [development reference](../plugin-development.md).
## 4. 实现后端与生命周期
## 4. Implement backend and lifecycle
后端 stdin/stdout 使用逐行 JSON RPC;stdout 仅输出协议,调试日志输出 stderr。Python 模板使用 `framely.serve` 处理请求、错误和生命周期。修改业务逻辑时校验参数长度与类型,并返回能由 JSON 序列化的结果。
Backend stdin/stdout use newline-delimited JSON RPC. Reserve stdout for protocol messages and log to stderr. Python `framely.serve` handles requests, exceptions and hooks. Validate parameter types and lengths and return JSON-serializable values.
写数据使用 `FRAMELY_DATA_DIR`,不要写到插件载荷或 Framely 程序目录。升级、回退和运行用户变更的状态迁移由插件设计;`steamos` 与 `root` 对应不同的数据目录。普通功能优先使用 `steamos`,确需系统权限时才使用 `root` 并解释原因。
Write to `FRAMELY_DATA_DIR`, not plugin payload or Framely binaries. Design migrations for updates, older versions and runtime-user changes. `steamos` and `root` have separate data directories. Prefer `steamos`; explain any required root privileges.
安装/更新钩子应可重复执行;停止/卸载时释放本插件持有的外部资源。关闭 UI 窗口不等于停止后端,React effect 清理不能替代后端钩子。系统依赖不会由 Framely 自动安装,需在说明中写清。[生命周期参考](../plugin-lifecycle.md)
Make install/update hooks repeatable and release owned external resources on stop/uninstall. Closing UI does not stop the backend; React effect cleanup cannot replace backend hooks. System dependencies are not installed automatically. See [lifecycle](../plugin-lifecycle.md).
## 5. 构建、打包、校验
## 5. Build, package and verify
在插件项目中执行:
Inside your plugin:
```bash
npm run build
@@ -61,24 +61,24 @@ cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest
cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify yourname.my-plugin-0.1.0.framely
```
将 `FRAMELY_REPO` 换成开发机上的 Framely 仓库绝对路径。也可在 Frame 使用已安装的 `/var/lib/framely/current/bin/framely pack` / `verify`,这些离线操作无需 sudo。
Set the repository's actual absolute path. Alternatively use installed `/var/lib/framely/current/bin/framely pack` / `verify` on Frame; these offline commands do not need sudo.
`payload/` 只放要分发的页面、后端、Python 协议库及必要资源。构建会复制 `backend.py`、`framely.py`;额外图标等资源需要自行放入该目录。打包检查入口、路径、清单和哈希,不允许软链接或额外包内文件。
Include only distributable pages, backend, Python helper and resources in `payload/`. Build copies `backend.py` and `framely.py`; add icons and other assets yourself. Packaging checks entries, paths, Manifest and hashes, rejecting symlinks and undeclared extra archive files.
## 6. 安装到 Frame 测试
## 6. Test on Frame
将包下载到手机/电脑,进入网络面板,在“插件”的本地导入入口上传并确认;或通过 SSH 复制到 Frame 后执行:
Import the package through the web panel and review confirmation, or transfer it and run on Frame:
```bash
sudo /var/lib/framely/current/bin/framely install ./yourname.my-plugin-0.1.0.framely --approve
```
首次协议确认后再管理插件。测试读取/保存、窗口和通知,停用再启用,重启后读回数据;修改版本后测试更新和数据保留,最后测试卸载钩子。涉及 VR 键盘、手柄或硬件的功能,记录实际 Frame 验证结果。
Accept the terms before management. Test read/save, windows and notifications, disabling/re-enabling, persistent data after reboot, updates after a version bump, and uninstall cleanup. Record actual device checks for VR input, controllers and hardware.
日志可通过 Framely 状态与 systemd 日志检查;后台插件 stdout 不应混入调试文字。更完整的安装操作见[用户教程](../user-guide/plugins.md)。
Inspect Framely status and systemd logs. Do not mix debug messages into backend stdout. See [installation](../user-guide/plugins.md).
## 7. 发布
## 7. Publish
GitHub Release 默认使用标签 `v<version>` 和附件 `<id>-<version>.framely`,源码清单可以省略 `downloadUrl`。社区登记会自动拼接地址;打包前按[发布指南](publishing.md)生成包含最终地址的临时清单,因为当前 `framely pack` 不会自己读取仓库地址。自定义下载地址再显式填写 `downloadUrl`。随后公开 Release、上传包,提供使用说明、运行用户、更新记录和实际测试范围,再自行托管插件源或登记固定源码提交。社区数据库还需校验整包预期哈希和字段兼容性。
Default GitHub Releases use tag `v<version>` and asset `<id>-<version>.framely`; source may omit `downloadUrl`. Community registration derives it automatically. Prepare a temporary packaged Manifest containing the final URL as shown in [publishing](publishing.md), because current `framely pack` does not read Git remotes. Set an explicit URL for custom downloads. Publish the Release/assets with usage, runtime-user details, changes and actual test coverage, then host a catalog or register a fixed source commit. Database registration also checks expected whole-package hashes and field compatibility.
后续更新:提升版本号 → 重新生成发布清单(自定义地址时更新地址)→ 构建打包并校验 → 实机验证 → 新 Release → 更新来源登记。不要替换旧版本附件。
For updates: bump version → regenerate the release Manifest (update explicit custom URLs if used) → rebuild/verify → device test → new Release → update the source pin. Never replace old assets.
+53 -53
View File
@@ -1,19 +1,19 @@
# Manifest 配置说明
# Manifest configuration
[English](../en/developer-guide/manifest.md)
[简体中文](../zh-CN/developer-guide/manifest.md)
[开发流程](README.md) · [SDK](sdk.md) · [生命周期](../plugin-lifecycle.md)
[Development](README.md) · [SDK](sdk.md) · [Lifecycle](../plugin-lifecycle.md)
项目根目录 `manifest.json` 描述插件;打包后同名文件位于包内。字段采用 camelCase,安装包清单拒绝未知字段,不要添加已废弃的 `permissions`。以[模板清单](../../templates/plugin/manifest.json)为起点。
The repository-root `manifest.json` describes a plugin and is included in its archive. Fields use camelCase. Package Manifests reject unknown fields; do not add retired `permissions`. Start from the [template](../../templates/plugin/manifest.json).
## 最小示例
## Minimal example
```json
{
"schemaVersion": 1,
"apiVersion": 1,
"id": "yourname.my-plugin",
"name": "我的插件",
"name": "My plugin",
"version": "0.1.0",
"author": "Your name",
"backend": {"entry": "backend.py", "runAs": "steamos"},
@@ -22,75 +22,75 @@
}
```
源码清单的 `files` 留空;`framely pack` 遍历 payload 并生成实际文件哈希。至少一个载荷文件,每个入口必须存在于 payload。完整模板见 [manifest.json](../../templates/plugin/manifest.json)。
Leave source `files` empty. `framely pack` scans payload and supplies hashes. At least one payload file is required; all declared entries must be included. See the [complete template](../../templates/plugin/manifest.json).
## 顶层字段
## Top-level fields
| 字段 | 要求与默认值 |
| Field | Requirements/default |
| --- | --- |
| `schemaVersion` / `apiVersion` | 必填,目前均为 `1` |
| `id` | 必填,最多 80 字符;允许小写字母、数字、点、短横线、下划线;不能以点开头,禁止 `..`;脚手架要求字母/数字开头,推荐 `namespace.name` |
| `name` / `author` | 必填且非空,各最多 120 字节 |
| `version` | 必填,最多 64 字节,仅 ASCII 字母、数字和 `.-+`;建议完整 SemVer,声明关系时必须 SemVer |
| `description` | 简介,默认空字符串;社区数据库要求最多 4096 字节 |
| `details` / `changelog` | 详细说明/本版本变化,默认空字符串;分别最多 32768 / 16384 字节 |
| `tags` | 标签字符串数组,默认空;最多 12 项,每项非空且最多 80 字节;用于搜索与筛选 |
| `icon` | 可选,payload 内 PNG 路径;最大 1 MiB、1024×1024 |
| `screenshots` | 默认空数组,最多 8 个 payload 内 PNG/JPEG 路径 |
| `authorUrl` / `documentationUrl` / `homepage` | 可选 HTTP/HTTPS 地址,禁止 URL 内用户名密码 |
| `downloadUrl` | 可选,固定版本 HTTPS 包下载地址;GitHub 数据库源码可省略,按仓库/版本/ID 拼接,社区登记的包内需补全;见发布指南 |
| `publish` | 可选外部商店图片配置,见下文 |
| `backend` / `lifecycle` / `ui` | 可选,配置后端、钩子和页面 |
| `dependencies` / `optionalDependencies` / `conflicts` | 默认空对象,见关系参考 |
| `exclusiveResources` | 默认空数组,禁止同时启用占用同名资源的插件 |
| `files` | 必填对象,打包自动生成路径到 SHA256 的映射,最多 2048 个文件 |
| `schemaVersion` / `apiVersion` | Required, currently both 1 |
| `id` | Required, max 80 characters; lowercase letters/digits/dots/hyphens/underscores; no leading dot or `..`; the scaffold requires a letter/digit first; prefer `namespace.name` |
| `name` / `author` | Required, nonempty, max 120 bytes each |
| `version` | Required, max 64 bytes, ASCII letters/digits and `.-+` only; full SemVer recommended and required when declaring relationships |
| `description` | Short summary, default empty; database registration limits it to 4096 bytes |
| `details` / `changelog` | Full description/version changes, default empty; max 32768 / 16384 bytes respectively |
| `tags` | String array, default empty; max 12 nonempty entries of at most 80 bytes each; search/filter tags |
| `icon` | Optional payload PNG path, max 1 MiB and 1024×1024 |
| `screenshots` | Default empty; up to eight payload PNG/JPEG paths |
| `authorUrl` / `documentationUrl` / `homepage` | Optional HTTP/HTTPS links, no embedded credentials |
| `downloadUrl` | Optional fixed-version HTTPS package URL; GitHub database source may omit it for repository/version/ID derivation, but community packages need the resolved URL; see publishing |
| `publish` | Optional external store image configuration |
| `backend` / `lifecycle` / `ui` | Optional backend/hooks/pages |
| `dependencies` / `optionalDependencies` / `conflicts` | Default empty maps |
| `exclusiveResources` | Default empty array; matching names prevent simultaneous enabling |
| `files` | Required path-to-SHA256 map, generated during packaging, max 2048 files |
旧 `category` 字段仅兼容读取,不再导出;新插件使用 `tags`。`downloadSha256` 是数据库源码登记专用的整包 SHA256,不能放入严格校验的包内清单。默认 GitHub Release 模式从附件 `digest` 读取整包哈希;自定义 HTTPS 地址需要填写源码专用哈希,打包时移除。省略源码 `downloadUrl` 时,社区登记会补全,但当前 `framely pack` 不会自动补全;[发布指南](publishing.md)提供临时清单命令。社区登记还有比本体严格的 ID、简介长度等校验,不能将本体校验通过等同于登记成功。
Legacy `category` is accepted only for compatibility and no longer exported; use tags. `downloadSha256` is a database source-registration field for whole-package SHA256, not a package field. Default GitHub Releases use asset digests; custom HTTPS URLs require a declared source hash, removed before packaging. The database resolves omitted source `downloadUrl`, but current `framely pack` does not. See [publishing](publishing.md) for a temporary-Manifest command. Database IDs and description lengths have stricter constraints than core validation; passing the core does not guarantee registration.
## `backend`
## Backend
| 字段 | 说明 |
| Field | Meaning |
| --- | --- |
| `entry` | 必填,包内可执行入口,脚本需有效 shebang |
| `args` | 默认空数组,最多 64 项,每项最多 4096 字节且无 NUL;每项是独立参数,不是 Shell 命令 |
| `runAs` | `steamos`(默认)或 `root` |
| `autostart` | 默认 false,按需启动;true 表示启动时常驻 |
| `restart` | `on-failure`(默认)或 `never` |
| `restartLimit` | 连续失败阈值 1–10,默认 3 |
| `memoryLimitMiB` | 内存上限,默认 512 MiB;1–4294967295 的整数,不能用 0 或 null 取消限制 |
| `entry` | Required executable payload entry; scripts need a valid shebang |
| `args` | Default empty; at most 64 independent args, each ≤4096 bytes without NUL; not a shell command |
| `runAs` | `steamos` (default) or `root` |
| `autostart` | false by default for on-demand startup; true for resident startup |
| `restart` | `on-failure` (default) or `never` |
| `restartLimit` | Consecutive failure threshold 1–10, default 3 |
| `memoryLimitMiB` | Memory limit, default 512 MiB; integer 1–4294967295, no 0/null for unlimited memory |
省略 backend 可制作纯 UI 插件。系统/Python 第三方依赖不会自动安装。运行用户变化需用户确认,数据目录按运行身份区分,不自动迁移。
Omit backend for UI-only plugins. System/third-party Python dependencies are not installed automatically. Runtime-user changes require confirmation and use separate data directories without automatic migration.
`backend.memoryLimitMiB` 映射到 systemd 的 `MemoryMax`,限制后端及其所有子进程的合计内存;它是上限,不会预分配内存。独立生命周期钩子沿用该值,没有后端的钩子使用 512 MiB。安装计划、CLI 和已安装插件管理页展示上限。省略或填写 512 时打包省略该字段,保持旧包兼容;使用其他值需要支持此字段的新版宿主及插件数据库。
`backend.memoryLimitMiB` maps to systemd `MemoryMax` for the combined memory of the backend and all its child processes. It is a ceiling, not a memory reservation. Independent lifecycle hooks inherit this limit; hooks without a backend use 512 MiB. The install plan, CLI and installed-plugin management view display the limit. Omitted or explicit 512 values are omitted when packing to preserve compatibility with older hosts; other values require a host and plugin database supporting this field.
例如,为较长回放声明 2 GiB:
For example, declare 2 GiB for a longer replay buffer:
```json
{"backend": {"entry": "backend", "memoryLimitMiB": 2048}}
```
## `ui`
## UI
`quickPage` 是可选包内页面 bundle 路径;`windows` 默认空对象,最多 8 个窗口,key 必须是合法 ID。每个窗口:
Optional `quickPage` points to a payload bundle. `windows` defaults to an empty map with at most eight windows; keys must be valid IDs:
| 字段 | 说明 |
| Field | Meaning |
| --- | --- |
| `entry` / `title` | 必填;入口需在 payload;标题非空且最多 120 字节 |
| `dockIcon` | 默认 false,true 为独立 Dock 窗口 |
| `width` / `height` | 默认 1600×900;宽 640–2560,高 360–1440 |
| `widthMeters` | 默认 3.0;有效值 0.4–4.0 米。源码可读取 null,但打包会省略,再读取采用默认 3.0 |
| `localWeb` | 默认 false;特殊本机网页窗口由后端 `window.get` 提供 localhost URL |
| `entry` / `title` | Required payload entry and nonempty title ≤120 bytes |
| `dockIcon` | false by default; true creates a Dock window |
| `width` / `height` | Default 1600×900; width 640–2560, height 360–1440 |
| `widthMeters` | Default 3.0; finite 0.4–4.0 meters. Source null is accepted but omitted during packaging, then reads back as default 3.0 |
| `localWeb` | false by default; specialized localhost windows obtain a URL from backend `window.get` |
`localWeb` 只支持 Frame 本机打开,后端 `window.get` 参数为 `{window: key}`,返回 `{url: "http://localhost:<port>/framely-window/<key>"}`;不得有查询串、锚点或使用管理面板端口。它使用本机网页所需的不同 sandbox 标志。普通 React 插件无需 `localWeb`。窗口 key 与 `framely.windows.open(key)` 及注册的组件映射一致。尺寸在重新打开窗口时生效。
`localWeb` opens on Frame only: backend `window.get` receives `{window: key}` and returns `{url: "http://localhost:<port>/framely-window/<key>"}` without query/fragment or the management port. It uses different sandbox flags for local web apps. Ordinary React plugins do not need `localWeb`. Match window keys to `framely.windows.open(key)` and component registration. Size changes apply when reopening.
## `lifecycle`
## Lifecycle
`onInstall`、`onUpdate`、`onUninstall`、`onCrashCleanup` 是可选 `{entry, args?}` 独立命令;`onStart` / `onStop` 是默认 false 的布尔开关,需要 backend。`timeoutSeconds` 默认 10,范围 1–15 秒。`runAs` 可选,有后端时必须与后端一致,纯 UI 插件独立钩子可用它声明身份。独立命令的 `args` 限制与 backend 相同。执行时机、失败行为和上下文见[生命周期](../plugin-lifecycle.md)。
`onInstall`, `onUpdate`, `onUninstall`, `onCrashCleanup` are optional independent `{entry, args?}` commands. `onStart` / `onStop` are booleans defaulting to false and require a backend. `timeoutSeconds` is 1–15, default 10. Optional `runAs` must match the backend when present and can specify identity for UI-only standalone hooks. Standalone command `args` have the same limits as backend args. See [lifecycle](../plugin-lifecycle.md) for timing, errors and context.
## 发布资料与关系
## Publication and relationships
`publish.icon` 为 HTTPS 外部图标 URL,`publish.screenshots` 为最多 8 个 HTTPS 截图 URL。包内 `icon` 和 `screenshots` 是文件路径,不能混用。下载地址优先使用 `downloadUrl`,同版本内容必须保持不变。[发布指南](publishing.md)
`publish.icon` is an external HTTPS icon, `publish.screenshots` contains up to eight external HTTPS images. Package `icon` and `screenshots` are paths; do not confuse them. Download URLs prefer `downloadUrl`; published versions must remain immutable. See [publishing](publishing.md).
依赖值可为版本范围字符串,或 `{version, source}`;source 是 HTTPS `catalog.json` 地址。冲突值是版本范围。可选依赖缺失不阻止安装,独占资源只按作者声明校验。[依赖与冲突](../plugin-relationships.md)
Dependencies use ranges or `{version, source}`, with source pointing to an HTTPS catalog. Conflicts use ranges. Missing optional dependencies do not prevent installation; exclusive resources rely on author declarations. See [relationships](../plugin-relationships.md).
清单 JSON 最大 256 KiB,载荷路径最多 512 字节。载荷路径仅允许 ASCII 字母、数字及 `/._-+`,禁止绝对路径、空段、`.`、`..`、软链接和未声明的附加包文件。清单通过校验后,运行权限仍需用户判断;文件哈希不代表插件安全审核。
Manifest JSON is limited to 256 KiB and payload paths to 512 bytes. Payload paths allow ASCII letters/digits and `/._-+` only. Absolute paths, empty segments, `.`/`..`, symlinks and undeclared archive files are forbidden. Successful validation is not a safety review; users still assess runtime privileges.
+39 -39
View File
@@ -1,24 +1,24 @@
# 发布插件与插件源
# Publish plugins and catalogs
[English](../en/developer-guide/publishing.md)
[简体中文](../zh-CN/developer-guide/publishing.md)
[文档首页](../README.md) · [开发全流程](README.md)
[Documentation](../README.md) · [Development workflow](README.md)
## GitHub Release 默认地址
## Default GitHub Release URL
公开 GitHub 插件仓库的**源码 `manifest.json` 可以省略 `downloadUrl`**。社区数据库从登记的仓库地址、版本和插件 ID 自动生成:
A public GitHub plugin repository's **source `manifest.json` may omit `downloadUrl`**. The community database derives it from the registered repository, version and plugin ID:
```text
https://github.com/<owner>/<repo>/releases/download/v<version>/<id>-<version>.framely
```
例如仓库 `yourname/my-plugin`、ID `yourname.my-plugin`、版本 `0.1.0` 对应标签 `v0.1.0` 和附件 `yourname.my-plugin-0.1.0.framely`。仓库名不必与 ID 相同。必须真正省略字段;空字符串或 null 不表示自动生成。不要使用 `latest`,不要替换同版本附件。
Repository `yourname/my-plugin`, ID `yourname.my-plugin`, version `0.1.0` means tag `v0.1.0` and asset `yourname.my-plugin-0.1.0.framely`. Repository names need not match IDs. Omit the field entirely; empty strings or null do not select automatic generation. Avoid `latest` and never replace same-version assets.
源码登记与打包有一项区别:数据库会补全源码清单的下载地址,并要求**包内清单含相同的最终 `downloadUrl`**。当前 `framely pack` 只计算载荷哈希,不读取 Git remote、不自动拼接 Release URL。若源码省略该字段,打包前生成临时清单;无需把地址手写回源码。
Source registration and packaging differ: the database normalizes the source URL and requires **the same final `downloadUrl` in the packaged Manifest**. Current `framely pack` computes payload hashes but does not inspect Git remotes or derive Release URLs. If your source omits the field, prepare a temporary Manifest before packaging; you do not need to write the URL back into source.
## 生成发布包
## Prepare the release package
先按[开发流程](README.md)创建插件、执行 `npm run build`,在自己的插件 Git 仓库配置正确的 GitHub `origin`。以下命令在插件目录执行,从源码生成 `.framely-build/manifest.json`,支持 HTTPS、SSH 两种常用 remote 写法;已有显式 `downloadUrl` 会保留,源码不会改写:
Follow the [development workflow](README.md), run `npm run build`, and configure the plugin repository's GitHub `origin`. Run this inside your plugin to create `.framely-build/manifest.json`. It supports common HTTPS/SSH remotes, preserves explicit `downloadUrl`, and leaves source unchanged:
```bash
python3 - <<'PY'
@@ -48,7 +48,7 @@ print(manifest['downloadUrl'])
PY
```
`.framely-build/` 是临时输出,应加入插件 `.gitignore`。模板已忽略它。包名必须与默认规则一致;在插件目录继续执行:
Ignore temporary `.framely-build/` in your plugin Git repository; the template already does so. Asset filenames must match the default rule. Continue inside your plugin:
```bash
FRAMELY_REPO=/absolute/path/to/framely
@@ -59,42 +59,42 @@ cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest
cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify "dist/$PLUGIN_ID-$PLUGIN_VERSION.framely"
```
提交源码、SDK、锁文件和许可证,将该提交标记为 `v<version>`;创建对应 GitHub Release,上传 `dist/` 内的 `.framely`,并公开 Release。包内清单和源码登记清单在补全默认地址、去掉源码专用字段后应一致。用于登记的 submodule 必须指向这个实际发布版本的源码提交。
Commit source, SDK, lockfile and license; tag that commit `v<version>`, create its GitHub Release, upload the `.framely` from `dist/`, and publish. Source and packaged declarations must match after normalizing the default URL and removing registration-only fields. Pin database submodules to the actual released source commit.
发行说明包含功能和使用方法、运行用户及 root 的原因、依赖、变化、实际 Frame 测试和已知限制。提供与包匹配的源码和构建脚本,遵守模板、SDK及依赖许可证。
Release notes describe usage, runtime user and reasons for root, dependencies, changes, actual Frame tests and limitations. Provide matching source/build scripts and comply with template, SDK and dependency licenses.
### 整包哈希与自定义地址
### Whole-package hashes and custom URLs
| 方式 | 源码登记 | 校验的预期整包哈希 |
| Method | Source registration | Expected whole-package hash |
| --- | --- | --- |
| 默认 GitHub Release | 省略 `downloadUrl`;标签和附件遵守上述命名 | 从对应 Release 附件的 `digest` 读取 SHA256,无需手写 |
| 显式固定 GitHub Release | 填 `downloadUrl`,可改变标签或附件名 | 未提供 `downloadSha256` 时读取 Release digest;显式提供时以该哈希校验 |
| 其他 HTTPS 下载地址 | 填 `downloadUrl` 和 `downloadSha256` | 使用源码登记中声明的 64 位整包 SHA256 |
| Default GitHub Release | Omit `downloadUrl`; use the tag/asset naming above | Read SHA256 from the Release asset `digest`; no handwritten hash needed |
| Explicit fixed GitHub Release | Set `downloadUrl` to choose a different tag/asset name | Read Release digest if `downloadSha256` is absent; otherwise validate against the declared hash |
| Other HTTPS download | Set `downloadUrl` and `downloadSha256` | Use the declared 64-character whole-package SHA256 |
自动地址模式即使另填 `downloadSha256`,也必须与 Release digest 相同。缺少 Release digest、附件未公开、无法下载或哈希不符会阻止登记,不会退回到信任刚下载的文件。
Automatic URLs always require a Release digest, even when a hash is supplied, and both must match. Missing digests, unpublished/missing assets, download failures or hash mismatches block registration; there is no fallback to trusting the downloaded bytes.
`downloadSha256` 只属于数据库源码登记,指完整 `.framely` 的哈希;不是载荷的 `files` 哈希,也不是 Framely 包内字段。先生成包,执行 `sha256sum dist/<id>-<version>.framely`,再把结果写入源码登记。打包必须移除它,上面的临时清单命令已处理;不要向包内写入自身哈希。直接给 `framely pack` 传含此字段的源码清单会被未知字段校验拒绝。
`downloadSha256` belongs only to database source registration, covers the complete `.framely`, and differs from payload `files` hashes. It is not a Framely package field. Build first, run `sha256sum dist/<id>-<version>.framely`, then add it to source registration. Remove it before packaging, as the temporary-Manifest command does. Do not embed a package's own hash inside it. Passing source containing this field directly to `framely pack` fails unknown-field validation.
### 图标与窗口兼容
### Icons and window compatibility
社区数据库的图标通常只需顶层 `icon: "icon.png"`:同一路径 PNG 同时提交到源码仓库和 `payload/`。数据库从登记的仓库、固定提交及路径生成 GitHub Raw 地址,并下载比较包内图标;不需要额外填写 `publish.icon`。自行托管目录的图片由 `publish.icon` / `publish.screenshots` 提供固定 HTTPS 地址。[商店资料参考](../plugin-development.md#商店资料与图标)
For the community database, set top-level `icon: "icon.png"` and include the same PNG path in source and payload. The database derives a GitHub Raw URL from the pinned repository/commit/path and verifies it against the packaged icon; `publish.icon` is unnecessary. Self-hosted catalogs use fixed HTTPS `publish.icon` / `publish.screenshots`. See [store metadata](../plugin-development.md).
社区数据库支持窗口 `entry`、`title`、`dockIcon`、`localWeb`、`width`、`height`、`widthMeters`。像素宽度为整数 640–2560,高度为整数 360–1440,物理宽度按本体 float32 规则校验 0.4–4.0 米。源码省略尺寸时,数据库会按 1600×900、3 米补全,与 pack 输出一致;自定义尺寸也会参与源码和包内清单比较,未声明的变化会被拒绝。模板包含的默认独立窗口已通过实际打包和数据库校验;登记仍需完成整包哈希、归属及其他字段检查。
The community database supports window `entry`, `title`, `dockIcon`, `localWeb`, `width`, `height`, `widthMeters`. Pixel width is an integer 640–2560, height an integer 360–1440, and physical width follows the core's float32 validation for 0.4–4.0 meters. Omitted dimensions normalize to 1600×900 and 3 meters, matching pack output. Custom dimensions participate in source/package comparison; undeclared changes are rejected. The template's default standalone window passed actual packaging and database validation. Registration still requires whole-package hash, ownership and other field checks.
## 方式一:自行托管插件源
## Option 1: host your own catalog
自托管不要求社区登记,也不要求 GitHub Release digest。可以省略包内 `downloadUrl`,由 `framely catalog` 使用 `--base-url` 与实际包文件名拼接;填写时优先采用包内地址。若包名来自默认 GitHub 规则,按前述方式发布即可。
Self-hosting requires neither community registration nor GitHub Release digests. You may omit packaged `downloadUrl`: `framely catalog` joins `--base-url` with the actual archive filename. Explicit package URLs take precedence. For default GitHub names, use the workflow above.
把要保留的各版本 `.framely` 放入 `packages/`,在 Framely 仓库执行:
Put all versions to retain in `packages/`. From the Framely repository:
```bash
mkdir -p target/plugin-catalog
cargo run --locked -- catalog --name '我的插件源' --base-url https://example.org/packages --packages ./packages --output ./target/plugin-catalog/catalog.json
cargo run --locked -- catalog --name 'My source' --base-url https://example.org/packages --packages ./packages --output ./target/plugin-catalog/catalog.json
```
将 `example.org` 换成实际 HTTPS 地址。CLI 计算完整包的 SHA256并生成 JSON,不上传包、不复制图片。没有外部 `publish.icon` 时,本地 CLI 不会把包内图标转换成商店 URL。
Replace `example.org` with your HTTPS address. The CLI computes whole-package SHA256 and generates JSON; it does not upload packages or copy images. Without external `publish.icon`, the local CLI does not convert a packaged icon into a store URL.
**上传整个生成目录,不能只上传 `catalog.json`:**
**Upload the entire generated directory, not only `catalog.json`:**
```text
catalog.json
@@ -103,15 +103,15 @@ plugins/
versions.json
```
主目录每个 ID 一条推荐最新版;历史条目在相对 `plugins/<ID>/versions.json`,不包含主目录当前条目。每次生成需提供要保留的所有版本,本地 CLI 不合并之前的历史。社区数据库会延续之前发布的历史,当前每插件最多保留 19 个历史条目。不要混淆两种工具的保留规则。
The main catalog has one recommended entry per ID. Relative `plugins/<ID>/versions.json` contains historical entries, excluding the current main entry. Supply every retained version each time; the CLI does not merge prior history. The community database does preserve previous history, currently up to 19 historical entries per plugin. These retention rules differ.
将目录放在 HTTPS 静态服务,用户添加完整 `catalog.json` URL;各条目指向的包和图片也必须可访问。多个渠道可再发布[源订阅文件](../source-subscriptions.md)。
Host the catalog over HTTPS and share its full `catalog.json` URL. Package/image URLs must also be reachable. Multiple channels may use a [source subscription](../source-subscriptions.md).
## 方式二:登记社区数据库
## Option 2: register in the community database
登记入口为 [framely-plugin-database](https://github.com/SteamFramelyHomebrew/framely-plugin-database),具体规则以其 [CONTRIBUTING.md](https://github.com/SteamFramelyHomebrew/framely-plugin-database/blob/testing/CONTRIBUTING.md) 和当前校验器为准。源码根目录须有 `manifest.json`,作者 Release 及附件须先公开,登记使用 Git submodule 固定提交;数据库不镜像包或证明源码与二进制可复现一致。
Use [framely-plugin-database](https://github.com/SteamFramelyHomebrew/framely-plugin-database) and its current [CONTRIBUTING.md](https://github.com/SteamFramelyHomebrew/framely-plugin-database/blob/testing/CONTRIBUTING.md)/validator. Plugin roots require `manifest.json`. Publish the author's Release/assets first, then register pinned Git submodule commits. The database does not mirror packages or establish reproducible source/binary equivalence.
首次登记:先 fork 数据库,使用自己 fork 的同名 `testing` 分支:
Fork the database and use your fork's matching `testing` branch:
```bash
git clone --branch testing https://github.com/yourname/framely-plugin-database.git
@@ -123,11 +123,11 @@ git commit -m 'Add yourname.my-plugin 0.1.0'
git push origin testing
```
向上游 `testing` 提交 PR,附实际 Frame 测试记录。只改 `.gitmodules` 与 `plugins/<ID>` 的 submodule 固定提交,其他文件变更不会走登记自动合并。
Open a PR to upstream `testing` with actual Frame test records. Change only `.gitmodules` and `plugins/<ID>` submodule pins; ordinary file changes do not qualify for registration auto-merge.
数据库 ID 要求 `namespace.name`:小写字母和数字,名称部分可带点、连字符;它比本体 ID 校验严格。路径必须是 `plugins/<ID>`,仓库必须是公开 GitHub 仓库。首次登记确认仓库归属和作者维护权限;命名空间绑定 GitHub 所有者数字 ID,每个所有者最多五个历史前缀,删除插件不释放归属。已登记 ID 不能换源仓库。协作者需要可验证的 write/maintain/admin 权限,不能以 manifest 的 `author` 或 Git 提交作者替代。[完整归属规则](https://github.com/SteamFramelyHomebrew/framely-plugin-database/blob/testing/CONTRIBUTING.md#插件归属与自动合并)
Database IDs use `namespace.name`, lowercase letters/digits and dots/hyphens in the name, stricter than core IDs. Paths must be `plugins/<ID>` and source repositories public GitHub repositories. Initial registration verifies ownership and maintenance permission. Namespaces bind to GitHub owner numeric IDs; each owner may reserve at most five historical prefixes, and removal does not release ownership. Registered IDs cannot change source repositories. Collaborators require verifiable write/maintain/admin permission; Manifest authors or Git commit authors do not establish permission. See [full ownership rules](https://github.com/SteamFramelyHomebrew/framely-plugin-database/blob/testing/CONTRIBUTING.md#插件归属与自动合并).
更新登记:
Update registration:
```bash
git -C plugins/yourname.my-plugin fetch --tags
@@ -137,8 +137,8 @@ git commit -m 'Update yourname.my-plugin to 0.2.0'
git push origin testing
```
stable 对应数据库 `main`,testing 对应 `testing`。自动合并要求 `testing → testing` 或 `main → main`,跨渠道 PR 留给人工处理。测试后按数据库规则提升到稳定渠道;`publish` 是生成分支,不提交插件变更。
Stable corresponds to `main`, testing to `testing`. Auto-merge requires `testing → testing` or `main → main`; cross-channel PRs require manual handling. Promote after tests under database rules. `publish` is generated, not a target for plugin changes.
## 发布后验证
## Verify publication
检查用户实际能访问的包、目录、图标、历史选择、确认安装、更新及数据保留。只有本体 `verify` 通过还不够,登记还须通过数据库对源码、整包哈希和字段的检查。后续按“提升版本 → 重新生成临时清单 → 构建打包 → 实机测试 → 新 Release → 更新固定登记”发布;不要覆盖旧附件。
Check public packages/catalogs/icons/history, installation confirmation, updates and retained data. Core `verify` alone is insufficient: registration must pass database source, whole-package hash and field checks. For updates: bump version → regenerate temporary Manifest → build/package → device test → new Release → update the source pin. Never overwrite old assets.
+49 -49
View File
@@ -1,36 +1,36 @@
# SDK API 文档
# SDK API reference
[English](../en/developer-guide/sdk.md)
[简体中文](../zh-CN/developer-guide/sdk.md)
[开发流程](README.md) · [Manifest](manifest.md) · [生命周期](../plugin-lifecycle.md)
[Development](README.md) · [Manifest](manifest.md) · [Lifecycle](../plugin-lifecycle.md)
SDK 源码位于 `sdk/src/index.tsx`、`sdk/src/lifecycle.ts` 和 `sdk/python/framely.py`。页面从 `@framely/sdk` 导入,Python 协议库由构建工具复制到 `payload/framely.py`。
Sources are `sdk/src/index.tsx`, `sdk/src/lifecycle.ts` and `sdk/python/framely.py`. Import page APIs from `@framely/sdk`; build copies the Python helper into `payload/framely.py`.
## 页面注册
## Register pages
```tsx
import {registerPlugin, Section} from '@framely/sdk';
function QuickPage() {return <Section title="我的插件">快捷内容</Section>;}
function MainWindow() {return <Section title="主窗口">详细内容</Section>;}
function QuickPage() {return <Section title="My plugin">Quick content</Section>;}
function MainWindow() {return <Section title="Main window">Details</Section>;}
registerPlugin({QuickPage, windows: {main: MainWindow}});
```
`QuickPage` 必填。`WindowPage` 是独立窗口的默认组件;`windows` 可按清单窗口 key 指定不同组件。没有对应组件时回退到 `WindowPage`,再回退到 `QuickPage`;`quick` 用于快捷页路由,避免将它用作窗口 key。页面运行在宿主的 sandbox iframe,宿主先建立桥接再加载你的 bundle。注册函数会挂载 React、基础样式和滚动条。
`QuickPage` is required. `WindowPage` is the default independent-window component; `windows` maps declared keys to different components. Missing keys fall back to `WindowPage`, then `QuickPage`. Avoid window key `quick`, which is used by the quick-page route. Pages run in host sandbox iframes, with the bridge installed before your bundle. Registration mounts React, base styles and scrollbars.
## 宿主接口 `framely`
## Host API: `framely`
| API | 参数与返回 | 用途 |
| API | Parameters/result | Purpose |
| --- | --- | --- |
| `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.dependencies()` | `Promise<DependencyStatus[]>` | 查询本插件声明的依赖状态 |
| `framely.language.get()` | `Promise<{preference, language}>` | 获取语言偏好和当前生效语言 |
| `framely.onEvent(callback)` | 返回取消订阅函数 | 接收宿主转发的事件 |
| `framely.call<T>(method, params = {})` | `Promise<T>` | Call your backend; failures reject |
| `framely.windows.open(key)` | Declared key; asynchronous result | Open a declared window |
| `framely.windows.close(key)` | Key; asynchronous result | Close a window without disabling the backend |
| `framely.notifications.send(notification)` | Notification; asynchronous result | Send or replace by ID |
| `framely.notifications.remove(id)` | Notification ID; asynchronous result | Withdraw notification |
| `framely.dependencies()` | `Promise<DependencyStatus[]>` | Query declared dependency state |
| `framely.language.get()` | `Promise<{preference, language}>` | Language preference and effective language |
| `framely.onEvent(callback)` | Unsubscribe function | Receive forwarded events |
所有调用只通过插件桥接,不提供管理员 API 或跨插件后端调用。用 `try/catch` 或 `.catch` 处理失败,异步操作期间禁用重复提交。
These use the plugin bridge, not administrator APIs or other plugins' backends. Handle failures and prevent duplicate submissions while waiting.
```tsx
try {
@@ -39,56 +39,56 @@ try {
} catch (error) {console.error(error);}
```
## 通知
## Notifications
```tsx
await framely.notifications.send({
id: 'task', title: '完成', body: '文件已处理', durationMs: 8000,
actions: [{id: 'open', label: '打开', icon: '↗'}],
id: 'task', title: 'Done', body: 'File processed', durationMs: 8000,
actions: [{id: 'open', label: 'Open', icon: '↗'}],
});
```
`id`、`title`、`body` 必填。`image` 可选,接受 PNG/JPEG data URL 或 HTTPS URL,字符串最大 1 MiB。`actions` 最多 3 项,每项有 `id`、`label`、`icon`。`durationMs` 范围 1000–60000,省略时默认 8000 ms。`id` 和按钮 `id` 遵循本体 ID 规则,按钮 ID 在同一通知内不得重复;`title` 最大 160 字节、`body` 4096 字节、按钮 `label` 非空且最多 80 字节、`icon` 最多 16 字节。每插件每 10 秒最多发送 10 次,全局最多 64 条同时存在的通知;同 ID 更新不额外占队列位置。
`id`, `title`, `body` are required. Optional `image` accepts PNG/JPEG data URLs or HTTPS, up to a 1 MiB string. At most three actions, each with `id`, `label`, `icon`. `durationMs` is 1000–60000; omission defaults to 8000 ms. Notification/action IDs follow core ID rules; action IDs must be unique within a notification. Title is at most 160 bytes, body 4096, nonempty action label 80, and action icon 16. Each plugin may send at most ten notifications per ten seconds; the global queue permits 64 concurrent notifications. Updating an existing ID does not occupy another slot.
按钮动作调用后端 `notification.action`,参数 `{id, action}`;同时转发 `notification.action` 页面事件。无后端插件可只监听页面事件。同一 ID 用于更新,调用 remove 撤回。
Buttons invoke backend `notification.action` with `{id, action}` and forward a page event of the same name. UI-only plugins can listen for the event. Reuse an ID to update; remove it to withdraw.
## React hooks
| Hook | 返回与行为 |
| Hook | Result/behavior |
| --- | --- |
| `useBackend<T>(method, params = {})` | `{data, error, loading, call}`;不会自动发起请求,主动调用 `call()`;该调用失败仍会抛出异常 |
| `usePluginEvent(callback)` | 自动订阅并在页面卸载时取消订阅 |
| `useDependencies()` | `{items, error}`;自动加载依赖并在 `dependencies.changed` 时刷新 |
| `useBackend<T>(method, params = {})` | `{data, error, loading, call}`; requests are explicit through `call()`, which still throws on failure |
| `usePluginEvent(callback)` | Automatically subscribes and unsubscribes on unmount |
| `useDependencies()` | `{items, error}`; loads automatically and refreshes on `dependencies.changed` |
手动订阅时通过 React effect 返回取消函数:
Return the unsubscribe function from an effect:
```tsx
React.useEffect(() => framely.onEvent(event => console.log(event)), []);
```
`DependencyStatus` 包含 `id`、`required`、`constraint`、`version`、`enabled`、`matches`、`available` 和可选 `state.phase`。`constraint` 为版本范围字符串或 `{version, source}`;未安装时 `version` 为 null。[关系参考](../plugin-relationships.md)
`DependencyStatus` includes `id`, `required`, `constraint`, `version`, `enabled`, `matches`, `available`, optional `state.phase`. Constraint is a range string or `{version, source}`; uninstalled versions are null. See [relationships](../plugin-relationships.md).
宿主语言变化转发 `{type: "language.changed", data: {preference, language}}`;依赖变化转发 `{type: "dependencies.changed"}`。`onEvent` 的类型为 unknown,访问字段前应做类型检查。Python `emit("progress", {...})` 转发为 `{type: "progress", data: {...}}`。
Language changes emit `{type: "language.changed", data: {preference, language}}`; dependency changes emit `{type: "dependencies.changed"}`. `onEvent` is typed as unknown, so narrow the type before reading fields. Python `emit("progress", {...})` becomes `{type: "progress", data: {...}}`.
## UI 控件
## UI components
| 组件 | 主要属性 |
| Component | Main props |
| --- | --- |
| `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` 显示错误 |
| `Section` | `title`, `children` |
| `Button` | Standard React button props, including `onClick`, `disabled` |
| `Toggle` | `label`, `checked`, `onChange(boolean)`; optional description/disabled |
| `Slider` | `label`, `value`, `onChange(number)`; defaults min 0, max 100, step 1 |
| `TextField` | `label`, `value`, `onChange(string)`; multiline/password/placeholder/disabled |
| `Select` | `label`, `{value,label}[]` options, value/onChange/disabled |
| `Tabs` | `{id,label}[]` tabs, value/onChange |
| `Notice` | children; error styling with `error=true` |
`Select` 默认单选,`value` 为 string;设置 `multiple` 时 `value` 为 string[],回调返回数组,可使用 `emptyLabel` 和 `clearLabel`。下拉在页面内渲染,适配离屏 CEF。
Select is single-valued by default. With `multiple`, value and callback use string arrays; `emptyLabel` and `clearLabel` customize labels. Menus render inside the page for off-screen CEF.
标准输入控件在 Frame 中自动连接 SteamVR 键盘,标准可交互控件自动获得悬停反馈。自定义控件应使用正确 ARIA role,可增加 `data-framely-interactive`。网页不自行控制其他窗口、手柄或系统账号。
Native input controls automatically use the SteamVR keyboard on Frame, and standard interactive controls get hover feedback. Custom controls need suitable ARIA roles and may use `data-framely-interactive`. Pages do not independently control other views, controllers or system accounts.
## Python 后端 SDK
## Python backend helper
```python
from framely import serve, emit
@@ -102,12 +102,12 @@ def dispatch(method, params):
serve(dispatch)
```
`serve(dispatch, lifecycle=None)` 读取逐行请求、调用处理器,并把返回值或异常转换成 JSON 响应。`emit(event, data)` 输出事件,页面侧收到 `{type, data}`;`notification` 事件可主动发送通知。日志写 stderr,stdout 专用于协议。生命周期字典写法见[生命周期文档](../plugin-lifecycle.md)。
`serve(dispatch, lifecycle=None)` reads newline-delimited requests and translates returns/exceptions into responses. `emit(event, data)` emits events forwarded as `{type, data}`; `notification` can send notifications. Log to stderr; stdout is protocol-only. See [lifecycle callbacks](../plugin-lifecycle.md).
## TypeScript 生命周期辅助 API
## TypeScript lifecycle helpers
从 `@framely/sdk/lifecycle` 导入 `registerLifecycle`、`LifecycleContext`、`LifecyclePhase`、`LifecycleCallbacks`。`registerLifecycle(callbacks)` 返回异步 `(method, context)` dispatcher;把保留的生命周期方法交给它处理。它不实现 stdin/stdout 协议、不启动进程,也不赋予网页后台权限。
Import `registerLifecycle`, `LifecycleContext`, `LifecyclePhase`, `LifecycleCallbacks` from `@framely/sdk/lifecycle`. Registration returns an asynchronous `(method, context)` dispatcher for reserved lifecycle methods. It does not provide stdin/stdout transport, launch a backend or grant page privileges.
## 预览与权限边界
## Preview and privilege boundaries
`npm run dev` 只模拟窗口、通知及部分桥接调用;后端、真实依赖状态、VR 输入、振动和身份行为需要实机测试。插件页面不能直接调用管理面板 API,也不能调用 `framely.lifecycle.*` 保留方法。SDK 本身不声明权限,运行用户由 Manifest 决定。
Development preview simulates windows, notifications and some bridge operations. Test backend calls, real dependencies, VR inputs, haptics and runtime identity on the device. Pages cannot directly call management APIs or reserved `framely.lifecycle.*` methods. Runtime identity is configured in Manifest, not granted by the SDK.