文档 / .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 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 导入与本地文件读取将被宿主环境拦截:
<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> 声明:
<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> 元素提供指导用户完成配置与凭据设置的纯文本说明:
<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 一个包含以下钩子的对象:
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 一同定义事件处理:
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。
以下为完整脚本示例,在刷新时保存采样记录,并读取当前记录数:
<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 属性显式声明(多个能力用空格分隔):
<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。原路径使用同读版本,其他路径要求不存在。冲突后应提示重新读取或另选目标,不应无条件重试覆盖。
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> 中显式声明:
<permissions>
<network origin="https://chatgpt.com" />
<file path="{{ inputs.auth }}" />
<file path="~/.claude/projects/" />
</permissions>// 读取用户在 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 来源:
<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 不播放音频。
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 计数能力限制,无参数或零时清除徽标;多个看板写入时以最后一次为准。