Minibar 插件契约(Plugin API)

版本 API v1(对应宿主 0.3.x)· 状态 ✅ 已确认 · 英文版开发者指南见 plugin-guide.en.md。

本文是插件与宿主之间的全部接口:manifest 格式、生命周期、ctx API、UI DSL、错误模型、兼容规则。实现以本文为准;本文未写的能力不存在。


1. 包结构

com.example.myplugin/
├── manifest.json      必需
├── main.js            必需(可通过 manifest.entry 改名)
├── package.json       发布到 npm 时必需(只做包元数据,不执行任何东西)
├── README.md          可选,市场详情页展示
└── screenshots/       可选,市场详情页展示(PNG,≤ 1 MB/张,≤ 5 张)

2. manifest.json

{
  "id": "com.minibar.weather",
  "name": "Weather",
  "version": "1.0.0",
  "description": "Current conditions and the next 6 hours.",
  "author": "Minibar",
  "homepage": "https://example.com/weather",
  "icon": "cloud.sun",
  "category": "life",
  "entry": "main.js",
  "refreshInterval": 900,
  "minHostVersion": "0.1.0",
  "permissions": ["network", "location"],
  "allowedHosts": ["open-meteo.com"],
  "settings": [
    { "key": "city", "type": "text", "label": "City", "placeholder": "Leave empty to use your location" },
    { "key": "unit", "type": "select", "label": "Unit", "default": "celsius",
      "options": [{ "value": "celsius", "label": "°C" }, { "value": "fahrenheit", "label": "°F" }] }
  ]
}

2.1 字段

字段必填类型默认约束
id✅string—反向域名,^[a-z0-9]+(\.[a-z0-9-]+)+$,≤ 64 字符,全局唯一
name✅string—1–32 字符
version✅string—SemVer MAJOR.MINOR.PATCH,可带 -pre
description❌string""≤ 200 字符
author❌string""≤ 64 字符
homepage❌string—https:// URL
icon❌string"square.grid.2x2"SF Symbol 名;无效时回退默认
category❌string"other"productivity / finance / developer / system / life / other
entry❌string"main.js"相对路径,不含 ..、不以 / 开头,必须存在
refreshInterval❌integer300秒,5 ≤ n ≤ 86400
minHostVersion❌string"0.1.0"SemVer;宿主低于此版本不装载,卡片提示升级
permissions❌string[][]见 §6 权限枚举;重复项报错
allowedHosts❌string[][]域名(不含协议/端口/路径)或 "*";声明 network 时必须非空
allowedSchemes❌string[][]ctx.openURL 可打开的其他 App 的 URL scheme(如 things、obsidian),≤ 20 项;需要 apps 权限;file/javascript/data 永远不允许
settings❌Setting[][]≤ 20 项,key 唯一
localizations❌object{}按语言标签(zh-Hans、ja、pt-BR)翻译 name、description 与设置项文案,见 §2.4;≤ 20 种语言

未知字段忽略(向前兼容)。

2.2 Setting 对象

字段必填说明
key✅^[a-zA-Z][a-zA-Z0-9_]{0,31}$
type✅text / secret / number / toggle / select
label✅表单标签,≤ 40 字符
description❌表单下方说明,≤ 120 字符
default❌与 type 匹配:text/secret → string,number → number,toggle → boolean,select → options 之一的 value
placeholder❌text / secret / number
required❌默认 false;为 true 且值为空时,卡片显示"需要设置 [去设置]"而不调用 render
min / max / step❌number
optionsselect 必填[{ "value": string, "label": string }] 或 string[],2–50 项

secret 在表单中以密码框显示,存储时与其他字段分开(M3 进 Keychain),永远不会出现在日志或错误信息中。

2.4 本地化

"localizations": {
  "zh-Hans": {
    "name": "天气",
    "description": "当前天气与未来几小时。",
    "settings": {
      "city": { "label": "城市", "description": "…", "placeholder": "留空则使用你的位置" },
      "unit": { "label": "单位", "options": { "celsius": "摄氏", "fahrenheit": "华氏" } }
    }
  }
}

2.3 校验

安装、本地加载、minibar-cli validate 使用同一套 ManifestValidator。错误是硬失败(拒绝装载),警告显示但放行:

级别情况
错误缺必填字段;格式不符;network 无 allowedHosts;entry 不存在;settings.key 重复;select 无 options;default 类型不符
警告allowedHosts 含 "*";refreshInterval < 30;icon 不是已知 SF Symbol;未声明 description

3. 生命周期

装载   宿主创建 VM → 注入 bootstrap → 执行 entry → 必须恰好调用一次 defineMiniApp,且 id 与 manifest 一致
首帧   装载成功后立即 render
定时   每 refreshInterval × 用户倍率 × (0.9…1.1) 秒 render 一次;上一次返回了 refreshIn 则下一次改用该值(一次性)
动作   用户点击 button → 执行对应 action → 立即 render
变更   用户改了设置 / 授予或撤销权限 / 点了刷新 → 立即 render
卸载   停止定时器 → 存储落盘 → 销毁 VM

4. defineMiniApp(config)

defineMiniApp({
  // 可选;若提供必须与 manifest.id 一致(用于防止拷错文件)
  id: 'com.minibar.weather',

  // 必需:返回 RenderResult(§5)
  render(ctx) { /* ... */ },

  // 可选:name → function(ctx, payload);payload 来自 button.payload,默认 null
  actions: {
    refresh(ctx) { ctx.storage.remove('cache'); },
    open(ctx, payload) { ctx.openURL(payload.url); }
  }
});

5. RenderResult

type RenderResult = {
  title: string | HeadlineTitle | null;   // 必需(可为 null)
  body: UINode[];                          // 必需,可为空数组
  menu?: MenuItem[];                       // 卡片 ⋯ 菜单里的次要动作,≤ 6 项(v0.7)
  urgency?: number;                        // 0–1,默认 0;≥ 0.8 触发抢占
  urgencyTTL?: number;                     // 秒,默认 60,范围 5–3600
  refreshIn?: number;                      // 秒,≥ 1;一次性覆盖下次刷新间隔
};

type MenuItem = {
  label: string;           // ≤ 200 字符
  action?: string;         // 与 button 相同:actions 里的名字,或 $openSettings / $refresh 等宿主动作
  url?: string;            // 与 button 相同:http(s) / mailto
  payload?: unknown;       // 原样传给 action
  icon?: string;           // SF Symbol,可选
};

type HeadlineTitle = {
  text: string;            // ≤ 40 字符;宿主再按用户设置截断
  icon?: string;           // SF Symbol;缺省用 manifest.icon
  color?: ColorToken;      // 仅 success / warning / danger 会着色,其他 token 按默认色
};

6. ctx API

所有 API 同步。按返回方式分两类:

6.1 基础

API签名说明
ctx.now()() => Date当前时间
ctx.localestring用户首选语言的 BCP 47 标签,如 "en-US"、"zh-Hans-CN";用于正文翻译与格式化(Intl 可用),不受 XPC 服务自身支持的界面语言限制
ctx.i18n(tables)({ [tag]: T }) => T按 ctx.locale 选一张表:先精确匹配(zh-Hans-CN),再逐级缩短(zh-Hans、zh),再 en,最后第一张表。表里放字符串或函数都可以;内置插件的正文翻译都这样写:const t = ctx.i18n({ en: {...}, zh: {...} })。Minibar 0.5.0+
ctx.host{ version: string, api: 1 }宿主版本与 API 版本
ctx.log(...args)(...any) => void写入宿主日志(插件设置页可查看);对象会 JSON 序列化;每条 ≤ 2 KB
ctx.permissions.granted(name)(string) => boolean用户是否已授予某项权限,用于优雅降级(没授权时换一个分支渲染,而不是抛错)
ctx.permissions.list()() => string[]当前全部已授予的权限

6.2 存储

API签名说明
ctx.storage.get(key, fallback?)(string, any?) => any不存在返回 fallback(默认 undefined)
ctx.storage.set(key, value)(string, any) => voidvalue 必须可 JSON 序列化;undefined 等价于 remove
ctx.storage.remove(key)(string) => void
ctx.storage.clear()() => void
ctx.storage.keys()() => string[]

6.3 设置

API签名说明
ctx.settings.get(key, fallback?)(string, any?) => any用户填写的值;未填返回 manifest default,再无则 fallback
ctx.settings.all()() => object全部设置的拷贝

设置只读;要让用户改设置,用 button 的 $openSettings 宿主动作。

6.4 网络(权限 network)

ctx.http.get(url: string, headers?: Record<string,string>): HttpResponse
ctx.http.getJSON(url: string, headers?: Record<string,string>): HttpJSONResponse
ctx.http.request(url: string, options: {
  method?: 'GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD';
  headers?: Record<string,string>;
  body?: string;               // 字符串;要发 JSON 自己 stringify 并设 Content-Type
  timeout?: number;            // 毫秒,默认 15000,上限 15000
  cache?: boolean;             // 仅 GET,默认 true:30 秒内存缓存
}): HttpResponse

type HttpResponse = {
  ok: boolean;                 // 2xx 且无传输错误
  status: number;              // 0 表示未得到响应
  body: string;
  headers: Record<string,string>;   // 键小写
  error?: 'timeout' | 'too_large' | 'network' | 'http_error';
  cached?: boolean;
};

type HttpJSONResponse = Omit<HttpResponse, 'body'> & { json: any; error?: HttpResponse['error'] | 'invalid_json' };

6.5 系统指标

ctx.system(): {
  timestamp: number;                      // ms
  osVersion: string;                      // "14.5"
  processorCount: number;
  hardware: { model: string | null; chip: string | null; memoryTotal: number;   // "Mac14,9", "Apple M2 Pro"
              cores: { performance: number; efficiency: number } | null };      // Apple 芯片有值,Intel 为 null
  cpu: { usage: number; user: number; system: number };      // 0–1
  load: { '1m': number; '5m': number; '15m': number };
  memory: { used: number; free: number; total: number; percent: number;      // 字节
            pressure: 'normal' | 'warning' | 'critical'; swapUsed: number };  // 内核内存压力;已用交换空间
  disk: { used: number; free: number; total: number; percent: number };     // 字节,启动卷
  network: { rxBytesPerSec: number; txBytesPerSec: number };
  battery: { percent: number; charging: boolean; timeToEmptyMinutes: number | null } | null;  // 台式机为 null
}

无需权限。采样共享,最多每秒更新一次。不包含主机名、用户名、序列号、IP、MAC。

6.6 日历(权限 calendar)

ctx.calendar.upcoming(options?: { hours?: number; limit?: number; includeAllDay?: boolean }): CalendarEvent[]
// hours 默认 24(1–168),limit 默认 10(1–50),includeAllDay 默认 false

type CalendarEvent = {
  id: string; title: string;
  start: number; end: number;             // ms
  allDay: boolean;
  location: string | null;
  calendar: { title: string; color: string | null };   // color: "#RRGGBB"
  url: string | null;
};

抛出:permission_denied(插件级未授权)、os_access_denied(系统日历权限被拒,卡片显示"去系统设置")。结果按 start 升序,已结束的事件不返回。

新建日程(仅 action):

ctx.calendar.add(options: {
  title: string; start: number; end?: number;   // ms;end 默认 start + 1 小时,须晚于 start 且不超过 1 年
  allDay?: boolean; calendar?: string;          // calendar:日历名称,缺省系统默认日历
  location?: string; notes?: string; url?: string; alert?: number;   // alert:提前多少分钟提醒
}): { id: string; title: string; start: number; end: number; allDay: boolean; calendar: string }

只能写入允许修改的日历;指定的名称不存在抛 invalid_argument。标题 ≤ 200 字,备注 ≤ 2000 字。

6.7 提醒事项(权限 reminders)

ctx.reminders.incomplete(options?: { list?: string; limit?: number }): Reminder[]
// list:清单名,缺省全部;limit 默认 50(1–200)

type Reminder = { id: string; title: string; due: number | null; priority: number; list: string };

抛出同 §6.6。按 due 升序,无截止日期的排最后。

新建与完成(仅 action):

ctx.reminders.add(options: { title: string; due?: number; allDay?: boolean; list?: string; notes?: string; priority?: number; url?: string }):
  { id: string; title: string; due: number | null; list: string }
ctx.reminders.complete(id: string, completed?: boolean): { id: string; title: string; completed: boolean }   // completed 默认 true

6.8 定位(权限 location,M2)

ctx.location(): { latitude: number; longitude: number; accuracy: number; city: string | null; updatedAt: number }

宿主缓存位置 15 分钟;city 来自系统反向地理编码,可能为 null。抛出 permission_denied / os_access_denied / unavailable。

6.9 通知(权限 notifications)

ctx.notify(title: string, body?: string, options?: { url?: string }): boolean

返回 false 表示被限流(每插件每 60 秒 1 条)。url 点击通知时打开。抛出 permission_denied、invalid_argument。

6.10 剪贴板(权限 clipboard)

ctx.clipboard(): string            // 当前剪贴板文本;无文本返回 ""(等价于 ctx.clipboard.read())
ctx.clipboard.read(): string
ctx.clipboard.write(text: string): { written: true }   // 仅 action;≤ 100 KB

抛出 permission_denied。每次 App 会话中的首次读取需要用户在卡片上再确认一次("Confirm Clipboard access for this session [Allow]"),确认前同样抛出 permission_denied。写入不需要会话确认,但只能在 action 中(render 中抛 not_in_action)。

6.11 打开链接

ctx.openURL(url: string): void

只能在 action 中调用;在 render 中调用抛 not_in_action。http / https / mailto 无需权限;manifest allowedSchemes 里声明的其他 scheme(如 things:///add?title=…)需要 apps 权限;其余一律 invalid_argument。

6.12 UI 构造器

ctx.ui 是纯粹的便捷函数,只返回普通对象;也可以手写对象字面量。

ctx.ui.text(value, opts?)                 // { style, color, maxLines }
ctx.ui.icon(systemName, opts?)            // { size, color }
ctx.ui.row(children, opts?)               // { spacing, align: 'top'|'center'|'bottom' }
ctx.ui.column(children, opts?)            // { spacing, align: 'leading'|'center'|'trailing' }
ctx.ui.spacer(size?)                      // 无参 = 弹性
ctx.ui.divider()
ctx.ui.progress(value, opts?)             // 0–1;{ color }
ctx.ui.button(label, action, opts?)       // action: string | { action?, payload?, url? };opts: { color, icon }
ctx.ui.chart(points, opts?)               // number[];{ color, height, min, max }
ctx.ui.badge(text, opts?)                 // { color }
ctx.ui.keyValue(key, value, opts?)        // { color }
ctx.ui.grid(children, opts?)              // { columns, spacing }
ctx.ui.horizontalScroll(children, opts?)  // { itemWidth, spacing };Minibar 0.5.0+

6.13 系统操作(权限 system)

对这台 Mac 做点什么。只能在 action 中调用(在 render 中调用抛 not_in_action),用户必须先授予 system 权限。

ctx.freeMemory(): { freeBefore: number; freeAfter: number; freedBytes: number; allocatedBytes: number; seconds: number }  // 字节
ctx.runShortcut(name: string, input?: string): { started: true }
ctx.keepAwake(enabled?: boolean): { enabled: boolean }   // 默认 true

6.14 其他 App(权限 apps)

ctx.apps.running(): { name: string; bundleID: string; active: boolean; hidden: boolean }[]   // 任意阶段
ctx.apps.open(bundleID: string): { opened: true }                                            // 仅 action

6.15 加密与编码(无需权限)

ctx.crypto.sha256(text: string): string                 // 小写 hex
ctx.crypto.hmacSHA256(key: string, message: string): string
ctx.crypto.base64Encode(text: string): string
ctx.crypto.base64Decode(text: string): string           // 非法输入抛 invalid_argument
ctx.crypto.randomUUID(): string

JavaScriptCore 没有 crypto、atob、btoa;需要签名 API 请求(交易所、云服务)时用这些。输入按 UTF-8 处理。

6.16 文件(权限 files)

插件读不到任何它没被给的路径:用户在系统文件对话框里亲手选了什么,插件就能读什么(只读)。

ctx.files.pick(options?: { folders?: boolean; multiple?: boolean; types?: string[]; message?: string }): { opened: true }   // 仅 action
ctx.files.list(): { id: string; name: string; path: string; kind: 'file' | 'folder'; available: boolean; addedAt: number }[]
ctx.files.entries(id: string, options?: { hidden?: boolean; limit?: number }):
  { id: string; name: string; kind: 'file' | 'folder' | 'symlink' | 'other'; size: number | null; modified: number | null }[]
ctx.files.read(id: string, options?: { encoding?: 'utf8' | 'base64'; maxBytes?: number }):
  { id: string; name: string; size: number; content: string; encoding: string; truncated: boolean; modified: number | null }
ctx.files.forget(id: string): { forgotten: boolean }   // 仅 action

7. UI DSL

每个节点是 { "type": string, ...fields }。未知 type 静默忽略;已知 type 但必需字段缺失或类型错误 → 该节点忽略并记录警告(不让整个 render 失败)。

type必需可选(默认)渲染
textvalue: stringstyle (body), color (primary), maxLines (0 = 不限)单段文本,自动换行
iconsystemName: stringsize (14), color (primary)SF Symbol,使用 size × size 的对齐区域
rowchildren: UINode[]spacing (6), align (center)水平排列
columnchildren: UINode[]spacing (6), align (leading)垂直排列
spacer—size无 size 为弹性,撑满父容器主轴
divider——细分隔线
progressvalue: numbercolor (accent)0–1,超出裁剪
buttonlabel: string + (action 或 url)payload (null), color, icon小号带边框按钮
chartpoints: number[](≥ 2)color (accent), height (32), min, max(缺省自动)折线 sparkline,≤ 500 点
badgetext: stringcolor (accent)胶囊徽章
keyValuekey: string, value: stringcolor(作用于 value)左 key 灰色、右 value
gridchildren: UINode[]columns (2,1–4), spacing (6)等宽网格
horizontalScrollchildren: UINode[]itemWidth (56,32–200), spacing (6,0–64)单行横向滚动,每项等宽、顶部对齐;高度随内容收缩,宽度限于卡片;Minibar 0.5.0+
websrc: stringheight (200,60–600), data插件包内的 HTML 页面内嵌渲染,见 §7.1

horizontalScroll 保留所有子项,不挤压成多行;使用系统横向滚动手势与滚动条,内容底部保留滚动条间距,纵向滚动仍由仪表盘处理。其子树沿用全局 500 节点 / 32 层限制。依赖此节点的插件应声明 minHostVersion: "0.5.0"。

style:title(17pt 半粗)/ headline(13pt 半粗)/ body(13pt)/ caption(11pt 次要色)/ mono(12pt 等宽,数字等宽)

ColorToken:primary / secondary / accent / success / warning / danger / muted

button.action:


7.1 Web 卡片(web 节点,微前端)

{ "type": "web", "src": "ui/index.html", "height": 200, "data": { "count": 3 } }
字段必需说明
src✅插件包内的 .html 文件(相对路径,不能逃出包)
height❌60–600 pt,默认 200;页面可用 minibar.resize(h) 调整
data❌任意 JSON(≤ 64 KB),每次 render 都会推给页面

页面里可用的 window.minibar(宿主在文档开始前注入):

minibar.data                       // 当前 data
minibar.onData((data) => {})       // 立即调用一次,之后每次 render 后再调用;返回取消函数
minibar.action(name, payload?)     // 触发插件 actions[name](也可用 $openSettings / $refresh / $allow:<permission>)
minibar.resize(height)             // 请求卡片高度(60–600)
minibar.log(...args)               // 写入插件日志
minibar.locale, minibar.theme.dark

规则:资源只能来自插件包(<script src="app.js"> 相对引用即可,React / Vue / Svelte 打包进包里就行,Vite 请设 base: './');没有网络、iframe、表单、持久存储——数据请在 main.js 里用 ctx.http 取好再放进 data;点击 http/https/mailto 链接会在浏览器打开;主题色以 --minibar-bg / --minibar-text / --minibar-secondary / --minibar-accent / --minibar-success / --minibar-warning / --minibar-danger / --minibar-separator CSS 变量提供,prefers-color-scheme 也有效;WebView 只在卡片可见时存在,面板关闭即销毁。完整示例:docs/examples/web-card/。

8. 错误模型与兼容

8.1 MinibarError

class MinibarError extends Error { code: string; }
code含义
permission_denied插件级权限未授予(卡片显示"需要 X 权限 [授予]")
os_access_denied系统级 TCC 权限被拒(卡片显示"去系统设置")
os_access_pending系统权限弹窗尚未回答(卡片显示"等待 macOS 授权";用户回答后宿主自动重新 render)
host_not_allowed请求域名不在白名单
scheme_not_allowed非 https
not_in_action在 render 中调用了仅限 action 的 API
invalid_argument参数类型/范围错误
storage_quota_exceeded存储超过 1 MB
unavailable宿主能力当前不可用(如定位服务关闭)

8.2 失败与自动禁用

情况宿主行为
render / action 抛出非权限类错误卡片显示错误信息与堆栈摘要;头条跳过;连续 3 次 → 自动禁用
单次执行的 JS 时间超过 5 秒立即终止并自动禁用,原因"timed out"(等待宿主的时间不计)
返回值结构非法视为一次失败
permission_denied / os_access_denied / os_access_pending不计入失败;显示引导横幅

自动禁用后用户可一键重新启用。

8.3 兼容规则


9. 安全约束汇总


10. 完整示例:股票

// com.minibar.stocks/main.js
defineMiniApp({
  id: 'com.minibar.stocks',

  render(ctx) {
    const symbols = String(ctx.settings.get('symbols', 'AAPL,NVDA,TSLA'))
      .split(',').map(s => s.trim().toUpperCase()).filter(Boolean).slice(0, 10);
    if (symbols.length === 0) {
      return { title: null, body: [ctx.ui.text('Add some symbols to get started.'),
                                   ctx.ui.button('Set symbols', '$openSettings')] };
    }

    const cache = ctx.storage.get('quotes', { ts: 0, data: {} });
    if (Date.now() - cache.ts > 60_000) {
      const data = { ...cache.data };
      for (const sym of symbols) {
        const res = ctx.http.getJSON(
          `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?range=1d&interval=1d`);
        const meta = res.ok && res.json?.chart?.result?.[0]?.meta;
        if (meta && meta.regularMarketPrice != null) {
          const prev = meta.chartPreviousClose ?? meta.previousClose ?? meta.regularMarketPrice;
          data[sym] = { price: meta.regularMarketPrice, change: prev ? (meta.regularMarketPrice - prev) / prev * 100 : 0 };
        }
      }
      ctx.storage.set('quotes', { ts: Date.now(), data });
    }

    const quotes = ctx.storage.get('quotes', { data: {} }).data;
    const first = quotes[symbols[0]];
    const rows = symbols.map(sym => {
      const q = quotes[sym];
      if (!q) return ctx.ui.keyValue(sym, '—');
      const up = q.change >= 0;
      return ctx.ui.row([
        ctx.ui.text(sym, { style: 'mono' }),
        ctx.ui.spacer(),
        ctx.ui.text(q.price.toFixed(2), { style: 'mono' }),
        ctx.ui.badge((up ? '+' : '') + q.change.toFixed(2) + '%', { color: up ? 'success' : 'danger' }),
      ], { spacing: 8 });
    });

    return {
      title: first
        ? { text: `${symbols[0]} ${first.price.toFixed(0)} ${first.change >= 0 ? '▲' : '▼'}${Math.abs(first.change).toFixed(1)}%`,
            icon: 'chart.line.uptrend.xyaxis', color: first.change >= 0 ? 'success' : 'danger' }
        : { text: 'Loading…', icon: 'chart.line.uptrend.xyaxis' },
      body: [
        ctx.ui.text('Watchlist', { style: 'headline' }),
        ctx.ui.divider(),
        ctx.ui.column(rows, { spacing: 6 }),
        ctx.ui.spacer(4),
        ctx.ui.row([ctx.ui.button('Refresh', 'refresh'), ctx.ui.spacer(), ctx.ui.button('Edit list', '$openSettings')]),
      ],
    };
  },

  actions: {
    refresh(ctx) { ctx.storage.remove('quotes'); },
  },
});

对应 manifest:

{
  "id": "com.minibar.stocks", "name": "Stocks", "version": "1.0.0",
  "description": "Your watchlist, color-coded.", "author": "Minibar",
  "icon": "chart.line.uptrend.xyaxis", "category": "finance",
  "refreshInterval": 60, "permissions": ["network"], "allowedHosts": ["finance.yahoo.com"],
  "settings": [{ "key": "symbols", "type": "text", "label": "Symbols", "default": "AAPL,NVDA,TSLA",
                 "description": "Comma separated, up to 10." }]
}

11. 开发与调试

分享安装链接

网页或文档里放 minibar://market/<插件 id>(例如 minibar://market/com.example.notesfolder):点击后 Minibar 打开市场并定位到该插件,用户确认权限后才安装;链接本身不会安装任何东西。minibar://install?id=<插件 id> 等价。插件必须已发布到 registry。


11. 市场源:官方源与外部源

市场的插件来自若干源,每个源就是一份 registry.json(§8.1 格式,schemaVersion 2)加上它指向的 npm 包:

源谁维护在 App 里
官方源 https://minibar.084817.xyz/registry.jsonMinibar始终存在,不能修改、不能删除;必须带 registry.sig,验签失败即拒绝;市场显示「已验证来源」
外部源(任意数量)运营它的人用户在「设置 › 市场 › 外部源」里按地址添加或移除;可以不签名(显示「未签名」);每个插件在列表里带来源主机名标签,详情页注明「外部源:host」

合并规则:

11.1 自己发布一个源

任何人都可以运营一个源,给团队内部、私有插件或镜像用:

  1. 插件照 §1 打成 npm 包,发布到任意 npm 兼容源(npmjs、公司内网的 Verdaccio、或者像官方一样放一份静态文件:<源>/<包名>/-/<basename>-<版本>.tgz)。
  2. 生成 registry.json:每个条目至少要有 id、name、version、npm: { name, version }、download(tgz 的完整地址)、sha256(整个 tgz 的哈希)、permissions、allowedHosts;可选 npm.registries 列出你希望 App 优先尝试的 npm 源(按地区)。仓库里的 scripts/build_registry.py 就是官方源的生成器,--resolve-from <你的 npm 源> 会替你下载已发布的包、校验 manifest 并计算哈希。
  3. 用 https 托管 registry.json(任何静态托管都行;本机测试可用 http://127.0.0.1)。签名可选:scripts/sign-registry.sh keygen 生成密钥,sign 产出 registry.sig 放在同一目录;App 只认官方公钥,所以外部源即使签了名也显示为"未签名/未验证",但你可以在自己的内部构建里替换 RegistryTrust 的公钥。
  4. 使用者把地址加进「设置 › 市场 › 外部源」。安装时 App 仍然按你索引里的 sha256 校验每个包、在沙箱里运行,并逐项征求权限。

想进入官方源,走 §10 / 08-plugin-submission.md 的提交流程。