开发动作
把业务函数变成按钮和流程可执行的动作。
Documentation content is currently available in Simplified Chinese.
在飞块中,**动作(Action)**是用户主动触发的核心业务操作。无论是轻触一下实体按键,还是在自动化流程中组合连招,动作都是最基础、最强大的构建单元。
常见的动作场景非常广泛:
- 数据转换类:比如“拆分文本”,接收一段文字并切分成数组供后续环节使用;
- 系统联动类:比如“打开应用”或“调节音量”,接收用户指定的参数并与操作系统深度交互;
- 网络服务类:比如“发送 Webhook”或“查询天气”,拉取外部服务并返回解析结果。
开发一个新动作的过程非常标准:先在清单中声明动作的输入与输出契约,再在后台入口中使用对应 ID 注册处理逻辑。
#1. 声明输入与输出(manifest.json)
输入与输出的契约是连接用户界面与后台逻辑的桥梁。下面是来自“文本工具箱”示例的清单声明,它接收一段主文本 text 和一个可选的分隔符 separator,并输出拆分后的列表与总项数:
{
"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 清单参考。
#2. 编写处理函数(index.js)
在后台 JavaScript 代码中,通过 definePlugin 注册该动作的具体业务逻辑:
"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:
if (!fileContent.includes("name")) {
throw new Error("处理失败:选中的 CSV 文件中未找到 'name' 列,请检查文件格式。");
}飞块会敏锐捕捉到抛出的异常,并在宿主界面中以红色醒目标记提示用户,同时中止后续依赖该结果的步骤。这样,用户能立即明白问题出在哪里,而不需要盲目排查。
#3. 在工作流中使用动作结果
动作不仅能单点绑定到按键上,更能在飞块的**动作流程(Workflow)**中大放异彩:
- 当用户在“步骤结果”中选中本动作时,填入
items即可提取拆分后的列表,填入count即可拿到项数。 - 请务必保持函数返回值的字段名与
outputSchema严格一致,否则工作流下游步骤将无法识别并获取到数据。
💡 概念辨析:动作(Action) vs 状态(State)
动作返回的结果是单次执行的瞬时产物,只属于本次触发。如果你需要展示的是持续变动的指标(例如系统音量、CPU 负载或歌曲播放进度),请移步阅读发布状态。
#4. 深度调用系统能力
动作处理函数的第二个参数 context 是飞块为插件准备的超级工具箱。例如,想要在用户默认浏览器中打开特定网址,无需引入第三方库,直接调用飞块提供的宿主能力:
await context.system.openExternal(url);⚠️ 权限申请提醒:调用敏感系统操作或发起网络请求时,务必先在
manifest.json的permissions数组中声明对应权限,以保障用户安全。具体 API 与权限对照请参阅 context.system 参考。
#5. 处理耗时任务与并发互斥
绝大多数动作使用飞块默认的执行策略即可。但在处理高频触发或大文件时,推荐了解以下高级配置:
- 防止界面假死(超时配置):如果动作涉及大型文件转码或复杂的网络请求,可以在清单的
execution.timeoutMs中合理配置等待超时(默认通常为合理区间)。 - 资源互斥锁(并发控制):如果有两个不同的动作会并发操作同一份临界资源(比如都在向同一个本地数据库文件写数据),可以在它们各自的
execution.resource中填入相同的资源标识字符串。飞块会自动为它们施加排队互斥锁,避免并发写冲突。
⚠️ 关于超时的注意事项:当触发
timeoutMs时,意味着飞块的工作流引擎停止继续等待并判定该步骤超时,但后台 Node.js 进程中的异步任务可能仍在后台运行。因此,对于具有外部副作用的操作(例如写磁盘、扣费扣款等),在重试前请做好幂等确认。具体定义见 ActionExecution 参考。
#动态候选项
当参数值来自用户环境或插件数据时,在普通动作的顶层字符串参数声明 dynamicOptions: true。宿主提供搜索界面,插件自行查询候选数据;宿主不识别候选项的业务含义。
{
"id": "choose-entry",
"title": "选择条目",
"argsSchema": {
"type": "object",
"properties": { "entryId": { "type": "string", "title": "条目", "dynamicOptions": true, "maxLength": 256 } },
"required": ["entryId"],
"additionalProperties": false
}
}后台按动作 ID 和参数名注册候选项处理器。下面的内存数据仅用于演示,实际插件可换成自己的文件或接口查询。
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,等待用户重新选择。字段联动、条件显示和全局设置动态候选项不在本接口范围内。