文档 / .bar 格式规范

.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。本地 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 交互与平台限制见看板入口与打开方式。

公开看板索引以 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 协议)
$metrix.openPath(path) / $metrix.showItemInFolder(path)(路径须在 <file> 权限内)
$metrix.beep()(系统提示音)
badge应用程序图标徽标计数navigator.setAppBadge(count)(设置 Dock 数字徽标)
navigator.clearAppBadge()(清除徽标)
shortcut全局热键监听$metrix.registerShortcut(accelerator)(如 "Cmd+Shift+P")
$metrix.unregisterShortcut(accelerator)
触发后派发至 event("shortcut", { accelerator })
dialog系统文件与消息对话框$metrix.showOpenDialog(options)(返回文件路径,自动临时加入 readFile 白名单)
$metrix.showSaveDialog(options)(返回保存路径)
$metrix.writeFile(path, data, {expectedVersion?}?)(单次上限 8 MB,仅限已确认的保存路径)
$metrix.showMessageBox(options)(弹出确认对话框)
app应用生命周期控制$metrix.quit()(退出 Metrix.Bar)
clipboard系统剪贴板读写navigator.clipboard.read() / write(items)
支持 ClipboardItem,支持纯文本、HTML 与 PNG 图片
match-media系统外观与媒体查询globalThis.matchMedia(query)
支持系统深浅色、减少动效、减少透明度与对比度查询;详情见下文
screen主显示器静态快照screen.width / height / availWidth / availHeight 等启动快照;最新多显示器或指针位置查询使用 $metrix.system('screen.*')
battery基础电源状态查询navigator.getBattery()
仅返回充电状态快照(charging: boolean,level 固定为 1 占位);支持 power.* 系统命令与休眠/唤醒事件
notification桌面系统通知new Notification(title, { body, tag })
仅基础投递,不提供 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.userNameWorker 初始化时捕获的宿主用户名,可用于已明确声明的凭据账号;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 等)与状态管理库的搭配指南,请参考 技术选型与推荐技术栈。


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:原生组件名称(见 组件参考)。
    • props:组件属性与事件回调(如 onClick、onChange、onSubmit)。
    • key:可选的稳定节点标识,在同级节点中必须唯一。作为 activeDescendant 或 scrollWith 目标的节点,其 key 还必须在面板内唯一。
    • children:子节点数组。

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

桌面系统命令

$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 计数能力限制,无参数或零时清除徽标;多个看板写入时以最后一次为准。