diff --git a/README.en.md b/README.en.md
deleted file mode 100644
index ab7dc26..0000000
--- a/README.en.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Framely
-
-
-
-
-
-
-[简体中文](README.md)
-
-A React plugin manager for Steam Frame, with a Rust core service, a separate CEF/OpenVR host, a network management panel, and a desktop installer. Manage plugins, sources, windows, notifications, and updates.
-
-## Installation
-
-The target device is a Linux ARM64 Steam Frame. Download the `Framely Installer` for your computer from [Releases](https://github.com/SteamFramelyHomebrew/framely/releases), connect to the device, and install Framely. Alternatively, run this in the Frame SSH terminal to install the latest stable release:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
-```
-
-For a Preview release, specify an actual published tag:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
-```
-
-Runtime packages include CEF; the device needs neither Node.js nor Rust. Installation requests sudo and does not automatically disable SteamOS read-only protection. Open the Framely Dock icon in the SteamVR Dashboard for the quick menu, then use Settings → Manage plugins to open the manager.
-
-The network panel uses port `15915` by default. Visit `http://DEVICE_IP:15915` on your phone or computer. When no password is configured, the first visit requires setting and confirming a password before login. The device password and panel password are independent.
-
-## Documentation
-
-Start at the [documentation index](docs/en/README.md):
-
-- [Install, update, repair, roll back, and uninstall](docs/en/user-guide/installation.md)
-- [Basic usage](docs/en/user-guide/basic-usage.md) · [Install and manage plugins](docs/en/user-guide/plugins.md)
-- [Network management panel](docs/en/user-guide/network-panel.md)
-- [Plugin development through publishing](docs/en/developer-guide/README.md) · [Publish plugins and sources](docs/en/developer-guide/publishing.md)
-- [SDK API](docs/en/developer-guide/sdk.md) · [Manifest configuration](docs/en/developer-guide/manifest.md) · [Lifecycle](docs/en/plugin-lifecycle.md)
-- [Plugin template](templates/plugin/README.en.md) · [Showcase example](examples/showcase/README.en.md)
-- [Core release maintenance](docs/en/releases.md) · [Build the installer](installer/README.en.md)
-
-## Create a plugin
-
-Install Node.js 22 and npm on your development computer, clone this repository, and run:
-
-```bash
-node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
-cd ../my-plugin
-npm install
-npm run dev
-npm run build
-```
-
-The generated project includes React pages, a Python backend, persistent storage, lifecycle callbacks, the SDK, and build tools. Browser preview does not start the backend; see the development guide for packaging and testing on Frame.
-
-## Build and validate
-
-Building the core from source requires Linux, Rust, Node.js, C/C++, and OpenGL/X11 development libraries. The production native host uses the pinned Linux ARM64 CEF version.
-
-```bash
-npm ci
-npm run typecheck
-npm run build
-cargo test --locked
-bash tools/test-native.sh
-python3 -m unittest discover -s tests -p 'test_*.py'
-```
-
-See [release maintenance](docs/en/releases.md) for CEF builds, integration tests, and publishing commands. Plugins support `steamos` and `root`; hash verification is not a security review, so check the source and runtime user.
-
-## License
-
-Original Framely code, including the core, installer, SDK, template, and repository examples, uses **AGPL-3.0-only**; see [LICENSE](LICENSE). Third-party dependencies and vendored files retain their respective licenses.
-
-AGPL allows commercial use. Distribution requires complete corresponding source under the license; modified software that interacts with users over a network must also offer source to those users. Release source corresponds to the repository's `v` or `installer-v` tags. Redistributors must provide modifications and build scripts matching their binaries.
diff --git a/README.md b/README.md
index eeb9b31..68b610c 100644
--- a/README.md
+++ b/README.md
@@ -5,43 +5,43 @@
-[English](README.en.md)
+[简体中文](README.zh-CN.md)
-Steam Frame 的 React 插件管理器:Rust 核心服务、独立 CEF/OpenVR 宿主、网络管理面板和电脑端安装器。可管理插件、来源、窗口、通知和更新。
+A React plugin manager for Steam Frame, with a Rust core service, a separate CEF/OpenVR host, a network management panel, and a desktop installer. Manage plugins, sources, windows, notifications, and updates.
-## 安装
+## Installation
-目标为 Linux ARM64 Steam Frame。电脑端从 [Releases](https://github.com/SteamFramelyHomebrew/framely/releases) 下载对应平台的 `Framely Installer`,连接设备后安装本体。也可在 Frame 的 SSH 终端安装最新正式版:
+The target device is a Linux ARM64 Steam Frame. Download the `Framely Installer` for your computer from [Releases](https://github.com/SteamFramelyHomebrew/framely/releases), connect to the device, and install Framely. Alternatively, run this in the Frame SSH terminal to install the latest stable release:
```bash
curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
```
-安装 Preview 时指定实际已发布的标签:
+For a Preview release, specify an actual published tag:
```bash
curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
```
-运行包包含 CEF,设备无需 Node.js 或 Rust。安装会请求 sudo;不会自动解除 SteamOS 只读保护。打开 SteamVR Dashboard 的 Framely Dock 图标进入快捷菜单,从“设置 → 管理插件”打开管理窗口。
+Runtime packages include CEF; the device needs neither Node.js nor Rust. Installation requests sudo and does not automatically disable SteamOS read-only protection. Open the Framely Dock icon in the SteamVR Dashboard for the quick menu, then use Settings → Manage plugins to open the manager.
-网络面板默认端口 `15915`,手机/电脑访问 `http://设备IP:15915`。未配置密码时首次访问必须设置并确认密码,保存后登录。设备密码与面板密码相互独立。
+The network panel uses port `15915` by default. Visit `http://DEVICE_IP:15915` on your phone or computer. When no password is configured, the first visit requires setting and confirming a password before login. The device password and panel password are independent.
-## 文档
+## Documentation
-从 [文档首页](docs/README.md) 开始:
+Start at the [documentation index](docs/README.md):
-- [安装、更新、修复、回滚与卸载](docs/user-guide/installation.md)
-- [基本使用](docs/user-guide/basic-usage.md) · [插件安装与管理](docs/user-guide/plugins.md)
-- [网络管理面板](docs/user-guide/network-panel.md)
-- [插件开发到发布完整流程](docs/developer-guide/README.md) · [发布插件和插件源](docs/developer-guide/publishing.md)
-- [SDK API](docs/developer-guide/sdk.md) · [Manifest 配置](docs/developer-guide/manifest.md) · [生命周期](docs/plugin-lifecycle.md)
-- [插件模板](templates/plugin/README.md) · [功能展示示例](examples/showcase/README.md)
-- [本体发行维护](docs/releases.md) · [安装器构建](installer/README.md)
+- [Install, update, repair, roll back, and uninstall](docs/user-guide/installation.md)
+- [Basic usage](docs/user-guide/basic-usage.md) · [Install and manage plugins](docs/user-guide/plugins.md)
+- [Network management panel](docs/user-guide/network-panel.md)
+- [Plugin development through publishing](docs/developer-guide/README.md) · [Publish plugins and sources](docs/developer-guide/publishing.md)
+- [SDK API](docs/developer-guide/sdk.md) · [Manifest configuration](docs/developer-guide/manifest.md) · [Lifecycle](docs/plugin-lifecycle.md)
+- [Plugin template](templates/plugin/README.md) · [Showcase example](examples/showcase/README.md)
+- [Core release maintenance](docs/releases.md) · [Build the installer](installer/README.md)
-## 创建插件
+## Create a plugin
-开发机准备 Node.js 22 和 npm,克隆本仓库后执行:
+Install Node.js 22 and npm on your development computer, clone this repository, and run:
```bash
node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
@@ -51,11 +51,11 @@ npm run dev
npm run build
```
-生成项目包含 React 页面、Python 后端、数据保存、生命周期回调、SDK 和构建工具。浏览器预览不启动后端;打包与实际 Frame 测试见开发指南。
+The generated project includes React pages, a Python backend, persistent storage, lifecycle callbacks, the SDK, and build tools. Browser preview does not start the backend; see the development guide for packaging and testing on Frame.
-## 构建与验证
+## Build and validate
-本体源码在 Linux 构建,需要 Rust、Node.js、C/C++ 和 OpenGL/X11 开发库,生产原生宿主使用锁定版本的 Linux ARM64 CEF。
+Building the core from source requires Linux, Rust, Node.js, C/C++, and OpenGL/X11 development libraries. The production native host uses the pinned Linux ARM64 CEF version.
```bash
npm ci
@@ -66,10 +66,10 @@ bash tools/test-native.sh
python3 -m unittest discover -s tests -p 'test_*.py'
```
-CEF 构建、集成测试和发布命令见[发行维护](docs/releases.md)。插件支持 `steamos` 和 `root`;哈希校验不代表安全审核,应核对来源和运行用户。
+See [release maintenance](docs/releases.md) for CEF builds, integration tests, and publishing commands. Plugins support `steamos` and `root`; hash verification is not a security review, so check the source and runtime user.
-## 许可证
+## License
-Framely 原创代码,包括本体、安装器、SDK、模板和仓库示例,采用 **AGPL-3.0-only**,全文见 [LICENSE](LICENSE)。第三方依赖和 vendored 文件保留各自许可证。
+Original Framely code, including the core, installer, SDK, template, and repository examples, uses **AGPL-3.0-only**; see [LICENSE](LICENSE). Third-party dependencies and vendored files retain their respective licenses.
-AGPL 允许商业使用;分发需按许可提供完整对应源码,修改后通过网络与用户交互还须向这些用户提供源码。发行源码对应本仓库的 `v<版本>` 或 `installer-v<版本>` 标签;重新分发者须提供与自己二进制匹配的修改和构建脚本。
+AGPL allows commercial use. Distribution requires complete corresponding source under the license; modified software that interacts with users over a network must also offer source to those users. Release source corresponds to the repository's `v` or `installer-v` tags. Redistributors must provide modifications and build scripts matching their binaries.
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 0000000..b4682f8
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,75 @@
+# Framely
+
+
+
+
+
+
+[English](README.md)
+
+Steam Frame 的 React 插件管理器:Rust 核心服务、独立 CEF/OpenVR 宿主、网络管理面板和电脑端安装器。可管理插件、来源、窗口、通知和更新。
+
+## 安装
+
+目标为 Linux ARM64 Steam Frame。电脑端从 [Releases](https://github.com/SteamFramelyHomebrew/framely/releases) 下载对应平台的 `Framely Installer`,连接设备后安装本体。也可在 Frame 的 SSH 终端安装最新正式版:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
+```
+
+安装 Preview 时指定实际已发布的标签:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
+```
+
+运行包包含 CEF,设备无需 Node.js 或 Rust。安装会请求 sudo;不会自动解除 SteamOS 只读保护。打开 SteamVR Dashboard 的 Framely Dock 图标进入快捷菜单,从“设置 → 管理插件”打开管理窗口。
+
+网络面板默认端口 `15915`,手机/电脑访问 `http://设备IP:15915`。未配置密码时首次访问必须设置并确认密码,保存后登录。设备密码与面板密码相互独立。
+
+## 文档
+
+从 [文档首页](docs/zh-CN/README.md) 开始:
+
+- [安装、更新、修复、回滚与卸载](docs/zh-CN/user-guide/installation.md)
+- [基本使用](docs/zh-CN/user-guide/basic-usage.md) · [插件安装与管理](docs/zh-CN/user-guide/plugins.md)
+- [网络管理面板](docs/zh-CN/user-guide/network-panel.md)
+- [插件开发到发布完整流程](docs/zh-CN/developer-guide/README.md) · [发布插件和插件源](docs/zh-CN/developer-guide/publishing.md)
+- [SDK API](docs/zh-CN/developer-guide/sdk.md) · [Manifest 配置](docs/zh-CN/developer-guide/manifest.md) · [生命周期](docs/zh-CN/plugin-lifecycle.md)
+- [插件模板](templates/plugin/README.zh-CN.md) · [功能展示示例](examples/showcase/README.zh-CN.md)
+- [本体发行维护](docs/zh-CN/releases.md) · [安装器构建](installer/README.zh-CN.md)
+
+## 创建插件
+
+开发机准备 Node.js 22 和 npm,克隆本仓库后执行:
+
+```bash
+node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
+cd ../my-plugin
+npm install
+npm run dev
+npm run build
+```
+
+生成项目包含 React 页面、Python 后端、数据保存、生命周期回调、SDK 和构建工具。浏览器预览不启动后端;打包与实际 Frame 测试见开发指南。
+
+## 构建与验证
+
+本体源码在 Linux 构建,需要 Rust、Node.js、C/C++ 和 OpenGL/X11 开发库,生产原生宿主使用锁定版本的 Linux ARM64 CEF。
+
+```bash
+npm ci
+npm run typecheck
+npm run build
+cargo test --locked
+bash tools/test-native.sh
+python3 -m unittest discover -s tests -p 'test_*.py'
+```
+
+CEF 构建、集成测试和发布命令见[发行维护](docs/zh-CN/releases.md)。插件支持 `steamos` 和 `root`;哈希校验不代表安全审核,应核对来源和运行用户。
+
+## 许可证
+
+Framely 原创代码,包括本体、安装器、SDK、模板和仓库示例,采用 **AGPL-3.0-only**,全文见 [LICENSE](LICENSE)。第三方依赖和 vendored 文件保留各自许可证。
+
+AGPL 允许商业使用;分发需按许可提供完整对应源码,修改后通过网络与用户交互还须向这些用户提供源码。发行源码对应本仓库的 `v<版本>` 或 `installer-v<版本>` 标签;重新分发者须提供与自己二进制匹配的修改和构建脚本。
diff --git a/assets/branding/README.md b/assets/branding/README.md
index 6cae519..abe2923 100644
--- a/assets/branding/README.md
+++ b/assets/branding/README.md
@@ -1,18 +1,18 @@
-# Framely 品牌资源 / Branding assets
-
-像素窗口小精灵是本体和安装器的统一标识。`framely-mark.svg` 和 `framely-logo.svg` 为透明底深色版本,`*-light.svg` 用于深色背景;`framely-app-icon.svg` 为浅蓝底应用图标。字标已转成路径,无需安装字体。
+# Framely branding assets / 品牌资源
The pixel window sprite is the shared identity for Framely and its installer. `framely-mark.svg` and `framely-logo.svg` are dark artwork on transparent backgrounds; `*-light.svg` variants are for dark surfaces. `framely-app-icon.svg` is the application tile on sky blue. The wordmark uses vector paths and needs no font installation.
-修改 SVG 后,在仓库根目录重新生成 PNG、Windows ICO 和原生入口图形,并一同提交生成文件:
+像素窗口小精灵是本体和安装器的统一标识。`framely-mark.svg` 和 `framely-logo.svg` 为透明底深色版本,`*-light.svg` 用于深色背景;`framely-app-icon.svg` 为浅蓝底应用图标。字标已转成路径,无需安装字体。
After editing the SVGs, regenerate PNGs, the Windows ICO and native Dock artwork from the repository root, then commit the generated files together:
+修改 SVG 后,在仓库根目录重新生成 PNG、Windows ICO 和原生入口图形,并一同提交生成文件:
+
```bash
npm ci
npm run branding:build
```
-macOS 打包时使用系统 `iconutil` 从 PNG 自动生成 `.icns`。运行时无需 Node.js;品牌资源采用仓库的 AGPL-3.0-only 许可证。
-
macOS packaging uses the system `iconutil` to generate `.icns` from the PNGs. Runtime does not require Node.js. These brand assets use the repository's AGPL-3.0-only license.
+
+macOS 打包时使用系统 `iconutil` 从 PNG 自动生成 `.icns`。运行时无需 Node.js;品牌资源采用仓库的 AGPL-3.0-only 许可证。
diff --git a/docs/README.md b/docs/README.md
index 8f1555f..7064ef2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,37 +1,35 @@
-# Framely 文档
+# Framely documentation
-[English](en/README.md)
+[简体中文](zh-CN/README.md)
-Framely 是 Steam Frame 的插件管理器,包含设备端服务、SteamVR 界面、网络管理面板和电脑端安装器。
+Framely manages plugins on Steam Frame through device services, a SteamVR interface, a web panel, and a desktop installer.
-## 用户指南
+## User guides
-按顺序阅读即可完成安装并使用第一个插件。
+1. [Install, update, repair and uninstall](user-guide/installation.md): desktop installer, SSH, Preview builds, local packages and rollback.
+2. [Basic usage](user-guide/basic-usage.md): Dock, manager, enabling plugins, favorites and safe mode.
+3. [Install and manage plugins](user-guide/plugins.md): store, sources, subscriptions, local packages and updates.
+4. [Network panel](user-guide/network-panel.md): address, first password setup, login and phone/desktop access.
-1. [安装、更新、修复与卸载](user-guide/installation.md):桌面安装器、SSH、Preview、本地包、回滚。
-2. [基本使用](user-guide/basic-usage.md):Dock 入口、管理窗口、启停、收藏、安全模式。
-3. [安装与管理插件](user-guide/plugins.md):商店、来源、订阅、本地包和更新。
-4. [网络管理面板](user-guide/network-panel.md):地址、首次密码、登录、手机/电脑使用。
+## Plugin development and publishing
-## 插件开发与发布
+- [Complete workflow](developer-guide/README.md): scaffold → preview → backend → package → device testing → publish.
+- [SDK API](developer-guide/sdk.md) · [Manifest configuration](developer-guide/manifest.md) · [Lifecycle](plugin-lifecycle.md)
+- [Publish plugins and catalogs](developer-guide/publishing.md): default GitHub URLs, custom URLs/hashes, catalog history and database registration.
+- [Plugin template](../templates/plugin/README.md): React and Python, generated projects or in-repository editing.
+- [Showcase plugin](../examples/showcase/README.md): additional UI and device examples.
-- [完整流程](developer-guide/README.md):生成项目 → 浏览器预览 → 后端 → 打包 → 实机测试 → 发布。
-- [SDK API](developer-guide/sdk.md) · [Manifest 配置说明](developer-guide/manifest.md) · [生命周期](plugin-lifecycle.md)
-- [发布插件与插件源](developer-guide/publishing.md):GitHub 默认地址生成、自定义地址与哈希、目录历史、数据库登记。
-- [可用插件模板](../templates/plugin/README.md):React + Python,脚手架与仓库内编辑两种使用方式。
-- [功能展示插件](../examples/showcase/README.md):更多 UI 和设备交互示例。
+## Technical references
-## 技术参考
-
-| 参考 | 内容 |
+| Reference | Contents |
| --- | --- |
-| [清单、SDK 与后端协议](plugin-development.md) | Manifest 字段、窗口、通知、JSON RPC、打包规则 |
-| [生命周期](plugin-lifecycle.md) | 安装、更新、启动、停止、卸载及恢复钩子 |
-| [依赖与冲突](plugin-relationships.md) | 版本范围、来源选择、可选依赖、独占资源 |
-| [源订阅格式](source-subscriptions.md) | JSON 格式、刷新、地址变更及去重 |
-| [语言与翻译](localization.md) | 语言包、文案键、社区翻译 |
+| [Plugin development reference](plugin-development.md) | Manifest, windows, notifications, JSON RPC and packaging |
+| [Lifecycle](plugin-lifecycle.md) | Install, update, start, stop, uninstall and recovery hooks |
+| [Dependencies and conflicts](plugin-relationships.md) | Version ranges, source selection and exclusive resources |
+| [Source subscriptions](source-subscriptions.md) | JSON format, refresh, URL changes and deduplication |
+| [Localization](localization.md) | Language packs and community translations |
-## 维护者资料
+## Maintainer documentation
-- [Framely 发行与更新机制](releases.md)
-- [电脑端安装器构建](../installer/README.md)
+- [Framely releases and updates](releases.md)
+- [Desktop installer](../installer/README.md)
diff --git a/docs/developer-guide/README.md b/docs/developer-guide/README.md
index 2ba6dcf..8bdd37c 100644
--- a/docs/developer-guide/README.md
+++ b/docs/developer-guide/README.md
@@ -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` 和附件 `-.framely`,源码清单可以省略 `downloadUrl`。社区登记会自动拼接地址;打包前按[发布指南](publishing.md)生成包含最终地址的临时清单,因为当前 `framely pack` 不会自己读取仓库地址。自定义下载地址再显式填写 `downloadUrl`。随后公开 Release、上传包,提供使用说明、运行用户、更新记录和实际测试范围,再自行托管插件源或登记固定源码提交。社区数据库还需校验整包预期哈希和字段兼容性。
+Default GitHub Releases use tag `v` and asset `-.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.
diff --git a/docs/developer-guide/manifest.md b/docs/developer-guide/manifest.md
index f96bfee..5bdbda3 100644
--- a/docs/developer-guide/manifest.md
+++ b/docs/developer-guide/manifest.md
@@ -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:/framely-window/"}`;不得有查询串、锚点或使用管理面板端口。它使用本机网页所需的不同 sandbox 标志。普通 React 插件无需 `localWeb`。窗口 key 与 `framely.windows.open(key)` 及注册的组件映射一致。尺寸在重新打开窗口时生效。
+`localWeb` opens on Frame only: backend `window.get` receives `{window: key}` and returns `{url: "http://localhost:/framely-window/"}` 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.
diff --git a/docs/developer-guide/publishing.md b/docs/developer-guide/publishing.md
index 33b74cb..91e8976 100644
--- a/docs/developer-guide/publishing.md
+++ b/docs/developer-guide/publishing.md
@@ -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///releases/download/v/-.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`;创建对应 GitHub Release,上传 `dist/` 内的 `.framely`,并公开 Release。包内清单和源码登记清单在补全默认地址、去掉源码专用字段后应一致。用于登记的 submodule 必须指向这个实际发布版本的源码提交。
+Commit source, SDK, lockfile and license; tag that commit `v`, 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/-.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/-.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//versions.json`,不包含主目录当前条目。每次生成需提供要保留的所有版本,本地 CLI 不合并之前的历史。社区数据库会延续之前发布的历史,当前每插件最多保留 19 个历史条目。不要混淆两种工具的保留规则。
+The main catalog has one recommended entry per ID. Relative `plugins//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/` 的 submodule 固定提交,其他文件变更不会走登记自动合并。
+Open a PR to upstream `testing` with actual Frame test records. Change only `.gitmodules` and `plugins/` submodule pins; ordinary file changes do not qualify for registration auto-merge.
-数据库 ID 要求 `namespace.name`:小写字母和数字,名称部分可带点、连字符;它比本体 ID 校验严格。路径必须是 `plugins/`,仓库必须是公开 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/` 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.
diff --git a/docs/developer-guide/sdk.md b/docs/developer-guide/sdk.md
index 96b70b4..2daac3f 100644
--- a/docs/developer-guide/sdk.md
+++ b/docs/developer-guide/sdk.md
@@ -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 快捷内容;}
-function MainWindow() {return 详细内容;}
+function QuickPage() {return Quick content;}
+function MainWindow() {return Details;}
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(method, params = {})` | `Promise` | 调用本插件后端方法;错误会 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` | 查询本插件声明的依赖状态 |
-| `framely.language.get()` | `Promise<{preference, language}>` | 获取语言偏好和当前生效语言 |
-| `framely.onEvent(callback)` | 返回取消订阅函数 | 接收宿主转发的事件 |
+| `framely.call(method, params = {})` | `Promise` | 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` | 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(method, params = {})` | `{data, error, loading, call}`;不会自动发起请求,主动调用 `call()`;该调用失败仍会抛出异常 |
-| `usePluginEvent(callback)` | 自动订阅并在页面卸载时取消订阅 |
-| `useDependencies()` | `{items, error}`;自动加载依赖并在 `dependencies.changed` 时刷新 |
+| `useBackend(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.
diff --git a/docs/en/README.md b/docs/en/README.md
deleted file mode 100644
index 6dbd333..0000000
--- a/docs/en/README.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Framely documentation
-
-[简体中文](../README.md)
-
-Framely manages plugins on Steam Frame through device services, a SteamVR interface, a web panel, and a desktop installer.
-
-## User guides
-
-1. [Install, update, repair and uninstall](user-guide/installation.md): desktop installer, SSH, Preview builds, local packages and rollback.
-2. [Basic usage](user-guide/basic-usage.md): Dock, manager, enabling plugins, favorites and safe mode.
-3. [Install and manage plugins](user-guide/plugins.md): store, sources, subscriptions, local packages and updates.
-4. [Network panel](user-guide/network-panel.md): address, first password setup, login and phone/desktop access.
-
-## Plugin development and publishing
-
-- [Complete workflow](developer-guide/README.md): scaffold → preview → backend → package → device testing → publish.
-- [SDK API](developer-guide/sdk.md) · [Manifest configuration](developer-guide/manifest.md) · [Lifecycle](plugin-lifecycle.md)
-- [Publish plugins and catalogs](developer-guide/publishing.md): default GitHub URLs, custom URLs/hashes, catalog history and database registration.
-- [Plugin template](../../templates/plugin/README.en.md): React and Python, generated projects or in-repository editing.
-- [Showcase plugin](../../examples/showcase/README.en.md): additional UI and device examples.
-
-## Technical references
-
-| Reference | Contents |
-| --- | --- |
-| [Plugin development reference](plugin-development.md) | Manifest, windows, notifications, JSON RPC and packaging |
-| [Lifecycle](plugin-lifecycle.md) | Install, update, start, stop, uninstall and recovery hooks |
-| [Dependencies and conflicts](plugin-relationships.md) | Version ranges, source selection and exclusive resources |
-| [Source subscriptions](source-subscriptions.md) | JSON format, refresh, URL changes and deduplication |
-| [Localization](localization.md) | Language packs and community translations |
-
-## Maintainer documentation
-
-- [Framely releases and updates](releases.md)
-- [Desktop installer](../../installer/README.en.md)
diff --git a/docs/en/developer-guide/README.md b/docs/en/developer-guide/README.md
deleted file mode 100644
index 678c2d0..0000000
--- a/docs/en/developer-guide/README.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Plugin development to publication
-
-[简体中文](../../developer-guide/README.md)
-
-[Documentation](../README.md) · [Publishing](publishing.md)
-
-## 1. Prepare the development environment
-
-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
-cd framely
-node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
-cd ../my-plugin
-npm install
-npm run dev
-```
-
-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. Understand the template
-
-| File | Purpose |
-| --- | --- |
-| `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 |
-
-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. Edit Manifest and UI
-
-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` 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.
-
-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.
-
-See [SDK API](sdk.md), [Manifest configuration](manifest.md) and the [development reference](../plugin-development.md).
-
-## 4. Implement backend and lifecycle
-
-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.
-
-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.
-
-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. Build, package and verify
-
-Inside your plugin:
-
-```bash
-npm run build
-FRAMELY_REPO=/absolute/path/to/framely
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest manifest.json --payload payload --output yourname.my-plugin-0.1.0.framely
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify yourname.my-plugin-0.1.0.framely
-```
-
-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.
-
-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. Test on 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
-```
-
-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.
-
-Inspect Framely status and systemd logs. Do not mix debug messages into backend stdout. See [installation](../user-guide/plugins.md).
-
-## 7. Publish
-
-Default GitHub Releases use tag `v` and asset `-.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.
-
-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.
diff --git a/docs/en/developer-guide/manifest.md b/docs/en/developer-guide/manifest.md
deleted file mode 100644
index 9cab7e6..0000000
--- a/docs/en/developer-guide/manifest.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Manifest configuration
-
-[简体中文](../../developer-guide/manifest.md)
-
-[Development](README.md) · [SDK](sdk.md) · [Lifecycle](../plugin-lifecycle.md)
-
-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": "My plugin",
- "version": "0.1.0",
- "author": "Your name",
- "backend": {"entry": "backend.py", "runAs": "steamos"},
- "ui": {"quickPage": "page.js"},
- "files": {}
-}
-```
-
-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` | 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 |
-
-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
-
-| Field | Meaning |
-| --- | --- |
-| `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 |
-
-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` 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.
-
-For example, declare 2 GiB for a longer replay buffer:
-
-```json
-{"backend": {"entry": "backend", "memoryLimitMiB": 2048}}
-```
-
-## UI
-
-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` | 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` opens on Frame only: backend `window.get` receives `{window: key}` and returns `{url: "http://localhost:/framely-window/"}` 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
-
-`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` 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).
-
-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).
-
-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.
diff --git a/docs/en/developer-guide/publishing.md b/docs/en/developer-guide/publishing.md
deleted file mode 100644
index 851ba22..0000000
--- a/docs/en/developer-guide/publishing.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# Publish plugins and catalogs
-
-[简体中文](../../developer-guide/publishing.md)
-
-[Documentation](../README.md) · [Development workflow](README.md)
-
-## Default GitHub Release URL
-
-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///releases/download/v/-.framely
-```
-
-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.
-
-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
-
-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'
-import json, re, subprocess
-from pathlib import Path
-
-manifest = json.loads(Path('manifest.json').read_text(encoding='utf-8'))
-manifest.pop('downloadSha256', None) # Source registration only; never package it.
-if 'downloadUrl' not in manifest:
- remote = subprocess.check_output(
- ['git', 'remote', 'get-url', 'origin'], text=True
- ).strip()
- match = re.fullmatch(
- r'(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)'
- r'([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?)(?:\.git)?/?', remote
- )
- if not match:
- raise SystemExit('Use a GitHub origin or set downloadUrl explicitly.')
- manifest['downloadUrl'] = (
- f"https://github.com/{match[1]}/releases/download/v{manifest['version']}/"
- f"{manifest['id']}-{manifest['version']}.framely"
- )
-output = Path('.framely-build/manifest.json')
-output.parent.mkdir(parents=True, exist_ok=True)
-output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
-print(manifest['downloadUrl'])
-PY
-```
-
-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
-PLUGIN_ID=$(python3 -c 'import json; print(json.load(open("manifest.json"))["id"])')
-PLUGIN_VERSION=$(python3 -c 'import json; print(json.load(open("manifest.json"))["version"])')
-mkdir -p dist
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest .framely-build/manifest.json --payload payload --output "dist/$PLUGIN_ID-$PLUGIN_VERSION.framely"
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify "dist/$PLUGIN_ID-$PLUGIN_VERSION.framely"
-```
-
-Commit source, SDK, lockfile and license; tag that commit `v`, 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.
-
-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 |
-| --- | --- | --- |
-| 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 |
-
-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` 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/-.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
-
-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).
-
-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
-
-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.
-
-Put all versions to retain in `packages/`. From the Framely repository:
-
-```bash
-mkdir -p target/plugin-catalog
-cargo run --locked -- catalog --name 'My source' --base-url https://example.org/packages --packages ./packages --output ./target/plugin-catalog/catalog.json
-```
-
-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.
-
-**Upload the entire generated directory, not only `catalog.json`:**
-
-```text
-catalog.json
-plugins/
- yourname.my-plugin/
- versions.json
-```
-
-The main catalog has one recommended entry per ID. Relative `plugins//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.
-
-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
-
-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 the database and use your fork's matching `testing` branch:
-
-```bash
-git clone --branch testing https://github.com/yourname/framely-plugin-database.git
-cd framely-plugin-database
-git submodule add https://github.com/yourname/my-plugin.git plugins/yourname.my-plugin
-git -C plugins/yourname.my-plugin checkout v0.1.0
-git add .gitmodules plugins/yourname.my-plugin
-git commit -m 'Add yourname.my-plugin 0.1.0'
-git push origin testing
-```
-
-Open a PR to upstream `testing` with actual Frame test records. Change only `.gitmodules` and `plugins/` submodule pins; ordinary file changes do not qualify for registration auto-merge.
-
-Database IDs use `namespace.name`, lowercase letters/digits and dots/hyphens in the name, stricter than core IDs. Paths must be `plugins/` 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
-git -C plugins/yourname.my-plugin checkout v0.2.0
-git add plugins/yourname.my-plugin
-git commit -m 'Update yourname.my-plugin to 0.2.0'
-git push origin testing
-```
-
-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
-
-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.
diff --git a/docs/en/developer-guide/sdk.md b/docs/en/developer-guide/sdk.md
deleted file mode 100644
index f0637d4..0000000
--- a/docs/en/developer-guide/sdk.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# SDK API reference
-
-[简体中文](../../developer-guide/sdk.md)
-
-[Development](README.md) · [Manifest](manifest.md) · [Lifecycle](../plugin-lifecycle.md)
-
-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 Quick content;}
-function MainWindow() {return Details;}
-registerPlugin({QuickPage, windows: {main: MainWindow}});
-```
-
-`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.
-
-## Host API: `framely`
-
-| API | Parameters/result | Purpose |
-| --- | --- | --- |
-| `framely.call(method, params = {})` | `Promise` | 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` | Query declared dependency state |
-| `framely.language.get()` | `Promise<{preference, language}>` | Language preference and effective language |
-| `framely.onEvent(callback)` | Unsubscribe function | Receive forwarded events |
-
-These use the plugin bridge, not administrator APIs or other plugins' backends. Handle failures and prevent duplicate submissions while waiting.
-
-```tsx
-try {
- const result = await framely.call<{text: string}>('settings.get');
- console.log(result.text);
-} catch (error) {console.error(error);}
-```
-
-## Notifications
-
-```tsx
-await framely.notifications.send({
- id: 'task', title: 'Done', body: 'File processed', durationMs: 8000,
- actions: [{id: 'open', label: 'Open', icon: '↗'}],
-});
-```
-
-`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.
-
-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 | Result/behavior |
-| --- | --- |
-| `useBackend(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` |
-
-Return the unsubscribe function from an effect:
-
-```tsx
-React.useEffect(() => framely.onEvent(event => console.log(event)), []);
-```
-
-`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).
-
-
-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 components
-
-| Component | Main props |
-| --- | --- |
-| `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 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.
-
-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 backend helper
-
-```python
-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)` 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 lifecycle helpers
-
-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
-
-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.
diff --git a/docs/en/localization.md b/docs/en/localization.md
deleted file mode 100644
index 4936653..0000000
--- a/docs/en/localization.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Languages and community translation
-
-[简体中文](../localization.md)
-
-Framely includes Simplified Chinese and English. Choose a language at the top of Settings; automatic follows the system and manual choices persist. Unsupported languages and missing translations fall back to built-in English.
-
-## Create a language file
-
-Download the English template from Settings or copy `ui/locales/en-US.json`. Use UTF-8 JSON:
-
-```json
-{
- "schemaVersion": 1,
- "locale": "fr-FR",
- "name": "Français",
- "messages": {"语言": "Langue", "保存": "Enregistrer", "启用 {0}": "Activer {0}"}
-}
-```
-
-- Use a locale code such as `en-US`, `zh-CN`, `fr-FR`; `auto` is not a file locale.
-- Name the language in that language.
-- Message keys are stable: translate values only, never keys.
-- Preserve all `{0}`, `{1}` placeholders; their positions may change.
-- Partial translations fall back to English; empty strings are valid translations.
-- Translate whole sentences for natural usage, taking fragment whitespace into account.
-- Files contain text only, not executable code or interpreted HTML. Limits: 1 MiB/3000 entries per file and 50 imported files.
-
-## Install
-
-In Settings → Language, install the JSON, then select it. Reimporting the same locale replaces that language file.
-
-Files live in `/var/lib/framely/locales/.json`. Administrators may place matching filenames there directly and refresh the panel. Invalid files are skipped and named in settings. Plugin names, descriptions, tags and pages remain the plugin author's responsibility; host language files do not translate them.
-
-## Validate the project
-
-Run `node tools/test-localization.mjs` for English coverage, placeholders, system matching, manual preferences and fallback behavior.
diff --git a/docs/en/plugin-development.md b/docs/en/plugin-development.md
deleted file mode 100644
index f205838..0000000
--- a/docs/en/plugin-development.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Plugin development reference
-
-[简体中文](../plugin-development.md)
-
-Start with the [workflow](developer-guide/README.md), [SDK](developer-guide/sdk.md), [Manifest](developer-guide/manifest.md) and [publishing](developer-guide/publishing.md).
-
-The Dock quick menu has Favorites/Installed/Settings tabs; management opens a separate Dock window. Installation and sources are handled there. See `examples/showcase` and `sdk/src/index.tsx`. Release `tools/source` includes UI/SDK/examples/template/tools. Bundle pages into browser JS and call `registerPlugin({QuickPage, WindowPage})`. The host installs the bridge first. Ordinary bundled page iframes use `sandbox="allow-scripts"` with opaque origins and cannot call administrator HTTP APIs.
-
-## Manifest and identities
-
-```json
-{
- "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 is optional. Runtime users are `steamos` (default) or `root`; autostart defaults false. Retired `permissions` is unsupported; windows, notifications and networking need no declarations. Backend networking uses the device; ordinary pages permit HTTP/HTTPS and WebSockets.
-
-IDs permit lowercase letters, numbers, dots, hyphens, underscores. Payload paths permit ASCII letters/numbers and `/._-+`, never absolute paths, empty/dot/dot-dot segments. Packaging supplies files hashes. Entries must be hashed; executable scripts need shebangs, e.g. `/usr/bin/python3`. Framely does not install system/Python dependencies.
-
-Published versions are immutable. User changes select `/var/lib/framely/data//`; old data remains without automatic migration, and reinstalling the old identity can access it again.
-
-## React bridge
-
-```tsx
-import {framely, registerPlugin, Button, Section} from '@framely/sdk';
-function QuickPage() {
- return
-
-
-
- ;
-}
-registerPlugin({QuickPage});
-```
-
-Keys must match Manifest. Closing windows does not stop backend. IDs update/withdraw notifications. Images accept PNG/JPEG data URL ≤1 MiB or HTTPS. At most three actions, 1–60 seconds and ten sends per ten seconds/plugin. `onEvent` returns unsubscribe and receives `{type,data}`. Each window has its own event cursor. Input focuses the native keyboard rather than web prompts.
-
-## Backend protocol
-
-Newline-delimited JSON on stdin/stdout; logs on stderr, rotating around 2 MiB. Messages ≤64 KiB; 5-second input or 15-second response timeouts stop the backend and allow later retry.
-
-```json
-{"id":1,"method":"save","params":{"text":"hello"}}
-{"id":1,"result":{"saved":"hello"}}
-{"id":2,"error":"Unknown method"}
-{"event":"progress","data":{"percent":50}}
-```
-
-Environment includes plugin ID, data directory and the user's HOME. Notification actions invoke `notification.action` with `{id,action}` and also reach pages; UI-only plugins can listen. Backend `notification` events send notifications under the same limits.
-
-## Packaging and catalogs
-
-```bash
-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
-```
-
-Archives contain Manifest and hashed payload only, without keys/signatures. Host the main catalog and relative history JSON. Authors can host packages on GitHub Releases. Current first initialization includes community stable/testing sources. Origins remain explicit; same-ID plugins in other sources do not silently replace existing origins. HTTP needs explicit development permission; up to five validated redirects, without HTTPS downgrade.
-
-## Hover feedback
-
-Interactive controls automatically receive one hover pulse per entry, excluding disabled/inert controls and movement between their children. Supported elements include buttons, links, inputs, interactive ARIA roles/focusable controls or pointer cursors. Custom controls may mark `data-framely-interactive`. Native feedback uses the current hit controller, 12 ms/120 Hz/amplitude 0.15 with at least 80 ms spacing. Pages cannot target other windows/controllers.
-
-## Metadata and publication
-
-Manifest supports payload PNG icon ≤1 MiB/1024×1024, details/tags/changelog and up to eight PNG/JPEG screenshots. Icons appear in lists/Dock; missing icons use the default. Optional `authorUrl`, `documentationUrl`, `homepage` are credential-free HTTP/HTTPS links, preserved in state/catalog. Author URLs link author labels; device default browsers open external pages.
-
-```json
-{
- "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"]}
-}
-```
-
-The fragment above shows explicit URLs; GitHub registration source may omit `downloadUrl`, deriving repository, `v` and `-.framely`. The packaged Manifest still requires the final URL; current pack does not derive it. See [publishing](developer-guide/publishing.md) for temporary-Manifest commands. Community icons normally use top-level `icon`, with derived and verified pinned Raw URLs, without `publish.icon`. The local catalog CLI uses external `publish` images and does not export packaged image paths; absent external icons use the store default. Community registration pins submodule commits, obtains expected SHA256 from Release digests or source `downloadSha256`, then validates downloaded package hashes, declarations and payloads. Generated channels provide catalog/history JSON without mirroring packages/images. It does not execute author code or establish reproducible source/binary equivalence.
-
-Catalog CLI only emits JSON, preferring `downloadUrl` over base URL/filename and using external publish images. It does not copy resources. Stores filter keywords/tags/origins/states, retain session caches on failures and install reviewed bytes without downloading again after confirmation.
-
-## Scaffold and preview
-
-```bash
-node tools/plugin-dev.mjs init ./my-plugin example.my-plugin
-cd my-plugin
-npm install
-npm run dev
-npm run build
-```
-
-Release tool location: `tools/source/tools/plugin-dev.mjs`. Preview binds `127.0.0.1:5173` and reloads changes, simulating windows/notifications only. Backend calls, identities, keyboards and haptics require Frame. SDK includes Section/Button/Toggle/Slider/TextField/Select/Tabs/Notice/useBackend/usePluginEvent; window maps can register different components. See [SDK](developer-guide/sdk.md).
-
-## Windows, tags and history
-
-Default windows are 1600×900 and 3 meters, with width 640–2560, height 360–1440, physical width 0.4–4.0. Reopen to apply changes. A `localWeb` window asks backend `window.get` for `http://localhost:/framely-window/`; this is a loading mode, not a permission.
-
-Use tags instead of legacy category, accepted only for compatibility. Database `tags.json` aggregates, deduplicates and sorts current channel tags; each channel is separate, clients merge sources locally. Tags change automatically with releases.
-
-Packages/dependencies/uploads stage in `/tmp/framely-package-*`, removed on completion/cancel/failure. Idle uploads/reviews expire after 15 minutes, checked every 30 seconds.
-
-Main `catalog.json` has one latest entry per ID. `plugins//versions.json` contains `{schemaVersion:1,id,versions:[...]}` without that main entry. Details load history on demand, as does resolution when latest fails a range. Unknown catalog fields are ignored while known fields are checked; changed required semantics need a schema bump. Package Manifests remain strict. See [relationships](plugin-relationships.md), [subscriptions](source-subscriptions.md) and [lifecycle](plugin-lifecycle.md).
diff --git a/docs/en/plugin-lifecycle.md b/docs/en/plugin-lifecycle.md
deleted file mode 100644
index 4d0371b..0000000
--- a/docs/en/plugin-lifecycle.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Plugin lifecycle
-
-[简体中文](../plugin-lifecycle.md)
-
-Lifecycle is scheduled by the root core, but hooks run as the plugin's declared user, defaulting to `steamos`. Standalone hooks must match backend identity. UI-only plugins can set `lifecycle.runAs` for standalone commands; users see this identity during installation.
-
-```json
-{
- "backend": {"entry": "backend.py", "runAs": "steamos", "autostart": false, "restart": "on-failure", "restartLimit": 3},
- "lifecycle": {
- "onInstall": {"entry": "backend.py"},
- "onUpdate": {"entry": "backend.py"},
- "onStart": true, "onStop": true,
- "onUninstall": {"entry": "backend.py"},
- "onCrashCleanup": {"entry": "backend.py", "args": ["--cleanup"]},
- "timeoutSeconds": 10
- }
-}
-```
-
-All hooks are optional. Command entries must be hashed payload files, never shell strings or outside paths. Hooks do not introduce extra network/identity permissions. Timeouts are per hook, 1–15 seconds, default 10.
-
-| Hook | Timing and execution |
-| --- | --- |
-| `onInstall` | Standalone command before first activation |
-| `onUpdate` | Target version's command after stopping the old backend, before activation; includes downgrades |
-| `onStart` | `framely.lifecycle.start` RPC after backend launch, before business calls |
-| `onStop` | `framely.lifecycle.stop` before process termination during disabling, restart, updates, uninstall or normal manager shutdown |
-| `onUninstall` | Standalone command after stopping backend, before removing payload |
-| `onCrashCleanup` | Standalone cleanup after crashes, signals, OOM, timeouts or initialization failure |
-
-Closing UI unmounts React, not the backend. Effects clean page subscriptions/leases, not backend resources. On-demand/autostart determines startup timing; restart policy handles failures after startup.
-
-## Context and SDK
-
-```ts
-interface LifecycleContext {
- pluginId: string;
- phase: 'onInstall'|'onUpdate'|'onStart'|'onStop'|'onUninstall'|'onCrashCleanup';
- reason: string;
- version: string;
- previousVersion: string|null;
- dataDir: string;
- exit: {reason:string;exitCode?:number|null;signal?:number|null;oom?:boolean;message?:string}|null;
-}
-```
-
-Standalone commands read `FRAMELY_LIFECYCLE` and JSON `FRAMELY_LIFECYCLE_CONTEXT`. Backends and commands receive `FRAMELY_PLUGIN_ID`, `FRAMELY_PLUGIN_VERSION`, `FRAMELY_DATA_DIR` and the runtime user's HOME. Standalone stdout/stderr are logs; exit 0 means success. Backend hooks reply with result/error. Pages cannot call reserved `framely.lifecycle.*` methods.
-
-The Python helper is copied from `@framely/sdk/python` into `framely.py`, without extra pip dependencies:
-
-```python
-from framely import serve
-
-def initialize(context):
- # Keep operations repeatable and limited to this plugin's resources.
- return {'ready': True}
-
-def cleanup(context):
- return {'cleaned': True}
-
-def dispatch(method, params):
- return {'ok': True}
-
-serve(dispatch, {
- 'onInstall': initialize, 'onUpdate': initialize,
- 'onStart': initialize, 'onStop': cleanup,
- 'onUninstall': cleanup, 'onCrashCleanup': cleanup,
-})
-```
-
-With a standalone hook environment, the helper calls that hook and exits instead of entering RPC. TypeScript backends can use `registerLifecycle` from `@framely/sdk/lifecycle` in their existing dispatcher; it does not provide process launch or JSON-line transport. Page exports include context types, not backend privileges.
-
-## Failure, cleanup and recovery
-
-Failed install/update commands prevent activation. If installation/update starts a backend immediately (resident or previously running), failed initialization restores the previous version/current link; first installation is undone. First installation of an on-demand plugin normally runs onInstall only. Its onStart runs when a page opens or a business call starts the backend; failure then follows startup recovery, without undoing a previously completed installation. Failed downgrade initialization restores the version used before downgrade. Files can be restored, but migrations and hardware operations cannot be automatically reversed. Make migrations repeatable, use backups/temporary files/atomic replacement, and retain compatibility with the preceding version.
-
-Failed/timed-out `onStop` is logged but the process group is still stopped. Crashes close plugin windows/notifications before standalone cleanup; cleanup failure does not suppress errors or future recovery. Cleanup must not depend on crashed memory or destructors. Remove owned resources only; do not restart SteamVR or delete other plugins' files.
-
-Failed `onUninstall` retains the plugin and allows retry or explicit force removal. Force still attempts bounded cleanup and removes the payload afterward; data remains unless the core's explicit purge option is used. It cannot guarantee restoration of external settings.
-
-Restart defaults to `on-failure`; `never` reports failure without retrying. Limits are 1–10, default 3 consecutive failures, reset after 60 seconds stable operation. Delays are 1, 2, 4, 8, 16, 32 seconds, capped at 32. Exit 0 and manager-directed stops do not count as failures. Exit codes, signals, systemd OOM and timeouts appear in status/logs.
-
-States include starting/running/stopping/stopped/recovering/failed. Core management is serialized. Normal SIGTERM/SIGINT attempts to stop plugins; forced termination or power loss cannot guarantee hooks. Support safe recovery on next startup.
-
-The log view includes backend and lifecycle logs; each retains a current 2 MiB file and a preceding copy. Failed pre-registration installs log to `/var/lib/framely/logs/.lifecycle.log`. User data is under `/var/lib/framely/data///`, relative to any custom core state root.
diff --git a/docs/en/plugin-relationships.md b/docs/en/plugin-relationships.md
deleted file mode 100644
index 5d75b33..0000000
--- a/docs/en/plugin-relationships.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Dependencies and conflicts
-
-[简体中文](../plugin-relationships.md)
-
-```json
-{
- "dependencies": {
- "example.shared-api": "^1.0.0",
- "example.camera-api": {"version": ">=1.2.0 <2.0.0", "source": "https://example.org/plugins/catalog.json"}
- },
- "optionalDependencies": {"example.notifications": "^1.0.0"},
- "conflicts": {"example.other-camera": "*"},
- "exclusiveResources": ["steamvr.passthrough-color"]
-}
-```
-
-All four fields are optional. Relationship-declaring plugins and matched dependency/conflict targets use valid SemVer. Ranges support exact versions, `^`, `~`, `*` and comma/space comparison conjunctions. `1.2.0` is exact; `>=1.2.0 <2.0.0` requires both. `||` is unsupported. Self-dependencies, duplicate required/optional entries, contradictory conflicts and repeated exclusive resources are rejected.
-
-`source` is an HTTPS catalog URL, not a subscription or package. Compatible installed versions/sources are reused first. Without explicit source, the originating catalog is preferred; otherwise choose explicitly. An unavailable explicit source is not silently replaced. New explicit sources join the confirmation plan; disabled sources need enabling first. Removing a source retains installed origin URLs for matching.
-
-Local packages with no default source require selecting missing dependency sources:
-
-```bash
-framely install plugin.framely --source my-source --approve
-framely install plugin.framely --dependency-source example.shared-api=https://example.org/catalog.json --approve
-```
-
-Dependency-source flags may repeat, cannot override explicit Manifest sources, and use the UI's resolver. Add `--approve-run-as` only after reviewing an identity change.
-
-The main catalog contains one recommended entry per ID, with history in relative `plugins//versions.json`. Users may select old versions or reinstall. ID/version/hash/relations are rechecked. Resolution reuses installed matches, otherwise the first matching source entry. Incompatible constraints, cycles or missing required dependencies stop installation. Missing optional dependencies neither prevent installation nor install automatically.
-
-Plans disclose packages, versions, sources, users, added sources, dependency enabling and conflict disabling. Confirmed operations use the same verified bytes; changed device/source state requires another inspection. Plans allow at most 32 plugins without a total package/expanded-size cap. Download/verify first, then install sequentially and check reverse dependencies.
-
-Persistent transaction logs restore links, origins/enabled state, undo newly added packages and attempt to restart previous backends on failure. Startup recovers interrupted transactions. Data remains; file rollback cannot undo migrations or hardware effects, so make them repeatable and recoverable.
-
-Enabling confirms required dependencies and conflicting plugins to disable. Either side's conflict declaration prevents simultaneous enabling, not installation. Matching exclusive resources work likewise. Disabling/removing a base plugin requires confirming dependent disabling; dependents are not automatically removed.
-
-Backends start in dependency order after base `onStart` succeeds; absent hooks use process startup. Stops reverse that order. Crashed bases pause running dependents in a waiting state without counting dependent failures, then resume them after recovery. Disabled/non-restarting bases need user action.
-
-```tsx
-import {framely, useDependencies} from '@framely/sdk';
-const states = await framely.dependencies();
-// In React: const {items, error} = useDependencies();
-```
-
-Only declared required/optional dependencies are returned, with version/enabling/range/availability/runtime state. Changes emit `dependencies.changed`. No cross-plugin backend calls or extra privileges are granted. Conflicts/resources depend on author declarations, not arbitrary file/device detection.
-
-There is no manual plugin rollback API; old-version installation uses the normal plan and `onUpdate`. Transaction recovery is failure handling, not a version picker. Keep same-ID/version content immutable.
diff --git a/docs/en/releases.md b/docs/en/releases.md
deleted file mode 100644
index 37df0b3..0000000
--- a/docs/en/releases.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Framely releases and updates
-
-[简体中文](../releases.md)
-
-Plugin catalogs and device-update sources are separate settings. Current initialization adds community stable/testing catalogs, subject to server availability. The distributor preconfigures device updates; About offers check/download/install/rollback. Without a source, use a release package. Packages do not require signatures or public keys.
-
-## Update descriptors
-
-After building an archive, generate a plain JSON descriptor containing version, architecture, URL, size and SHA256:
-
-```bash
-framely release-manifest --archive "./framely-0.2.0--linux-arm64.tar.gz" --url "https://example.org/releases/framely-0.2.0--linux-arm64.tar.gz" --output ./framely-release.json --changelog 'Release changes'
-```
-
-Publish archive/descriptor on HTTPS. Downloads allow up to five validated redirects without HTTPS downgrade. Configure the complete descriptor URL from a maintenance terminal:
-
-```bash
-framely call system.source.save '{"source":{"url":"https://example.org/framely-release.json"}}'
-```
-
-This is a distributor interface, not an ordinary settings item. The official engine and desktop installer initialize `https://github.com/SteamFramelyHomebrew/framely/releases/latest/download/framely-release.json` only when no source is configured, preserving existing sources. Hashes establish content consistency; identity depends on trusted sources, not signatures.
-
-Updates verify API/architecture, length, SHA256, safe extraction and internal `SHA256SUMS`. Downloads do not block the core. After user confirmation, a separate systemd helper switches releases and restarts Framely only. Log: `/var/lib/framely/logs/update.log`. Failed installation attempts to restore release/state; the UI can return to the preceding release.
-
-Sources contain URL only. Legacy signed descriptors/envelopes/public-key fields are unsupported. Rollback preserves plugin data without downgrading its format. Device upgrades, rollback and reboot still need real validation beyond local tests.
-
-## Recovery after SteamOS updates
-
-Management state resides at `/home/.framely/state`, including state, update source, logs, release links, Steam username and Framely UID. `/var/lib/framely` is a compatibility link. Old state migrates after writers stop; conflicting copies cause a stop rather than overwrite.
-
-Accounts/services depend on OS configuration. When writable, `sudo bash /home/.framely/repair.sh` restores links, account, service files and startup from the retained, verified release without another download. It invokes the current release's `install.sh --repair` without creating releases or changing the previous-version record. UID conflicts or changes stop recovery. It never disables read-only protection automatically.
-
-Deleted `/home` data cannot be recovered. OS updates may require manual intervention. After rolling back to a release without repair support, use a newer supporting package. Before stable publication, validate actual OS-update recovery, state/data/UIDs/services and SteamVR compatibility.
-
-## GitHub Actions
-
-Device and installer share the repository with independent versions/tags/workflows:
-
-- Device: root Cargo version, `v` (currently `v0.4.2-preview.6`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF.
-- Installer: `installer/Cargo.toml`, `installer-v` (currently `installer-v0.4.1-preview.4`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit.
-
-Versions need not match or ship together. Update the relevant Cargo.lock with version changes. Installer package/macOS versions use the installer version.
-
-Both workflows cache dependencies/builds by product/platform/toolchain/configuration, including failed builds. Installer uses `installer/target`; device CI uses `target/cargo`, separate from `target/cef`, for tests and packaging. Branch pushes do not build releases; matching tags or manual runs do. Tags may restore default-branch caches, not another tag's private cache. A manual main run can prime cache and builds artifacts only. Cold builds/toolchain changes need compilation; inspect cache steps rather than assuming a hit.
-
-```bash
-# After committing and pushing the intended source:
-git tag v0.4.2-preview.6
-git push origin v0.4.2-preview.6
-# Independent installer release:
-git tag installer-v0.4.1-preview.4
-git push origin installer-v0.4.1-preview.4
-```
-
-Prerelease suffixes create GitHub Prereleases without Latest. Installer releases are never Latest. Only stable device releases become Latest, keeping default installer/update URLs on the device product. The desktop installer filters device archive names and does not mistake ARM64 installer packages for device runtimes. Download desktop installers from their explicit installer tags.
-
-Only tags publish. After every product build succeeds, publication creates a draft, uploads assets/checksums, then makes it public. Failed drafts are not stable download targets. Manual runs leave Actions artifacts only.
-
-Device attachments include the runtime archive, `framely-release.json`, `bootstrap.py`, `install.sh` and `SHA256SUMS`. Installer releases have five platform packages and checksums only. Desktop installers download archive/checksums without user-managed scripts.
-
-External checksums cover downloaded archives; internal checksums cover extracted payload. Neither independently proves author identity. Online installation trusts the chosen repository/HTTPS, local installation the supplied archive/checksums. Redirects never downgrade HTTPS.
-
-The entry fetches the release engine. Default is latest stable; `--version TAG` chooses that Release for both engine and package and works for Preview-only repositories. Locally run `python3 tools/bootstrap.py install --archive /path/package.tar.gz --checksums /path/SHA256SUMS`. Updating requires existing installation; selecting the same installed release repairs it, another version installs while retaining state.
-
-Installation initializes the official update descriptor URL only if no source is configured, preserving existing sources for future in-app updates.
-
-## Unified uninstallation
-
-Scripts and desktop UI expose one uninstall operation. It stops session/downloads, then calls root-only `prepare-uninstall --approve`: persistently disable plugins, stop them in dependency order, run uninstall hooks, remove payloads. Only complete success removes services/core/runtime. Failure retains Framely, restores session access and leaves plugins disabled for troubleshooting/retry. There is no skip-failed-plugin manager uninstall.
-
-Root maintenance can uninstall before consent or after revocation; pages do not expose the maintenance method. Settings/data/accounts remain, and external side effects are not guaranteed reversible. Old releases without this endpoint need updating first.
diff --git a/docs/en/source-subscriptions.md b/docs/en/source-subscriptions.md
deleted file mode 100644
index 9c4f302..0000000
--- a/docs/en/source-subscriptions.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Plugin source subscriptions
-
-[简体中文](../source-subscriptions.md)
-
-In Sources → Source subscriptions, paste an HTTPS JSON URL, preview and confirm:
-
-```json
-{
- "schemaVersion": 1,
- "name": "Community sources",
- "sources": [
- {"id":"stable", "name":"Stable", "url":"https://example.org/stable/catalog.json"},
- {"id":"testing", "name":"Testing", "url":"https://example.org/testing/catalog.json"}
- ]
-}
-```
-
-Source IDs must remain stable and unique within a subscription. Duplicate URLs, nested subscriptions, HTTP or credential-bearing URLs are forbidden. Limits: 100 sources/512 KiB per subscription, 20 subscriptions/200 sources per device. Subscriptions aggregate catalog URLs, not packages or images.
-
-Automatic refresh defaults to daily and can be disabled or triggered manually. The running session checks due entries at startup, uses ETag/Last-Modified, and follows at most five HTTPS redirects. Failures preserve persisted source lists and retry with exponential backoff from one minute up to one day.
-
-New sources join automatically; names update. URL changes keep the old address until confirmed. Refreshing also refreshes catalogs. Catalog caches are session-local; offline catalogs after restart are not guaranteed.
-
-Canonical URLs deduplicate references from subscriptions and manual additions. Removing a subscription/source removes only its reference; sources disappear only without references. Installed plugins, origins and data remain; origins do not silently switch.
-
-Disabled preferences persist across refresh/removal/readdition. Manage subscription URL changes through pending changes. Refreshing changes source metadata and update hints, never automatically installs, updates or enables plugins.
diff --git a/docs/en/user-guide/basic-usage.md b/docs/en/user-guide/basic-usage.md
deleted file mode 100644
index b5c3524..0000000
--- a/docs/en/user-guide/basic-usage.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Basic usage
-
-[简体中文](../../user-guide/basic-usage.md)
-
-[Documentation](../README.md) · [Installation](installation.md)
-
-## Quick menu and manager
-
-Click Framely in the SteamVR Dashboard Dock. First use requires accepting the terms and privacy statement; plugins do not automatically start before consent.
-
-- **Favorites**: open favorite plugin quick pages.
-- **Installed**: browse local plugins and their pages.
-- **Settings**: language, safe mode and the plugin manager entry.
-
-The manager opens a separate large window with navigation for plugins, store, sources, settings and About. Installation, updates and source management happen here. A phone or computer can also use the [network panel](network-panel.md).
-
-## Plugins and windows
-
-Enable, disable or favorite plugins in the list. Enabled means available for use; backends normally start on demand. Only plugins declaring `autostart` remain resident after startup. Closing a plugin window generally does not disable its backend. Dependency or conflict problems are explained, and affected plugins are shown before changes.
-
-Dock windows use SteamVR close controls. Temporary windows may be destroyed when switching views. Individual plugin interfaces and behavior are maintained by their developers.
-
-## Settings and diagnostics
-
-Settings provides language, network panel and proxy configuration. HTTP and GitHub proxies have independent switches; saving an address does not automatically enable it.
-
-Safe mode pauses third-party plugins for troubleshooting; leave it after resolving the problem. About shows the device version, updates and terms. Revoking consent disables all plugins; accepting again does not automatically re-enable them.
-
-Log commands are in the [installation guide](installation.md). A working manager does not establish that all plugins work. Enable plugins individually and verify their sources and runtime users.
diff --git a/docs/en/user-guide/installation.md b/docs/en/user-guide/installation.md
deleted file mode 100644
index 4860114..0000000
--- a/docs/en/user-guide/installation.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# Install, update, repair and uninstall
-
-[简体中文](../../user-guide/installation.md)
-
-[Documentation](../README.md) · [Next: basic usage](basic-usage.md)
-
-## Before installation
-
-The target is Linux ARM64 Steam Frame. The development baseline is SteamOS VR 0.4.2 and SteamVR build 20260928.6175029; other versions need validation. The runtime includes CEF, so users do not need Node.js or Rust on the device.
-
-Connect your computer and Frame to the same network. Enable developer mode and SSH on Frame, and prepare the `steamos` account password. Verify the SSH fingerprint. Installation uses sudo to create an account, systemd services and release directories. It does not automatically disable SteamOS read-only protection; it stops if required directories cannot be written.
-
-## Option 1: desktop installer
-
-1. Download the appropriate installer from an `installer-v` entry in [Releases](https://github.com/SteamFramelyHomebrew/framely/releases). Linux uses tar.gz, Windows an EXE ZIP, and macOS an application ZIP.
-2. Extract and run it. Scan for the device or enter its IP and SSH port.
-3. Verify the fingerprint, enter `steamos` and its password, connect, and select a Framely device release.
-4. Enable testing releases to select a Preview. Installer and device versions are independent.
-5. Confirm the device, version and operation on the maintenance page. Alternatively, select a local device archive and its external `SHA256SUMS`.
-
-On Linux, run `bash install-desktop-entry.sh` from the extracted installer directory to add an entry with the Framely icon to your user application menu. The entry points to that directory; run the script again after moving it.
-
-Downloads are checked locally and again on the device. Passwords are not saved in configuration. Observe startup and rendering on Frame after installation.
-
-## Option 2: install from Frame's SSH terminal
-
-Run these commands **on Frame**. To install the latest stable release:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
-```
-
-To select a Preview or another tag:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
-```
-
-Replace the example with an existing device release tag. An explicit tag selects the engine and package from that Release and works without a stable release. Without a tag, the latest stable release is selected. Enter the sudo password when prompted.
-
-## Option 3: local archive
-
-Download `framely--linux-arm64.tar.gz`, the external `SHA256SUMS`, and `bootstrap.py` from the same Release. Place them together on Frame:
-
-```bash
-python3 bootstrap.py install --archive "./framely--linux-arm64.tar.gz" --checksums ./SHA256SUMS
-```
-
-Angle brackets are placeholders; substitute the actual filename. The engine verifies, stages and extracts under `/home`, then checks internal hashes, avoiding the device's limited `/tmp`.
-
-## Verify installation
-
-```bash
-systemctl is-active framely.service framely-session.service
-/var/lib/framely/current/bin/framely --version
-sudo /var/lib/framely/current/bin/framely status
-```
-
-Both services should be `active`. Open SteamVR Dashboard and find Framely in the Dock. Read and accept the terms and privacy statement before managing plugins. Continue with the [plugin installation guide](plugins.md).
-
-## Update and rollback
-
-When an update source is configured, check, download and confirm an update in **About**. Alternatively, use the desktop installer or run on Frame:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- update --version v0.4.2-preview.3
-```
-
-Updates preserve plugins, settings and data, retain the previous release, and restart Framely only. Preview updates require an explicit tag or local package; the default stable source does not automatically select Preview builds.
-
-Choose rollback in About, or run:
-
-```bash
-sudo bash /var/lib/framely/current/rollback.sh
-```
-
-Rollback preserves plugin data but does not guarantee conversion of newer data formats to older ones.
-
-## Repair and logs
-
-SteamOS updates may reset system accounts and services. State persists under `/home/.framely/state`; `/var/lib/framely` is a compatibility entry. If system configuration is writable and `/home` remains intact:
-
-```bash
-sudo bash /home/.framely/repair.sh
-sudo journalctl -u framely -u framely-session --no-pager -n 100
-```
-
-Repair restores accounts and services from the retained release and stops on UID conflicts. It cannot restore deleted `/home` data. Recovery after SteamOS updates still requires device validation.
-
-## Uninstall
-
-Use the desktop maintenance page or run:
-
-```bash
-sudo bash /var/lib/framely/current/uninstall.sh
-```
-
-Uninstallation disables and uninstalls every plugin, including uninstall hooks, before removing Framely programs and services. Failed plugin cleanup retains Framely and reports an error; resolve it and retry. Settings, plugin data and the dedicated account remain. External changes made by third-party plugins cannot always be automatically undone.
diff --git a/docs/en/user-guide/network-panel.md b/docs/en/user-guide/network-panel.md
deleted file mode 100644
index 6985b68..0000000
--- a/docs/en/user-guide/network-panel.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Network management panel
-
-[简体中文](../../user-guide/network-panel.md)
-
-[Documentation](../README.md) · [Plugin installation](plugins.md)
-
-## Find the address
-
-The panel is enabled by default on IPv4 port `15915` and does not require SteamVR to run. Connect your phone/computer to Frame's network and open:
-
-```text
-http://DEVICE_IP:15915
-```
-
-Settings → Network panel shows current IP addresses and full URLs. The desktop installer also provides an entry after connecting. If no IP is detected, check Wi-Fi/Ethernet. Port conflicts are reported in settings.
-
-## First password setup
-
-With no configured password, first access shows setup only and does not expose management APIs. Enter at least eight characters and confirm the password, save, then log in. Password setup may precede terms acceptance; consent is still required to manage plugins.
-
-A randomly salted PBKDF2 hash is stored, survives reboot, and is not displayed by the UI. Unauthenticated setup cannot overwrite an existing password. The panel password is independent of SSH/sudo credentials.
-
-Change passwords from authenticated settings or the device's native manager. If forgotten, prefer changing it from Frame's native window. Existing passwords are retained; a previously configured password with verification explicitly disabled retains that choice. The panel uses HTTP: use a trusted network and do not forward the port directly to the public internet.
-
-## Available operations
-
-Manage sources and subscriptions, browse the store, upload local packages, confirm installation, enable/disable plugins, change settings and check device updates. Backends always run on Frame; uploading a file transfers it from your phone/computer.
-
-VR windows, controller vibration and SteamVR keyboard need headset validation; a browser cannot replace those interactions. Some external links open in the device's default browser.
-
-## Port changes, shutdown and expired sessions
-
-Change the port or switch in Settings → Network panel. Password/authentication changes invalidate old sessions; use the new address after a port change. Turning the panel off disconnects web access without closing the native manager.
-
-Restarting the session service also invalidates old logins. Saving identical settings retains the session. Incorrect password checks are rate-limited; retry later when asked.
-
-If unreachable, check changing device IPs, network reachability, enabled state and port. Settings and passwords live in `/home/.framely/state` and are retained by uninstall; reinstalling may still require the old password.
diff --git a/docs/en/user-guide/plugins.md b/docs/en/user-guide/plugins.md
deleted file mode 100644
index bd5ed43..0000000
--- a/docs/en/user-guide/plugins.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Install and manage plugins
-
-[简体中文](../../user-guide/plugins.md)
-
-[Documentation](../README.md) · [Network panel](network-panel.md)
-
-## Install from the store
-
-1. Open **Store** in the manager. Current code initializes community stable/testing sources; availability depends on the actual service and device configuration. If no catalog is available, add a developer's complete HTTPS `catalog.json` URL in Sources.
-2. Filter by keywords, tags, source or installation state.
-3. Open details and review author, description, changes, runtime user and dependencies. The recommended version is shown first; history loads on demand.
-4. Install or inspect the selected version. Read the confirmation after download, verification and dependency resolution.
-5. Check actual package runtime users, including any root dependencies, then confirm. Enable or open the plugin from the installed list.
-
-Catalog runtime users are informational; final confirmation uses the downloaded package. `steamos` runs as the Steam session user; `root` can change system and device settings. Hashes check content, not developer identity or safety.
-
-## Sources and subscriptions
-
-For one catalog, add a name and `https://.../catalog.json` URL in Sources, test the connection, then save.
-
-For multiple catalogs, add a subscription JSON URL in Source subscriptions, preview its sources, then confirm. A subscription is different from a catalog. Refreshing it changes source lists and metadata, not installed plugins.
-
-Disabling or deleting a source does not uninstall its plugins. Subscription URL changes require confirmation. See the [subscription reference](../source-subscriptions.md).
-
-## Import a file or package URL
-
-Packages use the `.framely` extension and need no manual extraction. Open import in the manager's Plugins page:
-
-- **Local file**: select a downloaded `.framely` file on your phone/computer in the web panel; it uploads and is verified.
-- **Package URL**: paste a fixed-version HTTPS download URL from the author and wait for verification.
-
-Both require reviewing confirmation, with an additional confirmation for changed runtime users. Device tar.gz archives and ordinary ZIP files are not plugin packages.
-
-You can also install from Frame's SSH terminal:
-
-```bash
-sudo /var/lib/framely/current/bin/framely install ./plugin.framely --approve
-```
-
-Add `--approve-run-as` only when you have verified that changing the runtime user is intended. CLI approval does not assess the source's trustworthiness.
-
-## Updates, older versions and removal
-
-Open details for plugins marked updatable, review changes and confirm. Select history to inspect an older version. Switching sources is explicitly disclosed. Settings and favorites are generally retained; a different runtime user uses a different data directory and does not automatically migrate the old one.
-
-Uninstall through the management dialog. Hooks handle external cleanup and saved data is retained. Do not remove system plugin directories manually. Disabling stops use without removing programs or data.
-
-## Troubleshooting
-
-| Symptom | Action |
-| --- | --- |
-| Empty store | Check enabled sources, connectivity and filters |
-| Download/hash failure | Check network and URL; download again without bypassing verification |
-| Missing history | The source may lack its history file; use latest or a trusted local old package |
-| Cannot enable | Read dependency/conflict errors and install required plugins |
-| Repeated crashes | Disable, inspect logs, use safe mode if needed, and report to the author |
diff --git a/docs/localization.md b/docs/localization.md
index 6d4bf8e..09266ed 100644
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -1,40 +1,36 @@
-# 界面语言与社区翻译
+# Languages and community translation
-[English](en/localization.md)
+[简体中文](zh-CN/localization.md)
-Framely 内置简体中文和英语。在“设置”顶部选择语言;默认跟随系统语言,手动选择会保存在设备配置中。找不到对应语言时使用英语。语言文件缺少某条翻译时,该条内容回退到内置英语。
+Framely includes Simplified Chinese and English. Choose a language at the top of Settings; automatic follows the system and manual choices persist. Unsupported languages and missing translations fall back to built-in English.
-## 制作语言文件
+## Create a language file
-从设置下载英文模板,或复制 `ui/locales/en-US.json`。文件是 UTF-8 JSON:
+Download the English template from Settings or copy `ui/locales/en-US.json`. Use UTF-8 JSON:
```json
{
"schemaVersion": 1,
"locale": "fr-FR",
"name": "Français",
- "messages": {
- "语言": "Langue",
- "保存": "Enregistrer",
- "启用 {0}": "Activer {0}"
- }
+ "messages": {"语言": "Langue", "保存": "Enregistrer", "启用 {0}": "Activer {0}"}
}
```
-- `locale` 使用语言代码,例如 `en-US`、`zh-CN`、`fr-FR`。不能使用 `auto`。
-- `name` 使用该语言自己的名称,显示在语言选择列表中。
-- `messages` 左侧是稳定的文案键,不要翻译或改写左侧;右侧才是译文。
-- `{0}`、`{1}` 等是运行时参数。可以调整位置,但不能增删占位符。
-- 可以先提供部分翻译,其余条目显示英语。空字符串是有效译文,不视为缺失。
-- 按目标语言习惯重写整句,结合实际操作理解按钮和状态。部分键是句子片段,注意保留必要的空格。
-- 文件只包含文本,不执行代码,也不解释 HTML。单文件最多 1 MiB、3000 条翻译,最多导入 50 个语言文件。
+- Use a locale code such as `en-US`, `zh-CN`, `fr-FR`; `auto` is not a file locale.
+- Name the language in that language.
+- Message keys are stable: translate values only, never keys.
+- Preserve all `{0}`, `{1}` placeholders; their positions may change.
+- Partial translations fall back to English; empty strings are valid translations.
+- Translate whole sentences for natural usage, taking fragment whitespace into account.
+- Files contain text only, not executable code or interpreted HTML. Limits: 1 MiB/3000 entries per file and 50 imported files.
-## 安装
+## Install
-在“设置 → 语言”点击“安装语言文件”,选择 JSON 文件。导入后在语言列表中选择它;再次导入相同语言代码会更新原文件。
+In Settings → Language, install the JSON, then select it. Reimporting the same locale replaces that language file.
-安装后的文件位于 `/var/lib/framely/locales/.json`。也可以由管理员直接放入此目录,文件名必须与 `locale` 一致;刷新管理面板后读取。损坏的文件会跳过,并在设置中显示文件名。插件作者提供的名称、介绍、标签及插件页面由各插件维护,宿主语言文件不会自动翻译这些内容。
+Files live in `/var/lib/framely/locales/.json`. Administrators may place matching filenames there directly and refresh the panel. Invalid files are skipped and named in settings. Plugin names, descriptions, tags and pages remain the plugin author's responsibility; host language files do not translate them.
-## 项目校验
+## Validate the project
-执行 `node tools/test-localization.mjs` 检查内置英语是否覆盖全部界面文案键、占位符是否一致,以及系统语言匹配、手动选择和缺失翻译回退。
+Run `node tools/test-localization.mjs` for English coverage, placeholders, system matching, manual preferences and fallback behavior.
diff --git a/docs/plugin-development.md b/docs/plugin-development.md
index 83883ac..0dffbf8 100644
--- a/docs/plugin-development.md
+++ b/docs/plugin-development.md
@@ -1,64 +1,49 @@
-# 插件开发参考
+# Plugin development reference
-[English](en/plugin-development.md)
+[简体中文](zh-CN/plugin-development.md)
-第一次开发请先阅读[完整开发流程](developer-guide/README.md)和[发布指南](developer-guide/publishing.md)。
+Start with the [workflow](developer-guide/README.md), [SDK](developer-guide/sdk.md), [Manifest](developer-guide/manifest.md) and [publishing](developer-guide/publishing.md).
-Dock 右侧快捷入口为与主窗口同朝向的弧面临时浮层,常用/已安装/设置三个底部 tab;插件管理从设置打开独立的原生 Dock 大窗口。安装表单与插件源设置只在管理窗口中出现。
+The Dock quick menu has Favorites/Installed/Settings tabs; management opens a separate Dock window. Installation and sources are handled there. See `examples/showcase` and `sdk/src/index.tsx`. Release `tools/source` includes UI/SDK/examples/template/tools. Bundle pages into browser JS and call `registerPlugin({QuickPage, WindowPage})`. The host installs the bridge first. Ordinary bundled page iframes use `sandbox="allow-scripts"` with opaque origins and cannot call administrator HTTP APIs.
-示例源码在 `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
+## Manifest and identities
```json
{
- "schemaVersion": 1,
- "apiVersion": 1,
- "id": "example.plugin",
- "name": "Example",
- "author": "Developer",
- "version": "1.0.0",
+ "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}}
- },
+ "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。
+Backend is optional. Runtime users are `steamos` (default) or `root`; autostart defaults false. Retired `permissions` is unsupported; windows, notifications and networking need no declarations. Backend networking uses the device; ordinary pages permit HTTP/HTTPS and WebSockets.
-ID 只允许小写字母、数字、点、短横线、下划线;载荷路径限 ASCII 字母/数字、`/._-+`,禁止绝对路径、空段、`.`、`..`。入口必须在 files 中;pack 会自动计算 files,不必手写哈希。后端入口须可直接执行,脚本需要正确 shebang(示例 `/usr/bin/python3`)。Python/系统依赖不会由 Framely 自动下载。
+IDs permit lowercase letters, numbers, dots, hyphens, underscores. Payload paths permit ASCII letters/numbers and `/._-+`, never absolute paths, empty/dot/dot-dot segments. Packaging supplies files hashes. Entries must be hashed; executable scripts need shebangs, e.g. `/usr/bin/python3`. Framely does not install system/Python dependencies.
-版本不可覆盖,相同版本再次发布需要使用新版本号。运行身份变化会选择新的数据目录 `/var/lib/framely/data//`;旧身份数据保留,安装使用旧身份的版本可恢复访问,不自动跨身份迁移文件。
+Published versions are immutable. User changes select `/var/lib/framely/data//`; old data remains without automatic migration, and reinstalling the old identity can access it again.
-## React API
+## React bridge
```tsx
import {framely, registerPlugin, Button, Section} from '@framely/sdk';
-
function QuickPage() {
return
-
-
-
+
+
+
;
}
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 次发送。
+Keys must match Manifest. Closing windows does not stop backend. IDs update/withdraw notifications. Images accept PNG/JPEG data URL ≤1 MiB or HTTPS. At most three actions, 1–60 seconds and ten sends per ten seconds/plugin. `onEvent` returns unsubscribe and receives `{type,data}`. Each window has its own event cursor. Input focuses the native keyboard rather than web prompts.
-`framely.onEvent(callback)` 返回取消订阅函数;接收 `{type, data}`。多个窗口分别按游标接收同一后台事件。文本输入框获得焦点时请求 SteamVR 虚拟键盘,完成后通过原生 setter 触发 React input/change;不使用网页 prompt。
+## Backend protocol
-## 后端协议
-
-每行一个 JSON,stdin 请求、stdout 响应。stdout 只写协议,日志写 stderr(单文件最多约 2 MiB,保留一份轮转备份)。每条消息最多 64 KiB,输入写入超时 5 秒或响应超时 15 秒会停止后端,下次打开可重试。
+Newline-delimited JSON on stdin/stdout; logs on stderr, rotating around 2 MiB. Messages ≤64 KiB; 5-second input or 15-second response timeouts stop the backend and allow later retry.
```json
{"id":1,"method":"save","params":{"text":"hello"}}
@@ -67,9 +52,9 @@ registerPlugin({QuickPage});
{"event":"progress","data":{"percent":50}}
```
-系统传入 `FRAMELY_PLUGIN_ID`、`FRAMELY_DATA_DIR` 和对应身份 HOME。通知按钮向后端调用 `notification.action`,参数 `{id, action}`。后端主动发送通知使用 `{"event":"notification","data":{...通知...}}`,受同一 manifest 与频率限制。
+Environment includes plugin ID, data directory and the user's HOME. Notification actions invoke `notification.action` with `{id,action}` and also reach pages; UI-only plugins can listen. Backend `notification` events send notifications under the same limits.
-## 打包与源发布
+## Packaging and catalogs
```bash
framely pack --manifest manifest.json --payload payload --output example.plugin-1.0.0.framely
@@ -77,51 +62,28 @@ 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 与清单中声明的载荷文件,拒绝未声明的附加文件。
+Archives contain Manifest and hashed payload only, without keys/signatures. Host the main catalog and relative history JSON. Authors can host packages on GitHub Releases. Current first initialization includes community stable/testing sources. Origins remain explicit; same-ID plugins in other sources do not silently replace existing origins. HTTP needs explicit development permission; up to five validated redirects, without HTTPS downgrade.
-源需静态托管 catalog.json 及相对路径下的 plugins//versions.json;插件包可放在作者自己的 GitHub Release。在插件源管理中添加目录完整 URL;当前代码首次初始化加入社区 stable/testing 来源,实际内容以服务器为准。目录条目有版本、下载 URL、SHA256。每个源独立显示,插件记录所选来源;另一个源同 ID 不会静默替换。HTTP 必须用户明确允许;目录和包支持最多五次重定向,HTTPS 不允许降级为 HTTP。
+## Hover feedback
-### 悬停反馈
+Interactive controls automatically receive one hover pulse per entry, excluding disabled/inert controls and movement between their children. Supported elements include buttons, links, inputs, interactive ARIA roles/focusable controls or pointer cursors. Custom controls may mark `data-framely-interactive`. Native feedback uses the current hit controller, 12 ms/120 Hz/amplitude 0.15 with at least 80 ms spacing. Pages cannot target other windows/controllers.
-Framely 主页面和插件 iframe 自动为可交互控件提供轻微的手柄振动,插件无需自行触发。进入按钮、链接、输入控件、可操作 ARIA role、可聚焦控件或 `cursor:pointer` 区域时触发一次;同一控件的子元素之间移动不会重复触发,禁用或 inert 控件不触发。自定义 React 控件应采用正确的 ARIA role,也可以加 `data-framely-interactive` 标记。
+## Metadata and publication
-振动由 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` 导出到商店目录;作者主页链接直接显示在作者名称上,未提供时作者名称为普通文本;商店列表、详情、管理弹窗和安装确认使用相同行为。文档和插件主页保留独立入口,点击后由系统默认浏览器打开。
-
-```json
-{
- "authorUrl": "https://github.com/author",
- "documentationUrl": "https://github.com/author/plugin#readme",
- "homepage": "https://github.com/author/plugin"
-}
-```
-
-插件仓库可声明发布信息:
+Manifest supports payload PNG icon ≤1 MiB/1024×1024, details/tags/changelog and up to eight PNG/JPEG screenshots. Icons appear in lists/Dock; missing icons use the default. Optional `authorUrl`, `documentationUrl`, `homepage` are credential-free HTTP/HTTPS links, preserved in state/catalog. Author URLs link author labels; device default browsers open external pages.
```json
{
"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"]
- }
+ "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` 和 `-.framely` 自动生成。包内仍需最终地址,当前 pack 不会自动读取 Git remote;按[发布指南](developer-guide/publishing.md)生成临时清单即可。社区图标通常只需 `icon`,数据库从固定源码提交生成并校验 Raw 地址;无需额外 `publish.icon`。本地 catalog CLI 使用 `publish` 外部图片,省略时不会导出包内图标/截图,商店采用默认图标。包内资源仍用于已安装插件。
+The fragment above shows explicit URLs; GitHub registration source may omit `downloadUrl`, deriving repository, `v` and `-.framely`. The packaged Manifest still requires the final URL; current pack does not derive it. See [publishing](developer-guide/publishing.md) for temporary-Manifest commands. Community icons normally use top-level `icon`, with derived and verified pinned Raw URLs, without `publish.icon`. The local catalog CLI uses external `publish` images and does not export packaged image paths; absent external icons use the store default. Community registration pins submodule commits, obtains expected SHA256 from Release digests or source `downloadSha256`, then validates downloaded package hashes, declarations and payloads. Generated channels provide catalog/history JSON without mirroring packages/images. It does not execute author code or establish reproducible source/binary equivalence.
-插件数据库只登记 submodule 固定提交,读取该提交的清单,补全默认下载地址,先取得 Release digest 或源码 `downloadSha256` 中的预期整包哈希,再下载并比对 SHA256、清单声明和载荷。发布分支提供各渠道目录及历史版本 JSON,不保存插件包或图片,不需要 entries 登记。数据库 CI 自动展示版本和运行用户变化,发布时禁止同 ID、版本更换 SHA256;不执行作者代码,不要求逐插件人工源码审核,也不验证源码与二进制可复现关系。
+Catalog CLI only emits JSON, preferring `downloadUrl` over base URL/filename and using external publish images. It does not copy resources. Stores filter keywords/tags/origins/states, retain session caches on failures and install reviewed bytes without downloading again after confirmation.
-`framely catalog` 也只生成 JSON:下载 URL 优先使用 `downloadUrl`,没有该字段时采用 `--base-url` 加包文件名。图标与截图来自 `publish` 的外部 URL,不复制资源。商店支持关键词、标签、来源、已安装和可更新筛选;源暂时不可用时显示会话缓存。安装使用已经校验的同一份包,不在确认后重新下载。
-
-后端默认按失败自动恢复,包括已经启动的按需插件;可用 `backend.restart: "never"` 关闭。默认第三次连续失败后停用,运行稳定一分钟后重置计数,详见[生命周期](plugin-lifecycle.md)。
-
-## 脚手架与预览
+## Scaffold and preview
```bash
node tools/plugin-dev.mjs init ./my-plugin example.my-plugin
@@ -131,22 +93,14 @@ npm run dev
npm run build
```
-发行包中的工具位置为 `tools/source/tools/plugin-dev.mjs`。预览监听本机 127.0.0.1:5173,修改页面后自动重载;它不会启动后端或获得 Frame 系统权限。窗口和通知在浏览器里模拟,身份、键盘、振动及后台调用仍需安装到设备测试。
+Release tool location: `tools/source/tools/plugin-dev.mjs`. Preview binds `127.0.0.1:5173` and reloads changes, simulating windows/notifications only. Backend calls, identities, keyboards and haptics require Frame. SDK includes Section/Button/Toggle/Slider/TextField/Select/Tabs/Notice/useBackend/usePluginEvent; window maps can register different components. See [SDK](developer-guide/sdk.md).
-SDK 提供 `Section`、`Button`、`Toggle`、`Slider`、`TextField`、`Select`、`Tabs`、`Notice`、`useBackend`、`usePluginEvent`。Select 使用页面内弹层,兼容离屏 CEF。`registerPlugin` 还可传入 `windows: {main: Component, other: Component}` 为多个声明窗口选择不同 React 页面。
+## Windows, tags and history
-通知按钮向后端发送 `notification.action`,同时广播给当前打开的插件页面,事件为 `{type:'notification.action',data:{id,action}}`;无后端插件可以通过 `framely.onEvent` 接收。
+Default windows are 1600×900 and 3 meters, with width 640–2560, height 360–1440, physical width 0.4–4.0. Reopen to apply changes. A `localWeb` window asks backend `window.get` for `http://localhost:/framely-window/`; this is a loading mode, not a permission.
-插件安装、更新、停止和异常恢复的约定见[生命周期开发说明](plugin-lifecycle.md)。
+Use tags instead of legacy category, accepted only for compatibility. Database `tags.json` aggregates, deduplicates and sorts current channel tags; each channel is separate, clients merge sources locally. Tags change automatically with releases.
-窗口可选声明 `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}`。尺寸在重新打开窗口时生效。
+Packages/dependencies/uploads stage in `/tmp/framely-package-*`, removed on completion/cancel/failure. Idle uploads/reviews expire after 15 minutes, checked every 30 seconds.
-依赖、可选依赖、冲突和独占资源见[插件关系](plugin-relationships.md),多个插件源聚合见[源订阅](source-subscriptions.md)。
-
-插件只使用 `tags` 描述用途,不再使用分类;为兼容已发布的包,旧 `category` 字段读取时忽略,新的清单及目录不再导出它。插件数据库每次生成目录时同时生成 `tags.json`,格式为 `{ "schemaVersion": 1, "tags": ["工具", "温控"] }`,聚合当前目录全部插件的标签、精确去重并排序。stable/testing 分别生成自己的标签文件,不生成跨渠道并集;程序加载多个源后在本地聚合去重。标签随插件更新自动增删,无须手工维护。
-
-上传的插件包、商店下载及其依赖包暂存于 `/tmp/framely-package-*`,确认安装后才写入插件目录。取消、安装完成或失败后删除暂存包;未完成上传和待确认安装包闲置 15 分钟后过期,每 30 秒清理一次。
-
-本机网页窗口在 `ui.windows.` 设置 `localWeb: true`,宿主通过后端 `window.get` 获取窗口 URL;这是窗口加载方式,不是权限声明。URL 使用 `http://localhost:<端口>/framely-window/`。普通窗口无需设置此项。
-
-商店主目录 `catalog.json` 中每个插件 ID 仅出现一次,代表最新版。历史条目存放在相对目录 `plugins/<插件ID>/versions.json`,格式为 `{ "schemaVersion": 1, "id": "作者.插件", "versions": [...] }`;版本条目沿用目录条目的字段,列表不包含主目录的最新版。商店进入详情时按需读取历史,依赖解析仅在最新版不满足范围时读取历史。附加目录字段可扩展,读取端忽略未知字段,但校验已知字段;改变必需语义应升级 `schemaVersion`。插件安装包清单仍严格校验。
+Main `catalog.json` has one latest entry per ID. `plugins//versions.json` contains `{schemaVersion:1,id,versions:[...]}` without that main entry. Details load history on demand, as does resolution when latest fails a range. Unknown catalog fields are ignored while known fields are checked; changed required semantics need a schema bump. Package Manifests remain strict. See [relationships](plugin-relationships.md), [subscriptions](source-subscriptions.md) and [lifecycle](plugin-lifecycle.md).
diff --git a/docs/plugin-lifecycle.md b/docs/plugin-lifecycle.md
index ece940e..0a3d49d 100644
--- a/docs/plugin-lifecycle.md
+++ b/docs/plugin-lifecycle.md
@@ -1,23 +1,16 @@
-# 插件生命周期
+# Plugin lifecycle
-[English](en/plugin-lifecycle.md)
+[简体中文](zh-CN/plugin-lifecycle.md)
-生命周期由 root 核心服务调度,但钩子使用插件声明的运行身份;省略身份为 `steamos`。有后端时,独立钩子与后端身份必须一致;纯 UI 插件可在 `lifecycle.runAs` 声明身份,安装时展示运行用户。
+Lifecycle is scheduled by the root core, but hooks run as the plugin's declared user, defaulting to `steamos`. Standalone hooks must match backend identity. UI-only plugins can set `lifecycle.runAs` for standalone commands; users see this identity during installation.
```json
{
- "backend": {
- "entry": "backend.py",
- "runAs": "steamos",
- "autostart": false,
- "restart": "on-failure",
- "restartLimit": 3
- },
+ "backend": {"entry": "backend.py", "runAs": "steamos", "autostart": false, "restart": "on-failure", "restartLimit": 3},
"lifecycle": {
"onInstall": {"entry": "backend.py"},
"onUpdate": {"entry": "backend.py"},
- "onStart": true,
- "onStop": true,
+ "onStart": true, "onStop": true,
"onUninstall": {"entry": "backend.py"},
"onCrashCleanup": {"entry": "backend.py", "args": ["--cleanup"]},
"timeoutSeconds": 10
@@ -25,22 +18,20 @@
}
```
-所有钩子可省略。独立命令的 entry 必须是包内文件并包含在 files 哈希清单;不能指定 shell 字符串或包外路径。没有为插件增加身份或网络权限。timeoutSeconds 为 1–15 秒,默认 10 秒,每个钩子分别计时。
+All hooks are optional. Command entries must be hashed payload files, never shell strings or outside paths. Hooks do not introduce extra network/identity permissions. Timeouts are per hook, 1–15 seconds, default 10.
-| 钩子 | 执行时机与方式 |
+| Hook | Timing and execution |
| --- | --- |
-| onInstall | 第一次安装,新版本激活前执行独立命令 |
-| onUpdate | 更新或回退,停止旧后端后、新版本激活前执行目标版本的独立命令 |
-| onStart | 启动后端后发送 `framely.lifecycle.start` RPC,成功后才接受业务调用 |
-| onStop | 禁用、重启、更新、回退、卸载和管理器正常退出时,停止进程前发送 `framely.lifecycle.stop` RPC |
-| onUninstall | 停止后端后、删除包前执行独立命令 |
-| onCrashCleanup | 异常退出、信号、OOM、调用超时或初始化失败后,由独立进程执行清理 |
+| `onInstall` | Standalone command before first activation |
+| `onUpdate` | Target version's command after stopping the old backend, before activation; includes downgrades |
+| `onStart` | `framely.lifecycle.start` RPC after backend launch, before business calls |
+| `onStop` | `framely.lifecycle.stop` before process termination during disabling, restart, updates, uninstall or normal manager shutdown |
+| `onUninstall` | Standalone command after stopping backend, before removing payload |
+| `onCrashCleanup` | Standalone cleanup after crashes, signals, OOM, timeouts or initialization failure |
-关闭快捷菜单或大窗口只卸载对应 React 页面,不停止后端。React effect 清理仍用于页面订阅、预览租约等;不能替代后端生命周期。按需启动与 autostart 是启动时机,restart 是已经启动后发生失败的恢复策略。
+Closing UI unmounts React, not the backend. Effects clean page subscriptions/leases, not backend resources. On-demand/autostart determines startup timing; restart policy handles failures after startup.
-## 上下文与 SDK
-
-所有回调收到以下上下文:
+## Context and SDK
```ts
interface LifecycleContext {
@@ -54,42 +45,42 @@ interface LifecycleContext {
}
```
-独立命令读取 `FRAMELY_LIFECYCLE` 与 JSON 编码的 `FRAMELY_LIFECYCLE_CONTEXT`。所有后端与命令均有 `FRAMELY_PLUGIN_ID`、`FRAMELY_PLUGIN_VERSION`、`FRAMELY_DATA_DIR` 及对应身份 HOME。独立命令 stdout/stderr 是日志,退出码 0 表示成功;后端钩子按普通 JSON 行协议回复 result 或 error。`framely.lifecycle.*` 为核心服务保留,插件业务 API 不能调用。
+Standalone commands read `FRAMELY_LIFECYCLE` and JSON `FRAMELY_LIFECYCLE_CONTEXT`. Backends and commands receive `FRAMELY_PLUGIN_ID`, `FRAMELY_PLUGIN_VERSION`, `FRAMELY_DATA_DIR` and the runtime user's HOME. Standalone stdout/stderr are logs; exit 0 means success. Backend hooks reply with result/error. Pages cannot call reserved `framely.lifecycle.*` methods.
-Python SDK 位于 `@framely/sdk/python`,构建时复制为载荷中的 `framely.py`,不需额外 pip 依赖:
+The Python helper is copied from `@framely/sdk/python` into `framely.py`, without extra pip dependencies:
```python
from framely import serve
def initialize(context):
- # 可重复执行;仅操作本插件的数据和资源。
- return {"ready": True}
+ # Keep operations repeatable and limited to this plugin's resources.
+ return {'ready': True}
def cleanup(context):
- return {"cleaned": True}
+ return {'cleaned': True}
def dispatch(method, params):
- return {"ok": True}
+ return {'ok': True}
serve(dispatch, {
- "onInstall": initialize, "onUpdate": initialize,
- "onStart": initialize, "onStop": cleanup,
- "onUninstall": cleanup, "onCrashCleanup": cleanup,
+ 'onInstall': initialize, 'onUpdate': initialize,
+ 'onStart': initialize, 'onStop': cleanup,
+ 'onUninstall': cleanup, 'onCrashCleanup': cleanup,
})
```
-SDK 检测独立钩子环境时只执行相应回调并退出,不进入 RPC 循环。TypeScript 后端可从 `@framely/sdk/lifecycle` 导入 `registerLifecycle` 和类型,将保留方法交给返回的 dispatcher;它不负责启动进程或实现 JSON 行传输。网页侧 SDK 也导出上下文类型,但不提供后端运行权限。
+With a standalone hook environment, the helper calls that hook and exits instead of entering RPC. TypeScript backends can use `registerLifecycle` from `@framely/sdk/lifecycle` in their existing dispatcher; it does not provide process launch or JSON-line transport. Page exports include context types, not backend privileges.
-## 失败、清理与恢复
+## Failure, cleanup and recovery
-安装或更新的命令钩子失败时不激活新版本。若安装/更新当时需要启动后端(常驻或原本在运行),初始化失败也恢复原版本与 current 链接;初次安装则撤销安装。按需插件首次安装通常只执行 onInstall,onStart 在首次打开页面/业务调用等实际启动时执行;此时失败走启动恢复,不会追溯撤销之前已完成的安装。回退的初始化失败时恢复回退前的版本。文件版本可恢复,数据迁移和设备操作无法自动撤销:迁移应可重复执行、采用备份/临时文件与原子替换,并保持上一版本能读取数据。
+Failed install/update commands prevent activation. If installation/update starts a backend immediately (resident or previously running), failed initialization restores the previous version/current link; first installation is undone. First installation of an on-demand plugin normally runs onInstall only. Its onStart runs when a page opens or a business call starts the backend; failure then follows startup recovery, without undoing a previously completed installation. Failed downgrade initialization restores the version used before downgrade. Files can be restored, but migrations and hardware operations cannot be automatically reversed. Make migrations repeatable, use backups/temporary files/atomic replacement, and retain compatibility with the preceding version.
-onStop 失败或超时会记录错误,仍会停止整个插件进程组。崩溃后先关闭该插件的窗口、通知,再执行 onCrashCleanup;清理失败不会阻止错误记录或后续恢复。崩溃清理必须是独立命令,无法依赖已经崩溃进程里的内存、析构器或回调。确认资源属于本插件后再删除;不要重启 SteamVR 或删除其他插件的文件。
+Failed/timed-out `onStop` is logged but the process group is still stopped. Crashes close plugin windows/notifications before standalone cleanup; cleanup failure does not suppress errors or future recovery. Cleanup must not depend on crashed memory or destructors. Remove owned resources only; do not restart SteamVR or delete other plugins' files.
-onUninstall 失败保留插件并展示错误,用户可重试或选择强制卸载。强制卸载仍尝试有超时限制的清理,随后移除插件包;用户数据默认保留,仅 purge 删除。强制卸载不能保证外部设备设置已还原。
+Failed `onUninstall` retains the plugin and allows retry or explicit force removal. Force still attempts bounded cleanup and removes the payload afterward; data remains unless the core's explicit purge option is used. It cannot guarantee restoration of external settings.
-restart 默认为 on-failure;never 只记录失败,不自动重启。restartLimit 默认为 3,范围 1–10,达到连续失败阈值后停用;失败计数在后端稳定运行 60 秒后重置。重试等待为 1、2、4、8、16、32 秒,最多 32 秒。正常自主退出(退出码 0)不重启;管理器主动停止也不计入失败。退出码、信号、systemd OOM 和调用超时记录在状态与日志中。
+Restart defaults to `on-failure`; `never` reports failure without retrying. Limits are 1–10, default 3 consecutive failures, reset after 60 seconds stable operation. Delays are 1, 2, 4, 8, 16, 32 seconds, capped at 32. Exit 0 and manager-directed stops do not count as failures. Exit codes, signals, systemd OOM and timeouts appear in status/logs.
-状态包含 starting、running、stopping、stopped、recovering、failed 等阶段,并发管理操作沿核心服务的串行调度执行。管理器正常 SIGTERM/SIGINT 会尝试逐个停止插件;强制终止、断电不能保证任何钩子执行。清理应同时可在下次启动时安全恢复。
+States include starting/running/stopping/stopped/recovering/failed. Core management is serialized. Normal SIGTERM/SIGINT attempts to stop plugins; forced termination or power loss cannot guarantee hooks. Support safe recovery on next startup.
-日志页面合并展示后端日志与 lifecycle 日志;每种日志保留当前 2 MiB 与上一份。安装失败且插件尚未登记时,可从 `/var/lib/framely/logs/.lifecycle.log` 查看。运行身份的数据目录为 `/var/lib/framely/data//<身份>/`(自定义核心数据目录时相应变化)。
+The log view includes backend and lifecycle logs; each retains a current 2 MiB file and a preceding copy. Failed pre-registration installs log to `/var/lib/framely/logs/.lifecycle.log`. User data is under `/var/lib/framely/data///`, relative to any custom core state root.
diff --git a/docs/plugin-relationships.md b/docs/plugin-relationships.md
index c2175ad..7747c7a 100644
--- a/docs/plugin-relationships.md
+++ b/docs/plugin-relationships.md
@@ -1,15 +1,12 @@
-# 插件依赖与冲突(Framely 0.4)
+# Dependencies and conflicts
-[English](en/plugin-relationships.md)
+[简体中文](zh-CN/plugin-relationships.md)
```json
{
"dependencies": {
"example.shared-api": "^1.0.0",
- "example.camera-api": {
- "version": ">=1.2.0 <2.0.0",
- "source": "https://example.org/plugins/catalog.json"
- }
+ "example.camera-api": {"version": ">=1.2.0 <2.0.0", "source": "https://example.org/plugins/catalog.json"}
},
"optionalDependencies": {"example.notifications": "^1.0.0"},
"conflicts": {"example.other-camera": "*"},
@@ -17,39 +14,35 @@
}
```
-四个字段都可省略。声明关系的插件使用合法 SemVer;作为依赖或冲突版本匹配目标的插件也应使用 SemVer。版本范围支持精确版本、`^`、`~`、`*` 和逗号/空白分隔的比较条件。`1.2.0` 表示精确匹配;`>=1.2.0 <2.0.0` 表示两个条件同时满足,不支持 `||`。拒绝自身依赖、重复必需/可选声明、依赖与冲突矛盾和重复独占资源。
+All four fields are optional. Relationship-declaring plugins and matched dependency/conflict targets use valid SemVer. Ranges support exact versions, `^`, `~`, `*` and comma/space comparison conjunctions. `1.2.0` is exact; `>=1.2.0 <2.0.0` requires both. `||` is unsupported. Self-dependencies, duplicate required/optional entries, contradictory conflicts and repeated exclusive resources are rejected.
-`source` 必须是 HTTPS 插件目录 URL,不是订阅或包下载 URL。优先复用版本与来源满足要求的已安装插件;未声明来源时优先当前插件所在源,其他来源必须明确选择。指定来源不可用或没有匹配版本时不换源。明确依赖源尚未添加时,进入安装计划统一确认;已停用的源需用户先启用。删除源后保留已安装插件的来源 URL 记录,以便依赖约束继续识别其来源。
+`source` is an HTTPS catalog URL, not a subscription or package. Compatible installed versions/sources are reused first. Without explicit source, the originating catalog is preferred; otherwise choose explicitly. An unavailable explicit source is not silently replaced. New explicit sources join the confirmation plan; disabled sources need enabling first. Removing a source retains installed origin URLs for matching.
-本地包没有默认来源时,未安装依赖需要选择来源;也可通过 CLI 指定已添加源 ID:
+Local packages with no default source require selecting missing dependency sources:
```bash
framely install plugin.framely --source my-source --approve
-framely install plugin.framely \
- --dependency-source example.shared-api=https://example.org/catalog.json \
- --approve
+framely install plugin.framely --dependency-source example.shared-api=https://example.org/catalog.json --approve
```
-`--dependency-source` 可重复使用,不能覆盖清单中的明确来源。CLI 和界面使用同一解析器。需要接受已有插件运行用户变化时再加 `--approve-run-as`。
+Dependency-source flags may repeat, cannot override explicit Manifest sources, and use the UI's resolver. Add `--approve-run-as` only after reviewing an identity change.
-主目录每个插件 ID 只出现一次,历史版本来自相对路径 `plugins//versions.json`;主目录版本是推荐版本。用户可选择旧版本安装或重新安装当前版本;包的 ID、版本、哈希及依赖均重新校验。依赖解析优先复用已安装版本,否则选取源中第一个满足约束的版本。多个版本/来源约束无法同时满足、出现循环依赖、缺少必需依赖时停止。缺失可选依赖不阻止安装,也不自动安装。
+The main catalog contains one recommended entry per ID, with history in relative `plugins//versions.json`. Users may select old versions or reinstall. ID/version/hash/relations are rechecked. Resolution reuses installed matches, otherwise the first matching source entry. Incompatible constraints, cycles or missing required dependencies stop installation. Missing optional dependencies neither prevent installation nor install automatically.
-安装计划展示所有包、版本、来源、运行用户、新增源、依赖启用和冲突停用。确认后使用同一份已校验包;设备插件/来源状态改变后要求重新检查。最多 32 个插件,不限制插件包大小或解压后的总大小。先完成下载和校验,再依次安装,检查反向依赖,防止更新破坏已有依赖方。
+Plans disclose packages, versions, sources, users, added sources, dependency enabling and conflict disabling. Confirmed operations use the same verified bytes; changed device/source state requires another inspection. Plans allow at most 32 plugins without a total package/expanded-size cap. Download/verify first, then install sequentially and check reverse dependencies.
-批量操作记录持久事务日志;失败恢复版本链接、来源和启用状态,撤销本次新增的包安装,并尝试恢复此前运行的后端。安装期间管理器意外退出时,下次启动恢复原记录。用户数据保留;生命周期迁移和设备操作不能由文件版本回退自动撤销,插件必须设计可重复执行、可恢复的迁移。
+Persistent transaction logs restore links, origins/enabled state, undo newly added packages and attempt to restart previous backends on failure. Startup recovers interrupted transactions. Data remains; file rollback cannot undo migrations or hardware effects, so make them repeatable and recoverable.
-启用时统一确认需要启用的必需依赖及需要停用的冲突插件。冲突按任意一方声明处理,允许同时安装,禁止同时启用;同名独占资源同样禁止同时启用。停用/卸载基础插件需确认一起停用依赖方,依赖方不会被自动卸载。
+Enabling confirms required dependencies and conflicting plugins to disable. Either side's conflict declaration prevents simultaneous enabling, not installation. Matching exclusive resources work likewise. Disabling/removing a base plugin requires confirming dependent disabling; dependents are not automatically removed.
-后端按依赖顺序启动,`onStart` 成功才启动依赖方;没有该钩子的后端沿用进程启动成功判断。停止顺序相反。关闭页面不停止后端。基础后端异常时暂停正在运行的依赖方,进入“等待依赖”,不累计依赖方失败次数;基础后端恢复后重新按顺序启动。基础后端被停用或禁止重启时等待用户处理。
-
-SDK 提供:
+Backends start in dependency order after base `onStart` succeeds; absent hooks use process startup. Stops reverse that order. Crashed bases pause running dependents in a waiting state without counting dependent failures, then resume them after recovery. Disabled/non-restarting bases need user action.
```tsx
import {framely, useDependencies} from '@framely/sdk';
const states = await framely.dependencies();
-// React 页面:const {items, error} = useDependencies();
+// In React: const {items, error} = useDependencies();
```
-只返回当前插件声明的必需/可选依赖,含版本、启用状态、约束是否满足、后端是否可用和运行阶段。依赖状态变化触发 `dependencies.changed` 事件;不开放跨插件后端调用,不增加运行权限。独占资源和 conflicts 都依赖作者声明,不自动检测任意文件或设备冲突。
+Only declared required/optional dependencies are returned, with version/enabling/range/availability/runtime state. Changes emit `dependencies.changed`. No cross-plugin backend calls or extra privileges are granted. Conflicts/resources depend on author declarations, not arbitrary file/device detection.
-不提供插件手动回滚接口;安装旧版本统一走正常安装计划和 `onUpdate`。安装失败的事务恢复仍保留,不是用户选择版本的入口。同 ID 同版本必须保持发布内容不变。
+There is no manual plugin rollback API; old-version installation uses the normal plan and `onUpdate`. Transaction recovery is failure handling, not a version picker. Keep same-ID/version content immutable.
diff --git a/docs/releases.md b/docs/releases.md
index a9b47c3..9940d2d 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1,78 +1,71 @@
-# Framely 发行与更新
+# Framely releases and updates
-[English](en/releases.md)
+[简体中文](zh-CN/releases.md)
-插件源和 Framely 自身更新源是独立设置。当前代码首次初始化加入社区 stable/testing 来源;目录能否访问以实际服务为准。Framely 更新服务由发行者预配置,管理窗口“关于”提供检查、下载、安装和回滚;尚未配置时使用发行安装包更新。插件和 Framely 发行包均不要求签名或公钥。
+Plugin catalogs and device-update sources are separate settings. Current initialization adds community stable/testing catalogs, subject to server availability. The distributor preconfigures device updates; About offers check/download/install/rollback. Without a source, use a release package. Packages do not require signatures or public keys.
-构建发行包后生成包含版本、架构、下载地址、长度与 SHA256 的普通 JSON 更新清单:
+## Update descriptors
+
+After building an archive, generate a plain JSON descriptor containing version, architecture, URL, size and SHA256:
```bash
-framely release-manifest \
- --archive "./framely-0.2.0--linux-arm64.tar.gz" \
- --url "https://example.org/releases/framely-0.2.0--linux-arm64.tar.gz" \
- --output ./framely-release.json \
- --changelog '本次更新说明'
+framely release-manifest --archive "./framely-0.2.0--linux-arm64.tar.gz" --url "https://example.org/releases/framely-0.2.0--linux-arm64.tar.gz" --output ./framely-release.json --changelog 'Release changes'
```
-将包与清单发布到 HTTPS 静态服务;下载最多跟随五次经过 URL 校验的重定向,不允许从 HTTPS 降级到 HTTP。发行维护终端配置清单完整 URL:
+Publish archive/descriptor on HTTPS. Downloads allow up to five validated redirects without HTTPS downgrade. Configure the complete descriptor URL from a maintenance terminal:
```bash
framely call system.source.save '{"source":{"url":"https://example.org/framely-release.json"}}'
```
-此接口用于发行维护,不作为普通用户设置项。当前官方安装引擎和桌面安装器会在更新源尚未配置时写入 `https://github.com/SteamFramelyHomebrew/framely/releases/latest/download/framely-release.json`,已有配置保留。哈希用于检查下载内容一致性,发行者身份依赖更新源;不再通过签名验证。
+This is a distributor interface, not an ordinary settings item. The official engine and desktop installer initialize `https://github.com/SteamFramelyHomebrew/framely/releases/latest/download/framely-release.json` only when no source is configured, preserving existing sources. Hashes establish content consistency; identity depends on trusted sources, not signatures.
-更新依次检查 API/架构兼容、声明长度、SHA256,安全解压后检查发行包自身 SHA256SUMS;下载不阻塞核心服务。用户确认安装后,独立 systemd 助手切换发行版本,只重启 Framely 服务。日志在 `/var/lib/framely/logs/update.log`。安装失败会尝试恢复原发行和安装前状态;UI 可回滚上一版本。
+Updates verify API/architecture, length, SHA256, safe extraction and internal `SHA256SUMS`. Downloads do not block the core. After user confirmation, a separate systemd helper switches releases and restarts Framely only. Log: `/var/lib/framely/logs/update.log`. Failed installation attempts to restore release/state; the UI can return to the preceding release.
-更新源仅包含 URL,发行清单使用普通 JSON,不支持旧签名包、签名信封或公钥字段。回滚切换上一发行版本,保留插件数据,不进行旧版格式迁移。
+Sources contain URL only. Legacy signed descriptors/envelopes/public-key fields are unsupported. Rollback preserves plugin data without downgrading its format. Device upgrades, rollback and reboot still need real validation beyond local tests.
-设备上的实际升级/回滚和重启恢复需要验收;本地自动测试验证无签名格式、包边界和插件安装更新,不能代替设备服务切换测试。
+## Recovery after SteamOS updates
-## SteamOS 更新后的恢复
+Management state resides at `/home/.framely/state`, including state, update source, logs, release links, Steam username and Framely UID. `/var/lib/framely` is a compatibility link. Old state migrates after writers stop; conflicting copies cause a stop rather than overwrite.
-安装器将整个管理目录保存在 `/home/.framely/state`,包括 `state.json`、更新源、日志、当前/上一发行链接、Steam 用户名和 Framely 账号 UID;`/var/lib/framely` 仅作兼容链接。旧安装会在停止服务后迁移。两份状态冲突时停止,不自动覆盖任何一份。
+Accounts/services depend on OS configuration. When writable, `sudo bash /home/.framely/repair.sh` restores links, account, service files and startup from the retained, verified release without another download. It invokes the current release's `install.sh --repair` without creating releases or changing the previous-version record. UID conflicts or changes stop recovery. It never disables read-only protection automatically.
-系统服务和账号仍依赖 SteamOS 系统配置,操作系统更新后可能需要手动修复。在系统配置可写时运行 `sudo bash /home/.framely/repair.sh`;无需重新下载发行包,恢复兼容链接、专用账号、服务文件和开机启动。修复先校验保留的发行,再使用当前发行的 `install.sh --repair`,不创建发行或改写上一版本记录。保存的账号 UID 若被占用或现有账号 UID 已变化则停止,避免错误地接管旧数据。系统只读时停止,不自动解除保护。
+Deleted `/home` data cannot be recovered. OS updates may require manual intervention. After rolling back to a release without repair support, use a newer supporting package. Before stable publication, validate actual OS-update recovery, state/data/UIDs/services and SteamVR compatibility.
-该机制不能恢复已被系统清除的 `/home` 数据,也不保证操作系统更新后无需人工操作。回滚到不支持修复模式的旧发行后,需要使用支持该模式的新发行包恢复安装。正式发布前应进行一次真实 SteamOS 更新,验证设置、插件数据、账号身份、服务恢复和 SteamVR 宿主兼容性。
+## GitHub Actions
-## GitHub Actions 发布
+Device and installer share the repository with independent versions/tags/workflows:
-Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流:
+- Device: root Cargo version, `v` (currently `v0.4.2-preview.6`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF.
+- Installer: `installer/Cargo.toml`, `installer-v` (currently `installer-v0.4.1-preview.4`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit.
-- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.6`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。
-- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.1-preview.4`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。
+Versions need not match or ship together. Update the relevant Cargo.lock with version changes. Installer package/macOS versions use the installer version.
-两个版本不需要相同,也不需要同时发布。修改各自版本时同步对应的 `Cargo.lock`;安装器包名及 macOS 应用版本使用安装器版本。
-
-两个工作流均缓存 Cargo 下载的依赖和已编译的依赖产物,按产品、系统、架构、Rust 工具链及依赖配置区分。安装器使用 `installer/target`;本体在 CI 使用 `target/cargo`,与单独缓存的 `target/cef` 分开,测试和发行打包共用同一 Cargo 目录。失败构建也会保存已完成的依赖编译。
-
-普通分支推送不触发发行构建;只有对应产品的标签推送或手动运行会触发。缓存恢复、编译和缓存保存均在同一次构建内完成。GitHub 允许标签构建读取默认分支的缓存,不允许一个标签读取另一个标签的专属缓存;需要跨标签复用时,可手动在 `main` 运行一次对应工作流来生成默认分支缓存,该运行只生成构建产物,不发布 Release。首次构建、没有可访问的缓存或工具链变化后仍需重新编译,缓存命中情况见 “Cache Cargo dependencies and build artifacts” 步骤。
+Both workflows cache dependencies/builds by product/platform/toolchain/configuration, including failed builds. Installer uses `installer/target`; device CI uses `target/cargo`, separate from `target/cef`, for tests and packaging. Branch pushes do not build releases; matching tags or manual runs do. Tags may restore default-branch caches, not another tag's private cache. A manual main run can prime cache and builds artifacts only. Cold builds/toolchain changes need compilation; inspect cache steps rather than assuming a hit.
```bash
-# 提交并推送代码后,选择需要发布的产品
+# After committing and pushing the intended source:
git tag v0.4.2-preview.6
git push origin v0.4.2-preview.6
-
-# 独立发布安装器,不触发 Framely 构建
+# Independent installer release:
git tag installer-v0.4.1-preview.4
git push origin installer-v0.4.1-preview.4
```
-带 `-preview.N` 等预发布后缀的标签会发布为 GitHub Prerelease,不设置为 Latest。安装器 Release 始终设置为非 Latest;只有 Framely 正式 Release 设置为 Latest。因此设备安装入口和默认更新清单的 `releases/latest/download/...` 继续指向 Framely 本体。安装器读取发行列表时按本体包名筛选,不把 Linux ARM64 安装器误当成设备发行包。安装器下载应使用具体的 `installer-v<版本>` Release。
+Prerelease suffixes create GitHub Prereleases without Latest. Installer releases are never Latest. Only stable device releases become Latest, keeping default installer/update URLs on the device product. The desktop installer filters device archive names and does not mistake ARM64 installer packages for device runtimes. Download desktop installers from their explicit installer tags.
-只有标签触发会发布。各工作流在自己的构建全部成功后创建草稿 Release,上传本产品产物及外部 `SHA256SUMS`,最后公开草稿。Framely Release 额外包含 `bootstrap.py`、入口脚本和更新清单;安装器 Release 只包含五个平台的安装器包和校验文件。失败的草稿不作为最新稳定版提供下载。手动触发用于验证,仅保留 Actions artifacts。
+Only tags publish. After every product build succeeds, publication creates a draft, uploads assets/checksums, then makes it public. Failed drafts are not stable download targets. Manual runs leave Actions artifacts only.
-Framely 本体 Release 附件用途:`framely-*-linux-arm64.tar.gz` 为设备运行包;`framely-release.json` 为内置更新功能的版本清单;`install.sh` 是命令行入口,调用 `bootstrap.py` 完成下载、校验和维护;`SHA256SUMS` 为附件校验清单。桌面安装器只需自动下载运行包和校验清单,用户无需手动下载脚本。安装器的五个平台包发布在独立的安装器 Release 中。
+Device attachments include the runtime archive, `framely-release.json`, `bootstrap.py`, `install.sh` and `SHA256SUMS`. Installer releases have five platform packages and checksums only. Desktop installers download archive/checksums without user-managed scripts.
-外部 `SHA256SUMS` 包含压缩包哈希,校验传输内容;包内同名文件校验解压后的载荷。哈希不能独立证明作者身份,在线安装依赖指定 GitHub 仓库与 HTTPS;本地安装依赖用户提供的可信包与校验文件。下载遵循 HTTPS 重定向,不允许降级。
+External checksums cover downloaded archives; internal checksums cover extracted payload. Neither independently proves author identity. Online installation trusts the chosen repository/HTTPS, local installation the supplied archive/checksums. Redirects never downgrade HTTPS.
-入口脚本从 Release 下载设备引擎;在线安装默认选择最新正式版,也支持 `--version TAG`。指定标签时,入口从该 Release 下载 `bootstrap.py`,引擎再选择同标签运行包;入口脚本本身来自用户下载或 curl 指定的位置,可在仅有 Preview、尚无正式版时安装预发布版本。本地使用 `python3 tools/bootstrap.py install --archive /path/package.tar.gz --checksums /path/SHA256SUMS`。更新要求已经安装;指定已安装版本会修复服务,指定其他版本会安装目标发行,保留状态。
+The entry fetches the release engine. Default is latest stable; `--version TAG` chooses that Release for both engine and package and works for Preview-only repositories. Locally run `python3 tools/bootstrap.py install --archive /path/package.tar.gz --checksums /path/SHA256SUMS`. Updating requires existing installation; selecting the same installed release repairs it, another version installs while retaining state.
-安装器将官方更新清单地址预配置到尚未配置更新源的设备;既有更新源保留。之后可继续通过 Framely 内置更新界面更新。
+Installation initializes the official update descriptor URL only if no source is configured, preserving existing sources for future in-app updates.
-## 统一卸载
+## Unified uninstallation
-脚本和桌面安装器均只有一个卸载选项。`uninstall.sh` 先停止会话及下载工作,再通过 root 专用 `prepare-uninstall --approve` 请求核心服务:持久停用所有插件、按依赖顺序停止插件、执行各插件 `onUninstall`,移除插件包。全部完成后才取消 Framely 自启、停止核心并移除程序。失败时保留本体并恢复会话,插件保持停用,允许处理原因后重试;不支持跳过失败插件强制卸载本体。
+Scripts and desktop UI expose one uninstall operation. It stops session/downloads, then calls root-only `prepare-uninstall --approve`: persistently disable plugins, stop them in dependency order, run uninstall hooks, remove payloads. Only complete success removes services/core/runtime. Failure retains Framely, restores session access and leaves plugins disabled for troubleshooting/retry. There is no skip-failed-plugin manager uninstall.
-协议尚未确认或已撤回也允许 root 通过此专用维护接口卸载;普通会话及插件界面不开放该接口。保存的设置、插件数据和系统账号保留,不承诺清除外部副作用。旧发行没有该接口时需先更新。
+Root maintenance can uninstall before consent or after revocation; pages do not expose the maintenance method. Settings/data/accounts remain, and external side effects are not guaranteed reversible. Old releases without this endpoint need updating first.
diff --git a/docs/source-subscriptions.md b/docs/source-subscriptions.md
index 12692e8..9a4fd37 100644
--- a/docs/source-subscriptions.md
+++ b/docs/source-subscriptions.md
@@ -1,26 +1,26 @@
-# 插件源订阅(Framely 0.4)
+# Plugin source subscriptions
-[English](en/source-subscriptions.md)
+[简体中文](zh-CN/source-subscriptions.md)
-在管理窗口“插件源 → 源订阅”粘贴 HTTPS JSON 地址,预览后确认添加:
+In Sources → Source subscriptions, paste an HTTPS JSON URL, preview and confirm:
```json
{
"schemaVersion": 1,
- "name": "Framely 社区源",
+ "name": "Community sources",
"sources": [
- {"id":"stable", "name":"社区稳定版", "url":"https://example.org/stable/catalog.json"},
- {"id":"testing", "name":"社区测试版", "url":"https://example.org/testing/catalog.json"}
+ {"id":"stable", "name":"Stable", "url":"https://example.org/stable/catalog.json"},
+ {"id":"testing", "name":"Testing", "url":"https://example.org/testing/catalog.json"}
]
}
```
-每个源 id 在订阅内唯一且稳定,用于识别地址变更。禁止重复 URL、嵌套订阅、HTTP、带认证信息的 URL,最多 100 个源、512 KiB;每台设备最多 20 个订阅、200 个插件源。目录、包、图片继续由各源或作者托管,订阅只聚合目录地址。
+Source IDs must remain stable and unique within a subscription. Duplicate URLs, nested subscriptions, HTTP or credential-bearing URLs are forbidden. Limits: 100 sources/512 KiB per subscription, 20 subscriptions/200 sources per device. Subscriptions aggregate catalog URLs, not packages or images.
-默认每天自动刷新,也可关闭自动刷新或立即刷新。自动检查在 Framely 会话运行期间进行,启动后检查到期项;支持 ETag/Last-Modified 和最多五次 HTTPS 重定向。离线/失败保留已持久化的上次成功源列表,记录错误,按一分钟起的指数退避重试,最多一天。
+Automatic refresh defaults to daily and can be disabled or triggered manually. The running session checks due entries at startup, uses ETag/Last-Modified, and follows at most five HTTPS redirects. Failures preserve persisted source lists and retry with exponential backoff from one minute up to one day.
-新增源自动纳入;名称自动更新。相同 id 的 URL 改变时保留旧地址,显示待确认变更,确认后才采用新地址。刷新订阅后同时刷新插件目录。目录缓存沿用会话内缓存,重启后的离线商店不保证有目录缓存。
+New sources join automatically; names update. URL changes keep the old address until confirmed. Refreshing also refreshes catalogs. Catalog caches are session-local; offline catalogs after restart are not guaranteed.
-目录 URL 规范化后去重,一个源可由多个订阅和手动添加共同引用。删除订阅或移除订阅中的源只移除对应引用;没有任何引用才从源列表移除。已安装插件、来源记录和数据保留,不静默切换来源。
+Canonical URLs deduplicate references from subscriptions and manual additions. Removing a subscription/source removes only its reference; sources disappear only without references. Installed plugins, origins and data remain; origins do not silently switch.
-可在插件源列表单独停用源,刷新不会重新开启;即使移除后又加入同一 URL,也保留停用偏好。订阅源的 URL 在待确认变更中管理。刷新只更新源列表、商店资料和更新提示,不自动安装、更新或启用插件。
+Disabled preferences persist across refresh/removal/readdition. Manage subscription URL changes through pending changes. Refreshing changes source metadata and update hints, never automatically installs, updates or enables plugins.
diff --git a/docs/user-guide/basic-usage.md b/docs/user-guide/basic-usage.md
index c855fc0..c38fb2a 100644
--- a/docs/user-guide/basic-usage.md
+++ b/docs/user-guide/basic-usage.md
@@ -1,29 +1,29 @@
-# 基本使用
+# Basic usage
-[English](../en/user-guide/basic-usage.md)
+[简体中文](../zh-CN/user-guide/basic-usage.md)
-[文档首页](../README.md) · [安装指南](installation.md)
+[Documentation](../README.md) · [Installation](installation.md)
-## 快捷菜单与管理窗口
+## Quick menu and manager
-在 SteamVR Dashboard 点击 Framely Dock 图标打开快捷菜单。首次访问需阅读并同意协议及隐私声明;未同意时插件不会自动启动。
+Click Framely in the SteamVR Dashboard Dock. First use requires accepting the terms and privacy statement; plugins do not automatically start before consent.
-- **常用**:打开收藏的插件快捷页。
-- **已安装**:查看本机插件,进入插件页面。
-- **设置**:语言、安全模式和“管理插件”入口。
+- **Favorites**: open favorite plugin quick pages.
+- **Installed**: browse local plugins and their pages.
+- **Settings**: language, safe mode and the plugin manager entry.
-“管理插件”打开独立大窗口,左侧导航进入插件、商店、插件源、设置和关于。安装、更新及来源管理在这里完成;电脑或手机也可使用[网络管理面板](network-panel.md)。
+The manager opens a separate large window with navigation for plugins, store, sources, settings and About. Installation, updates and source management happen here. A phone or computer can also use the [network panel](network-panel.md).
-## 插件启停与窗口
+## Plugins and windows
-在插件列表启用、停用或收藏插件。启用表示允许使用;默认后端按需启动,只有声明 `autostart` 的插件会常驻。关闭插件窗口通常不会停用后台功能。依赖不满足或有冲突时,界面会给出原因,并在变更前显示受影响插件。
+Enable, disable or favorite plugins in the list. Enabled means available for use; backends normally start on demand. Only plugins declaring `autostart` remain resident after startup. Closing a plugin window generally does not disable its backend. Dependency or conflict problems are explained, and affected plugins are shown before changes.
-带 Dock 图标的独立窗口由 SteamVR 提供关闭控件;临时窗口可能在切换界面时销毁。不同插件的 UI 和功能由各自开发者决定。
+Dock windows use SteamVR close controls. Temporary windows may be destroyed when switching views. Individual plugin interfaces and behavior are maintained by their developers.
-## 设置与诊断
+## Settings and diagnostics
-设置中可修改语言、网络面板及代理。HTTP 代理和 GitHub 代理各有开关,保存地址不会自动启用代理。
+Settings provides language, network panel and proxy configuration. HTTP and GitHub proxies have independent switches; saving an address does not automatically enable it.
-“安全模式”暂停第三方插件,适合出现故障时检查;排查后再退出安全模式。在“关于”查看本体版本、检查更新和复查协议。撤回协议会停用全部插件,再次同意后需要手动启用。
+Safe mode pauses third-party plugins for troubleshooting; leave it after resolving the problem. About shows the device version, updates and terms. Revoking consent disables all plugins; accepting again does not automatically re-enable them.
-日志排查命令见[安装指南](installation.md#修复与日志)。界面能打开不代表所有插件都正常;先逐个启用,确认运行身份与来源可信。
+Log commands are in the [installation guide](installation.md). A working manager does not establish that all plugins work. Enable plugins individually and verify their sources and runtime users.
diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md
index a350a57..1d65036 100644
--- a/docs/user-guide/installation.md
+++ b/docs/user-guide/installation.md
@@ -1,54 +1,54 @@
-# 安装、更新、修复与卸载
+# Install, update, repair and uninstall
-[English](../en/user-guide/installation.md)
+[简体中文](../zh-CN/user-guide/installation.md)
-[文档首页](../README.md) · [下一步:基本使用](basic-usage.md)
+[Documentation](../README.md) · [Next: basic usage](basic-usage.md)
-## 安装前
+## Before installation
-目标设备为 Linux ARM64 Steam Frame。当前开发基线为 SteamOS VR 0.4.2、SteamVR build 20260928.6175029;其他系统版本需验证。运行包自带 CEF,设备使用时无需安装 Node.js 或 Rust。
+The target is Linux ARM64 Steam Frame. The development baseline is SteamOS VR 0.4.2 and SteamVR build 20260928.6175029; other versions need validation. The runtime includes CEF, so users do not need Node.js or Rust on the device.
-电脑与 Frame 连接同一网络,在 Frame 开启开发者模式并启用 SSH,准备 Steam 用户 `steamos` 的登录密码。安装会使用 sudo 创建账号、systemd 服务和发行目录;请核对 SSH 指纹。安装器不会自动解除 SteamOS 只读保护;实际所需目录不可写时会停止。
+Connect your computer and Frame to the same network. Enable developer mode and SSH on Frame, and prepare the `steamos` account password. Verify the SSH fingerprint. Installation uses sudo to create an account, systemd services and release directories. It does not automatically disable SteamOS read-only protection; it stops if required directories cannot be written.
-## 方式一:电脑端安装器
+## Option 1: desktop installer
-1. 从 [Framely Releases](https://github.com/SteamFramelyHomebrew/framely/releases) 的 `installer-v<版本>` 下载适合电脑的安装器。Linux 为 tar.gz,Windows 为 EXE ZIP,macOS 为应用 ZIP。
-2. 解压并运行,扫描设备或手动填写设备 IP 与 SSH 端口。
-3. 核对指纹,填写 `steamos` 和密码,连接后选择 Framely 本体发行版本。
-4. 如果要安装 Preview,开启显示测试版;安装器版本与设备版版本相互独立。
-5. 在“安装与维护”确认设备、目标版本及操作,等待完成。也可选择本地运行包和对应的外部 `SHA256SUMS`。
+1. Download the appropriate installer from an `installer-v` entry in [Releases](https://github.com/SteamFramelyHomebrew/framely/releases). Linux uses tar.gz, Windows an EXE ZIP, and macOS an application ZIP.
+2. Extract and run it. Scan for the device or enter its IP and SSH port.
+3. Verify the fingerprint, enter `steamos` and its password, connect, and select a Framely device release.
+4. Enable testing releases to select a Preview. Installer and device versions are independent.
+5. Confirm the device, version and operation on the maintenance page. Alternatively, select a local device archive and its external `SHA256SUMS`.
-Linux 用户可在安装器解压目录执行 `bash install-desktop-entry.sh`,把带 Framely 图标的入口添加到当前用户的应用菜单。入口指向解压目录;移动目录后需重新执行脚本。
+On Linux, run `bash install-desktop-entry.sh` from the extracted installer directory to add an entry with the Framely icon to your user application menu. The entry points to that directory; run the script again after moving it.
-安装器会校验下载内容,并在设备端再次校验。密码不保存到配置文件。连接后仍需在实际设备上观察 Framely 的启动和显示。
+Downloads are checked locally and again on the device. Passwords are not saved in configuration. Observe startup and rendering on Frame after installation.
-## 方式二:在 Frame 的 SSH 终端安装
+## Option 2: install from Frame's SSH terminal
-以下命令在 **Frame 上** 执行。默认安装最新正式版:
+Run these commands **on Frame**. To install the latest stable release:
```bash
curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
```
-指定 Preview 或其他标签:
+To select a Preview or another tag:
```bash
curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
```
-将示例标签换成 Releases 中实际存在的本体标签。指定标签时从该 Release 下载引擎和包,不要求已有正式版;不指定标签时使用最新正式版。终端会按需请求 sudo 密码。
+Replace the example with an existing device release tag. An explicit tag selects the engine and package from that Release and works without a stable release. Without a tag, the latest stable release is selected. Enter the sudo password when prompted.
-## 方式三:本地发行包
+## Option 3: local archive
-下载设备运行包 `framely-<版本及构建号>-linux-arm64.tar.gz`、外部 `SHA256SUMS` 和同一 Release 的 `bootstrap.py`,放入 Frame 的同一目录:
+Download `framely--linux-arm64.tar.gz`, the external `SHA256SUMS`, and `bootstrap.py` from the same Release. Place them together on Frame:
```bash
-python3 bootstrap.py install --archive "./framely-<版本及构建号>-linux-arm64.tar.gz" --checksums ./SHA256SUMS
+python3 bootstrap.py install --archive "./framely--linux-arm64.tar.gz" --checksums ./SHA256SUMS
```
-尖括号是占位符,执行前替换为真实文件名。引擎校验后在 `/home` 暂存、解压并检查包内哈希,避免耗尽设备的 `/tmp`。
+Angle brackets are placeholders; substitute the actual filename. The engine verifies, stages and extracts under `/home`, then checks internal hashes, avoiding the device's limited `/tmp`.
-## 验证安装
+## Verify installation
```bash
systemctl is-active framely.service framely-session.service
@@ -56,43 +56,43 @@ systemctl is-active framely.service framely-session.service
sudo /var/lib/framely/current/bin/framely status
```
-两个服务正常应显示 `active`。打开 SteamVR Dashboard,在 Dock 找到 Framely;首次使用需阅读用户协议和隐私声明。之后按[插件安装教程](plugins.md)添加第一个插件。
+Both services should be `active`. Open SteamVR Dashboard and find Framely in the Dock. Read and accept the terms and privacy statement before managing plugins. Continue with the [plugin installation guide](plugins.md).
-## 更新与回滚
+## Update and rollback
-已配置更新服务时,从管理窗口“关于”检查、下载并确认安装。也可以用桌面安装器,或在 Frame 执行:
+When an update source is configured, check, download and confirm an update in **About**. Alternatively, use the desktop installer or run on Frame:
```bash
curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- update --version v0.4.2-preview.3
```
-更新保留插件、设置和数据,并保留前一个发行用于回滚;只重启 Framely。Preview 更新需要明确指定标签或本地包,默认正式版更新地址不会自动选择 Preview。
+Updates preserve plugins, settings and data, retain the previous release, and restart Framely only. Preview updates require an explicit tag or local package; the default stable source does not automatically select Preview builds.
-需要回滚时,从“关于”选择上一版本,或执行:
+Choose rollback in About, or run:
```bash
sudo bash /var/lib/framely/current/rollback.sh
```
-回滚本体保留插件数据,但不保证将新版本的数据格式转换为旧格式。
+Rollback preserves plugin data but does not guarantee conversion of newer data formats to older ones.
-## 修复与日志
+## Repair and logs
-SteamOS 更新可能重置账号和系统服务。管理数据保存在 `/home/.framely/state`,`/var/lib/framely` 为兼容入口。在系统配置可写且 `/home` 数据仍存在时执行:
+SteamOS updates may reset system accounts and services. State persists under `/home/.framely/state`; `/var/lib/framely` is a compatibility entry. If system configuration is writable and `/home` remains intact:
```bash
sudo bash /home/.framely/repair.sh
sudo journalctl -u framely -u framely-session --no-pager -n 100
```
-修复会使用已保留的发行恢复账号及服务;UID 冲突时停止。它不能找回已删除的 `/home` 数据,SteamOS 更新后的恢复仍需实机验证。
+Repair restores accounts and services from the retained release and stops on UID conflicts. It cannot restore deleted `/home` data. Recovery after SteamOS updates still requires device validation.
-## 卸载
+## Uninstall
-桌面安装器“安装与维护”提供卸载,也可直接执行:
+Use the desktop maintenance page or run:
```bash
sudo bash /var/lib/framely/current/uninstall.sh
```
-卸载先停用并卸载全部插件、执行其卸载钩子,成功后再移除 Framely 程序和服务。某个插件清理失败会保留 Framely并报错,处理后重试。已保存的设置、插件数据和专用账号保留;不承诺自动清除第三方插件在其他位置造成的修改。
+Uninstallation disables and uninstalls every plugin, including uninstall hooks, before removing Framely programs and services. Failed plugin cleanup retains Framely and reports an error; resolve it and retry. Settings, plugin data and the dedicated account remain. External changes made by third-party plugins cannot always be automatically undone.
diff --git a/docs/user-guide/network-panel.md b/docs/user-guide/network-panel.md
index f979024..09b4413 100644
--- a/docs/user-guide/network-panel.md
+++ b/docs/user-guide/network-panel.md
@@ -1,37 +1,37 @@
-# 网络管理面板
+# Network management panel
-[English](../en/user-guide/network-panel.md)
+[简体中文](../zh-CN/user-guide/network-panel.md)
-[文档首页](../README.md) · [插件安装](plugins.md)
+[Documentation](../README.md) · [Plugin installation](plugins.md)
-## 找到地址
+## Find the address
-网络面板默认开启,监听 IPv4 端口 `15915`,不依赖 SteamVR 正在运行。手机/电脑与 Frame 连接同一网络,在浏览器打开:
+The panel is enabled by default on IPv4 port `15915` and does not require SteamVR to run. Connect your phone/computer to Frame's network and open:
```text
-http://设备IP:15915
+http://DEVICE_IP:15915
```
-管理窗口“设置 → 网络管理面板”显示设备当前 IP 和完整地址,也可在桌面安装器连接设备后使用网络面板入口。没有检测到 IP 时先检查 Wi-Fi/网线;端口被占用时设置中会提示错误。
+Settings → Network panel shows current IP addresses and full URLs. The desktop installer also provides an entry after connecting. If no IP is detected, check Wi-Fi/Ethernet. Port conflicts are reported in settings.
-## 首次设置密码
+## First password setup
-尚未配置访问密码时,首次访问只显示设置页面,不能进入管理 API。输入至少 8 个字符的密码并再次确认,保存后使用该密码登录。密码设置可以先于用户协议确认;登录后仍需阅读并同意协议才能管理插件。
+With no configured password, first access shows setup only and does not expose management APIs. Enter at least eight characters and confirm the password, save, then log in. Password setup may precede terms acceptance; consent is still required to manage plugins.
-密码以带随机盐的 PBKDF2 哈希保存,重启保留,不会在界面中回显。首次设置入口不能覆盖已有密码。网络面板密码和设备 SSH/sudo 密码相互独立。
+A randomly salted PBKDF2 hash is stored, survives reboot, and is not displayed by the UI. Unauthenticated setup cannot overwrite an existing password. The panel password is independent of SSH/sudo credentials.
-后续在已登录的设置页面修改密码,或从设备原生界面修改。忘记密码时优先从 Frame 的原生管理窗口设置新密码。已有版本的密码保留;用户已明确关闭密码验证且已有密码记录的配置保持其选择。网络面板使用 HTTP,请只在可信网络使用,不要把端口直接转发到公网。
+Change passwords from authenticated settings or the device's native manager. If forgotten, prefer changing it from Frame's native window. Existing passwords are retained; a previously configured password with verification explicitly disabled retains that choice. The panel uses HTTP: use a trusted network and do not forward the port directly to the public internet.
-## 可以做什么
+## Available operations
-登录后可以管理来源和订阅、浏览商店、导入本地插件包、确认安装、启停插件、修改设置及检查本体更新。插件后端始终运行在 Frame,上传本地包只是从手机/电脑把文件传到设备。
+Manage sources and subscriptions, browse the store, upload local packages, confirm installation, enable/disable plugins, change settings and check device updates. Backends always run on Frame; uploading a file transfers it from your phone/computer.
-VR 窗口、手柄振动和 SteamVR 虚拟键盘需要头显端验收;浏览器无法替代这些设备交互。部分外部链接由设备默认浏览器打开。
+VR windows, controller vibration and SteamVR keyboard need headset validation; a browser cannot replace those interactions. Some external links open in the device's default browser.
-## 更改端口、关闭与登录过期
+## Port changes, shutdown and expired sessions
-在“设置 → 网络管理面板”修改端口或开关,保存后生效。修改认证设置或密码会使旧登录会话失效;端口改变后访问新的地址,关闭面板后网络连接断开。本机原生界面不因此关闭。
+Change the port or switch in Settings → Network panel. Password/authentication changes invalidate old sessions; use the new address after a port change. Turning the panel off disconnects web access without closing the native manager.
-会话服务重启也会使旧登录失效,需要重新登录。保存完全相同的设置不会撤销当前会话。错误密码验证有速率限制,提示尝试过多时稍后再试。
+Restarting the session service also invalidates old logins. Saving identical settings retains the session. Incorrect password checks are rate-limited; retry later when asked.
-无法访问时检查设备 IP 是否变化、两台设备是否处于可互通的网络、面板是否开启以及端口是否正确。设置/密码保存在 `/home/.framely/state`,卸载本体保留这些状态,重装后可能仍需原密码。
+If unreachable, check changing device IPs, network reachability, enabled state and port. Settings and passwords live in `/home/.framely/state` and are retained by uninstall; reinstalling may still require the old password.
diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md
index fd1f6b0..bab0df3 100644
--- a/docs/user-guide/plugins.md
+++ b/docs/user-guide/plugins.md
@@ -1,56 +1,56 @@
-# 安装与管理插件
+# Install and manage plugins
-[English](../en/user-guide/plugins.md)
+[简体中文](../zh-CN/user-guide/plugins.md)
-[文档首页](../README.md) · [网络面板](network-panel.md)
+[Documentation](../README.md) · [Network panel](network-panel.md)
-## 从商店安装
+## Install from the store
-1. 打开管理窗口“商店”。当前代码首次初始化加入社区 stable/testing 来源;来源内容取决于服务和设备配置。没有目录或插件时,进入“插件源”添加开发者提供的完整 HTTPS `catalog.json` 地址。
-2. 可按关键词、标签、来源或安装状态筛选。
-3. 打开详情,查看作者、介绍、更新记录、运行用户和依赖。默认显示推荐版本,历史版本会按需加载。
-4. 点击安装或检查所选版本。下载、校验和依赖解析完成后,阅读确认页。
-5. 检查包内实际运行用户以及依赖带来的 root 权限,确认后安装。安装后在插件列表启用或打开。
+1. Open **Store** in the manager. Current code initializes community stable/testing sources; availability depends on the actual service and device configuration. If no catalog is available, add a developer's complete HTTPS `catalog.json` URL in Sources.
+2. Filter by keywords, tags, source or installation state.
+3. Open details and review author, description, changes, runtime user and dependencies. The recommended version is shown first; history loads on demand.
+4. Install or inspect the selected version. Read the confirmation after download, verification and dependency resolution.
+5. Check actual package runtime users, including any root dependencies, then confirm. Enable or open the plugin from the installed list.
-商店提供的运行用户只是目录资料,最终确认以下载包内声明为准。`steamos` 使用 Steam 会话用户,`root` 可修改系统和设备设置。包哈希用于校验内容,不等于对作者身份或安全性的担保。
+Catalog runtime users are informational; final confirmation uses the downloaded package. `steamos` runs as the Steam session user; `root` can change system and device settings. Hashes check content, not developer identity or safety.
-## 添加源与订阅
+## Sources and subscriptions
-单个目录地址:在“插件源”添加名称和 `https://.../catalog.json`,测试连接后保存。
+For one catalog, add a name and `https://.../catalog.json` URL in Sources, test the connection, then save.
-多个源的订阅:在“源订阅”粘贴开发者提供的订阅 JSON 地址,预览所含来源后确认添加。它和单个目录不是同一种文件。订阅刷新仅更新来源和商店资料,不自动安装或更新插件。
+For multiple catalogs, add a subscription JSON URL in Source subscriptions, preview its sources, then confirm. A subscription is different from a catalog. Refreshing it changes source lists and metadata, not installed plugins.
-停用或删除来源不会自动卸载已安装插件;订阅管理的 URL 变更需另行确认。格式与刷新行为见[源订阅参考](../source-subscriptions.md)。
+Disabling or deleting a source does not uninstall its plugins. Subscription URL changes require confirmation. See the [subscription reference](../source-subscriptions.md).
-## 导入本地包或包 URL
+## Import a file or package URL
-插件包扩展名为 `.framely`,无需自行解压。进入管理窗口“插件”,打开导入入口:
+Packages use the `.framely` extension and need no manual extraction. Open import in the manager's Plugins page:
-- **本地文件**:在手机/电脑网络面板选择本机下载的 `.framely` 文件;文件上传后校验。
-- **包 URL**:粘贴作者提供的固定版本 HTTPS 下载地址,等待下载和检查。
+- **Local file**: select a downloaded `.framely` file on your phone/computer in the web panel; it uploads and is verified.
+- **Package URL**: paste a fixed-version HTTPS download URL from the author and wait for verification.
-两种方式都需要阅读安装确认,涉及运行用户变更时另行确认。导入的是插件包;Framely 本体 tar.gz 和普通 ZIP 不能作为插件安装。
+Both require reviewing confirmation, with an additional confirmation for changed runtime users. Device tar.gz archives and ordinary ZIP files are not plugin packages.
-在 Frame 的 SSH 终端也可安装:
+You can also install from Frame's SSH terminal:
```bash
sudo /var/lib/framely/current/bin/framely install ./plugin.framely --approve
```
-只有在确认旧版到新版确实需要改变运行用户时才增加 `--approve-run-as`。命令行不会代替你判断来源是否可信。
+Add `--approve-run-as` only when you have verified that changing the runtime user is intended. CLI approval does not assess the source's trustworthiness.
-## 更新、选择旧版与卸载
+## Updates, older versions and removal
-商店提示“可更新”时进入详情,查看变化后按安装确认流程更新。需要旧版时,在详情选择历史版本再检查;换来源也会明确提示。设置和收藏通常保留,但运行用户改变会使用另一份数据目录,不自动迁移旧身份的数据。
+Open details for plugins marked updatable, review changes and confirm. Select history to inspect an older version. Switching sources is explicitly disclosed. Settings and favorites are generally retained; a different runtime user uses a different data directory and does not automatically migrate the old one.
-从插件管理弹窗卸载。卸载钩子负责清理该插件的外部资源,保存的数据保留;不要直接删除系统中的插件目录。停用只停止使用,不卸载程序和数据。
+Uninstall through the management dialog. Hooks handle external cleanup and saved data is retained. Do not remove system plugin directories manually. Disabling stops use without removing programs or data.
-## 常见问题
+## Troubleshooting
-| 现象 | 处理 |
+| Symptom | Action |
| --- | --- |
-| 商店空白 | 检查是否添加并启用了来源、连接能否成功、筛选是否过窄 |
-| 下载或校验失败 | 检查网络和包地址,重新下载;不要跳过哈希校验 |
-| 历史版本不可用 | 来源可能没有历史文件,可先安装最新版或导入可信旧包 |
-| 插件无法启用 | 查看依赖、冲突和错误说明,确认其他必要插件已安装 |
-| 插件反复崩溃 | 停用后检查日志,必要时进入安全模式,再向插件作者反馈 |
+| Empty store | Check enabled sources, connectivity and filters |
+| Download/hash failure | Check network and URL; download again without bypassing verification |
+| Missing history | The source may lack its history file; use latest or a trusted local old package |
+| Cannot enable | Read dependency/conflict errors and install required plugins |
+| Repeated crashes | Disable, inspect logs, use safe mode if needed, and report to the author |
diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md
new file mode 100644
index 0000000..936703f
--- /dev/null
+++ b/docs/zh-CN/README.md
@@ -0,0 +1,37 @@
+# Framely 文档
+
+[English](../README.md)
+
+Framely 是 Steam Frame 的插件管理器,包含设备端服务、SteamVR 界面、网络管理面板和电脑端安装器。
+
+## 用户指南
+
+按顺序阅读即可完成安装并使用第一个插件。
+
+1. [安装、更新、修复与卸载](user-guide/installation.md):桌面安装器、SSH、Preview、本地包、回滚。
+2. [基本使用](user-guide/basic-usage.md):Dock 入口、管理窗口、启停、收藏、安全模式。
+3. [安装与管理插件](user-guide/plugins.md):商店、来源、订阅、本地包和更新。
+4. [网络管理面板](user-guide/network-panel.md):地址、首次密码、登录、手机/电脑使用。
+
+## 插件开发与发布
+
+- [完整流程](developer-guide/README.md):生成项目 → 浏览器预览 → 后端 → 打包 → 实机测试 → 发布。
+- [SDK API](developer-guide/sdk.md) · [Manifest 配置说明](developer-guide/manifest.md) · [生命周期](plugin-lifecycle.md)
+- [发布插件与插件源](developer-guide/publishing.md):GitHub 默认地址生成、自定义地址与哈希、目录历史、数据库登记。
+- [可用插件模板](../../templates/plugin/README.zh-CN.md):React + Python,脚手架与仓库内编辑两种使用方式。
+- [功能展示插件](../../examples/showcase/README.zh-CN.md):更多 UI 和设备交互示例。
+
+## 技术参考
+
+| 参考 | 内容 |
+| --- | --- |
+| [清单、SDK 与后端协议](plugin-development.md) | Manifest 字段、窗口、通知、JSON RPC、打包规则 |
+| [生命周期](plugin-lifecycle.md) | 安装、更新、启动、停止、卸载及恢复钩子 |
+| [依赖与冲突](plugin-relationships.md) | 版本范围、来源选择、可选依赖、独占资源 |
+| [源订阅格式](source-subscriptions.md) | JSON 格式、刷新、地址变更及去重 |
+| [语言与翻译](localization.md) | 语言包、文案键、社区翻译 |
+
+## 维护者资料
+
+- [Framely 发行与更新机制](releases.md)
+- [电脑端安装器构建](../../installer/README.zh-CN.md)
diff --git a/docs/zh-CN/developer-guide/README.md b/docs/zh-CN/developer-guide/README.md
new file mode 100644
index 0000000..4642e3f
--- /dev/null
+++ b/docs/zh-CN/developer-guide/README.md
@@ -0,0 +1,84 @@
+# 插件开发到发布:完整流程
+
+[English](../../developer-guide/README.md)
+
+[文档首页](../README.md) · [发布指南](publishing.md)
+
+## 1. 准备开发环境
+
+开发机准备 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 验证。
+
+```bash
+git clone https://github.com/SteamFramelyHomebrew/framely.git
+cd framely
+node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
+cd ../my-plugin
+npm install
+npm run dev
+```
+
+这里的 `yourname.my-plugin` 是示例 ID,发布前换成你自己的稳定命名空间。脚手架拒绝覆盖已有目录,复制 SDK 和构建工具,生成后不依赖原 Framely 仓库的位置。将生成项目放入自己的 Git 仓库,并提交 `package-lock.json` 和 vendored SDK;不要提交 `node_modules/`、`payload/`、发行包和密码。
+
+## 2. 认识模板
+
+| 文件 | 用途 |
+| --- | --- |
+| `manifest.json` | ID、名称、版本、运行用户、页面、生命周期、依赖 |
+| `page.tsx` | React 快捷页和独立窗口 |
+| `backend.py` | Python 后端及数据保存 |
+| `vendor/framely-sdk/` | UI、桥接接口和 Python 协议辅助库 |
+| `dev.mjs` | 构建和本地预览工具 |
+| `LICENSE` | AGPLv3 许可证全文 |
+
+模板的“读取/保存”使用 `settings.get` / `settings.set`,内容保存在后端的 `FRAMELY_DATA_DIR/settings.json`。它还演示打开 `main` 窗口、发送通知、启动/停止和安装/更新/卸载回调。默认身份 `steamos`,默认按需启动。
+
+## 3. 修改清单与界面
+
+先修改 `manifest.json` 中的 ID、作者、名称、描述、版本、标签和 `changelog`。使用完整 SemVer,例如 `0.1.0`、`0.2.0-preview.1`;已发布版本不覆盖。
+
+`ui.quickPage` 和 `ui.windows.main.entry` 指向构建后的 `page.js`。页面通过 `registerPlugin({QuickPage, WindowPage})` 注册。窗口 key 必须与清单一致。`files` 留空,打包时自动计算哈希。
+
+在浏览器打开 `http://127.0.0.1:5173`;修改页面后自动重载。预览只模拟窗口和通知,不启动 Python 后端,不拥有 Frame 权限;读取/保存按钮在预览中报出后端不可用是正常情况。不要据此声称设备功能已验收。
+
+UI 与后端 API 见 [SDK 文档](sdk.md),字段、默认值和范围见 [Manifest 配置](manifest.md);窗口与商店资料补充见[插件开发参考](../plugin-development.md)。
+
+## 4. 实现后端与生命周期
+
+后端 stdin/stdout 使用逐行 JSON RPC;stdout 仅输出协议,调试日志输出 stderr。Python 模板使用 `framely.serve` 处理请求、错误和生命周期。修改业务逻辑时校验参数长度与类型,并返回能由 JSON 序列化的结果。
+
+写数据使用 `FRAMELY_DATA_DIR`,不要写到插件载荷或 Framely 程序目录。升级、回退和运行用户变更的状态迁移由插件设计;`steamos` 与 `root` 对应不同的数据目录。普通功能优先使用 `steamos`,确需系统权限时才使用 `root` 并解释原因。
+
+安装/更新钩子应可重复执行;停止/卸载时释放本插件持有的外部资源。关闭 UI 窗口不等于停止后端,React effect 清理不能替代后端钩子。系统依赖不会由 Framely 自动安装,需在说明中写清。[生命周期参考](../plugin-lifecycle.md)
+
+## 5. 构建、打包、校验
+
+在插件项目中执行:
+
+```bash
+npm run build
+FRAMELY_REPO=/absolute/path/to/framely
+cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest manifest.json --payload payload --output yourname.my-plugin-0.1.0.framely
+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。
+
+`payload/` 只放要分发的页面、后端、Python 协议库及必要资源。构建会复制 `backend.py`、`framely.py`;额外图标等资源需要自行放入该目录。打包检查入口、路径、清单和哈希,不允许软链接或额外包内文件。
+
+## 6. 安装到 Frame 测试
+
+将包下载到手机/电脑,进入网络面板,在“插件”的本地导入入口上传并确认;或通过 SSH 复制到 Frame 后执行:
+
+```bash
+sudo /var/lib/framely/current/bin/framely install ./yourname.my-plugin-0.1.0.framely --approve
+```
+
+首次协议确认后再管理插件。测试读取/保存、窗口和通知,停用再启用,重启后读回数据;修改版本后测试更新和数据保留,最后测试卸载钩子。涉及 VR 键盘、手柄或硬件的功能,记录实际 Frame 验证结果。
+
+日志可通过 Framely 状态与 systemd 日志检查;后台插件 stdout 不应混入调试文字。更完整的安装操作见[用户教程](../user-guide/plugins.md)。
+
+## 7. 发布
+
+GitHub Release 默认使用标签 `v` 和附件 `-.framely`,源码清单可以省略 `downloadUrl`。社区登记会自动拼接地址;打包前按[发布指南](publishing.md)生成包含最终地址的临时清单,因为当前 `framely pack` 不会自己读取仓库地址。自定义下载地址再显式填写 `downloadUrl`。随后公开 Release、上传包,提供使用说明、运行用户、更新记录和实际测试范围,再自行托管插件源或登记固定源码提交。社区数据库还需校验整包预期哈希和字段兼容性。
+
+后续更新:提升版本号 → 重新生成发布清单(自定义地址时更新地址)→ 构建打包并校验 → 实机验证 → 新 Release → 更新来源登记。不要替换旧版本附件。
diff --git a/docs/zh-CN/developer-guide/manifest.md b/docs/zh-CN/developer-guide/manifest.md
new file mode 100644
index 0000000..d2ae639
--- /dev/null
+++ b/docs/zh-CN/developer-guide/manifest.md
@@ -0,0 +1,96 @@
+# Manifest 配置说明
+
+[English](../../developer-guide/manifest.md)
+
+[开发流程](README.md) · [SDK](sdk.md) · [生命周期](../plugin-lifecycle.md)
+
+项目根目录 `manifest.json` 描述插件;打包后同名文件位于包内。字段采用 camelCase,安装包清单拒绝未知字段,不要添加已废弃的 `permissions`。以[模板清单](../../../templates/plugin/manifest.json)为起点。
+
+## 最小示例
+
+```json
+{
+ "schemaVersion": 1,
+ "apiVersion": 1,
+ "id": "yourname.my-plugin",
+ "name": "我的插件",
+ "version": "0.1.0",
+ "author": "Your name",
+ "backend": {"entry": "backend.py", "runAs": "steamos"},
+ "ui": {"quickPage": "page.js"},
+ "files": {}
+}
+```
+
+源码清单的 `files` 留空;`framely pack` 遍历 payload 并生成实际文件哈希。至少一个载荷文件,每个入口必须存在于 payload。完整模板见 [manifest.json](../../../templates/plugin/manifest.json)。
+
+## 顶层字段
+
+| 字段 | 要求与默认值 |
+| --- | --- |
+| `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 个文件 |
+
+旧 `category` 字段仅兼容读取,不再导出;新插件使用 `tags`。`downloadSha256` 是数据库源码登记专用的整包 SHA256,不能放入严格校验的包内清单。默认 GitHub Release 模式从附件 `digest` 读取整包哈希;自定义 HTTPS 地址需要填写源码专用哈希,打包时移除。省略源码 `downloadUrl` 时,社区登记会补全,但当前 `framely pack` 不会自动补全;[发布指南](publishing.md)提供临时清单命令。社区登记还有比本体严格的 ID、简介长度等校验,不能将本体校验通过等同于登记成功。
+
+## `backend`
+
+| 字段 | 说明 |
+| --- | --- |
+| `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 取消限制 |
+
+省略 backend 可制作纯 UI 插件。系统/Python 第三方依赖不会自动安装。运行用户变化需用户确认,数据目录按运行身份区分,不自动迁移。
+
+`backend.memoryLimitMiB` 映射到 systemd 的 `MemoryMax`,限制后端及其所有子进程的合计内存;它是上限,不会预分配内存。独立生命周期钩子沿用该值,没有后端的钩子使用 512 MiB。安装计划、CLI 和已安装插件管理页展示上限。省略或填写 512 时打包省略该字段,保持旧包兼容;使用其他值需要支持此字段的新版宿主及插件数据库。
+
+例如,为较长回放声明 2 GiB:
+
+```json
+{"backend": {"entry": "backend", "memoryLimitMiB": 2048}}
+```
+
+## `ui`
+
+`quickPage` 是可选包内页面 bundle 路径;`windows` 默认空对象,最多 8 个窗口,key 必须是合法 ID。每个窗口:
+
+| 字段 | 说明 |
+| --- | --- |
+| `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 |
+
+`localWeb` 只支持 Frame 本机打开,后端 `window.get` 参数为 `{window: key}`,返回 `{url: "http://localhost:/framely-window/"}`;不得有查询串、锚点或使用管理面板端口。它使用本机网页所需的不同 sandbox 标志。普通 React 插件无需 `localWeb`。窗口 key 与 `framely.windows.open(key)` 及注册的组件映射一致。尺寸在重新打开窗口时生效。
+
+## `lifecycle`
+
+`onInstall`、`onUpdate`、`onUninstall`、`onCrashCleanup` 是可选 `{entry, args?}` 独立命令;`onStart` / `onStop` 是默认 false 的布尔开关,需要 backend。`timeoutSeconds` 默认 10,范围 1–15 秒。`runAs` 可选,有后端时必须与后端一致,纯 UI 插件独立钩子可用它声明身份。独立命令的 `args` 限制与 backend 相同。执行时机、失败行为和上下文见[生命周期](../plugin-lifecycle.md)。
+
+## 发布资料与关系
+
+`publish.icon` 为 HTTPS 外部图标 URL,`publish.screenshots` 为最多 8 个 HTTPS 截图 URL。包内 `icon` 和 `screenshots` 是文件路径,不能混用。下载地址优先使用 `downloadUrl`,同版本内容必须保持不变。[发布指南](publishing.md)
+
+依赖值可为版本范围字符串,或 `{version, source}`;source 是 HTTPS `catalog.json` 地址。冲突值是版本范围。可选依赖缺失不阻止安装,独占资源只按作者声明校验。[依赖与冲突](../plugin-relationships.md)
+
+清单 JSON 最大 256 KiB,载荷路径最多 512 字节。载荷路径仅允许 ASCII 字母、数字及 `/._-+`,禁止绝对路径、空段、`.`、`..`、软链接和未声明的附加包文件。清单通过校验后,运行权限仍需用户判断;文件哈希不代表插件安全审核。
diff --git a/docs/zh-CN/developer-guide/publishing.md b/docs/zh-CN/developer-guide/publishing.md
new file mode 100644
index 0000000..1596dcd
--- /dev/null
+++ b/docs/zh-CN/developer-guide/publishing.md
@@ -0,0 +1,144 @@
+# 发布插件与插件源
+
+[English](../../developer-guide/publishing.md)
+
+[文档首页](../README.md) · [开发全流程](README.md)
+
+## GitHub Release 默认地址
+
+公开 GitHub 插件仓库的**源码 `manifest.json` 可以省略 `downloadUrl`**。社区数据库从登记的仓库地址、版本和插件 ID 自动生成:
+
+```text
+https://github.com///releases/download/v/-.framely
+```
+
+例如仓库 `yourname/my-plugin`、ID `yourname.my-plugin`、版本 `0.1.0` 对应标签 `v0.1.0` 和附件 `yourname.my-plugin-0.1.0.framely`。仓库名不必与 ID 相同。必须真正省略字段;空字符串或 null 不表示自动生成。不要使用 `latest`,不要替换同版本附件。
+
+源码登记与打包有一项区别:数据库会补全源码清单的下载地址,并要求**包内清单含相同的最终 `downloadUrl`**。当前 `framely pack` 只计算载荷哈希,不读取 Git remote、不自动拼接 Release URL。若源码省略该字段,打包前生成临时清单;无需把地址手写回源码。
+
+## 生成发布包
+
+先按[开发流程](README.md)创建插件、执行 `npm run build`,在自己的插件 Git 仓库配置正确的 GitHub `origin`。以下命令在插件目录执行,从源码生成 `.framely-build/manifest.json`,支持 HTTPS、SSH 两种常用 remote 写法;已有显式 `downloadUrl` 会保留,源码不会改写:
+
+```bash
+python3 - <<'PY'
+import json, re, subprocess
+from pathlib import Path
+
+manifest = json.loads(Path('manifest.json').read_text(encoding='utf-8'))
+manifest.pop('downloadSha256', None) # Source registration only; never package it.
+if 'downloadUrl' not in manifest:
+ remote = subprocess.check_output(
+ ['git', 'remote', 'get-url', 'origin'], text=True
+ ).strip()
+ match = re.fullmatch(
+ r'(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)'
+ r'([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?)(?:\.git)?/?', remote
+ )
+ if not match:
+ raise SystemExit('Use a GitHub origin or set downloadUrl explicitly.')
+ manifest['downloadUrl'] = (
+ f"https://github.com/{match[1]}/releases/download/v{manifest['version']}/"
+ f"{manifest['id']}-{manifest['version']}.framely"
+ )
+output = Path('.framely-build/manifest.json')
+output.parent.mkdir(parents=True, exist_ok=True)
+output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
+print(manifest['downloadUrl'])
+PY
+```
+
+`.framely-build/` 是临时输出,应加入插件 `.gitignore`。模板已忽略它。包名必须与默认规则一致;在插件目录继续执行:
+
+```bash
+FRAMELY_REPO=/absolute/path/to/framely
+PLUGIN_ID=$(python3 -c 'import json; print(json.load(open("manifest.json"))["id"])')
+PLUGIN_VERSION=$(python3 -c 'import json; print(json.load(open("manifest.json"))["version"])')
+mkdir -p dist
+cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest .framely-build/manifest.json --payload payload --output "dist/$PLUGIN_ID-$PLUGIN_VERSION.framely"
+cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify "dist/$PLUGIN_ID-$PLUGIN_VERSION.framely"
+```
+
+提交源码、SDK、锁文件和许可证,将该提交标记为 `v`;创建对应 GitHub Release,上传 `dist/` 内的 `.framely`,并公开 Release。包内清单和源码登记清单在补全默认地址、去掉源码专用字段后应一致。用于登记的 submodule 必须指向这个实际发布版本的源码提交。
+
+发行说明包含功能和使用方法、运行用户及 root 的原因、依赖、变化、实际 Frame 测试和已知限制。提供与包匹配的源码和构建脚本,遵守模板、SDK及依赖许可证。
+
+### 整包哈希与自定义地址
+
+| 方式 | 源码登记 | 校验的预期整包哈希 |
+| --- | --- | --- |
+| 默认 GitHub Release | 省略 `downloadUrl`;标签和附件遵守上述命名 | 从对应 Release 附件的 `digest` 读取 SHA256,无需手写 |
+| 显式固定 GitHub Release | 填 `downloadUrl`,可改变标签或附件名 | 未提供 `downloadSha256` 时读取 Release digest;显式提供时以该哈希校验 |
+| 其他 HTTPS 下载地址 | 填 `downloadUrl` 和 `downloadSha256` | 使用源码登记中声明的 64 位整包 SHA256 |
+
+自动地址模式即使另填 `downloadSha256`,也必须与 Release digest 相同。缺少 Release digest、附件未公开、无法下载或哈希不符会阻止登记,不会退回到信任刚下载的文件。
+
+`downloadSha256` 只属于数据库源码登记,指完整 `.framely` 的哈希;不是载荷的 `files` 哈希,也不是 Framely 包内字段。先生成包,执行 `sha256sum dist/-.framely`,再把结果写入源码登记。打包必须移除它,上面的临时清单命令已处理;不要向包内写入自身哈希。直接给 `framely pack` 传含此字段的源码清单会被未知字段校验拒绝。
+
+### 图标与窗口兼容
+
+社区数据库的图标通常只需顶层 `icon: "icon.png"`:同一路径 PNG 同时提交到源码仓库和 `payload/`。数据库从登记的仓库、固定提交及路径生成 GitHub Raw 地址,并下载比较包内图标;不需要额外填写 `publish.icon`。自行托管目录的图片由 `publish.icon` / `publish.screenshots` 提供固定 HTTPS 地址。[商店资料参考](../plugin-development.md#商店资料与图标)
+
+社区数据库支持窗口 `entry`、`title`、`dockIcon`、`localWeb`、`width`、`height`、`widthMeters`。像素宽度为整数 640–2560,高度为整数 360–1440,物理宽度按本体 float32 规则校验 0.4–4.0 米。源码省略尺寸时,数据库会按 1600×900、3 米补全,与 pack 输出一致;自定义尺寸也会参与源码和包内清单比较,未声明的变化会被拒绝。模板包含的默认独立窗口已通过实际打包和数据库校验;登记仍需完成整包哈希、归属及其他字段检查。
+
+## 方式一:自行托管插件源
+
+自托管不要求社区登记,也不要求 GitHub Release digest。可以省略包内 `downloadUrl`,由 `framely catalog` 使用 `--base-url` 与实际包文件名拼接;填写时优先采用包内地址。若包名来自默认 GitHub 规则,按前述方式发布即可。
+
+把要保留的各版本 `.framely` 放入 `packages/`,在 Framely 仓库执行:
+
+```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
+```
+
+将 `example.org` 换成实际 HTTPS 地址。CLI 计算完整包的 SHA256并生成 JSON,不上传包、不复制图片。没有外部 `publish.icon` 时,本地 CLI 不会把包内图标转换成商店 URL。
+
+**上传整个生成目录,不能只上传 `catalog.json`:**
+
+```text
+catalog.json
+plugins/
+ yourname.my-plugin/
+ versions.json
+```
+
+主目录每个 ID 一条推荐最新版;历史条目在相对 `plugins//versions.json`,不包含主目录当前条目。每次生成需提供要保留的所有版本,本地 CLI 不合并之前的历史。社区数据库会延续之前发布的历史,当前每插件最多保留 19 个历史条目。不要混淆两种工具的保留规则。
+
+将目录放在 HTTPS 静态服务,用户添加完整 `catalog.json` URL;各条目指向的包和图片也必须可访问。多个渠道可再发布[源订阅文件](../source-subscriptions.md)。
+
+## 方式二:登记社区数据库
+
+登记入口为 [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 固定提交;数据库不镜像包或证明源码与二进制可复现一致。
+
+首次登记:先 fork 数据库,使用自己 fork 的同名 `testing` 分支:
+
+```bash
+git clone --branch testing https://github.com/yourname/framely-plugin-database.git
+cd framely-plugin-database
+git submodule add https://github.com/yourname/my-plugin.git plugins/yourname.my-plugin
+git -C plugins/yourname.my-plugin checkout v0.1.0
+git add .gitmodules plugins/yourname.my-plugin
+git commit -m 'Add yourname.my-plugin 0.1.0'
+git push origin testing
+```
+
+向上游 `testing` 提交 PR,附实际 Frame 测试记录。只改 `.gitmodules` 与 `plugins/` 的 submodule 固定提交,其他文件变更不会走登记自动合并。
+
+数据库 ID 要求 `namespace.name`:小写字母和数字,名称部分可带点、连字符;它比本体 ID 校验严格。路径必须是 `plugins/`,仓库必须是公开 GitHub 仓库。首次登记确认仓库归属和作者维护权限;命名空间绑定 GitHub 所有者数字 ID,每个所有者最多五个历史前缀,删除插件不释放归属。已登记 ID 不能换源仓库。协作者需要可验证的 write/maintain/admin 权限,不能以 manifest 的 `author` 或 Git 提交作者替代。[完整归属规则](https://github.com/SteamFramelyHomebrew/framely-plugin-database/blob/testing/CONTRIBUTING.md#插件归属与自动合并)
+
+更新登记:
+
+```bash
+git -C plugins/yourname.my-plugin fetch --tags
+git -C plugins/yourname.my-plugin checkout v0.2.0
+git add plugins/yourname.my-plugin
+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` 是生成分支,不提交插件变更。
+
+## 发布后验证
+
+检查用户实际能访问的包、目录、图标、历史选择、确认安装、更新及数据保留。只有本体 `verify` 通过还不够,登记还须通过数据库对源码、整包哈希和字段的检查。后续按“提升版本 → 重新生成临时清单 → 构建打包 → 实机测试 → 新 Release → 更新固定登记”发布;不要覆盖旧附件。
diff --git a/docs/zh-CN/developer-guide/sdk.md b/docs/zh-CN/developer-guide/sdk.md
new file mode 100644
index 0000000..936a026
--- /dev/null
+++ b/docs/zh-CN/developer-guide/sdk.md
@@ -0,0 +1,113 @@
+# SDK API 文档
+
+[English](../../developer-guide/sdk.md)
+
+[开发流程](README.md) · [Manifest](manifest.md) · [生命周期](../plugin-lifecycle.md)
+
+SDK 源码位于 `sdk/src/index.tsx`、`sdk/src/lifecycle.ts` 和 `sdk/python/framely.py`。页面从 `@framely/sdk` 导入,Python 协议库由构建工具复制到 `payload/framely.py`。
+
+## 页面注册
+
+```tsx
+import {registerPlugin, Section} from '@framely/sdk';
+function QuickPage() {return 快捷内容;}
+function MainWindow() {return 详细内容;}
+registerPlugin({QuickPage, windows: {main: MainWindow}});
+```
+
+`QuickPage` 必填。`WindowPage` 是独立窗口的默认组件;`windows` 可按清单窗口 key 指定不同组件。没有对应组件时回退到 `WindowPage`,再回退到 `QuickPage`;`quick` 用于快捷页路由,避免将它用作窗口 key。页面运行在宿主的 sandbox iframe,宿主先建立桥接再加载你的 bundle。注册函数会挂载 React、基础样式和滚动条。
+
+## 宿主接口 `framely`
+
+| API | 参数与返回 | 用途 |
+| --- | --- | --- |
+| `framely.call(method, params = {})` | `Promise` | 调用本插件后端方法;错误会 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` | 查询本插件声明的依赖状态 |
+| `framely.language.get()` | `Promise<{preference, language}>` | 获取语言偏好和当前生效语言 |
+| `framely.onEvent(callback)` | 返回取消订阅函数 | 接收宿主转发的事件 |
+
+所有调用只通过插件桥接,不提供管理员 API 或跨插件后端调用。用 `try/catch` 或 `.catch` 处理失败,异步操作期间禁用重复提交。
+
+```tsx
+try {
+ const result = await framely.call<{text: string}>('settings.get');
+ console.log(result.text);
+} catch (error) {console.error(error);}
+```
+
+## 通知
+
+```tsx
+await framely.notifications.send({
+ id: 'task', title: '完成', body: '文件已处理', durationMs: 8000,
+ actions: [{id: 'open', label: '打开', 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 更新不额外占队列位置。
+
+按钮动作调用后端 `notification.action`,参数 `{id, action}`;同时转发 `notification.action` 页面事件。无后端插件可只监听页面事件。同一 ID 用于更新,调用 remove 撤回。
+
+## React hooks
+
+| Hook | 返回与行为 |
+| --- | --- |
+| `useBackend(method, params = {})` | `{data, error, loading, call}`;不会自动发起请求,主动调用 `call()`;该调用失败仍会抛出异常 |
+| `usePluginEvent(callback)` | 自动订阅并在页面卸载时取消订阅 |
+| `useDependencies()` | `{items, error}`;自动加载依赖并在 `dependencies.changed` 时刷新 |
+
+手动订阅时通过 React 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)
+
+
+宿主语言变化转发 `{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
+
+```python
+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 专用于协议。生命周期字典写法见[生命周期文档](../plugin-lifecycle.md)。
+
+## 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 决定。
diff --git a/docs/zh-CN/localization.md b/docs/zh-CN/localization.md
new file mode 100644
index 0000000..024653f
--- /dev/null
+++ b/docs/zh-CN/localization.md
@@ -0,0 +1,40 @@
+# 界面语言与社区翻译
+
+[English](../localization.md)
+
+Framely 内置简体中文和英语。在“设置”顶部选择语言;默认跟随系统语言,手动选择会保存在设备配置中。找不到对应语言时使用英语。语言文件缺少某条翻译时,该条内容回退到内置英语。
+
+## 制作语言文件
+
+从设置下载英文模板,或复制 `ui/locales/en-US.json`。文件是 UTF-8 JSON:
+
+```json
+{
+ "schemaVersion": 1,
+ "locale": "fr-FR",
+ "name": "Français",
+ "messages": {
+ "语言": "Langue",
+ "保存": "Enregistrer",
+ "启用 {0}": "Activer {0}"
+ }
+}
+```
+
+- `locale` 使用语言代码,例如 `en-US`、`zh-CN`、`fr-FR`。不能使用 `auto`。
+- `name` 使用该语言自己的名称,显示在语言选择列表中。
+- `messages` 左侧是稳定的文案键,不要翻译或改写左侧;右侧才是译文。
+- `{0}`、`{1}` 等是运行时参数。可以调整位置,但不能增删占位符。
+- 可以先提供部分翻译,其余条目显示英语。空字符串是有效译文,不视为缺失。
+- 按目标语言习惯重写整句,结合实际操作理解按钮和状态。部分键是句子片段,注意保留必要的空格。
+- 文件只包含文本,不执行代码,也不解释 HTML。单文件最多 1 MiB、3000 条翻译,最多导入 50 个语言文件。
+
+## 安装
+
+在“设置 → 语言”点击“安装语言文件”,选择 JSON 文件。导入后在语言列表中选择它;再次导入相同语言代码会更新原文件。
+
+安装后的文件位于 `/var/lib/framely/locales/.json`。也可以由管理员直接放入此目录,文件名必须与 `locale` 一致;刷新管理面板后读取。损坏的文件会跳过,并在设置中显示文件名。插件作者提供的名称、介绍、标签及插件页面由各插件维护,宿主语言文件不会自动翻译这些内容。
+
+## 项目校验
+
+执行 `node tools/test-localization.mjs` 检查内置英语是否覆盖全部界面文案键、占位符是否一致,以及系统语言匹配、手动选择和缺失翻译回退。
diff --git a/docs/zh-CN/plugin-development.md b/docs/zh-CN/plugin-development.md
new file mode 100644
index 0000000..820826a
--- /dev/null
+++ b/docs/zh-CN/plugin-development.md
@@ -0,0 +1,152 @@
+# 插件开发参考
+
+[English](../plugin-development.md)
+
+第一次开发请先阅读[完整开发流程](developer-guide/README.md)和[发布指南](developer-guide/publishing.md)。
+
+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
+
+```json
+{
+ "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//`;旧身份数据保留,安装使用旧身份的版本可恢复访问,不自动跨身份迁移文件。
+
+## React API
+
+```tsx
+import {framely, registerPlugin, Button, Section} from '@framely/sdk';
+
+function QuickPage() {
+ return
+
+
+
+ ;
+}
+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 秒会停止后端,下次打开可重试。
+
+```json
+{"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 与频率限制。
+
+## 打包与源发布
+
+```bash
+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。每个源独立显示,插件记录所选来源;另一个源同 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` 导出到商店目录;作者主页链接直接显示在作者名称上,未提供时作者名称为普通文本;商店列表、详情、管理弹窗和安装确认使用相同行为。文档和插件主页保留独立入口,点击后由系统默认浏览器打开。
+
+```json
+{
+ "authorUrl": "https://github.com/author",
+ "documentationUrl": "https://github.com/author/plugin#readme",
+ "homepage": "https://github.com/author/plugin"
+}
+```
+
+插件仓库可声明发布信息:
+
+```json
+{
+ "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` 和 `-.framely` 自动生成。包内仍需最终地址,当前 pack 不会自动读取 Git remote;按[发布指南](developer-guide/publishing.md)生成临时清单即可。社区图标通常只需 `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"` 关闭。默认第三次连续失败后停用,运行稳定一分钟后重置计数,详见[生命周期](plugin-lifecycle.md)。
+
+## 脚手架与预览
+
+```bash
+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` 接收。
+
+插件安装、更新、停止和异常恢复的约定见[生命周期开发说明](plugin-lifecycle.md)。
+
+窗口可选声明 `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}`。尺寸在重新打开窗口时生效。
+
+依赖、可选依赖、冲突和独占资源见[插件关系](plugin-relationships.md),多个插件源聚合见[源订阅](source-subscriptions.md)。
+
+插件只使用 `tags` 描述用途,不再使用分类;为兼容已发布的包,旧 `category` 字段读取时忽略,新的清单及目录不再导出它。插件数据库每次生成目录时同时生成 `tags.json`,格式为 `{ "schemaVersion": 1, "tags": ["工具", "温控"] }`,聚合当前目录全部插件的标签、精确去重并排序。stable/testing 分别生成自己的标签文件,不生成跨渠道并集;程序加载多个源后在本地聚合去重。标签随插件更新自动增删,无须手工维护。
+
+上传的插件包、商店下载及其依赖包暂存于 `/tmp/framely-package-*`,确认安装后才写入插件目录。取消、安装完成或失败后删除暂存包;未完成上传和待确认安装包闲置 15 分钟后过期,每 30 秒清理一次。
+
+本机网页窗口在 `ui.windows.` 设置 `localWeb: true`,宿主通过后端 `window.get` 获取窗口 URL;这是窗口加载方式,不是权限声明。URL 使用 `http://localhost:<端口>/framely-window/`。普通窗口无需设置此项。
+
+商店主目录 `catalog.json` 中每个插件 ID 仅出现一次,代表最新版。历史条目存放在相对目录 `plugins/<插件ID>/versions.json`,格式为 `{ "schemaVersion": 1, "id": "作者.插件", "versions": [...] }`;版本条目沿用目录条目的字段,列表不包含主目录的最新版。商店进入详情时按需读取历史,依赖解析仅在最新版不满足范围时读取历史。附加目录字段可扩展,读取端忽略未知字段,但校验已知字段;改变必需语义应升级 `schemaVersion`。插件安装包清单仍严格校验。
diff --git a/docs/zh-CN/plugin-lifecycle.md b/docs/zh-CN/plugin-lifecycle.md
new file mode 100644
index 0000000..7d5493c
--- /dev/null
+++ b/docs/zh-CN/plugin-lifecycle.md
@@ -0,0 +1,95 @@
+# 插件生命周期
+
+[English](../plugin-lifecycle.md)
+
+生命周期由 root 核心服务调度,但钩子使用插件声明的运行身份;省略身份为 `steamos`。有后端时,独立钩子与后端身份必须一致;纯 UI 插件可在 `lifecycle.runAs` 声明身份,安装时展示运行用户。
+
+```json
+{
+ "backend": {
+ "entry": "backend.py",
+ "runAs": "steamos",
+ "autostart": false,
+ "restart": "on-failure",
+ "restartLimit": 3
+ },
+ "lifecycle": {
+ "onInstall": {"entry": "backend.py"},
+ "onUpdate": {"entry": "backend.py"},
+ "onStart": true,
+ "onStop": true,
+ "onUninstall": {"entry": "backend.py"},
+ "onCrashCleanup": {"entry": "backend.py", "args": ["--cleanup"]},
+ "timeoutSeconds": 10
+ }
+}
+```
+
+所有钩子可省略。独立命令的 entry 必须是包内文件并包含在 files 哈希清单;不能指定 shell 字符串或包外路径。没有为插件增加身份或网络权限。timeoutSeconds 为 1–15 秒,默认 10 秒,每个钩子分别计时。
+
+| 钩子 | 执行时机与方式 |
+| --- | --- |
+| onInstall | 第一次安装,新版本激活前执行独立命令 |
+| onUpdate | 更新或回退,停止旧后端后、新版本激活前执行目标版本的独立命令 |
+| onStart | 启动后端后发送 `framely.lifecycle.start` RPC,成功后才接受业务调用 |
+| onStop | 禁用、重启、更新、回退、卸载和管理器正常退出时,停止进程前发送 `framely.lifecycle.stop` RPC |
+| onUninstall | 停止后端后、删除包前执行独立命令 |
+| onCrashCleanup | 异常退出、信号、OOM、调用超时或初始化失败后,由独立进程执行清理 |
+
+关闭快捷菜单或大窗口只卸载对应 React 页面,不停止后端。React effect 清理仍用于页面订阅、预览租约等;不能替代后端生命周期。按需启动与 autostart 是启动时机,restart 是已经启动后发生失败的恢复策略。
+
+## 上下文与 SDK
+
+所有回调收到以下上下文:
+
+```ts
+interface LifecycleContext {
+ pluginId: string;
+ phase: 'onInstall'|'onUpdate'|'onStart'|'onStop'|'onUninstall'|'onCrashCleanup';
+ reason: string;
+ version: string;
+ previousVersion: string|null;
+ dataDir: string;
+ exit: {reason:string;exitCode?:number|null;signal?:number|null;oom?:boolean;message?:string}|null;
+}
+```
+
+独立命令读取 `FRAMELY_LIFECYCLE` 与 JSON 编码的 `FRAMELY_LIFECYCLE_CONTEXT`。所有后端与命令均有 `FRAMELY_PLUGIN_ID`、`FRAMELY_PLUGIN_VERSION`、`FRAMELY_DATA_DIR` 及对应身份 HOME。独立命令 stdout/stderr 是日志,退出码 0 表示成功;后端钩子按普通 JSON 行协议回复 result 或 error。`framely.lifecycle.*` 为核心服务保留,插件业务 API 不能调用。
+
+Python SDK 位于 `@framely/sdk/python`,构建时复制为载荷中的 `framely.py`,不需额外 pip 依赖:
+
+```python
+from framely import serve
+
+def initialize(context):
+ # 可重复执行;仅操作本插件的数据和资源。
+ return {"ready": True}
+
+def cleanup(context):
+ return {"cleaned": True}
+
+def dispatch(method, params):
+ return {"ok": True}
+
+serve(dispatch, {
+ "onInstall": initialize, "onUpdate": initialize,
+ "onStart": initialize, "onStop": cleanup,
+ "onUninstall": cleanup, "onCrashCleanup": cleanup,
+})
+```
+
+SDK 检测独立钩子环境时只执行相应回调并退出,不进入 RPC 循环。TypeScript 后端可从 `@framely/sdk/lifecycle` 导入 `registerLifecycle` 和类型,将保留方法交给返回的 dispatcher;它不负责启动进程或实现 JSON 行传输。网页侧 SDK 也导出上下文类型,但不提供后端运行权限。
+
+## 失败、清理与恢复
+
+安装或更新的命令钩子失败时不激活新版本。若安装/更新当时需要启动后端(常驻或原本在运行),初始化失败也恢复原版本与 current 链接;初次安装则撤销安装。按需插件首次安装通常只执行 onInstall,onStart 在首次打开页面/业务调用等实际启动时执行;此时失败走启动恢复,不会追溯撤销之前已完成的安装。回退的初始化失败时恢复回退前的版本。文件版本可恢复,数据迁移和设备操作无法自动撤销:迁移应可重复执行、采用备份/临时文件与原子替换,并保持上一版本能读取数据。
+
+onStop 失败或超时会记录错误,仍会停止整个插件进程组。崩溃后先关闭该插件的窗口、通知,再执行 onCrashCleanup;清理失败不会阻止错误记录或后续恢复。崩溃清理必须是独立命令,无法依赖已经崩溃进程里的内存、析构器或回调。确认资源属于本插件后再删除;不要重启 SteamVR 或删除其他插件的文件。
+
+onUninstall 失败保留插件并展示错误,用户可重试或选择强制卸载。强制卸载仍尝试有超时限制的清理,随后移除插件包;用户数据默认保留,仅 purge 删除。强制卸载不能保证外部设备设置已还原。
+
+restart 默认为 on-failure;never 只记录失败,不自动重启。restartLimit 默认为 3,范围 1–10,达到连续失败阈值后停用;失败计数在后端稳定运行 60 秒后重置。重试等待为 1、2、4、8、16、32 秒,最多 32 秒。正常自主退出(退出码 0)不重启;管理器主动停止也不计入失败。退出码、信号、systemd OOM 和调用超时记录在状态与日志中。
+
+状态包含 starting、running、stopping、stopped、recovering、failed 等阶段,并发管理操作沿核心服务的串行调度执行。管理器正常 SIGTERM/SIGINT 会尝试逐个停止插件;强制终止、断电不能保证任何钩子执行。清理应同时可在下次启动时安全恢复。
+
+日志页面合并展示后端日志与 lifecycle 日志;每种日志保留当前 2 MiB 与上一份。安装失败且插件尚未登记时,可从 `/var/lib/framely/logs/.lifecycle.log` 查看。运行身份的数据目录为 `/var/lib/framely/data//<身份>/`(自定义核心数据目录时相应变化)。
diff --git a/docs/zh-CN/plugin-relationships.md b/docs/zh-CN/plugin-relationships.md
new file mode 100644
index 0000000..c355ba2
--- /dev/null
+++ b/docs/zh-CN/plugin-relationships.md
@@ -0,0 +1,55 @@
+# 插件依赖与冲突(Framely 0.4)
+
+[English](../plugin-relationships.md)
+
+```json
+{
+ "dependencies": {
+ "example.shared-api": "^1.0.0",
+ "example.camera-api": {
+ "version": ">=1.2.0 <2.0.0",
+ "source": "https://example.org/plugins/catalog.json"
+ }
+ },
+ "optionalDependencies": {"example.notifications": "^1.0.0"},
+ "conflicts": {"example.other-camera": "*"},
+ "exclusiveResources": ["steamvr.passthrough-color"]
+}
+```
+
+四个字段都可省略。声明关系的插件使用合法 SemVer;作为依赖或冲突版本匹配目标的插件也应使用 SemVer。版本范围支持精确版本、`^`、`~`、`*` 和逗号/空白分隔的比较条件。`1.2.0` 表示精确匹配;`>=1.2.0 <2.0.0` 表示两个条件同时满足,不支持 `||`。拒绝自身依赖、重复必需/可选声明、依赖与冲突矛盾和重复独占资源。
+
+`source` 必须是 HTTPS 插件目录 URL,不是订阅或包下载 URL。优先复用版本与来源满足要求的已安装插件;未声明来源时优先当前插件所在源,其他来源必须明确选择。指定来源不可用或没有匹配版本时不换源。明确依赖源尚未添加时,进入安装计划统一确认;已停用的源需用户先启用。删除源后保留已安装插件的来源 URL 记录,以便依赖约束继续识别其来源。
+
+本地包没有默认来源时,未安装依赖需要选择来源;也可通过 CLI 指定已添加源 ID:
+
+```bash
+framely install plugin.framely --source my-source --approve
+framely install plugin.framely \
+ --dependency-source example.shared-api=https://example.org/catalog.json \
+ --approve
+```
+
+`--dependency-source` 可重复使用,不能覆盖清单中的明确来源。CLI 和界面使用同一解析器。需要接受已有插件运行用户变化时再加 `--approve-run-as`。
+
+主目录每个插件 ID 只出现一次,历史版本来自相对路径 `plugins//versions.json`;主目录版本是推荐版本。用户可选择旧版本安装或重新安装当前版本;包的 ID、版本、哈希及依赖均重新校验。依赖解析优先复用已安装版本,否则选取源中第一个满足约束的版本。多个版本/来源约束无法同时满足、出现循环依赖、缺少必需依赖时停止。缺失可选依赖不阻止安装,也不自动安装。
+
+安装计划展示所有包、版本、来源、运行用户、新增源、依赖启用和冲突停用。确认后使用同一份已校验包;设备插件/来源状态改变后要求重新检查。最多 32 个插件,不限制插件包大小或解压后的总大小。先完成下载和校验,再依次安装,检查反向依赖,防止更新破坏已有依赖方。
+
+批量操作记录持久事务日志;失败恢复版本链接、来源和启用状态,撤销本次新增的包安装,并尝试恢复此前运行的后端。安装期间管理器意外退出时,下次启动恢复原记录。用户数据保留;生命周期迁移和设备操作不能由文件版本回退自动撤销,插件必须设计可重复执行、可恢复的迁移。
+
+启用时统一确认需要启用的必需依赖及需要停用的冲突插件。冲突按任意一方声明处理,允许同时安装,禁止同时启用;同名独占资源同样禁止同时启用。停用/卸载基础插件需确认一起停用依赖方,依赖方不会被自动卸载。
+
+后端按依赖顺序启动,`onStart` 成功才启动依赖方;没有该钩子的后端沿用进程启动成功判断。停止顺序相反。关闭页面不停止后端。基础后端异常时暂停正在运行的依赖方,进入“等待依赖”,不累计依赖方失败次数;基础后端恢复后重新按顺序启动。基础后端被停用或禁止重启时等待用户处理。
+
+SDK 提供:
+
+```tsx
+import {framely, useDependencies} from '@framely/sdk';
+const states = await framely.dependencies();
+// React 页面:const {items, error} = useDependencies();
+```
+
+只返回当前插件声明的必需/可选依赖,含版本、启用状态、约束是否满足、后端是否可用和运行阶段。依赖状态变化触发 `dependencies.changed` 事件;不开放跨插件后端调用,不增加运行权限。独占资源和 conflicts 都依赖作者声明,不自动检测任意文件或设备冲突。
+
+不提供插件手动回滚接口;安装旧版本统一走正常安装计划和 `onUpdate`。安装失败的事务恢复仍保留,不是用户选择版本的入口。同 ID 同版本必须保持发布内容不变。
diff --git a/docs/zh-CN/releases.md b/docs/zh-CN/releases.md
new file mode 100644
index 0000000..67ca512
--- /dev/null
+++ b/docs/zh-CN/releases.md
@@ -0,0 +1,78 @@
+# Framely 发行与更新
+
+[English](../releases.md)
+
+插件源和 Framely 自身更新源是独立设置。当前代码首次初始化加入社区 stable/testing 来源;目录能否访问以实际服务为准。Framely 更新服务由发行者预配置,管理窗口“关于”提供检查、下载、安装和回滚;尚未配置时使用发行安装包更新。插件和 Framely 发行包均不要求签名或公钥。
+
+构建发行包后生成包含版本、架构、下载地址、长度与 SHA256 的普通 JSON 更新清单:
+
+```bash
+framely release-manifest \
+ --archive "./framely-0.2.0--linux-arm64.tar.gz" \
+ --url "https://example.org/releases/framely-0.2.0--linux-arm64.tar.gz" \
+ --output ./framely-release.json \
+ --changelog '本次更新说明'
+```
+
+将包与清单发布到 HTTPS 静态服务;下载最多跟随五次经过 URL 校验的重定向,不允许从 HTTPS 降级到 HTTP。发行维护终端配置清单完整 URL:
+
+```bash
+framely call system.source.save '{"source":{"url":"https://example.org/framely-release.json"}}'
+```
+
+此接口用于发行维护,不作为普通用户设置项。当前官方安装引擎和桌面安装器会在更新源尚未配置时写入 `https://github.com/SteamFramelyHomebrew/framely/releases/latest/download/framely-release.json`,已有配置保留。哈希用于检查下载内容一致性,发行者身份依赖更新源;不再通过签名验证。
+
+更新依次检查 API/架构兼容、声明长度、SHA256,安全解压后检查发行包自身 SHA256SUMS;下载不阻塞核心服务。用户确认安装后,独立 systemd 助手切换发行版本,只重启 Framely 服务。日志在 `/var/lib/framely/logs/update.log`。安装失败会尝试恢复原发行和安装前状态;UI 可回滚上一版本。
+
+更新源仅包含 URL,发行清单使用普通 JSON,不支持旧签名包、签名信封或公钥字段。回滚切换上一发行版本,保留插件数据,不进行旧版格式迁移。
+
+设备上的实际升级/回滚和重启恢复需要验收;本地自动测试验证无签名格式、包边界和插件安装更新,不能代替设备服务切换测试。
+
+## SteamOS 更新后的恢复
+
+安装器将整个管理目录保存在 `/home/.framely/state`,包括 `state.json`、更新源、日志、当前/上一发行链接、Steam 用户名和 Framely 账号 UID;`/var/lib/framely` 仅作兼容链接。旧安装会在停止服务后迁移。两份状态冲突时停止,不自动覆盖任何一份。
+
+系统服务和账号仍依赖 SteamOS 系统配置,操作系统更新后可能需要手动修复。在系统配置可写时运行 `sudo bash /home/.framely/repair.sh`;无需重新下载发行包,恢复兼容链接、专用账号、服务文件和开机启动。修复先校验保留的发行,再使用当前发行的 `install.sh --repair`,不创建发行或改写上一版本记录。保存的账号 UID 若被占用或现有账号 UID 已变化则停止,避免错误地接管旧数据。系统只读时停止,不自动解除保护。
+
+该机制不能恢复已被系统清除的 `/home` 数据,也不保证操作系统更新后无需人工操作。回滚到不支持修复模式的旧发行后,需要使用支持该模式的新发行包恢复安装。正式发布前应进行一次真实 SteamOS 更新,验证设置、插件数据、账号身份、服务恢复和 SteamVR 宿主兼容性。
+
+## GitHub Actions 发布
+
+Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流:
+
+- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.6`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。
+- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.1-preview.4`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。
+
+两个版本不需要相同,也不需要同时发布。修改各自版本时同步对应的 `Cargo.lock`;安装器包名及 macOS 应用版本使用安装器版本。
+
+两个工作流均缓存 Cargo 下载的依赖和已编译的依赖产物,按产品、系统、架构、Rust 工具链及依赖配置区分。安装器使用 `installer/target`;本体在 CI 使用 `target/cargo`,与单独缓存的 `target/cef` 分开,测试和发行打包共用同一 Cargo 目录。失败构建也会保存已完成的依赖编译。
+
+普通分支推送不触发发行构建;只有对应产品的标签推送或手动运行会触发。缓存恢复、编译和缓存保存均在同一次构建内完成。GitHub 允许标签构建读取默认分支的缓存,不允许一个标签读取另一个标签的专属缓存;需要跨标签复用时,可手动在 `main` 运行一次对应工作流来生成默认分支缓存,该运行只生成构建产物,不发布 Release。首次构建、没有可访问的缓存或工具链变化后仍需重新编译,缓存命中情况见 “Cache Cargo dependencies and build artifacts” 步骤。
+
+```bash
+# 提交并推送代码后,选择需要发布的产品
+git tag v0.4.2-preview.6
+git push origin v0.4.2-preview.6
+
+# 独立发布安装器,不触发 Framely 构建
+git tag installer-v0.4.1-preview.4
+git push origin installer-v0.4.1-preview.4
+```
+
+带 `-preview.N` 等预发布后缀的标签会发布为 GitHub Prerelease,不设置为 Latest。安装器 Release 始终设置为非 Latest;只有 Framely 正式 Release 设置为 Latest。因此设备安装入口和默认更新清单的 `releases/latest/download/...` 继续指向 Framely 本体。安装器读取发行列表时按本体包名筛选,不把 Linux ARM64 安装器误当成设备发行包。安装器下载应使用具体的 `installer-v<版本>` Release。
+
+只有标签触发会发布。各工作流在自己的构建全部成功后创建草稿 Release,上传本产品产物及外部 `SHA256SUMS`,最后公开草稿。Framely Release 额外包含 `bootstrap.py`、入口脚本和更新清单;安装器 Release 只包含五个平台的安装器包和校验文件。失败的草稿不作为最新稳定版提供下载。手动触发用于验证,仅保留 Actions artifacts。
+
+Framely 本体 Release 附件用途:`framely-*-linux-arm64.tar.gz` 为设备运行包;`framely-release.json` 为内置更新功能的版本清单;`install.sh` 是命令行入口,调用 `bootstrap.py` 完成下载、校验和维护;`SHA256SUMS` 为附件校验清单。桌面安装器只需自动下载运行包和校验清单,用户无需手动下载脚本。安装器的五个平台包发布在独立的安装器 Release 中。
+
+外部 `SHA256SUMS` 包含压缩包哈希,校验传输内容;包内同名文件校验解压后的载荷。哈希不能独立证明作者身份,在线安装依赖指定 GitHub 仓库与 HTTPS;本地安装依赖用户提供的可信包与校验文件。下载遵循 HTTPS 重定向,不允许降级。
+
+入口脚本从 Release 下载设备引擎;在线安装默认选择最新正式版,也支持 `--version TAG`。指定标签时,入口从该 Release 下载 `bootstrap.py`,引擎再选择同标签运行包;入口脚本本身来自用户下载或 curl 指定的位置,可在仅有 Preview、尚无正式版时安装预发布版本。本地使用 `python3 tools/bootstrap.py install --archive /path/package.tar.gz --checksums /path/SHA256SUMS`。更新要求已经安装;指定已安装版本会修复服务,指定其他版本会安装目标发行,保留状态。
+
+安装器将官方更新清单地址预配置到尚未配置更新源的设备;既有更新源保留。之后可继续通过 Framely 内置更新界面更新。
+
+## 统一卸载
+
+脚本和桌面安装器均只有一个卸载选项。`uninstall.sh` 先停止会话及下载工作,再通过 root 专用 `prepare-uninstall --approve` 请求核心服务:持久停用所有插件、按依赖顺序停止插件、执行各插件 `onUninstall`,移除插件包。全部完成后才取消 Framely 自启、停止核心并移除程序。失败时保留本体并恢复会话,插件保持停用,允许处理原因后重试;不支持跳过失败插件强制卸载本体。
+
+协议尚未确认或已撤回也允许 root 通过此专用维护接口卸载;普通会话及插件界面不开放该接口。保存的设置、插件数据和系统账号保留,不承诺清除外部副作用。旧发行没有该接口时需先更新。
diff --git a/docs/zh-CN/source-subscriptions.md b/docs/zh-CN/source-subscriptions.md
new file mode 100644
index 0000000..b4be90e
--- /dev/null
+++ b/docs/zh-CN/source-subscriptions.md
@@ -0,0 +1,26 @@
+# 插件源订阅(Framely 0.4)
+
+[English](../source-subscriptions.md)
+
+在管理窗口“插件源 → 源订阅”粘贴 HTTPS JSON 地址,预览后确认添加:
+
+```json
+{
+ "schemaVersion": 1,
+ "name": "Framely 社区源",
+ "sources": [
+ {"id":"stable", "name":"社区稳定版", "url":"https://example.org/stable/catalog.json"},
+ {"id":"testing", "name":"社区测试版", "url":"https://example.org/testing/catalog.json"}
+ ]
+}
+```
+
+每个源 id 在订阅内唯一且稳定,用于识别地址变更。禁止重复 URL、嵌套订阅、HTTP、带认证信息的 URL,最多 100 个源、512 KiB;每台设备最多 20 个订阅、200 个插件源。目录、包、图片继续由各源或作者托管,订阅只聚合目录地址。
+
+默认每天自动刷新,也可关闭自动刷新或立即刷新。自动检查在 Framely 会话运行期间进行,启动后检查到期项;支持 ETag/Last-Modified 和最多五次 HTTPS 重定向。离线/失败保留已持久化的上次成功源列表,记录错误,按一分钟起的指数退避重试,最多一天。
+
+新增源自动纳入;名称自动更新。相同 id 的 URL 改变时保留旧地址,显示待确认变更,确认后才采用新地址。刷新订阅后同时刷新插件目录。目录缓存沿用会话内缓存,重启后的离线商店不保证有目录缓存。
+
+目录 URL 规范化后去重,一个源可由多个订阅和手动添加共同引用。删除订阅或移除订阅中的源只移除对应引用;没有任何引用才从源列表移除。已安装插件、来源记录和数据保留,不静默切换来源。
+
+可在插件源列表单独停用源,刷新不会重新开启;即使移除后又加入同一 URL,也保留停用偏好。订阅源的 URL 在待确认变更中管理。刷新只更新源列表、商店资料和更新提示,不自动安装、更新或启用插件。
diff --git a/docs/zh-CN/user-guide/basic-usage.md b/docs/zh-CN/user-guide/basic-usage.md
new file mode 100644
index 0000000..e68d2a2
--- /dev/null
+++ b/docs/zh-CN/user-guide/basic-usage.md
@@ -0,0 +1,29 @@
+# 基本使用
+
+[English](../../user-guide/basic-usage.md)
+
+[文档首页](../README.md) · [安装指南](installation.md)
+
+## 快捷菜单与管理窗口
+
+在 SteamVR Dashboard 点击 Framely Dock 图标打开快捷菜单。首次访问需阅读并同意协议及隐私声明;未同意时插件不会自动启动。
+
+- **常用**:打开收藏的插件快捷页。
+- **已安装**:查看本机插件,进入插件页面。
+- **设置**:语言、安全模式和“管理插件”入口。
+
+“管理插件”打开独立大窗口,左侧导航进入插件、商店、插件源、设置和关于。安装、更新及来源管理在这里完成;电脑或手机也可使用[网络管理面板](network-panel.md)。
+
+## 插件启停与窗口
+
+在插件列表启用、停用或收藏插件。启用表示允许使用;默认后端按需启动,只有声明 `autostart` 的插件会常驻。关闭插件窗口通常不会停用后台功能。依赖不满足或有冲突时,界面会给出原因,并在变更前显示受影响插件。
+
+带 Dock 图标的独立窗口由 SteamVR 提供关闭控件;临时窗口可能在切换界面时销毁。不同插件的 UI 和功能由各自开发者决定。
+
+## 设置与诊断
+
+设置中可修改语言、网络面板及代理。HTTP 代理和 GitHub 代理各有开关,保存地址不会自动启用代理。
+
+“安全模式”暂停第三方插件,适合出现故障时检查;排查后再退出安全模式。在“关于”查看本体版本、检查更新和复查协议。撤回协议会停用全部插件,再次同意后需要手动启用。
+
+日志排查命令见[安装指南](installation.md#修复与日志)。界面能打开不代表所有插件都正常;先逐个启用,确认运行身份与来源可信。
diff --git a/docs/zh-CN/user-guide/installation.md b/docs/zh-CN/user-guide/installation.md
new file mode 100644
index 0000000..db71d94
--- /dev/null
+++ b/docs/zh-CN/user-guide/installation.md
@@ -0,0 +1,98 @@
+# 安装、更新、修复与卸载
+
+[English](../../user-guide/installation.md)
+
+[文档首页](../README.md) · [下一步:基本使用](basic-usage.md)
+
+## 安装前
+
+目标设备为 Linux ARM64 Steam Frame。当前开发基线为 SteamOS VR 0.4.2、SteamVR build 20260928.6175029;其他系统版本需验证。运行包自带 CEF,设备使用时无需安装 Node.js 或 Rust。
+
+电脑与 Frame 连接同一网络,在 Frame 开启开发者模式并启用 SSH,准备 Steam 用户 `steamos` 的登录密码。安装会使用 sudo 创建账号、systemd 服务和发行目录;请核对 SSH 指纹。安装器不会自动解除 SteamOS 只读保护;实际所需目录不可写时会停止。
+
+## 方式一:电脑端安装器
+
+1. 从 [Framely Releases](https://github.com/SteamFramelyHomebrew/framely/releases) 的 `installer-v<版本>` 下载适合电脑的安装器。Linux 为 tar.gz,Windows 为 EXE ZIP,macOS 为应用 ZIP。
+2. 解压并运行,扫描设备或手动填写设备 IP 与 SSH 端口。
+3. 核对指纹,填写 `steamos` 和密码,连接后选择 Framely 本体发行版本。
+4. 如果要安装 Preview,开启显示测试版;安装器版本与设备版版本相互独立。
+5. 在“安装与维护”确认设备、目标版本及操作,等待完成。也可选择本地运行包和对应的外部 `SHA256SUMS`。
+
+Linux 用户可在安装器解压目录执行 `bash install-desktop-entry.sh`,把带 Framely 图标的入口添加到当前用户的应用菜单。入口指向解压目录;移动目录后需重新执行脚本。
+
+安装器会校验下载内容,并在设备端再次校验。密码不保存到配置文件。连接后仍需在实际设备上观察 Framely 的启动和显示。
+
+## 方式二:在 Frame 的 SSH 终端安装
+
+以下命令在 **Frame 上** 执行。默认安装最新正式版:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install
+```
+
+指定 Preview 或其他标签:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- install --version v0.4.2-preview.3
+```
+
+将示例标签换成 Releases 中实际存在的本体标签。指定标签时从该 Release 下载引擎和包,不要求已有正式版;不指定标签时使用最新正式版。终端会按需请求 sudo 密码。
+
+## 方式三:本地发行包
+
+下载设备运行包 `framely-<版本及构建号>-linux-arm64.tar.gz`、外部 `SHA256SUMS` 和同一 Release 的 `bootstrap.py`,放入 Frame 的同一目录:
+
+```bash
+python3 bootstrap.py install --archive "./framely-<版本及构建号>-linux-arm64.tar.gz" --checksums ./SHA256SUMS
+```
+
+尖括号是占位符,执行前替换为真实文件名。引擎校验后在 `/home` 暂存、解压并检查包内哈希,避免耗尽设备的 `/tmp`。
+
+## 验证安装
+
+```bash
+systemctl is-active framely.service framely-session.service
+/var/lib/framely/current/bin/framely --version
+sudo /var/lib/framely/current/bin/framely status
+```
+
+两个服务正常应显示 `active`。打开 SteamVR Dashboard,在 Dock 找到 Framely;首次使用需阅读用户协议和隐私声明。之后按[插件安装教程](plugins.md)添加第一个插件。
+
+## 更新与回滚
+
+已配置更新服务时,从管理窗口“关于”检查、下载并确认安装。也可以用桌面安装器,或在 Frame 执行:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/SteamFramelyHomebrew/framely/main/install.sh | bash -s -- update --version v0.4.2-preview.3
+```
+
+更新保留插件、设置和数据,并保留前一个发行用于回滚;只重启 Framely。Preview 更新需要明确指定标签或本地包,默认正式版更新地址不会自动选择 Preview。
+
+需要回滚时,从“关于”选择上一版本,或执行:
+
+```bash
+sudo bash /var/lib/framely/current/rollback.sh
+```
+
+回滚本体保留插件数据,但不保证将新版本的数据格式转换为旧格式。
+
+## 修复与日志
+
+SteamOS 更新可能重置账号和系统服务。管理数据保存在 `/home/.framely/state`,`/var/lib/framely` 为兼容入口。在系统配置可写且 `/home` 数据仍存在时执行:
+
+```bash
+sudo bash /home/.framely/repair.sh
+sudo journalctl -u framely -u framely-session --no-pager -n 100
+```
+
+修复会使用已保留的发行恢复账号及服务;UID 冲突时停止。它不能找回已删除的 `/home` 数据,SteamOS 更新后的恢复仍需实机验证。
+
+## 卸载
+
+桌面安装器“安装与维护”提供卸载,也可直接执行:
+
+```bash
+sudo bash /var/lib/framely/current/uninstall.sh
+```
+
+卸载先停用并卸载全部插件、执行其卸载钩子,成功后再移除 Framely 程序和服务。某个插件清理失败会保留 Framely并报错,处理后重试。已保存的设置、插件数据和专用账号保留;不承诺自动清除第三方插件在其他位置造成的修改。
diff --git a/docs/zh-CN/user-guide/network-panel.md b/docs/zh-CN/user-guide/network-panel.md
new file mode 100644
index 0000000..d53a2fc
--- /dev/null
+++ b/docs/zh-CN/user-guide/network-panel.md
@@ -0,0 +1,37 @@
+# 网络管理面板
+
+[English](../../user-guide/network-panel.md)
+
+[文档首页](../README.md) · [插件安装](plugins.md)
+
+## 找到地址
+
+网络面板默认开启,监听 IPv4 端口 `15915`,不依赖 SteamVR 正在运行。手机/电脑与 Frame 连接同一网络,在浏览器打开:
+
+```text
+http://设备IP:15915
+```
+
+管理窗口“设置 → 网络管理面板”显示设备当前 IP 和完整地址,也可在桌面安装器连接设备后使用网络面板入口。没有检测到 IP 时先检查 Wi-Fi/网线;端口被占用时设置中会提示错误。
+
+## 首次设置密码
+
+尚未配置访问密码时,首次访问只显示设置页面,不能进入管理 API。输入至少 8 个字符的密码并再次确认,保存后使用该密码登录。密码设置可以先于用户协议确认;登录后仍需阅读并同意协议才能管理插件。
+
+密码以带随机盐的 PBKDF2 哈希保存,重启保留,不会在界面中回显。首次设置入口不能覆盖已有密码。网络面板密码和设备 SSH/sudo 密码相互独立。
+
+后续在已登录的设置页面修改密码,或从设备原生界面修改。忘记密码时优先从 Frame 的原生管理窗口设置新密码。已有版本的密码保留;用户已明确关闭密码验证且已有密码记录的配置保持其选择。网络面板使用 HTTP,请只在可信网络使用,不要把端口直接转发到公网。
+
+## 可以做什么
+
+登录后可以管理来源和订阅、浏览商店、导入本地插件包、确认安装、启停插件、修改设置及检查本体更新。插件后端始终运行在 Frame,上传本地包只是从手机/电脑把文件传到设备。
+
+VR 窗口、手柄振动和 SteamVR 虚拟键盘需要头显端验收;浏览器无法替代这些设备交互。部分外部链接由设备默认浏览器打开。
+
+## 更改端口、关闭与登录过期
+
+在“设置 → 网络管理面板”修改端口或开关,保存后生效。修改认证设置或密码会使旧登录会话失效;端口改变后访问新的地址,关闭面板后网络连接断开。本机原生界面不因此关闭。
+
+会话服务重启也会使旧登录失效,需要重新登录。保存完全相同的设置不会撤销当前会话。错误密码验证有速率限制,提示尝试过多时稍后再试。
+
+无法访问时检查设备 IP 是否变化、两台设备是否处于可互通的网络、面板是否开启以及端口是否正确。设置/密码保存在 `/home/.framely/state`,卸载本体保留这些状态,重装后可能仍需原密码。
diff --git a/docs/zh-CN/user-guide/plugins.md b/docs/zh-CN/user-guide/plugins.md
new file mode 100644
index 0000000..021fe87
--- /dev/null
+++ b/docs/zh-CN/user-guide/plugins.md
@@ -0,0 +1,56 @@
+# 安装与管理插件
+
+[English](../../user-guide/plugins.md)
+
+[文档首页](../README.md) · [网络面板](network-panel.md)
+
+## 从商店安装
+
+1. 打开管理窗口“商店”。当前代码首次初始化加入社区 stable/testing 来源;来源内容取决于服务和设备配置。没有目录或插件时,进入“插件源”添加开发者提供的完整 HTTPS `catalog.json` 地址。
+2. 可按关键词、标签、来源或安装状态筛选。
+3. 打开详情,查看作者、介绍、更新记录、运行用户和依赖。默认显示推荐版本,历史版本会按需加载。
+4. 点击安装或检查所选版本。下载、校验和依赖解析完成后,阅读确认页。
+5. 检查包内实际运行用户以及依赖带来的 root 权限,确认后安装。安装后在插件列表启用或打开。
+
+商店提供的运行用户只是目录资料,最终确认以下载包内声明为准。`steamos` 使用 Steam 会话用户,`root` 可修改系统和设备设置。包哈希用于校验内容,不等于对作者身份或安全性的担保。
+
+## 添加源与订阅
+
+单个目录地址:在“插件源”添加名称和 `https://.../catalog.json`,测试连接后保存。
+
+多个源的订阅:在“源订阅”粘贴开发者提供的订阅 JSON 地址,预览所含来源后确认添加。它和单个目录不是同一种文件。订阅刷新仅更新来源和商店资料,不自动安装或更新插件。
+
+停用或删除来源不会自动卸载已安装插件;订阅管理的 URL 变更需另行确认。格式与刷新行为见[源订阅参考](../source-subscriptions.md)。
+
+## 导入本地包或包 URL
+
+插件包扩展名为 `.framely`,无需自行解压。进入管理窗口“插件”,打开导入入口:
+
+- **本地文件**:在手机/电脑网络面板选择本机下载的 `.framely` 文件;文件上传后校验。
+- **包 URL**:粘贴作者提供的固定版本 HTTPS 下载地址,等待下载和检查。
+
+两种方式都需要阅读安装确认,涉及运行用户变更时另行确认。导入的是插件包;Framely 本体 tar.gz 和普通 ZIP 不能作为插件安装。
+
+在 Frame 的 SSH 终端也可安装:
+
+```bash
+sudo /var/lib/framely/current/bin/framely install ./plugin.framely --approve
+```
+
+只有在确认旧版到新版确实需要改变运行用户时才增加 `--approve-run-as`。命令行不会代替你判断来源是否可信。
+
+## 更新、选择旧版与卸载
+
+商店提示“可更新”时进入详情,查看变化后按安装确认流程更新。需要旧版时,在详情选择历史版本再检查;换来源也会明确提示。设置和收藏通常保留,但运行用户改变会使用另一份数据目录,不自动迁移旧身份的数据。
+
+从插件管理弹窗卸载。卸载钩子负责清理该插件的外部资源,保存的数据保留;不要直接删除系统中的插件目录。停用只停止使用,不卸载程序和数据。
+
+## 常见问题
+
+| 现象 | 处理 |
+| --- | --- |
+| 商店空白 | 检查是否添加并启用了来源、连接能否成功、筛选是否过窄 |
+| 下载或校验失败 | 检查网络和包地址,重新下载;不要跳过哈希校验 |
+| 历史版本不可用 | 来源可能没有历史文件,可先安装最新版或导入可信旧包 |
+| 插件无法启用 | 查看依赖、冲突和错误说明,确认其他必要插件已安装 |
+| 插件反复崩溃 | 停用后检查日志,必要时进入安全模式,再向插件作者反馈 |
diff --git a/examples/showcase/README.en.md b/examples/showcase/README.en.md
deleted file mode 100644
index ea0e3a4..0000000
--- a/examples/showcase/README.en.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# SDK showcase
-
-[简体中文](README.md)
-
-Demonstrates a React quick page, persistent data, separate windows, and notifications. It is a plugin development reference and is not automatically enabled when Framely is installed.
-
-Run `npm ci && npm run build` at the repository root to generate `payload/page.js` and `payload/backend.py`, then package and install as described in the [plugin development reference](../../docs/en/plugin-development.md). `payload/icon.png` is the example's source icon.
diff --git a/examples/showcase/README.md b/examples/showcase/README.md
index 2678da5..37152f0 100644
--- a/examples/showcase/README.md
+++ b/examples/showcase/README.md
@@ -1,7 +1,7 @@
-# SDK 示例
+# SDK showcase
-[English](README.en.md)
+[简体中文](README.zh-CN.md)
-展示 React 快捷页面、持久数据、独立窗口和通知。它是插件开发参考,不会随 Framely 安装自动启用。
+Demonstrates a React quick page, persistent data, separate windows, and notifications. It is a plugin development reference and is not automatically enabled when Framely is installed.
-在仓库根目录执行 `npm ci && npm run build` 生成 `payload/page.js` 和 `payload/backend.py`,然后按 [插件开发文档](../../docs/plugin-development.md) 打包和安装。`payload/icon.png` 是示例的源图标。
+Run `npm ci && npm run build` at the repository root to generate `payload/page.js` and `payload/backend.py`, then package and install as described in the [plugin development reference](../../docs/plugin-development.md). `payload/icon.png` is the example's source icon.
diff --git a/examples/showcase/README.zh-CN.md b/examples/showcase/README.zh-CN.md
new file mode 100644
index 0000000..950f6c9
--- /dev/null
+++ b/examples/showcase/README.zh-CN.md
@@ -0,0 +1,7 @@
+# SDK 示例
+
+[English](README.md)
+
+展示 React 快捷页面、持久数据、独立窗口和通知。它是插件开发参考,不会随 Framely 安装自动启用。
+
+在仓库根目录执行 `npm ci && npm run build` 生成 `payload/page.js` 和 `payload/backend.py`,然后按 [插件开发文档](../../docs/zh-CN/plugin-development.md) 打包和安装。`payload/icon.png` 是示例的源图标。
diff --git a/installer/README.en.md b/installer/README.en.md
deleted file mode 100644
index f1bdb65..0000000
--- a/installer/README.en.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Framely Installer
-
-[简体中文](README.md)
-
-A Rust + GPUI Kit desktop installer that connects to Steam Frame to install, update, repair, roll back, and uninstall Framely. The default release repository is `SteamFramelyHomebrew/framely`.
-
-## License
-
-Original installer code uses **GNU Affero General Public License v3.0 only** (`AGPL-3.0-only`). See [LICENSE](../LICENSE); release packages include it. Third-party dependencies retain their own licenses. AGPL allows commercial use and requires complete corresponding source when distributing software under its terms.
-
-Corresponding source is available in the [SteamFramelyHomebrew/framely](https://github.com/SteamFramelyHomebrew/framely) repository at `installer-v` tags. Build instructions follow below. Redistributors of modified versions must provide source and necessary build scripts matching their binaries.
-
-Only one installer instance can run per user; additional instances exit immediately. The title bar supports dragging and window controls. macOS uses system window buttons; Linux/Windows use GPUI Kit or system controls. Closing the last window exits the application and releases the instance lock.
-
-## Usage
-
-1. Enable developer mode and set a password on Frame. Connect the computer and device to the same LAN.
-2. Discovery prioritizes `192.168.*` subnets, with at most 1024 addresses per scan. Name discovery and IP probing run together. Only devices whose hostname is `frame` are listed, with SSH availability indicated. Other devices and addresses without a confirmed name are omitted. Manual IP and SSH port entry is always available.
-3. Check the SSH handshake and displayed host fingerprint, then enter credentials. The default user is `steamos`. Changed saved fingerprints cause the connection to be rejected. After verifying a device reinstall, you can remove its entry from `known-hosts.json` in the configuration directory.
-4. Select a release or a local `framely-*-linux-arm64.tar.gz` together with its external `SHA256SUMS` or matching `.sha256` file. Local packages do not require GitHub access. Prereleases are hidden by default.
-5. Choose an operation on Install & Maintenance and confirm. The installer does not disable SteamOS root filesystem read-only protection; it stops with an error when required directories are not writable.
-
-After local verification, built-in SSH/SFTP uploads the package. The device verifies it again and extracts it safely. Passwords are used only in memory and are not written to configuration, command arguments, or environment variables. Operations provide the sudo password through SSH standard input. SSH and sudo use the same entered password, matching the default Frame account configuration.
-
-Uninstall has one path: disable all plugins and run their uninstall hooks before removing Framely services and binaries. If any hook fails, Framely remains installed with plugins disabled for repair and retry. Settings, plugin data, and dedicated accounts remain. Changes plugins made elsewhere may remain. Update older releases that lack `prepare-uninstall` before uninstalling.
-
-Available operations depend on device state. An uninstalled device offers installation only. An installed device opens maintenance with update, repair, and uninstall; rollback is offered only when a previous release exists. Repair, rollback, and uninstall require no package selection. Install/update without a package returns to version selection. Selecting the exact installed package prompts repair instead of updating again. State is re-read before execution to reject stale actions. Changing devices, versions, or files clears prior confirmation, and navigation is disabled during operations.
-
-After an operation, maintenance shows a separate success/failure card and a reconnect button. Success states which operation completed; failure shows the cause. Logs remain available for diagnosis. The SSH connection and installed-version snapshot are discarded; reconnect before another operation.
-
-Scanning, downloads, and SSH work run on background threads, with progress and logs in the UI. The network panel button assumes port 15915; use the actual address if the device port has changed.
-
-## Build
-
-Linux requires C/C++ build tools, CMake, pkg-config, and development libraries for Fontconfig/Freetype, X11/XCB, XKBCommon, Wayland, and Vulkan. macOS requires Xcode command-line tools. Windows requires MSVC, CMake, and Perl for built-in OpenSSL. GPUI Kit is pinned to 0.7.0; matching dependencies are locked in `Cargo.lock`.
-
-```bash
-cargo test --manifest-path installer/Cargo.toml --locked --no-default-features --lib
-cargo check --manifest-path installer/Cargo.toml --locked
-cargo run --manifest-path installer/Cargo.toml --locked
-cargo build --manifest-path installer/Cargo.toml --locked --release
-```
-
-Set `CARGO_TARGET_DIR` to use another build-output directory; packaging also reads this variable. By default, packaging reads `installer/target/release/framely-installer`. CI requires no override.
-
-```bash
-INSTALLER_PLATFORM=linux-x64 python3 tools/package-installer.py
-```
-
-The UI shares Framely's dark surfaces, thin borders, and pale blue selection state. It has Connect device, Select version, and Install & Maintenance pages. Connection progress covers SSH, authentication, permission checks, version retrieval, and fingerprint storage. Failures show their cause and restore retry controls. Next-step buttons explain missing device/package requirements. Render simulated pages and connecting, failure, and success states without connecting to a real device:
-
-```bash
-cargo build --manifest-path installer/Cargo.toml --locked --features visual-test
-FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/framely-installer
-```
-
-Preview requires an available GPU or software Vulkan renderer. Default release builds do not include preview support.
-
-Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v` tag (currently `installer-v0.4.1-preview.4`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs.
-
-Actions builds Linux x64/ARM64, Windows x64, and macOS Intel/Apple Silicon. macOS produces an `.app` ZIP, Windows an EXE ZIP, and Linux a tar.gz. Apple notarization and Windows code signing are not configured; verify first-launch behavior on each system before publishing.
-
-The titlebar and application icons share the pixel logo. Windows builds embed the ICO in the EXE; macOS packaging uses the system `iconutil` to generate and configure `.icns`. On Linux, run `bash install-desktop-entry.sh` from the extracted archive to register an icon-bearing entry in the current user's application menu. The entry points to that directory; rerun the script after moving it.
-
-Automated tests and Actions builds validate software and artifacts. Discovery, login, permissions, update, rollback, and uninstall still need acceptance testing on actual computers and Frame hardware.
-
-Discovery queries `frame.local` mDNS address records directly. It does not require an advertised SSH service or system DNS support for `.local`. Run the same discovery logic independently for diagnosis:
-
-```bash
-cargo run --manifest-path installer/Cargo.toml --locked --no-default-features --example scan -- 192.168.5.0/24
-```
diff --git a/installer/README.md b/installer/README.md
index a2b792f..93e5196 100644
--- a/installer/README.md
+++ b/installer/README.md
@@ -1,38 +1,38 @@
# Framely Installer
-[English](README.en.md)
+[简体中文](README.zh-CN.md)
-Rust + GPUI Kit 的桌面安装器,连接 Steam Frame,安装、更新、修复、回滚和卸载 Framely。默认发行仓库为 `SteamFramelyHomebrew/framely`。
+A Rust + GPUI Kit desktop installer that connects to Steam Frame to install, update, repair, roll back, and uninstall Framely. The default release repository is `SteamFramelyHomebrew/framely`.
-## 许可证
+## License
-安装器原创代码采用 **GNU Affero General Public License v3.0 only**(`AGPL-3.0-only`)。源码仓库内的全文见 [LICENSE](../LICENSE),发行包内附带 `LICENSE`;第三方依赖保留各自的许可证。AGPL 允许商业使用,分发时须按许可证提供完整对应源码。
+Original installer code uses **GNU Affero General Public License v3.0 only** (`AGPL-3.0-only`). See [LICENSE](../LICENSE); release packages include it. Third-party dependencies retain their own licenses. AGPL allows commercial use and requires complete corresponding source when distributing software under its terms.
-对应源码位于 [SteamFramelyHomebrew/framely](https://github.com/SteamFramelyHomebrew/framely) 的 `installer-v<版本>` 标签,构建说明见下文。重新分发修改版时,须提供与所发布二进制匹配的源码及必要构建脚本。
+Corresponding source is available in the [SteamFramelyHomebrew/framely](https://github.com/SteamFramelyHomebrew/framely) repository at `installer-v` tags. Build instructions follow below. Redistributors of modified versions must provide source and necessary build scripts matching their binaries.
-同一用户只能运行一个安装器实例;重复启动会直接退出。顶部标题栏支持拖拽移动及窗口操作;macOS 使用系统窗口按钮,Linux/Windows 使用 GPUI Kit 标题栏按钮或系统提供的按钮。关闭最后一个窗口会退出程序并释放实例锁。
+Only one installer instance can run per user; additional instances exit immediately. The title bar supports dragging and window controls. macOS uses system window buttons; Linux/Windows use GPUI Kit or system controls. Closing the last window exits the application and releases the instance lock.
-## 使用
+## Usage
-1. 在 Frame 开启开发者模式并设置密码;电脑和设备连接同一局域网。
-2. 默认优先扫描 `192.168.*` 网段(每次最多 1024 个地址)。名称发现和 IP 探测同时开始;只列出主机名为 `frame` 的设备,并标注 SSH 是否可用。其他设备和无法确认名称的地址不会显示。手动 IP 和 SSH 端口始终可用。
-3. 检查 SSH 握手,核实显示的主机指纹,再输入账号密码登录。用户名默认 `steamos`。已保存的指纹发生变化时拒绝连接;核实设备重装后可手动移除配置目录中的对应 `known-hosts.json` 条目。
-4. 从 Releases 选择版本,或选择本地 `framely-*-linux-arm64.tar.gz` 和压缩包外部的 `SHA256SUMS`(也可选择对应 `.sha256` 文件)。本地包不需要访问 GitHub。默认隐藏测试版。
-5. 在“安装与维护”选择操作并确认。安装器不会解除 SteamOS 根分区的只读保护,所需目录不可写时会报错停止。
+1. Enable developer mode and set a password on Frame. Connect the computer and device to the same LAN.
+2. Discovery prioritizes `192.168.*` subnets, with at most 1024 addresses per scan. Name discovery and IP probing run together. Only devices whose hostname is `frame` are listed, with SSH availability indicated. Other devices and addresses without a confirmed name are omitted. Manual IP and SSH port entry is always available.
+3. Check the SSH handshake and displayed host fingerprint, then enter credentials. The default user is `steamos`. Changed saved fingerprints cause the connection to be rejected. After verifying a device reinstall, you can remove its entry from `known-hosts.json` in the configuration directory.
+4. Select a release or a local `framely-*-linux-arm64.tar.gz` together with its external `SHA256SUMS` or matching `.sha256` file. Local packages do not require GitHub access. Prereleases are hidden by default.
+5. Choose an operation on Install & Maintenance and confirm. The installer does not disable SteamOS root filesystem read-only protection; it stops with an error when required directories are not writable.
-本机校验完成后使用内置 SSH/SFTP 上传,设备端再次校验并安全解压。密码只在内存中使用,不写入配置文件,不作为命令参数或环境变量传输;操作通过 SSH 标准输入向 sudo 提交密码。SSH 登录和 sudo 密码使用同一输入,适用于 Frame 默认账号配置。
+After local verification, built-in SSH/SFTP uploads the package. The device verifies it again and extracts it safely. Passwords are used only in memory and are not written to configuration, command arguments, or environment variables. Operations provide the sudo password through SSH standard input. SSH and sudo use the same entered password, matching the default Frame account configuration.
-卸载只有一个入口:先停用全部插件并运行卸载钩子,全部成功后才移除 Framely 服务和程序。失败时保留本体、禁用插件,供修复后重试。保存的设置、插件数据和专用账号保留;不承诺清除插件在其他位置造成的修改。旧发行若不支持 `prepare-uninstall`,应先更新到支持此接口的新发行。
+Uninstall has one path: disable all plugins and run their uninstall hooks before removing Framely services and binaries. If any hook fails, Framely remains installed with plugins disabled for repair and retry. Settings, plugin data, and dedicated accounts remain. Changes plugins made elsewhere may remain. Update older releases that lack `prepare-uninstall` before uninstalling.
-连接成功后按设备状态提供操作:未安装时仅提供安装;已安装时默认进入维护页面,提供更新、修复与卸载;设备存在上一发行版本时才提供回滚。修复、回滚和卸载无需选择在线或本地包;安装和更新缺少包时,“选择版本”按钮会返回版本页面。选择与当前安装完全相同的包时提示使用修复,不重复更新。执行前会重新读取设备状态,拒绝过期操作;连接其他设备、切换版本或文件时清除旧确认,操作期间禁止切换页面。
+Available operations depend on device state. An uninstalled device offers installation only. An installed device opens maintenance with update, repair, and uninstall; rollback is offered only when a previous release exists. Repair, rollback, and uninstall require no package selection. Install/update without a package returns to version selection. Selecting the exact installed package prompts repair instead of updating again. State is re-read before execution to reject stale actions. Changing devices, versions, or files clears prior confirmation, and navigation is disabled during operations.
-操作结束后在维护页面顶部显示独立的成功/失败结果卡片及重新连接按钮。成功明确显示对应操作完成,失败显示原因;日志继续保留用于排查。结束后丢弃 SSH 连接及已安装版本快照,重新连接才能再次操作。
+After an operation, maintenance shows a separate success/failure card and a reconnect button. Success states which operation completed; failure shows the cause. Logs remain available for diagnosis. The SSH connection and installed-version snapshot are discarded; reconnect before another operation.
-扫描、下载和 SSH 工作在后台线程运行,界面展示进度和日志。默认网络管理面板按钮使用端口 15915;设备修改端口后应使用实际地址。
+Scanning, downloads, and SSH work run on background threads, with progress and logs in the UI. The network panel button assumes port 15915; use the actual address if the device port has changed.
-## 构建
+## Build
-Linux 需要 C/C++ 构建工具、CMake、pkg-config、Fontconfig/Freetype、X11/XCB、XKBCommon、Wayland 和 Vulkan 开发库。macOS 需要 Xcode 命令行工具;Windows 需要 MSVC、CMake 和 Perl(用于内置 OpenSSL)。GPUI Kit 固定为 0.7.0,配套依赖由 `Cargo.lock` 锁定。
+Linux requires C/C++ build tools, CMake, pkg-config, and development libraries for Fontconfig/Freetype, X11/XCB, XKBCommon, Wayland, and Vulkan. macOS requires Xcode command-line tools. Windows requires MSVC, CMake, and Perl for built-in OpenSSL. GPUI Kit is pinned to 0.7.0; matching dependencies are locked in `Cargo.lock`.
```bash
cargo test --manifest-path installer/Cargo.toml --locked --no-default-features --lib
@@ -41,30 +41,30 @@ cargo run --manifest-path installer/Cargo.toml --locked
cargo build --manifest-path installer/Cargo.toml --locked --release
```
-可配置 `CARGO_TARGET_DIR` 将构建产物放入其他目录,打包工具也会读取此设置。默认读取 `installer/target/release/framely-installer`,CI 构建时无需配置此变量。
+Set `CARGO_TARGET_DIR` to use another build-output directory; packaging also reads this variable. By default, packaging reads `installer/target/release/framely-installer`. CI requires no override.
```bash
INSTALLER_PLATFORM=linux-x64 python3 tools/package-installer.py
```
-界面采用与 Framely 一致的深色、细边框和淡蓝色选中状态,分为连接设备、选择版本、安装与维护三个页面。连接时显示建立 SSH、认证、权限检查、版本读取及指纹保存的进度;失败在页面中显示原因,并恢复按钮以便重试。未连接设备或未选择安装包时,下一步按钮说明当前缺少的条件。可用模拟数据渲染页面以及连接中、失败和成功状态的预览图,不连接真实设备:
+The UI shares Framely's dark surfaces, thin borders, and pale blue selection state. It has Connect device, Select version, and Install & Maintenance pages. Connection progress covers SSH, authentication, permission checks, version retrieval, and fingerprint storage. Failures show their cause and restore retry controls. Next-step buttons explain missing device/package requirements. Render simulated pages and connecting, failure, and success states without connecting to a real device:
```bash
cargo build --manifest-path installer/Cargo.toml --locked --features visual-test
FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/framely-installer
```
-预览依赖可用的 GPU 或软件 Vulkan 渲染器;默认发行构建不包含此预览功能。
+Preview requires an available GPU or software Vulkan renderer. Default release builds do not include preview support.
-安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.1-preview.4`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。
+Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v` tag (currently `installer-v0.4.1-preview.4`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs.
-Actions 构建 Linux x64/ARM64、Windows x64、macOS Intel/Apple Silicon。macOS 输出 `.app` ZIP,Windows 输出 EXE ZIP,Linux 输出 tar.gz。当前未配置 Apple 公证或 Windows 代码签名,发布前应分别验证系统的首次启动体验。
+Actions builds Linux x64/ARM64, Windows x64, and macOS Intel/Apple Silicon. macOS produces an `.app` ZIP, Windows an EXE ZIP, and Linux a tar.gz. Apple notarization and Windows code signing are not configured; verify first-launch behavior on each system before publishing.
-安装器标题栏和应用图标使用统一的像素 Logo。Windows 构建将 ICO 嵌入 EXE,macOS 打包使用系统 `iconutil` 生成并配置 `.icns`。Linux 解压后可在解压目录执行 `bash install-desktop-entry.sh`,把带图标的入口注册到当前用户的应用菜单;入口指向该目录,移动后需重新执行脚本。
+The titlebar and application icons share the pixel logo. Windows builds embed the ICO in the EXE; macOS packaging uses the system `iconutil` to generate and configure `.icns`. On Linux, run `bash install-desktop-entry.sh` from the extracted archive to register an icon-bearing entry in the current user's application menu. The entry points to that directory; rerun the script after moving it.
-所有自动测试和 Actions 构建只能验证软件及产物;仍需在真实电脑和 Frame 上完成扫描、登录、权限、升级回滚及卸载验收。
+Automated tests and Actions builds validate software and artifacts. Discovery, login, permissions, update, rollback, and uninstall still need acceptance testing on actual computers and Frame hardware.
-扫描会直接查询 `frame.local` 的 mDNS 地址记录,不要求设备广播 SSH 服务,也不依赖电脑的系统 DNS 支持 `.local`。可单独运行与界面相同的发现逻辑进行诊断:
+Discovery queries `frame.local` mDNS address records directly. It does not require an advertised SSH service or system DNS support for `.local`. Run the same discovery logic independently for diagnosis:
```bash
cargo run --manifest-path installer/Cargo.toml --locked --no-default-features --example scan -- 192.168.5.0/24
diff --git a/installer/README.zh-CN.md b/installer/README.zh-CN.md
new file mode 100644
index 0000000..aa61fcd
--- /dev/null
+++ b/installer/README.zh-CN.md
@@ -0,0 +1,71 @@
+# Framely Installer
+
+[English](README.md)
+
+Rust + GPUI Kit 的桌面安装器,连接 Steam Frame,安装、更新、修复、回滚和卸载 Framely。默认发行仓库为 `SteamFramelyHomebrew/framely`。
+
+## 许可证
+
+安装器原创代码采用 **GNU Affero General Public License v3.0 only**(`AGPL-3.0-only`)。源码仓库内的全文见 [LICENSE](../LICENSE),发行包内附带 `LICENSE`;第三方依赖保留各自的许可证。AGPL 允许商业使用,分发时须按许可证提供完整对应源码。
+
+对应源码位于 [SteamFramelyHomebrew/framely](https://github.com/SteamFramelyHomebrew/framely) 的 `installer-v<版本>` 标签,构建说明见下文。重新分发修改版时,须提供与所发布二进制匹配的源码及必要构建脚本。
+
+同一用户只能运行一个安装器实例;重复启动会直接退出。顶部标题栏支持拖拽移动及窗口操作;macOS 使用系统窗口按钮,Linux/Windows 使用 GPUI Kit 标题栏按钮或系统提供的按钮。关闭最后一个窗口会退出程序并释放实例锁。
+
+## 使用
+
+1. 在 Frame 开启开发者模式并设置密码;电脑和设备连接同一局域网。
+2. 默认优先扫描 `192.168.*` 网段(每次最多 1024 个地址)。名称发现和 IP 探测同时开始;只列出主机名为 `frame` 的设备,并标注 SSH 是否可用。其他设备和无法确认名称的地址不会显示。手动 IP 和 SSH 端口始终可用。
+3. 检查 SSH 握手,核实显示的主机指纹,再输入账号密码登录。用户名默认 `steamos`。已保存的指纹发生变化时拒绝连接;核实设备重装后可手动移除配置目录中的对应 `known-hosts.json` 条目。
+4. 从 Releases 选择版本,或选择本地 `framely-*-linux-arm64.tar.gz` 和压缩包外部的 `SHA256SUMS`(也可选择对应 `.sha256` 文件)。本地包不需要访问 GitHub。默认隐藏测试版。
+5. 在“安装与维护”选择操作并确认。安装器不会解除 SteamOS 根分区的只读保护,所需目录不可写时会报错停止。
+
+本机校验完成后使用内置 SSH/SFTP 上传,设备端再次校验并安全解压。密码只在内存中使用,不写入配置文件,不作为命令参数或环境变量传输;操作通过 SSH 标准输入向 sudo 提交密码。SSH 登录和 sudo 密码使用同一输入,适用于 Frame 默认账号配置。
+
+卸载只有一个入口:先停用全部插件并运行卸载钩子,全部成功后才移除 Framely 服务和程序。失败时保留本体、禁用插件,供修复后重试。保存的设置、插件数据和专用账号保留;不承诺清除插件在其他位置造成的修改。旧发行若不支持 `prepare-uninstall`,应先更新到支持此接口的新发行。
+
+连接成功后按设备状态提供操作:未安装时仅提供安装;已安装时默认进入维护页面,提供更新、修复与卸载;设备存在上一发行版本时才提供回滚。修复、回滚和卸载无需选择在线或本地包;安装和更新缺少包时,“选择版本”按钮会返回版本页面。选择与当前安装完全相同的包时提示使用修复,不重复更新。执行前会重新读取设备状态,拒绝过期操作;连接其他设备、切换版本或文件时清除旧确认,操作期间禁止切换页面。
+
+操作结束后在维护页面顶部显示独立的成功/失败结果卡片及重新连接按钮。成功明确显示对应操作完成,失败显示原因;日志继续保留用于排查。结束后丢弃 SSH 连接及已安装版本快照,重新连接才能再次操作。
+
+扫描、下载和 SSH 工作在后台线程运行,界面展示进度和日志。默认网络管理面板按钮使用端口 15915;设备修改端口后应使用实际地址。
+
+## 构建
+
+Linux 需要 C/C++ 构建工具、CMake、pkg-config、Fontconfig/Freetype、X11/XCB、XKBCommon、Wayland 和 Vulkan 开发库。macOS 需要 Xcode 命令行工具;Windows 需要 MSVC、CMake 和 Perl(用于内置 OpenSSL)。GPUI Kit 固定为 0.7.0,配套依赖由 `Cargo.lock` 锁定。
+
+```bash
+cargo test --manifest-path installer/Cargo.toml --locked --no-default-features --lib
+cargo check --manifest-path installer/Cargo.toml --locked
+cargo run --manifest-path installer/Cargo.toml --locked
+cargo build --manifest-path installer/Cargo.toml --locked --release
+```
+
+可配置 `CARGO_TARGET_DIR` 将构建产物放入其他目录,打包工具也会读取此设置。默认读取 `installer/target/release/framely-installer`,CI 构建时无需配置此变量。
+
+```bash
+INSTALLER_PLATFORM=linux-x64 python3 tools/package-installer.py
+```
+
+界面采用与 Framely 一致的深色、细边框和淡蓝色选中状态,分为连接设备、选择版本、安装与维护三个页面。连接时显示建立 SSH、认证、权限检查、版本读取及指纹保存的进度;失败在页面中显示原因,并恢复按钮以便重试。未连接设备或未选择安装包时,下一步按钮说明当前缺少的条件。可用模拟数据渲染页面以及连接中、失败和成功状态的预览图,不连接真实设备:
+
+```bash
+cargo build --manifest-path installer/Cargo.toml --locked --features visual-test
+FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/framely-installer
+```
+
+预览依赖可用的 GPU 或软件 Vulkan 渲染器;默认发行构建不包含此预览功能。
+
+安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.1-preview.4`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。
+
+Actions 构建 Linux x64/ARM64、Windows x64、macOS Intel/Apple Silicon。macOS 输出 `.app` ZIP,Windows 输出 EXE ZIP,Linux 输出 tar.gz。当前未配置 Apple 公证或 Windows 代码签名,发布前应分别验证系统的首次启动体验。
+
+安装器标题栏和应用图标使用统一的像素 Logo。Windows 构建将 ICO 嵌入 EXE,macOS 打包使用系统 `iconutil` 生成并配置 `.icns`。Linux 解压后可在解压目录执行 `bash install-desktop-entry.sh`,把带图标的入口注册到当前用户的应用菜单;入口指向该目录,移动后需重新执行脚本。
+
+所有自动测试和 Actions 构建只能验证软件及产物;仍需在真实电脑和 Frame 上完成扫描、登录、权限、升级回滚及卸载验收。
+
+扫描会直接查询 `frame.local` 的 mDNS 地址记录,不要求设备广播 SSH 服务,也不依赖电脑的系统 DNS 支持 `.local`。可单独运行与界面相同的发现逻辑进行诊断:
+
+```bash
+cargo run --manifest-path installer/Cargo.toml --locked --no-default-features --example scan -- 192.168.5.0/24
+```
diff --git a/templates/plugin/README.en.md b/templates/plugin/README.en.md
deleted file mode 100644
index e2db764..0000000
--- a/templates/plugin/README.en.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Framely plugin template
-
-[简体中文](README.md)
-
-This template includes a React quick page, a separate Dock window, notifications, a Python JSON RPC backend, and persistent storage. It runs as `steamos` by default and does not request root.
-
-Generate a standalone project from the Framely repository. The scaffolder copies the SDK, licenses, and build tools:
-
-```bash
-node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
-cd ../my-plugin
-npm install
-npm run dev
-npm run build
-```
-
-Edit the ID, name, author, description, version, tags, and update notes in `manifest.json`. Page source is in `page.tsx`, backend source in `backend.py`, and build output in `payload/`. Leave `files` as an empty object; the packager calculates it.
-
-Browser preview is available at `http://127.0.0.1:5173` and simulates windows and notifications. Read/save buttons call the actual backend and require installation on Frame for testing; the browser reports that preview does not start a backend.
-
-On Linux/WSL with Rust, set the Framely repository path, then package and verify (the core CLI uses Linux-specific interfaces):
-
-```bash
-FRAMELY_REPO=/absolute/path/to/framely
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest manifest.json --payload payload --output my-plugin-0.1.0.framely
-cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify my-plugin-0.1.0.framely
-```
-
-Upload the package to Frame using Plugins → Import plugin in the network panel. Review the runtime user and confirm. For updates, increment the version, rebuild, and package again. Store backend data in `FRAMELY_DATA_DIR`, not in the program directory.
-
-To edit this template inside the Framely repository, run `npm install`, `npm run dev`, and `npm run build` in `templates/plugin/`. Copying only this directory elsewhere requires adjusting SDK and build-tool paths in `package.json`; the scaffolder handles this automatically.
-
-The template and bundled SDK use `AGPL-3.0-only`. See Framely's `docs/en/developer-guide/README.md` for the full development and publishing workflow. The template lifecycle callbacks preserve user data.
-
-For community GitHub registration, source may omit `downloadUrl`; default assets use `-.framely`. Release packages need a temporary Manifest with the derived URL. See Framely's `docs/en/developer-guide/publishing.md` for commands, Release digests, custom URLs and database window-dimension validation. The direct packaging command above is for core testing and does not establish community registration acceptance.
diff --git a/templates/plugin/README.md b/templates/plugin/README.md
index a21e884..e17d132 100644
--- a/templates/plugin/README.md
+++ b/templates/plugin/README.md
@@ -1,10 +1,10 @@
-# Framely 插件模板
+# Framely plugin template
-[English](README.en.md)
+[简体中文](README.zh-CN.md)
-模板包含 React 快捷页、独立 Dock 窗口、通知、Python JSON RPC 后端和持久数据示例。默认使用 `steamos` 身份,不申请 root。
+This template includes a React quick page, a separate Dock window, notifications, a Python JSON RPC backend, and persistent storage. It runs as `steamos` by default and does not request root.
-推荐从 Framely 仓库生成独立项目,脚手架会复制 SDK、许可证和构建工具:
+Generate a standalone project from the Framely repository. The scaffolder copies the SDK, licenses, and build tools:
```bash
node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
@@ -14,11 +14,11 @@ npm run dev
npm run build
```
-生成后修改 `manifest.json` 的 ID、名称、作者、描述、版本、标签及更新记录。页面源码在 `page.tsx`,后端在 `backend.py`,构建结果在 `payload/`。`files` 由打包工具计算,保持空对象即可。
+Edit the ID, name, author, description, version, tags, and update notes in `manifest.json`. Page source is in `page.tsx`, backend source in `backend.py`, and build output in `payload/`. Leave `files` as an empty object; the packager calculates it.
-浏览器预览为 `http://127.0.0.1:5173`,只模拟窗口和通知。读取/保存按钮调用真实后端,必须安装到 Frame 测试;浏览器中会显示“预览不启动后端”的提示。
+Browser preview is available at `http://127.0.0.1:5173` and simulates windows and notifications. Read/save buttons call the actual backend and require installation on Frame for testing; the browser reports that preview does not start a backend.
-在安装了 Rust 的 Linux/WSL 环境设置 Framely 仓库位置,打包并验证(本体 CLI 依赖 Linux 接口):
+On Linux/WSL with Rust, set the Framely repository path, then package and verify (the core CLI uses Linux-specific interfaces):
```bash
FRAMELY_REPO=/absolute/path/to/framely
@@ -26,10 +26,10 @@ cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest
cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify my-plugin-0.1.0.framely
```
-把包上传到 Frame,在网络管理面板“插件 → 导入插件”选择本地文件,检查运行用户后确认。升级时递增版本号、重新构建打包;后端数据保存在 `FRAMELY_DATA_DIR`,不要写到程序目录。
+Upload the package to Frame using Plugins → Import plugin in the network panel. Review the runtime user and confirm. For updates, increment the version, rebuild, and package again. Store backend data in `FRAMELY_DATA_DIR`, not in the program directory.
-如果直接在 Framely 仓库内编辑本模板,在 `templates/plugin/` 执行 `npm install`、`npm run dev`、`npm run build`。直接复制模板目录到其他位置时,需同时调整 `package.json` 的 SDK/构建工具路径;使用脚手架可自动完成这些步骤。
+To edit this template inside the Framely repository, run `npm install`, `npm run dev`, and `npm run build` in `templates/plugin/`. Copying only this directory elsewhere requires adjusting SDK and build-tool paths in `package.json`; the scaffolder handles this automatically.
-模板及随附 SDK 使用 `AGPL-3.0-only`。完整开发到发布说明见 Framely 的 `docs/developer-guide/README.md`;模板的生命周期回调不会删除用户数据。
+The template and bundled SDK use `AGPL-3.0-only`. See Framely's `docs/developer-guide/README.md` for the full development and publishing workflow. The template lifecycle callbacks preserve user data.
-用于社区 GitHub Release 登记时,源码可省略 `downloadUrl`,默认附件名为 `-.framely`。发布包要用包含自动生成地址的临时清单打包;完整命令、Release digest 校验、自定义地址和数据库窗口尺寸校验规则见 Framely 的 `docs/developer-guide/publishing.md`。上面的直接打包命令用于本体测试,不代表社区登记已经通过。
+For community GitHub registration, source may omit `downloadUrl`; default assets use `-.framely`. Release packages need a temporary Manifest with the derived URL. See Framely's `docs/developer-guide/publishing.md` for commands, Release digests, custom URLs and database window-dimension validation. The direct packaging command above is for core testing and does not establish community registration acceptance.
diff --git a/templates/plugin/README.zh-CN.md b/templates/plugin/README.zh-CN.md
new file mode 100644
index 0000000..cb44cb3
--- /dev/null
+++ b/templates/plugin/README.zh-CN.md
@@ -0,0 +1,35 @@
+# Framely 插件模板
+
+[English](README.md)
+
+模板包含 React 快捷页、独立 Dock 窗口、通知、Python JSON RPC 后端和持久数据示例。默认使用 `steamos` 身份,不申请 root。
+
+推荐从 Framely 仓库生成独立项目,脚手架会复制 SDK、许可证和构建工具:
+
+```bash
+node tools/plugin-dev.mjs init ../my-plugin yourname.my-plugin
+cd ../my-plugin
+npm install
+npm run dev
+npm run build
+```
+
+生成后修改 `manifest.json` 的 ID、名称、作者、描述、版本、标签及更新记录。页面源码在 `page.tsx`,后端在 `backend.py`,构建结果在 `payload/`。`files` 由打包工具计算,保持空对象即可。
+
+浏览器预览为 `http://127.0.0.1:5173`,只模拟窗口和通知。读取/保存按钮调用真实后端,必须安装到 Frame 测试;浏览器中会显示“预览不启动后端”的提示。
+
+在安装了 Rust 的 Linux/WSL 环境设置 Framely 仓库位置,打包并验证(本体 CLI 依赖 Linux 接口):
+
+```bash
+FRAMELY_REPO=/absolute/path/to/framely
+cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- pack --manifest manifest.json --payload payload --output my-plugin-0.1.0.framely
+cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify my-plugin-0.1.0.framely
+```
+
+把包上传到 Frame,在网络管理面板“插件 → 导入插件”选择本地文件,检查运行用户后确认。升级时递增版本号、重新构建打包;后端数据保存在 `FRAMELY_DATA_DIR`,不要写到程序目录。
+
+如果直接在 Framely 仓库内编辑本模板,在 `templates/plugin/` 执行 `npm install`、`npm run dev`、`npm run build`。直接复制模板目录到其他位置时,需同时调整 `package.json` 的 SDK/构建工具路径;使用脚手架可自动完成这些步骤。
+
+模板及随附 SDK 使用 `AGPL-3.0-only`。完整开发到发布说明见 Framely 的 `docs/zh-CN/developer-guide/README.md`;模板的生命周期回调不会删除用户数据。
+
+用于社区 GitHub Release 登记时,源码可省略 `downloadUrl`,默认附件名为 `-.framely`。发布包要用包含自动生成地址的临时清单打包;完整命令、Release digest 校验、自定义地址和数据库窗口尺寸校验规则见 Framely 的 `docs/zh-CN/developer-guide/publishing.md`。上面的直接打包命令用于本体测试,不代表社区登记已经通过。
diff --git a/tools/package-installer.py b/tools/package-installer.py
index 181b6d6..695802e 100644
--- a/tools/package-installer.py
+++ b/tools/package-installer.py
@@ -45,7 +45,7 @@ with __import__('tempfile').TemporaryDirectory() as temp:
for asset in ('framely-logo.svg', 'framely-logo-light.svg', 'framely-app-icon.svg', 'framely-app-icon.png', 'framely.ico'):
shutil.copy2(root / 'assets/branding' / asset, stage / 'assets/branding' / asset)
shutil.copy2(root / 'installer/README.md', stage / 'README.md')
- shutil.copy2(root / 'installer/README.en.md', stage / 'README.en.md')
+ shutil.copy2(root / 'installer/README.zh-CN.md', stage / 'README.zh-CN.md')
shutil.copy2(root / 'LICENSE', stage / 'LICENSE')
name = f'framely-installer-{version}-{platform}'
if platform.startswith('linux'):
diff --git a/tools/package-release.sh b/tools/package-release.sh
index 18640c4..45213fe 100755
--- a/tools/package-release.sh
+++ b/tools/package-release.sh
@@ -11,7 +11,7 @@ npm run build
bash tools/build-native.sh "$cef"
binary=${CARGO_TARGET_DIR:-target}/release/framely
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
-build_id="$version-$(find "$binary" target/native ui/dist sdk templates/plugin examples/showcase packaging tools docs assets/branding README.md README.en.md LICENSE -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c1-12)"
+build_id="$version-$(find "$binary" target/native ui/dist sdk templates/plugin examples/showcase packaging tools docs assets/branding README.md README.zh-CN.md LICENSE -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c1-12)"
stage="$base/release/framely-$build_id"
[[ ! -e $stage ]] || { echo 'Release already exists.' >&2; exit 1; }
mkdir -p "$stage"/{bin,lib/cef,lib/openvr,share/ui,share/licenses,tools}
@@ -29,7 +29,7 @@ cp native/vendor/openvr/LICENSE "$stage/share/licenses/openvr.txt"
[[ ! -f $cef/CREDITS.html ]] || cp "$cef/CREDITS.html" "$stage/share/licenses/cef-credits.html"
cp native/vendor/json.hpp "$stage/share/licenses/nlohmann-json.hpp"
cp packaging/*.sh "$stage/"
-cp README.md README.en.md "$stage/"
+cp README.md README.zh-CN.md "$stage/"
mkdir -p "$stage/assets"
cp -a assets/branding "$stage/assets/"
cp LICENSE "$stage/LICENSE"