# 开发动作

在飞块中，**动作（Action）**是用户主动触发的核心业务操作。无论是轻触一下实体按键，还是在自动化流程中组合连招，动作都是最基础、最强大的构建单元。

常见的动作场景非常广泛：
- **数据转换类**：比如“拆分文本”，接收一段文字并切分成数组供后续环节使用；
- **系统联动类**：比如“打开应用”或“调节音量”，接收用户指定的参数并与操作系统深度交互；
- **网络服务类**：比如“发送 Webhook”或“查询天气”，拉取外部服务并返回解析结果。

开发一个新动作的过程非常标准：**先在清单中声明动作的输入与输出契约，再在后台入口中使用对应 ID 注册处理逻辑**。

---

## 1. 声明输入与输出（manifest.json）

输入与输出的契约是连接用户界面与后台逻辑的桥梁。下面是来自“文本工具箱”示例的清单声明，它接收一段主文本 `text` 和一个可选的分隔符 `separator`，并输出拆分后的列表与总项数：



```json
{
  "apiVersion": 1,
  "id": "example.text-toolkit",
  "name": "文本处理工具",
  "version": "1.0.0",
  "description": "文本处理工具教学示例",
  "hostPlatforms": [
    "windows",
    "macos",
    "linux"
  ],
  "main": "index.js",
  "permissions": [],
  "contributes": {
    "actions": [
      {
        "id": "split",
        "title": "整理文本",
        "description": "按分隔符拆分，去除空白项，最多返回前 100 项",
        "argsSchema": {
          "type": "object",
          "properties": {
            "text": {
              "type": "string",
              "title": "文本",
              "format": "multiline",
              "maxLength": 2000,
              "titleKey": "actions.split.argsSchema.properties.text.title"
            },
            "separator": {
              "type": "string",
              "title": "分隔符",
              "default": ",",
              "maxLength": 8,
              "titleKey": "actions.split.argsSchema.properties.separator.title"
            }
          },
          "required": [
            "text"
          ],
          "additionalProperties": false
        },
        "outputSchema": {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 100
            },
            "count": {
              "type": "integer"
            }
          },
          "required": [
            "items",
            "count"
          ],
          "additionalProperties": false
        },
        "titleKey": "actions.split.title",
        "descriptionKey": "actions.split.description"
      }
    ]
  },
  "i18n": {
    "defaultLocale": "zh-CN",
    "resources": {
      "zh-CN": "locales/zh-CN.json",
      "en-US": "locales/en-US.json"
    }
  },
  "nameKey": "plugin.name",
  "descriptionKey": "plugin.description"
}
```



**为什么这样设计？**
- **`argsSchema`（参数表单）**：飞块会根据你声明的数据结构，自动在动作设置面板中为用户生成直观易用的输入表单（支持文本、下拉选择、数字输入乃至文件选择器）。
- **`outputSchema`（步骤结果）**：定义了动作执行完成后的返回结构。这里之所以推荐返回一个对象（如 `{ items, count }`），是因为后续流程中的其它动作可能各取所需——有的步骤只需要处理数组 `items`，而有的步骤仅需利用 `count` 做条件分支判断。

> 📚 **深入参考**：完整的参数校验规则与字段属性，请参阅 [Action 清单参考](../reference/actions.md)。

---

## 2. 编写处理函数（index.js）

在后台 JavaScript 代码中，通过 `definePlugin` 注册该动作的具体业务逻辑：



```js
"use strict";
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);
definePlugin({
  actions: {
    split({ text, separator = "," }) {
      const items = text
        .split(separator)
        .map((s) => s.trim())
        .filter(Boolean)
        .slice(0, 100);
      return { items, count: items.length };
    },
  },
});
```



### 参数处理与默认值
飞块会在调用前根据 `argsSchema` 自动完成数据类型的基础校验。然而，对于可选字段（如这里的 `separator`），当用户没有在界面填写时，参数可能为 `undefined`。因此，推荐使用 ES6 的**函数默认参数**或空值合并运算符（`??`）赋予安全初始值。

### 优雅的错误反馈
当用户传入的参数符合数据类型、但因逻辑冲突无法继续执行时（例如处理 CSV 时发现“数据缺少关键列”），请**直接抛出一个包含友好说明的 `Error`**：

```javascript
if (!fileContent.includes("name")) {
  throw new Error("处理失败：选中的 CSV 文件中未找到 'name' 列，请检查文件格式。");
}
```

飞块会敏锐捕捉到抛出的异常，并在宿主界面中以红色醒目标记提示用户，同时中止后续依赖该结果的步骤。这样，用户能立即明白问题出在哪里，而不需要盲目排查。

---

## 3. 在工作流中使用动作结果

动作不仅能单点绑定到按键上，更能在飞块的**动作流程（Workflow）**中大放异彩：

- 当用户在“步骤结果”中选中本动作时，填入 `items` 即可提取拆分后的列表，填入 `count` 即可拿到项数。
- **请务必保持函数返回值的字段名与 `outputSchema` 严格一致**，否则工作流下游步骤将无法识别并获取到数据。

> 💡 **概念辨析：动作（Action） vs 状态（State）**  
> 动作返回的结果是**单次执行的瞬时产物**，只属于本次触发。如果你需要展示的是持续变动的指标（例如系统音量、CPU 负载或歌曲播放进度），请移步阅读[发布状态](./states.md)。

---

## 4. 深度调用系统能力

动作处理函数的第二个参数 `context` 是飞块为插件准备的超级工具箱。例如，想要在用户默认浏览器中打开特定网址，无需引入第三方库，直接调用飞块提供的宿主能力：

```javascript
await context.system.openExternal(url);
```

> ⚠️ **权限申请提醒**：调用敏感系统操作或发起网络请求时，务必先在 `manifest.json` 的 `permissions` 数组中声明对应权限，以保障用户安全。具体 API 与权限对照请参阅 [context.system 参考](../reference/system.md)。

---

## 5. 处理耗时任务与并发互斥

绝大多数动作使用飞块默认的执行策略即可。但在处理高频触发或大文件时，推荐了解以下高级配置：

- **防止界面假死（超时配置）**：如果动作涉及大型文件转码或复杂的网络请求，可以在清单的 `execution.timeoutMs` 中合理配置等待超时（默认通常为合理区间）。
- **资源互斥锁（并发控制）**：如果有两个不同的动作会并发操作同一份临界资源（比如都在向同一个本地数据库文件写数据），可以在它们各自的 `execution.resource` 中填入相同的资源标识字符串。飞块会自动为它们施加排队互斥锁，避免并发写冲突。

> ⚠️ **关于超时的注意事项**：当触发 `timeoutMs` 时，意味着飞块的工作流引擎停止继续等待并判定该步骤超时，但后台 Node.js 进程中的异步任务可能仍在后台运行。因此，对于具有外部副作用的操作（例如写磁盘、扣费扣款等），在重试前请做好幂等确认。具体定义见 [ActionExecution 参考](../reference/actions.md#actionexecution)。

## 动态候选项

当参数值来自用户环境或插件数据时，在普通动作的顶层字符串参数声明 `dynamicOptions: true`。宿主提供搜索界面，插件自行查询候选数据；宿主不识别候选项的业务含义。

```json
{
  "id": "choose-entry",
  "title": "选择条目",
  "argsSchema": {
    "type": "object",
    "properties": { "entryId": { "type": "string", "title": "条目", "dynamicOptions": true, "maxLength": 256 } },
    "required": ["entryId"],
    "additionalProperties": false
  }
}
```

后台按动作 ID 和参数名注册候选项处理器。下面的内存数据仅用于演示，实际插件可换成自己的文件或接口查询。

```js
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);
const entries = [{ label: "条目 A", value: "entry-a" }, { label: "条目 B", value: "entry-b" }];
definePlugin({
  actions: {
    "choose-entry"({ entryId }) {
      const entry = entries.find(item => item.value === entryId);
      if (!entry) throw new Error("所选条目已不存在，请重新选择");
      return { entryId: entry.value };
    }
  },
  actionOptions: {
    "choose-entry": {
      entryId({ query, selectedValue }) {
        const matched = entries.filter(item => item.label.includes(query));
        const selected = entries.find(item => item.value === selectedValue);
        return [...(selected ? [selected] : []), ...matched.filter(item => item.value !== selectedValue)].slice(0, 50);
      }
    }
  }
});
```

按钮动作、按钮预设、数值控件的固定参数均使用搜索选择器；流程动作参数旁的选择入口打开同一选择器。流程仍可把参数绑定到状态、输入或步骤结果；选择固定值前需先移除已连接的表达式。

查询不会执行动作。保存的配置只包含 value，不保存候选项列表或显示名称。插件返回错误时保留已有值并显示错误；选项更名后通过 selectedValue 回查新名称；目标已删除时保留原 ID，等待用户重新选择。字段联动、条件显示和全局设置动态候选项不在本接口范围内。
