# 发布状态

如果说“动作”是插件接收指令的手脚，那么**状态（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 参考文档](../reference/states.md)。

---

## 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()](../reference/widgets-api.md) 查询当前已放置的所有实例清单。

> 🔍 **更多作用域**：关于设备作用域或其它更高级的隔离规则，请查阅 [StateSource 作用域和可见范围](../reference/states.md#作用域和可见范围)。

---

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

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

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

---

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

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