Appearance
节点开发规范
本文说明如何新增工作流节点,涵盖类型定义、执行器、配置面板与 UI 组件的完整开发流程。
概述
Automa Agent 的每个节点由类型定义、执行器、配置面板与画布 UI 四部分组成。开发新节点时须按规范在多处注册,确保设计器与运行时行为一致。
目录结构
text
src/
├── automa/types/
│ ├── command-types.ts # 命令分类定义(节点面板显示)
│ ├── node-config.ts # 节点枚举、UI配置、表单配置
│ └── node-types.ts # 节点类型定义(TypeScript接口)
├── main/engine/
│ ├── BaseNodeExecutor.ts # 执行器基类
│ └── executors/ # 节点执行器目录
│ ├── MouseClickExecutor.ts
│ ├── MouseMoveExecutor.ts
│ ├── KeyboardInputExecutor.ts
│ ├── OpenAppExecutor.ts
│ ├── OpenBrowserWindowExecutor.ts
│ └── DelayExecutor.ts
└── renderer/pages/workflow/
├── nodes/ # 节点 UI 组件
│ ├── MouseClickNode.vue
│ └── OpenAppNode.vue
└── components/ConfigPanel/
├── ConfigPanel.vue # 配置面板
├── ResourceSelector.vue # 资源选择器
├── AppSelector.vue # 应用选择器
└── WorkflowSelector.vue # 工作流选择器节点开发完整检查清单
开发新节点时,必须完成以下所有步骤,缺一不可:
| 步骤 | 文件 | 操作 | 必需 |
|---|---|---|---|
| 1 | command-types.ts | 在 CommandCategories 中添加命令定义 | ✓ |
| 2 | node-config.ts | 在 NodeKey 枚举中添加节点键 | ✓ |
| 3 | node-config.ts | 在 NodeTypesConfig 中添加节点UI配置 | ✓ |
| 4 | node-config.ts | 定义节点表单配置(xxxFormConfig) | ✓ |
| 5 | node-types.ts | 定义节点配置接口(xxxNodeDefinition) | ✓ |
| 6 | node-types.ts | 在 SpecificNodeDefinition 联合类型中添加 | ✓ |
| 7 | node-types.ts | 在 NodeDefinitionMap 映射中添加 | ✓ |
| 8 | executors/ | 创建节点执行器类 | ✓ |
| 9 | nodes/ | 创建节点 UI 组件 | ✓ |
| 10 | ConfigPanel.vue | 导入并注册表单配置 | ✓ |
节点开发步骤详解
1. 注册命令类型(command-types.ts)
在 src/automa/types/command-types.ts 的 CommandCategories 中添加命令定义:
typescript
// 找到对应的分类,添加命令
{
key: 'desktop',
label: '桌面自动化',
icon: 'icon-desktop',
commands: [
// ... 其他命令
{
id: 'custom-node', // 唯一ID
name: '自定义节点', // 显示名称
type: 'custom-node', // 节点类型(与 NodeKey 对应)
icon: 'icon-xxx', // 图标
desc: '节点功能描述' // 描述文字
},
],
},2. 定义节点类型(node-types.ts)
在 src/automa/types/node-types.ts 中定义节点配置接口:
typescript
/**
* 自定义节点配置接口
* @interface CustomNodeDefinition
* @extends BaseNodeDefinition
*/
export interface CustomNodeDefinition extends BaseNodeDefinition {
type: NodeKey.CUSTOM_NODE;
config: {
/** 配置项1 */
option1: string;
/** 配置项2 */
option2?: number;
};
}
// 在 SpecificNodeDefinition 联合类型中添加
export type SpecificNodeDefinition =
| DelayNodeDefinition
// ... 其他节点
| CustomNodeDefinition;
// 在 NodeDefinitionMap 映射中添加
export type NodeDefinitionMap = {
// ... 其他节点
'custom-node': CustomNodeDefinition;
};3. 注册节点类型(node-config.ts)
在 src/automa/types/node-config.ts 中添加节点键、UI配置和表单配置:
typescript
// 1. 在 NodeKey 枚举中添加节点键
export enum NodeKey {
// ... 其他节点
CUSTOM_NODE = 'custom-node', // 与 command-types.ts 中的 type 保持一致
}
// 2. 在 NodeTypesConfig 中添加节点UI配置
export const NodeTypesConfig: NodeTypeI[] = [
// ... 其他节点
{
label: '自定义节点',
key: NodeKey.CUSTOM_NODE,
type: NodeTreeType.NODE,
desc: '自定义节点描述',
color: '#1890ff',
icon: 'erpfont icon-xxx',
},
];
// 3. 定义表单配置
export const customNodeFormConfig: DynamicFormI = {
showSide: false,
cols: 1,
fields: [
{
label: '配置项1',
key: 'option1',
type: 'text',
span: 1,
required: true,
},
{
label: '配置项2',
key: 'option2',
type: 'integer',
span: 1,
},
],
};4. 创建执行器(executors/)
在 src/main/engine/executors/ 目录创建执行器类,文件名为 CustomNodeExecutor.ts:
typescript
import { BaseNodeExecutor } from '../BaseNodeExecutor';
import type { ExecutionContext, NodeExecutionResult } from '@automa/types/engine-types';
import type { CustomNodeDefinition } from '@automa/types/node-types';
import { NodeKey } from '@automa/types/node-config';
/**
* 自定义节点执行器
* @description 实现自定义节点的执行逻辑
*/
export default class CustomNodeExecutor extends BaseNodeExecutor {
type = NodeKey.CUSTOM_NODE;
async execute(node: CustomNodeDefinition, context: ExecutionContext): Promise<NodeExecutionResult> {
const option1 = this.getConfig<string>(node, 'option1', '');
const option2 = this.getConfig<number>(node, 'option2', 0);
try {
// 实现执行逻辑
// 可以使用 this.desktopNative 访问原生功能
// 解析变量表达式
const resolvedValue = this.resolveExpression(option1, context);
// 使用延时
await this.delay(100);
// 返回成功结果
return this.success({ option1: resolvedValue, option2 });
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return this.failure(`执行失败: ${errMsg}`);
}
}
}注意:执行器文件名必须以 Executor.ts 结尾,类名必须使用 export default 导出。
5. 注册表单配置(ConfigPanel.vue)
在 src/renderer/pages/workflow/components/ConfigPanel/ConfigPanel.vue 中注册表单配置:
typescript
// 1. 导入表单配置
import {
// ... 其他配置
customNodeFormConfig,
NodeKey,
} from '@automa/types/node-config';
// 2. 在 currentFormConfig 计算属性的 switch 中添加 case
const currentFormConfig = computed<DynamicFormI>(() => {
const nodeType = parseNodeType(node.value?.type);
switch (nodeType) {
// ... 其他节点
case NodeKey.CUSTOM_NODE:
return customNodeFormConfig;
default:
return null;
}
});完整示例:OpenBrowserWindow 节点
以下是 open-browser-window 节点的完整实现示例,展示了所有步骤的实际代码:
command-types.ts
typescript
{
key: 'desktop',
label: '桌面自动化',
commands: [
{ id: 'open-browser-window', name: '打开窗口', type: 'open-browser-window', icon: 'icon-desktop', desc: '打开一个 BrowserWindow 窗口' },
],
},node-config.ts
typescript
// NodeKey 枚举
export enum NodeKey {
OPEN_BROWSER_WINDOW = 'open-browser-window',
}
// NodeTypesConfig 配置
{
label: '打开浏览器窗口',
key: NodeKey.OPEN_BROWSER_WINDOW,
type: NodeTreeType.NODE,
desc: '打开一个 BrowserWindow 窗口',
color: '#1890ff',
icon: 'erpfont icon-desktop',
},
// 表单配置
export const openBrowserWindowFormConfig: DynamicFormI = {
showSide: false,
cols: 1,
fields: [
{ label: '选择窗口配置', key: 'workflowId', type: 'custom', component: 'workflow-selector' },
{ label: '窗口名称', key: 'name', type: 'text' },
{ label: '窗口地址', key: 'host', type: 'text' },
// ... 更多字段
],
};node-types.ts
typescript
export interface OpenBrowserWindowNodeDefinition extends BaseNodeDefinition {
type: NodeKey.OPEN_BROWSER_WINDOW;
config: {
workflowId?: string;
name?: string;
host?: string;
preload?: string;
preloadCode?: string;
json?: string;
};
}
// 添加到 SpecificNodeDefinition
export type SpecificNodeDefinition =
| ...
| OpenBrowserWindowNodeDefinition;
// 添加到 NodeDefinitionMap
export type NodeDefinitionMap = {
'open-browser-window': OpenBrowserWindowNodeDefinition;
};OpenBrowserWindowExecutor.ts
typescript
export default class OpenBrowserWindowExecutor extends BaseNodeExecutor {
type = NodeKey.OPEN_BROWSER_WINDOW;
async execute(node: OpenBrowserWindowNodeDefinition, _context: ExecutionContext): Promise<NodeExecutionResult> {
// 实现执行逻辑
}
}ConfigPanel.vue
typescript
import { openBrowserWindowFormConfig } from '@automa/types/node-config';
// switch 中添加
case NodeKey.OPEN_BROWSER_WINDOW:
return openBrowserWindowFormConfig;执行器基类 API
属性
| 属性 | 类型 | 说明 |
|---|---|---|
type | NodeKey | 节点类型标识 |
desktopNative | DesktopNative | 原生功能访问 |
elementStorageService | ElementStorageService | 元素存储服务 |
方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
execute | node, context | Promise<NodeExecutionResult> | 执行节点 |
success | output?, nextNodeId? | NodeExecutionResult | 创建成功结果 |
failure | error | NodeExecutionResult | 创建失败结果 |
getConfig<T> | node, key, defaultValue? | T | 获取节点配置值 |
getVariable<T> | context, name, defaultValue? | T | 获取上下文变量 |
setVariable | context, name, value | void | 设置上下文变量 |
resolveExpression | expression, context | string | 解析变量表达式 |
delay | ms | Promise<void> | 延时 |
getPositionByElement | element | Promise<{x, y}> | 获取元素/图像位置 |
表单字段类型
| 类型 | 说明 | 示例 |
|---|---|---|
text | 文本输入 | 名称、路径 |
textarea | 多行文本 | 描述 |
integer | 整数输入 | 超时时间 |
number | 数字输入 | 坐标、速度 |
select | 下拉选择 | 按钮类型 |
switch | 开关 | 启用/禁用 |
custom | 自定义组件 | 资源选择器 |
自定义组件
typescript
{
label: '选择资源',
key: 'element',
type: 'custom',
component: 'resource-selector', // 组件名
span: 1,
}已注册的自定义组件:
color-picker- 颜色选择器resource-selector- 资源选择器(元素/图像)app-selector- 应用选择器workflow-selector- 工作流选择器monaco-editor- 代码编辑器
条件显示
使用 show 函数控制字段显示:
typescript
{
label: '超时时间',
key: 'timeout',
type: 'integer',
show(modelValue) {
return modelValue.waitForStart === true;
},
}最佳实践
- 错误处理:始终使用 try-catch 包装执行逻辑
- 类型安全:为配置项定义明确的类型
- 变量解析:使用
resolveExpression支持动态值 - 日志记录:使用
createLog记录执行过程 - 资源定位:使用
getPositionByElement统一元素/图像定位 - 命名一致性:确保 command-types.ts 中的 type、NodeKey 枚举值、NodeDefinitionMap 键保持一致
常见问题
节点在面板中不显示
检查 command-types.ts 中是否添加了命令定义。
节点配置面板为空
检查 ConfigPanel.vue 中是否:
- 导入了表单配置
- 在 switch 语句中添加了对应的 case
节点执行报类型错误
检查 node-types.ts 中是否:
- 定义了节点接口
- 添加到了
SpecificNodeDefinition联合类型 - 添加到了
NodeDefinitionMap映射
执行器未被调用
检查执行器文件:
- 文件名是否以
Executor.ts结尾 - 类是否使用
export default导出 type属性是否与NodeKey枚举值一致