API v1
Documentation/开发指南

发布状态

让按钮、流程和超级块读取插件当前的值。

Plugin API v1

Documentation content is currently available in Simplified Chinese.

如果说“动作”是插件接收指令的手脚,那么**状态(State)**就是插件向外表达的眼睛与窗口。

当你的插件需要持续向用户展示实时数值或运行状态时,发布状态是最佳选择。例如:

  • 音乐播放器发布“正在播放 / 暂停”状态及曲目信息;
  • 系统监控组件发布“当前 CPU 占用率”或“网络上下行速率”;
  • 智能家居开关发布设备当前的“通电 / 断电”状态。

飞块会妥善缓存插件最新发布的状态快照。无论是物理按键的外观切换、自动化流程的条件判断,还是超级块(Widget)屏幕的实时渲染,都可以随时订阅并读取这些数据。


#1. 声明状态来源(manifest.json)

要让飞块与用户理解你的状态,首先需要在清单的 stateSources 中为它定义清晰的契约规格。

下面是来自“状态开关”示例的清单片段。该示例使用底层枚举值 on 与 off 表示开关状态:

json
{
  "apiVersion": 1,
  "id": "example.state-button",
  "name": "状态按钮",
  "version": "1.0.0",
  "description": "状态按钮教学示例",
  "hostPlatforms": [
    "windows",
    "macos",
    "linux"
  ],
  "main": "index.js",
  "permissions": [],
  "contributes": {
    "actions": [
      {
        "id": "enable",
        "title": "开启",
        "argsSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        },
        "outputSchema": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string"
            }
          },
          "required": [
            "status"
          ],
          "additionalProperties": false
        },
        "titleKey": "actions.enable.title"
      },
      {
        "id": "disable",
        "title": "关闭",
        "argsSchema": {
          "type": "object",
          "properties": {},
          "required": [],
          "additionalProperties": false
        },
        "outputSchema": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string"
            }
          },
          "required": [
            "status"
          ],
          "additionalProperties": false
        },
        "titleKey": "actions.disable.title"
      }
    ],
    "stateSources": [
      {
        "id": "status",
        "title": "开关状态",
        "description": "教学开关,不修改实际系统静音",
        "scope": "global",
        "visibility": "global",
        "readAccess": {
          "workflows": true,
          "widgets": true
        },
        "writeAccess": "owner-plugin",
        "lifetime": "plugin",
        "schema": {
          "type": "string",
          "enum": [
            "on",
            "off"
          ],
          "enumLabels": [
            "已开启",
            "已关闭"
          ]
        },
        "titleKey": "stateSources.status.title",
        "descriptionKey": "stateSources.status.description"
      }
    ],
    "buttonPresets": [
      {
        "id": "switch",
        "title": "教学状态开关",
        "actionId": "enable",
        "button": {
          "title": "开关",
          "icon": {
            "kind": "emoji",
            "text": "💡"
          },
          "size": "1x1",
          "titleKey": "buttonPresets.switch.button.title"
        },
        "multiState": {
          "sourceId": "status",
          "states": [
            {
              "id": "on",
              "actionId": "disable",
              "button": {
                "title": "已开启",
                "icon": {
                  "kind": "emoji",
                  "text": "💡"
                },
                "titleKey": "buttonPresets.switch.multiState.states.on.button.title"
              }
            },
            {
              "id": "off",
              "actionId": "enable",
              "button": {
                "title": "已关闭",
                "icon": {
                  "kind": "emoji",
                  "text": "💤"
                },
                "titleKey": "buttonPresets.switch.multiState.states.off.button.title"
              }
            },
            {
              "id": "unknown",
              "button": {
                "title": "等待状态",
                "icon": {
                  "kind": "none"
                },
                "titleKey": "buttonPresets.switch.multiState.states.unknown.button.title"
              }
            }
          ]
        },
        "titleKey": "buttonPresets.switch.title"
      }
    ]
  },
  "i18n": {
    "defaultLocale": "zh-CN",
    "resources": {
      "zh-CN": "locales/zh-CN.json",
      "en-US": "locales/en-US.json"
    }
  },
  "nameKey": "plugin.name",
  "descriptionKey": "plugin.description"
}

设计亮点与细节:

  • 程序语义与友好名称分离:代码内部使用整洁的机器码(如 "on" / "off"),而通过 enumLabels 为用户呈现直观的中文标签(“已开启” / “已关闭”)。当用户在工作流中设置条件分支时,无需手动盲猜字符串,可以直接在下拉菜单中选择。
  • 清晰的说明文案:description 字段会显示在编辑器中,帮助用户理解该状态的具体含义。如果是连续数值(如室内温度或下载速度),建议在描述中注明单位(如 摄氏度 (℃) 或 MB/s)。

📚 深入参考:更多关于数据类型校验与可视性约束的细节,请参阅 StateSource 参考文档。


#2. 在后台发布最新值(index.js)

在插件后台逻辑中,随时可以通过 context.states.publish() 向外广播最新的数值:

js
"use strict";
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);
let status = "off";
async function set(value, context) {
  status = value;
  await context.states.publish("status", status);
  return { status };
}
definePlugin({
  onStart(context) {
    return set("off", context);
  },
  actions: {
    enable(_args, context) {
      return set("on", context);
    },
    disable(_args, context) {
      return set("off", context);
    },
  },
});

发布机制要点:

  • 及时初始化:推荐像示例一样,在插件启动时(onStart 或构造阶段)立即发布一个初始状态(如 "off"),避免界面因等待首个数据包而处于空白等待。
  • 动态响应变化:每当有外部事件、定时轮询或用户执行动作改变了业务逻辑时,调用 publish(sourceId, value)。
    • 第一个参数为清单中声明的状态来源 ID;
    • 第二个参数必须严格契合该状态来源定义的 schema 数据类型。

#3. 决定状态的作用域归属(Scope)

状态应该被谁看见?飞块提供了灵活的作用域机制,最常用的两种类型是:

#全局状态(global)

  • 典型场景:系统静音状态、网络连接状态、全局授权状态等。
  • 特点:对于同一台电脑,无论把按钮放在哪个物理按键上,或者在不同手机、平板上打开页面,它们看到的都应当是同一份唯一的真实数据。

#实例级状态(widget-instance)

  • 典型场景:超级块计数器、不同城市的独立天气卡片、多路计时器。
  • 特点:即使同一个插件被用户同时放置到了屏幕的多个位置,各个位置也能拥有互不干扰、独立运作的私有状态。
  • 发布方式:发布实例状态时,需要额外指定目标实例的 deviceId、screenId 和 blockId。你可以通过 context.widgets.instances() 查询当前已放置的所有实例清单。

🔍 更多作用域:关于设备作用域或其它更高级的隔离规则,请查阅 StateSource 作用域和可见范围。


#4. 界面如何联动与消费状态?

状态发布出去之后,可以极为方便地被各种界面元件直接消费:

  • 实体按键外观联动:按键预设可以根据枚举状态的切换,自动变更图标与背景颜色(例如开启时变绿、关闭时变灰)。详见提供按钮预设。
  • 超级块实时渲染:在超级块的 HTML 页面中,通过引入页面 SDK 并监听 FlyonDeckWidget.onState(),每当后台有新数据发布,页面便会毫秒级收到通知并重绘 UI。

#5. 生命周期与数据持久化实践

  • 内存存储机制:飞块默认在运行内存中维护状态快照。当插件被主动停止、禁用或发生重启时,订阅者可能会短暂收到 null。前端页面应当做好健壮性处理,优雅地展示“等待连接”或骨架屏占位,避免页面脚本崩溃。
  • 需要开机保持的数据:状态发布系统不等于持久化数据库。如果你的插件记录了需要长期保留的业务数据(如用户的自定义待办事项或历史统计总数),请在后台借助本地存储保存到数据目录,并在插件下次启动后重新计算并发布。