---
title: ".bar 格式规范：v0 看板标准 · Metrix.Bar"
description: "Metrix.Bar .bar v0 格式完整参考：根标签 <bar>、网络权限、配置项、常驻 JavaScript Worker、生命周期钩子、宿主 API 与原生 UI 树。"
canonical: "https://metrix.bar/zh/docs/bar-format"
language: "zh-CN"
---

# .bar 格式规范

> Metrix.Bar 单文件 .bar v0 看板规格说明。

## 概述

`.bar` 文件是用于定义单个完整看板的声明式规范。单个文件整合了网络权限声明、用户配置项定义以及一个常驻的 JavaScript Worker，由 Worker 负责数据抓取、状态维护与原生托盘及面板界面的渲染。

- **单文件定义**：无需分离的布局文件或样式配置，一个文件即为一个独立看板。
- **常驻 Worker**：基于 ES Module 标准常驻后台运行，支持定时调度、本地持久化与异步取数。
- **原生 UI 树**：Worker 直接返回结构轻量的 JSON 树（`{type, props, children}`），直接映射为桌面原生组件。
- **ESM 库引入**：支持从白名单内的 CDN 动态导入 JavaScript 模块（如 `htm`）。

## 文件结构

`.bar` 文档以根标签 `<bar protocol-version="0">` 开头，包含声明权限、输入项、配置说明与脚本 Worker 的子块：

```bar
<bar protocol-version="0" version="1.0.0" icon="chart.bar" description="Website traffic metrics" homepage="https://agili.app" id="unique-id" title="看板标题" refresh="5m" material="glass">
  <permissions>
    <network origin="https://api.example.com" />
  </permissions>

  <inputs>
    <input name="endpoint" type="string" label="API 接口" default="https://api.example.com" />
  </inputs>

  <docs><![CDATA[配置 API 接口地址以开始收集数据。]]></docs>

  <script>
  let cache = null;

  export default {
    async scheduled(env) {
      const res = await fetch(`${env.inputs.endpoint}/stats`);
      if (res.ok) {
        cache = await res.json();
      }
    },
    render(env) {
      return {
        tray: {
          title: cache ? `${cache.count} 次` : "加载中..."
        },
        panel: {
          type: "Column",
          props: { padding: 16, gap: 12 },
          children: [
            { type: "Text", props: { bold: true }, children: ["服务状态"] },
            { type: "Text", children: [cache ? `活跃数：${cache.count}` : "等待数据..."] }
          ]
        }
      };
    }
  };
  </script>
</bar>
```

| 元素            | 是否必填 | 格式               | 用途                                                                                                |
| --------------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------- |
| `<bar …>`       | 是       | 属性声明           | 根定义：`protocol-version`、`id`、可选元数据以及刷新间隔 `refresh`                                  |
| `<permissions>` | 否       | XML 标签           | 声明网络权限（`fetch`、动态 `import`）与本地文件读取权限（`$metrix.readFile`）                      |
| `<inputs>`      | 否       | XML `<input>` 标签 | 声明看板供用户配置的参数与安全密钥                                                                  |
| `<docs>`        | 否       | 纯文本 / CDATA     | 供用户查看的纯文本配置与使用说明，展示在目录、应用界面与 API 中                                     |
| `<script …>`    | 是       | 纯 JavaScript      | 常驻 ES Module Worker，可选 `apis` 属性声明扩展系统能力，实现 `scheduled`、`render` 与 `event` 钩子 |

---

## `<bar>` 根元素

根元素声明看板的基础元信息：

| 属性               | 类型     | 必填   | 描述                                                                                                                                                     |
| ------------------ | -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocol-version` | 字符串   | **是** | 协议版本，必须为 `"0"`。                                                                                                                                 |
| `material`         | 字符串   | 否     | 面板内容材质：`"glass"`（缺省）或 `"solid"`；仅接受精确小写值，空串非法；不改变 Dialog 遮罩。                                                            |
| `placements`       | 字符串   | 否     | 作者默认入口：空白分隔的 `menubar`、`sidebar-left`、`sidebar-right`、`sidebar-bottom` 任意集合；省略为 `menubar`，空串禁用全部入口，未知值或重复值报错。 |
| `open-mode`        | 字符串   | 否     | 作者默认打开方式：`flyout`（缺省）或 `dialog`；空串及其他值报错。                                                                                        |
| `version`          | 字符串   | 否     | 看板代码版本，如 `"1.0.0"`，与 App 版本无关。                                                                                                            |
| `icon`             | 字符串   | 否     | 图片引用：URL、`data:` URI 或命名图标。                                                                                                                  |
| `description`      | 字符串   | 否     | 看板描述。                                                                                                                                               |
| `license`          | 字符串   | 否     | 许可证声明，如 SPDX 标识；缺省不授予开源许可。                                                                                                           |
| `homepage`         | 字符串   | 否     | 开发者主页。                                                                                                                                             |
| `category`         | 字符串   | 否     | 看板源中的分组 slug（`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`），缺省为 `other`。                                                                                  |
| `id`               | 字符串   | **是** | 看板唯一标识符，支持字母、数字、连字符与下划线。                                                                                                         |
| `title`            | 字符串   | **是** | 在托盘菜单与窗口中展示的看板标题。                                                                                                                       |
| `refresh`          | 时间间隔 | 否     | 定时刷新周期。默认为 `5m`，最小值为 `10s`。                                                                                                              |

macOS 托盘的两种材质均保留原生 NSPopover 气泡箭头、圆角和阴影；材质由 Host 外壳管理，Worker 仅负责内部内容。macOS 14 及以上的 solid 外壳（含箭头）使用不透明主题 Surface；macOS 12/13 的 solid 正文不透明，箭头保留系统材质。其他平台保持既有浮窗样式。

仅 `protocol-version` 参与协议支持性校验；旧的 `version="0"` 不能代替协议声明，缺少 `protocol-version` 时会被拒绝。描述性元数据按字符串保存；`material`、`category`、`placements` 和 `open-mode` 按上表校验。主窗口与看板目录使用 `icon` 展示图标：URL 与命名图标异步加载并缓存，`data:` URI 在本地解码。托盘使用 `render` 返回的 `tray.icon`。官方看板源 `/dashboards/index.json` 分发官方 `.bar` 源文件（每个均声明 `license="AGPL-3.0-only"`）。App 仅内置 GitHub 仓库看板，其余官方看板均由看板源安装——安装时短 ID 会改写为[复合安装 ID](/zh/docs/registries#安装-id)。本地 API 的列表与创建摘要返回 `protocol_version` 和非空元数据；公开看板索引还包含 `id` 并保留 `name`、`file`。

例如，`placements="sidebar-bottom" open-mode="dialog"` 将底部屏幕 Sidebar 设为默认入口，并以 Dialog 打开面板。`.bar` 根标签仍仅使用 `open-mode` 声明作者默认打开方式，不增加按来源配置的 XML 字段。所有官方和第三方看板均支持全部四种视觉入口与独立快捷键，无需逐个声明支持列表。用户可在设置中为菜单栏、三侧 Sidebar 与快捷键全部五种触发来源分别选择 Flyout 或 Dialog（例如菜单栏打开 Flyout，快捷键与右侧 Sidebar 打开 Dialog）。用户覆盖在源码更新后保留；未覆盖的设置随作者默认值更新。

多个入口共用一个 Worker、同一面板状态和既有 `render` 返回值 `{ tray, panel }`。Sidebar 仅显示图标，依次使用 `tray.icon`、`bar.icon`、通用看板 glyph；提示文字依次使用 `tray.tooltip`、`tray.title`、`bar.title`，指标数值仍由看板脚本格式化。边缘触发、Dialog 交互与平台限制见[看板入口与打开方式](/zh/docs/dashboards#看板入口与打开方式)。

公开看板索引以 `placements` 数组和 `open_mode` 字符串提供作者默认值，不包含用户设置。

**周期格式**：数字后接 `s`（秒）、`m`（分）、`h`（小时）或 `d`（天），例如 `30s`、`5m`、`1h`。

---

## `<permissions>` 权限声明

声明 Worker 运行时允许访问的网络权限与本地文件权限。未在权限中声明的网络请求、动态 ESM 导入与本地文件读取将被宿主环境拦截：

```xml
<permissions>
  <network origin="https://api.github.com" />
  <network origin="https://esm.sh" />
  <file path="{{ inputs.auth }}" />
  <file path="~/.claude/projects/" />
</permissions>
```

### 网络权限（`<network>`）

- 每个 `<network origin="..." />` 声明允许请求的网络源，必须包含协议和域名（例如 `https://domain.com`）。
- 若使用非默认端口，需要显式指定端口号（例如 `http://localhost:8080`）。
- 支持 `{{ inputs.x }}` 模板变量展开（展开后为空的规则自动忽略）。

### 本地文件权限（`<file>`）

读取本机文件必须在 `<permissions>` 中显式声明：

- 每个 `<file path="..." />` 声明一项文件或目录读取授权，可重复出现，`path` 属性必填。
- **路径展开**：支持 `~` 展开为当前用户主目录，支持 `{{ inputs.x }}` 模板变量展开；展开后为空的规则自动忽略；展开后的结果必须是绝对路径。
- **目录与单文件**：路径以 `/` 结尾表示授权整个目录（包含所有子目录及文件）；不以 `/` 结尾则仅允许访问该指定文件。

---

### 外部凭据（`<credential>`）

声明 `<credential service="Example App" account="{{ user.name }}" />` 后，可用 `await $metrix.readCredential("Example App", $metrix.userName)` 读取系统凭据。每项声明只授权精确的 service/account 二元组，不可跨声明组合。两属性必填，单项上限 256 字节，不得为空白或含 NUL。service 不支持模板，不得含 `/` 或 `\`；account 仅支持静态值或完整的 `{{ user.name }}`。旧 `source` 格式不再支持。

`$metrix.userName` 在 Worker 初始化时捕获当前宿主用户名，优先使用 `USER`，否则使用系统用户名。服务对应的账号规则、兜底与凭据读取顺序由看板决定。宿主通过 MyGO 的平台凭据存储后台读取，系统可能要求用户授权；条目不存在、拒绝访问或平台存储不可用均拒绝 Promise，不透传原始系统错误。返回 UTF-8 字符串，上限 1 MiB，不复制到 Metrix.Bar 的凭据存储。不区分大小写禁止以 `bar.metrix` 开头的 service，保护 App 私有凭据与实例 secret。

fixture 使用自身的 `user_name`（缺省 `fixture-user`），`credentials` 为 `{service, account, value}` 对象数组，按二元组精确匹配，缺失时失败且不回退到真实系统。

## `<inputs>` 参数配置

`<inputs>` 包含描述配置表单项的 XML `<input>` 声明：

```xml
<inputs>
  <input name="repo" type="string" label="仓库" placeholder="owner/repo" default="egoist/mygo" required="true" />
  <input name="token" type="secret" label="访问令牌" help="具有读取权限的 GitHub Token。" />
</inputs>
```

### 支持的数据类型

| 类型      | 说明                                                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `string`  | 单行文本输入                                                                                                                                                              |
| `integer` | 整数输入                                                                                                                                                                  |
| `number`  | 浮点数输入                                                                                                                                                                |
| `bool`    | 布尔开关复选框                                                                                                                                                            |
| `secret`  | 敏感密钥，安全保存在系统安全凭据库中（如 macOS Keychain）                                                                                                                 |
| `file`    | 本地文件路径选择器。供用户选择或修改文件路径，Worker 通过 `env.inputs.<名字>` 获取路径传给 `$metrix.readFile(path)`，需通过 `<file path="{{ inputs.<名字> }}"/>` 声明权限 |
| `select`  | 下拉选择器，配合子元素 `<option value="..." label="..." />` 声明使用                                                                                                      |

### 常见字段属性

- `name` _(字符串, 必填)_：变量名，在代码中通过 `env.inputs[name]` 获取。
- `type` _(字符串, 必填)_：上述支持的类型之一。
- `label` _(字符串)_：表单中展示的标签文字。
- `default` _(任意)_：初始默认值。
- `required` _(布尔值)_：是否为必填项。
- `placeholder` _(字符串)_：空输入框的占位提示。
- `help` _(字符串)_：辅助说明文案。

---

## `<docs>` 配置说明

可选 `<docs>` 元素提供指导用户完成配置与凭据设置的纯文本说明：

```xml
<docs><![CDATA[需要在管理后台获取 API Key，并填写至上方 API Key 输入框中。]]></docs>
```

- **纯文本 / CDATA**：可直接书写纯文本，或用 `<![CDATA[ ... ]]>` 包裹以避免 XML 特殊字符转义。
- **空白处理**：解析器会自动去除首尾空白（`strings.TrimSpace`），同时完整保留内部换行与缩进。
- **严格约束**：不得包含任何属性，不得包含嵌套 XML 元素。不进行 Markdown 渲染，内容仅作为提示信息展示。
- **不执行命令**：`<docs>` 内的代码或命令文本仅供用户参考阅读，宿主应用与 AI Agent 严禁自动执行其中的任何命令。
- **全链路流转**：说明信息随 `.bar` 定义自动打包进注册表 `index.json`（`docs` 字段），并展示在官网目录、桌面端应用界面（看板详情、安装确认抽屉与输入配置抽屉）以及本地 API 的摘要和快照中。缺失或为空时不展示对应板块。

---

## `<script>` Worker 运行时与生命周期

`<script>` 块运行在内存中持续常驻的 Worker 风格 ES Module 环境中，拥有专属的 `scheduled`、`render` 与 `event` 生命周期：

- **全局环境**：全局对象为 `self` / `globalThis`，**没有** `window` 或 `document`。模块级变量在多次渲染间保持状态，同时可使用 `localStorage` 进行跨生命周期持久化。
- **标准与扩展边界**：提供 Web 标准 API 的精简子集（如 `fetch`、streams、`TextEncoder` 等）。`localStorage`、`matchMedia`、`screen`、`navigator.getBattery` 与 `navigator.clipboard` 属于 Window 式 opt-in 扩展；桌面专有能力通过 `$metrix` 提供（基础方法如 `log`、`invalidate`、`appVersion` 等直接可用，系统级或敏感调用按各接口要求声明对应 `apis` 或 `<permissions>`）。定时器句柄返回 Node 风格对象并提供 `Buffer`，但这些非 Web 标准；不要假定环境具备完整 Worker 或 Node.js 兼容性。
- **高精度时间**：`performance.now()` 返回浮点单调毫秒数；`performance.timeOrigin` 为 Worker 创建时的 Unix 毫秒时间戳。
- **扩展快照与受限语义**：
  - `Notification`（需 `apis="notification"`）：提供桌面通知基础投递（支持 `title`、`body`、`tag`），不提供 `permission`、`requestPermission()`、`close()` 及点击 / 错误事件。
  - `navigator.getBattery()`（需 `apis="battery"`）：仅提供充电状态快照（`charging: boolean`），`level` 固定为 `1`（占位），不提供真实电量百分比或变化事件。
  - `screen`（需 `apis="screen"`）：为 Worker 启动时捕获的主显示器静态快照（`width`、`height`、`availWidth`、`availHeight`、`colorDepth`、`pixelDepth`）；最新多显示器或指针查询需使用 `$metrix.system('screen.*')`。

Worker 需 `export default` 一个包含以下钩子的对象：

```javascript
let count = 0;

export default {
  async scheduled(env) {
    // 轮询或手动刷新时执行：请求网络并更新状态
    count++;
    $metrix.invalidate();
  },

  render(env) {
    // 同步返回托盘和面板的 UI 树
    return {
      tray: {
        title: `数量：${count}`,
        icon: "activity",
      },
      panel: {
        type: "Column",
        props: { padding: 12, gap: 8 },
        children: [
          { type: "Text", props: { text: `当前总数：${count}`, fontSize: 16, bold: true } },
          {
            type: "Button",
            props: {
              text: "重置",
              onClick: () => {
                count = 0;
                $metrix.invalidate();
              },
            },
          },
        ],
      },
    };
  },

  async event(name, payload) {
    // 响应通过本地 HTTP API 提交的外部事件
    if (name === "reset") {
      count = 0;
    }
  },
};
```

### 生命周期方法

| 方法                   | 说明                                                                                         |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `scheduled(env)`       | 在定时器触发或手动刷新时调用，适合执行网络抓取和数据处理。                                   |
| `render(env)`          | 返回 `{ tray, panel }`。每次界面刷新时被调用，应为纯函数并保持同步执行。                     |
| `event(name, payload)` | 处理系统事件与通过本地 HTTP API 提交的自定义事件（`POST /v0/dashboards/:id/events/:name`）。 |

### 宿主系统事件

当看板生命周期、面板可见性变化或触发全局快捷键时，宿主会调用 Worker 的 `event(name, payload)` 钩子，支持 `async`：

| 事件                                         | 触发时机                                                         |
| -------------------------------------------- | ---------------------------------------------------------------- |
| `event("lifecycle", { type: "start" })`      | Worker 启动或故障重建后触发一次。                                |
| `event("lifecycle", { type: "stop" })`       | 移除看板、成功重载替换旧 Worker 或应用正常退出时，在销毁前触发。 |
| `event("lifecycle", { type: "expand" })`     | Flyout 或 Dialog 面板显示时。                                    |
| `event("lifecycle", { type: "collapse" })`   | Flyout 或 Dialog 面板隐藏时。                                    |
| `event("shortcut", { accelerator: string })` | 按下当前看板注册的全局快捷键时触发。                             |

`lifecycle` 取代原 `panel` 事件。Sidebar 栏的展开或收起不触发面板可见性事件。已配置且可用的 Sidebar 即使收起也保持看板活跃并刷新；没有任何可用入口、预览或打开的面板时，看板按既有活跃规则闲置，仅配置不可用的 Sidebar 不会维持活跃。

`stop` 在取消当前调用后执行，提供独立的 **2 秒 JS / Promise 清理预算**；抛错或超时后仍销毁 Worker，且不再渲染。崩溃、强杀或 runtime 损坏时不保证送达；预算不能强制中断阻塞的 Go host 调用。

普通启动顺序为 `start → scheduled(initial) → render`。导入与重载预检先执行 `scheduled(initial) → render`，期间不派发 start / stop；成功激活后再执行 `start → render`，不重复 initial。因此 `scheduled` 不应依赖 `start` 初始化数据；初始数据应放在模块状态或 `scheduled` 中。成功重载时，旧 Worker 停止并销毁后才启动新 Worker。

在 default export 中与必需的 `render` 一同定义事件处理：

```js
async event(name, payload) {
  if (name !== "lifecycle") return;
  switch (payload.type) {
    case "start":
      // 建立常驻资源
      break;
    case "stop":
      // 等待资源清理，预算 2 秒
      break;
    case "expand":
      // 面板展开
      break;
    case "collapse":
      // 面板收起
      break;
  }
}
```

---

## SQLite 持久数据库

声明 `<script apis="sqlite">`（或 `all`）后，可通过 `$metrix.sqlite` 为当前看板打开一个或多个命名数据库。数据库按看板 ID 和库名隔离，保存在应用数据目录的 `SQLite/` 下；Worker 重建或应用重启不会清空数据。库名须为 1–64 个 ASCII 字母、数字、下划线或连字符，不能指定文件路径。数据库不随 `.bar` 导出，也不用于保存密钥。

| 接口                                                        | 返回值                                                                            |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `await $metrix.sqlite.open(name)`                           | 数据库连接                                                                        |
| `db.prepare(sql)`                                           | 同步创建 prepared statement                                                       |
| `statement.bind(...params)`                                 | 同步返回绑定参数的新 statement，原对象不变                                        |
| `await statement.all()` / `await statement.run()`           | `{success: true, results: [行对象, ...], meta: {changes, last_row_id, duration}}` |
| `await statement.raw({columnNames: true})`                  | 二维值数组，可选首行为列名；默认不包含列名                                        |
| `await statement.first()` / `await statement.first(column)` | 首行对象 / 指定列值；没有行时返回 null，列名不存在时报错                          |
| `await db.batch([statement, ...])`                          | 按顺序返回 all 结果数组；失败时整体回滚                                           |
| `await db.close()`                                          | 关闭连接，不删除数据库；可重复调用                                                |

每条 SQL 只能包含一条语句。`all` 和 `run` 语义相同，都支持读写及 RETURNING；没有结果行时 `results` 为 `[]`。`meta.changes` 是语句影响的行数，`last_row_id` 是连接最近插入的 rowid，`duration` 为语句执行与结果收集的毫秒数；不提供虚构的远端服务或读写行数统计。同名结果列在行对象中保留最后一列，需保留全部列时使用 `raw`。

参数通过 `bind(...params)` 传入，支持 null、boolean（存为 0/1）、有限 number、64 位有符号 BigInt、string 和二进制缓冲区/视图；绑定时复制二进制数据。BigInt 与二进制结果是本接口的扩展：整数超出 JavaScript 安全范围时必须用 BigInt，读取时也返回 BigInt；BLOB 返回 ArrayBuffer，可用 `new Uint8Array(value)` 读取。`prepare` / `bind` 同步校验输入，SQL 解析和执行在异步调用时进行。无效输入可能同步抛错，数据库操作错误拒绝 Promise；统一用 try/catch 处理。

同一个 statement 可复用，`bind` 不修改之前的绑定。`batch` 仅接受来自该连接的 statement，即使另一个连接打开同名数据库也不能混用。连接操作相互串行，但并发提交的独立调用不保证执行顺序；依赖顺序请逐一 await，原子操作使用 `batch`。每次执行均在宿主管理的事务内完成写入和结果收集；SQL 错误、取消或结果超限时回滚。`first` 仍受完整结果预算约束，需要少量行时在 SQL 中使用 LIMIT。

以下为完整脚本示例，在刷新时保存采样记录，并读取当前记录数：

```bar
<script apis="sqlite"><![CDATA[
let database;
let count = 0;
async function getDatabase() {
  if (!database) {
    const db = await $metrix.sqlite.open('history');
    await db.prepare('CREATE TABLE IF NOT EXISTS samples (time INTEGER, value REAL)').run();
    database = db;
  }
  return database;
}
export default {
  async scheduled() {
    const db = await getDatabase();
    await db.prepare('INSERT INTO samples VALUES (?, ?)').bind(Date.now(), 42).run();
    count = await db.prepare('SELECT COUNT(*) AS count FROM samples').first('count');
  },
  render() {
    return {tray: {title: String(count)}};
  }
};
]]></script>
```

再次调用 `open('archive')` 即可使用另一个独立数据库。每个看板最多 8 个持久命名库，每 Worker 最多 8 个连接、4 个待完成操作。每次操作最多 10 秒，且受所属 hook 的取消约束；每次执行或整个 batch 的 SQL 与参数合计最多 1 MiB，batch 最多 100 条语句；每次执行或整个 batch 合计最多 10,000 行及 8 MiB 结果（含结构开销），每个主库与内存临时库分别最多 32 MiB 数据页。所有看板共享 128 MiB SQLite 堆内存预算；这不是应用总内存上限，其他看板的查询可能占用这一共同预算。

这里提供 D1 的常用 prepared statement 与 batch 接口，不包含 exec、dump、session、交互式事务或流式查询。预先打包的 `kysely@0.27.6` + `kysely-d1@0.4.0` 已通过真实 Worker 的建表和基础增删改查（含 RETURNING）验证。`kysely@0.28.18` 与 `0.29.7` 初始化会写入 `Symbol.asyncDispose`，当前 Worker 不支持；`kysely-d1` 自身也不支持交互式事务或流式查询。

支持表、索引、JOIN 和聚合等 SQL 能力；用户 PRAGMA、ATTACH/DETACH、VACUUM、扩展加载、虚拟表及直接事务控制不可用。宿主在 Worker 关闭时释放连接。原生开发检查使用独立临时数据库并在结束后删除；浏览器 Playground / WASM Sandbox 不提供 SQLite。

## `<script apis="…">` 系统扩展能力

默认情况下，Worker 处于安全沙箱中。若看板需要使用系统级能力，需在 `<script>` 标签中通过 `apis` 属性显式声明（多个能力用空格分隔）：

```xml
<script apis="shell badge shortcut dialog app clipboard match-media">
```

| API 名称       | 提供的能力             | 核心方法与规范                                                                                                                                                                                                                                                                                     |
| -------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shell`        | 外部应用与系统声音交互 | `$metrix.openURL(url)`（仅限 `http`、`https`、`mailto` 协议）<br />`$metrix.openPath(path)` / `$metrix.showItemInFolder(path)`（路径须在 `<file>` 权限内）<br />`$metrix.beep()`（系统提示音）                                                                                                     |
| `badge`        | 应用程序图标徽标计数   | `navigator.setAppBadge(count)`（设置 Dock 数字徽标）<br />`navigator.clearAppBadge()`（清除徽标）                                                                                                                                                                                                  |
| `shortcut`     | 全局热键监听           | `$metrix.registerShortcut(accelerator)`（如 `"Cmd+Shift+P"`）<br />`$metrix.unregisterShortcut(accelerator)`<br />触发后派发至 `event("shortcut", { accelerator })`                                                                                                                                |
| `dialog`       | 系统文件与消息对话框   | `$metrix.showOpenDialog(options)`（返回文件路径，自动临时加入 `readFile` 白名单）<br />`$metrix.showSaveDialog(options)`（返回保存路径）<br />`$metrix.writeFile(path, data, {expectedVersion?}?)`（单次上限 8 MB，仅限已确认的保存路径）<br />`$metrix.showMessageBox(options)`（弹出确认对话框） |
| `app`          | 应用生命周期控制       | `$metrix.quit()`（退出 Metrix.Bar）                                                                                                                                                                                                                                                                |
| `clipboard`    | 系统剪贴板读写         | `navigator.clipboard.read()` / `write(items)`<br />支持 `ClipboardItem`，支持纯文本、HTML 与 PNG 图片                                                                                                                                                                                              |
| `match-media`  | 系统外观与媒体查询     | `globalThis.matchMedia(query)`<br />支持系统深浅色、减少动效、减少透明度与对比度查询；详情见下文                                                                                                                                                                                                   |
| `screen`       | 主显示器静态快照       | `screen.width` / `height` / `availWidth` / `availHeight` 等启动快照；最新多显示器或指针位置查询使用 `$metrix.system('screen.*')`                                                                                                                                                                   |
| `battery`      | 基础电源状态查询       | `navigator.getBattery()`<br />仅返回充电状态快照（`charging: boolean`，`level` 固定为 1 占位）；支持 `power.*` 系统命令与休眠/唤醒事件                                                                                                                                                             |
| `notification` | 桌面系统通知           | `new Notification(title, { body, tag })`<br />仅基础投递，不提供 `permission`、`requestPermission()`、`close()` 及点击 / 错误事件                                                                                                                                                                  |

---

### 媒体查询 (`match-media`)

声明 `apis="match-media"` 后，通过 `globalThis.matchMedia(query)` 查询系统外观并监听变化：

- `(prefers-color-scheme: dark | light)`：深色 / 浅色模式。
- `(prefers-reduced-motion: reduce | no-preference)`：系统减少动态效果偏好。
- `(prefers-reduced-transparency: reduce | no-preference)`：系统减少透明度偏好。
- `(prefers-contrast: more | no-preference)`：系统高对比度偏好（仅 `more` 与 `no-preference` 根据系统设置真实匹配，`less` / `custom` 永不匹配）。
- 支持通过 `addEventListener("change", listener)` 监听系统外观与辅助功能切换。

---

### 带版本的文本读取与保存

以下方法需声明 `apis="dialog"`。`readTextFile(path)` 返回 `Promise<{data:string,version:string}>`，包含**同一次读取**的有效 UTF-8 文本和小写 SHA-256 十六进制摘要，上限 8 MiB。读取授权来自 `<permissions><file>` 或打开对话框；保存对话框不授予内容读取权限。旧 `readFile` 保留 1 MiB 上限。`fileInfo(path)` 对读取或保存授权的普通文件返回 `Promise<{size:number,version:string}>`，上限 8 MiB；文件不存在则拒绝。

`writeFile(path,data,{expectedVersion?}?)` 接受字符串、ArrayBuffer 或 TypedArray，上限 8 MiB，返回 `Promise<{version:string}>`，摘要来自本次实际写入的 bytes。仅可写当前 Worker 经保存对话框确认的目标；file 权限本身不授予写入。省略 expectedVersion 表示覆盖，`null` 要求目标不存在，摘要字符串要求当前内容匹配。冲突报 `file changed since it was read`。宿主内同目标检查与保存串行；这是写前乐观冲突检测，不是相对其他进程的原子 CAS。

以下为完整 `<script apis="dialog">` 模块内容，点击按钮才会打开对话框或保存。示例原样读写文本；需要编辑时，将 document.data 接入编辑器 onChange。原路径使用同读版本，其他路径要求不存在。冲突后应提示重新读取或另选目标，不应无条件重试覆盖。

```js
let document;
export default {
  render() {
    return {
      panel: {
        type: "Column",
        children: [
          {
            type: "Button",
            props: {
              text: "Open",
              onClick: async () => {
                const [path] = await $metrix.showOpenDialog({});
                if (path) document = { path, ...(await $metrix.readTextFile(path)) };
              },
            },
          },
          {
            type: "Button",
            props: {
              text: "Save",
              onClick: async () => {
                if (!document) return;
                const path = await $metrix.showSaveDialog({});
                if (path === null) return;
                // A different destination must not silently overwrite an existing file.
                const expectedVersion = path === document.path ? document.version : null;
                const result = await $metrix.writeFile(path, document.data, { expectedVersion });
                document = { ...document, path, version: result.version };
              },
            },
          },
        ],
      },
    };
  },
};
```

## 宿主环境 (`env`)

传递给各钩子函数的 `env` 对象提供了配置访问与桌面宿主 API：

| 属性 / 方法                                | 说明                                                                                                                                                                          |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `env.inputs`                               | 当前普通配置项对象（`env.inputs.repo`）。                                                                                                                                     |
| `$metrix.secret(name)`                     | 同步返回已声明 secret 输入的安全存储字符串；未声明或不可用时抛错。密钥不进入 env.inputs，env 没有 secrets 属性。                                                              |
| `$metrix.readFile(path)`                   | 读取声明授权的本地文件内容（返回 `Promise<string>`）。直接接收路径（支持 `~`），解析 `..` 与软链接防范越界；只能读取普通文件，最大 1 MB；未声明、越界或文件不存在将抛出异常。 |
| `$metrix.toast(message)`                   | 弹出短暂的桌面 Toast 提示。                                                                                                                                                   |
| `$metrix.notify(title, body?)`             | 发送桌面系统通知。                                                                                                                                                            |
| `$metrix.log(...args)`                     | 输出日志到应用控制台。                                                                                                                                                        |
| `$metrix.invalidate()`                     | 请求宿主重新渲染当前看板。                                                                                                                                                    |
| `$metrix.togglePanel()`                    | 按最终有效的 Flyout 或 Dialog 设置切换当前看板面板，菜单栏入口关闭时仍可用，无需 opt-in。                                                                                     |
| `$metrix.appVersion`                       | 只读宿主应用版本号字符串（如 `"0.0.1"`）。无需 `apis` 权限声明。                                                                                                              |
| `$metrix.userName`                         | Worker 初始化时捕获的宿主用户名，可用于已明确声明的凭据账号；fixture 缺省为 `fixture-user`。                                                                                  |
| `$metrix.readCredential(service, account)` | 后台读取一个已声明的系统凭据二元组；拒绝未声明项目和 App 保留服务。                                                                                                           |
| `$metrix.timeZone`                         | 只读宿主 IANA 时区名（如 `"Asia/Shanghai"`），与 `Date` 本地时区一致，可直接传给服务端。无需 `apis` 权限声明。                                                                |

### 读取本地文件示例

使用 `$metrix.readFile(path)` 读取本机文件时，目标路径必须在 `<permissions>` 中显式声明：

```xml
<permissions>
  <network origin="https://chatgpt.com" />
  <file path="{{ inputs.auth }}" />
  <file path="~/.claude/projects/" />
</permissions>
```

```javascript
// 读取用户在 file 类型输入项中选择的文件
const raw = await $metrix.readFile(env.inputs.auth);

// 读取授权目录下的指定文件
const raw2 = await $metrix.readFile("~/.claude/projects/a.jsonl");
```

---

## 动态 ESM 导入

可以从 ESM CDN 导入 JavaScript 依赖包。请求 URL、完整重定向链与最终 URL 都必须使用 HTTPS，并在 `<permissions>` 中获准。

导入或创建看板时，会先递归下载静态 `import` 和 re-export 的依赖，不执行顶层 JavaScript。预下载最多使用 15 秒、128 个远程模块和 16 MiB 源码，单模块上限为 1 MiB；下载或缓存写入失败会中止导入。动态 `import()` 在首次调用时下载并缓存依赖。

导入前先声明 CDN 来源：

```bar
<permissions>
  <network origin="https://esm.sh" />
</permissions>

<script>
let html;

export default {
  async scheduled(env) {
    if (!html) {
      const htm = await import("https://esm.sh/htm");
      const h = (type, props, ...children) => ({
        type,
        props: props || {},
        children: children.flat().filter(Boolean)
      });
      html = htm.default.bind(h);
    }
  },

  render(env) {
    return {
      tray: { title: "已启用 JSX/HTM" },
      panel: html`
        <Column padding=${16} gap=${8}>
          <Text text="通过 htm 渲染" bold=${true} />
        </Column>
      `
    };
  }
};
</script>
```

有关主流 UI 构树库（如 Preact、Vue、React、Snabbdom 等）与状态管理库的搭配指南，请参考 [技术选型与推荐技术栈](/zh/docs/tech-stack.md)。

---

## UI 树模型

`render(env)` 钩子返回的对象结构如下：

- `tray`：菜单栏 / 系统托盘展示项：
  - `title`：直接显示在托盘上的简短文本。
  - `icon`：系统图标名称或网络图标地址。
  - `iconTemplate`：可选布尔值。设为 `true` 时在 macOS 强制启用系统 Template 模式（深色模式或高亮时自动反色）。
  - `tooltip`：鼠标悬浮托盘图标时的提示文本。
  - `menu`：托盘右键上下文菜单项数组。菜单项支持：
    - `label`：菜单项文本。
    - `onClick`：点击触发的处理函数。
    - `disabled`：布尔值，是否禁用。
    - `separator`：布尔值，是否为分割线。
    - `checked`：布尔值，显示勾选状态。
    - `radio`：布尔值，单选标记（逻辑由脚本维护）。
    - `shortcut`：快捷键提示（如 `"Cmd+Shift+P"`）。
    - `tooltip`：浮动提示文本。
    - `items`：递归嵌套的子菜单项数组。
- `panel`：点击托盘后弹出的面板内容树：
  - `type`：原生组件名称（见 [组件参考](/docs/components)）。
  - `props`：组件属性与事件回调（如 `onClick`、`onChange`、`onSubmit`）。
  - `key`：可选的稳定节点标识，在同级节点中必须唯一。作为 `activeDescendant` 或 `scrollWith` 目标的节点，其 key 还必须在面板内唯一。
  - `children`：子节点数组。

同一次 UI 构建收集的原生输入回调按顺序派发，批次开始执行后不会在回调之间插入渲染。每批最多 32 个回调；若执行前视图已被替换，旧回调仍可能过期。文本输入观察事件与节点引用详见[组件参考](/zh/docs/components)。

## 桌面系统命令

`$metrix.system(command,args?)` 返回 Promise。在 `<script apis="...">` 声明下表 opt-in；`all` 包含这些能力。省略参数等同 `{}`；未知命令、字段、类型或缺少权限均拒绝。

| 命令                                                    | 参数                 | opt-in        |
| ------------------------------------------------------- | -------------------- | ------------- |
| `screen.displays`, `screen.primary`, `screen.cursor`    | —                    | `screen`      |
| `screen.nearest`                                        | `{x,y}`              | `screen`      |
| `screen.matching`                                       | `{x,y,width,height}` | `screen`      |
| `theme.source`, `theme.preferences`, `theme.isDark`     | —                    | `match-media` |
| `network.status`                                        | —                    | —             |
| `power.idleTime`                                        | —                    | `battery`     |
| `power.keepAwake`                                       | `{reason,display?}`  | `battery`     |
| `power.release`                                         | `{token}`            | `battery`     |
| `clipboard.formats`, `clipboard.clear`                  | —                    | `clipboard`   |
| `shell.installedApps`                                   | —                    | `shell`       |
| `shell.launchApp`                                       | `{token}`            | `shell`       |
| `shell.appIcon`                                         | `{token,size}`       | `shell`       |
| `shell.fileIcon`                                        | `{path,size}`        | `shell`       |
| `shell.trash`                                           | `{path}`             | `shell`       |
| `app.locale`, `app.languages`, `app.region`, `app.info` | —                    | `app`         |
| `app.path`                                              | `{name}`             | `app`         |
| `app.focus`, `app.hide`, `app.show`, `app.relaunch`     | —                    | `app`         |
| `watch.start`                                           | `{path,recursive?}`  | `watch`       |
| `watch.stop`                                            | `{token}`            | `watch`       |

屏幕坐标为整数，matching 尺寸为正整数。`network.status` 返回 `{online,metered}`，`navigator.onLine` 读取宿主连接快照；主题查询不改变全局主题。`power.idleTime` 返回毫秒，keepAwake/watch.start 返回 Worker 范围的 token。应用枚举最多 4096 项，再次枚举使旧应用 token 失效，launch/appIcon 仅接受这些 token。图标 size 为 1–256 整数，返回最多 1 MiB 的 PNG data URL。fileIcon/watch 复用文件读取权限，目录监听需完整目录授权。删除授权尚未实现，`shell.trash` 明确拒绝。

`app.info` 返回 `{name,version,packaged}`。app.path 的 name 仅接受 `home`、`appData`、`userData`、`cache`、`logs`、`resources`、`temp`、`exe`、`desktop`、`documents`、`downloads`、`music`、`pictures`、`videos`；返回路径不授予读写权限，部分官方目录查询会创建应用目录。`app.relaunch` 的 `{accepted:true}` 表示请求已接受，随后宿主清理并重启。

Worker 关闭/替换时释放资源。`event("system",payload)` 的 `type:"watch"` 事件含 `token`、相对 `path`、`oldPath`、`kind:"create"|"write"|"remove"|"rename"`、`isDir`；battery opt-in 启用 `power.suspend`、`power.resume`、`power.lock`、`power.unlock` 事件。staging Worker 可查询，但激活前不能创建/释放资源、操作生命周期、启动应用或清空剪贴板。不开放远程 WebView、Protocol、Terminal 或动态库。

## 面板控制

`$metrix.panel.open()`、`close()`、`toggle()`、`state()` 均返回 `Promise<{open:boolean}>`，无需 opt-in，仅作用于绑定当前 Worker 的看板。打开和切换遵循最终有效的 Flyout 或 Dialog 设置，无菜单栏入口时仍可使用，`{open}` 响应不变。原生屏幕定位能力不可用时，Dialog 明确拒绝打开，不回退为普通浮窗。staging Worker 拒绝 open/close/toggle，允许 state 且查询不会创建面板。取消后未执行的动作不再执行。既有 `$metrix.togglePanel()` 保留。

## 音频播放器

声明 `apis="audio"`（或 `all`）。`$metrix.audio.load({url?,path?,stream?})` 必须且只能提供 URL 或 path 其中一个，返回 AudioPlayer Promise。stream 为 boolean，默认 true；URL 复用网络权限和强制请求标识，允许已授权局域网 HTTP，path 需读取授权。宿主完整下载压缩源后才开始流式解码。

播放器所有方法均返回 Promise：`play()`、`pause()`、`stop()`、`seek(seconds)`、`configure({volume?,pan?,playbackRate?,loop?,loopStart?,loopEnd?})`、`status()`、`close()`。volume 为 0–1（默认 1），pan 为 -1–1（默认 0），playbackRate 为 0.1–16（默认 1）；loop 默认 false，loopStart/loopEnd 默认 0。时间为有限秒数 0–1800；loopEnd 为 0 表示到末尾，否则大于 loopStart。配置全部校验后应用。status 返回 `playing`、`position`、`duration`（未知为 null）及配置；close 幂等。staging 可加载/查询，激活前 play 拒绝。播放状态仅代表逻辑播放，不保证声音输出。

支持 WAV、MP3、Ogg Vorbis，8000–192000 Hz、1–2 声道，最长 30 分钟（含未知长度流）。每 Worker 最多八个 player（含在途），每源压缩数据最多 8 MiB、压缩总预算 16 MiB（每个在途加载预留 8 MiB）；`stream:false` 每 effect 最多 4 MiB 有效解码样本、解码池 16 MiB。流 ring 按元数据最多 1,536,000 字节，不含 codec 分配。decoder 关闭完成才归还流 slot 和预算。Worker 关闭取消加载并阻止晚到结果发布；这些预算不构成 decoder 硬内存隔离。

### PCM Sound 与独立 Voice

声明 `apis="audio"`（或 `all`）后，`audio.now()` 同步返回当前 Worker 音频时钟的单调秒数，独立于 Date。`audio.createSound({samples:Float32Array,sampleRate,channels})` 返回 `Promise<AudioSound>`。sampleRate 为整数 8000–192000，channels 为 1 或 2，样本非空、声道对齐、每项有限且在 [-1,1]。仅接受真实 Float32Array（含偏移视图），拒绝空/detached 视图、Float64Array、普通数组、Proxy 与伪品牌对象。宿主在**全部选项 getter 完成后**复制可见样本；后续修改数组不影响 Sound。

Sound 共享 PCM，每次 `sound.play({when?,volume?,pan?,playbackRate?,loop?,attackMs?})` 创建独立 Voice。默认与范围：volume 默认 1、范围 0–1，pan 默认 0、范围 -1–1，playbackRate 默认 1、范围 0.1–16，loop 默认 false，attackMs 默认 0、范围 0–2000。暂存 Worker 在激活前拒绝 play。when 使用 audio.now() 的绝对时基，省略或过期表示立即，最多提前 5 秒。`voice.release({when?,durationMs?})` 从当前包络增益线性淡出至零并终结，durationMs 默认 80、范围 0–2000。尚未开始的 Voice 在 release 到时取消。重复 release 覆盖旧预约，基础 volume 与包络相乘。

音频预约是控制级调度，不保证采样精确。ended 声部不能由 release 恢复。

`voice.configure({volume?,pan?,playbackRate?,loop?})`、`status()`、`release()`、`stop()`、`close()` 返回 Promise。status 含 `state:scheduled|playing|releasing|ended|closed`、playing、position、duration 及四项配置。自然/淡出完成为 ended；stop/close 幂等进入 closed，后端关闭后 resolve。ended/closed 可读 status，但拒绝 configure/release；逻辑状态不保证可听输出。

每 Worker 最多 64 Sound（含预约）、每个 4 MiB，32 独立 Voice（含预约、scheduled、releasing），PCM 加旧 effect 解码预约共 32 MiB，待提交宿主 PCM 副本共 16 MiB（不限制 JS backing buffer）。旧 API 仍限 8 player、16 MiB 压缩总量与独立 16 MiB effect 解码池。普通命令队列最多 256 项，满时拒绝并回滚未交付预约。

`sound.close()` 共享同一个 Promise，立即禁止新 play，已有 Voice 继续；最后一个终结后清 PCM 并归还预算。保留关闭的 JS Sound/Voice 对象不会保留 PCM/后端播放器；Voice 自然终结也释放资源。Worker 关闭冻结预约与命令、取消待执行命令，等待 Go 清理，不依赖 JS 事件循环。

以下为完整 `<script apis="audio">` 模块内容，首次点击才创建共享音色，每次点击播放独立 Voice；初始 render 或 Check 不播放音频。

```js
let sound;
export default {
  render() {
    return {
      panel: {
        type: "Button",
        props: {
          text: "Play tone",
          onClick: async () => {
            if (!sound) {
              const samples = new Float32Array(4800);
              for (let i = 0; i < samples.length; i++) {
                samples[i] = 0.2 * Math.sin((2 * Math.PI * 440 * i) / 48000);
              }
              sound = await $metrix.audio.createSound({ samples, sampleRate: 48000, channels: 1 });
            }
            const when = $metrix.audio.now() + 0.05;
            const voice = await sound.play({ when, attackMs: 10 });
            await voice.release({ when: when + 0.08, durationMs: 20 });
          },
        },
      },
    };
  },
  async event(name, payload) {
    if (name === "lifecycle" && payload.type === "stop" && sound) await sound.close();
  },
};
```

## 类型化剪贴板与语言

声明 `apis="clipboard"`。`navigator.clipboard.read()` 返回 `ClipboardItem[]` Promise，`write(items)` 返回 Promise。`new ClipboardItem({[mime]: BlobOrPromise})` 与 `ClipboardItem.supports(type)` 精确接受 `text/plain`、`text/html`、`image/png`、`text/markdown`、`text/uri-list`、`application/x-mygo-file-list`，拒绝 `web ` 自定义 MIME。types 保留构造顺序并冻结。写入等待并校验所有 representation 后才单次提交宿主；最多 64 items、总计 8 MiB，PNG 最多 16,777,216 像素；空字节有效，原生错误 reject。file URL 必须为空 host 或 localhost、本机绝对路径，读写均逐条检查读取授权。macOS 保留多项，Windows/Linux 可能合并文本、其他格式取首个匹配项。这是 Worker 扩展，并非完整浏览器标准，预算不限制 native 瞬时分配。

`navigator.languages` 是冻结的 preferred-language 快照，`navigator.language === navigator.languages[0]`。BCP 47 标签规范化去重并保留 script，缺省 en-US。App 就绪时读取官方 preferred languages、locale、region，每 Worker 固定使用创建时快照；不派发 languagechange。无桌面环境按 LANGUAGE 和 locale 环境变量回退。app.locale 与 app.region 是独立快照，Intl 沿用既有 polyfill。

### Web Crypto 与徽标能力子集

声明 `apis="crypto"` 后，`crypto.getRandomValues(view)` 仅接受整数 TypedArray（包括 Uint8ClampedArray 与引擎支持的 BigInt 数组），返回原视图且只填充其可见字节。浮点数组、DataView、ArrayBuffer 与伪造对象抛出 `TypeMismatchError` DOMException；单次超过 65536 字节抛出 `QuotaExceededError` DOMException。加密算法支持范围仍为摘要与 HMAC 子集。

声明 `apis="badge"` 后，`navigator.setAppBadge(count?)` 与 `navigator.clearAppBadge()` 返回 `Promise<void>`，参数错误会拒绝 Promise。受 MyGO 计数能力限制，无参数或零时清除徽标；多个看板写入时以最后一次为准。
