chore: initial commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import {Worker} from 'node:worker_threads';
|
||||
import type {BranchState, NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import type {Json} from '../../../types/bot/workflow/workflow.ts';
|
||||
import {describe, failure_port, output, success_port} from '../metadata.ts';
|
||||
import {assert_json} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'code', version: '1.0.0',
|
||||
i18n: describe('执行代码', '执行 JavaScript 异步函数体,通过 state 读取状态副本,并返回 JSON 值;是否可用由宿主配置决定。'),
|
||||
config: [
|
||||
{name: 'source', type: 'code', language: 'javascript', required: true, max_length: 100_000,
|
||||
i18n: describe('JavaScript 代码', '最多 100000 个字符,可使用 await;通过 return 返回有效 JSON 值,修改 state 副本不会写回工作流状态。')},
|
||||
],
|
||||
output: output('json', '代码返回值', '代码通过 return 返回并经校验的 JSON 值。'),
|
||||
ports: [success_port, failure_port],
|
||||
availability: {available: false, reason: 'CODE_EXECUTION_DISABLED'},
|
||||
};
|
||||
|
||||
const worker_source = `
|
||||
const {parentPort, workerData} = require('node:worker_threads');
|
||||
function serialize_output(output) {
|
||||
const ancestors = new Set();
|
||||
let entries = 0;
|
||||
let bytes = 0;
|
||||
function visit(value, depth) {
|
||||
if (depth > 64 || ++entries > 100000) throw new Error('Code output exceeds JSON limits');
|
||||
if (value === null || typeof value === 'boolean') bytes += 5;
|
||||
else if (typeof value === 'number' && Number.isFinite(value)) bytes += String(value).length;
|
||||
else if (typeof value === 'string') {
|
||||
if (value.length > 1048576) throw new Error('Code output exceeds JSON limits');
|
||||
bytes += Buffer.byteLength(JSON.stringify(value));
|
||||
} else if (typeof value === 'object') {
|
||||
const array = Array.isArray(value);
|
||||
if (!array && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
||||
throw new Error('Code output must contain plain JSON objects');
|
||||
if (ancestors.has(value)) throw new Error('Code output contains a cycle');
|
||||
ancestors.add(value);
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (keys.length > 100000 || (array && value.length > 100000)) throw new Error('Code output exceeds JSON limits');
|
||||
let count = 0;
|
||||
bytes += 2;
|
||||
for (const key of keys) {
|
||||
if (array && key === 'length') continue;
|
||||
if (typeof key !== 'string' || ['__proto__', 'prototype', 'constructor'].includes(key))
|
||||
throw new Error('Code output contains an unsafe key');
|
||||
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
||||
if (!descriptor.enumerable || !('value' in descriptor)) throw new Error('Code output must contain data properties');
|
||||
if (array) {
|
||||
const index = Number(key);
|
||||
if (!Number.isSafeInteger(index) || index < 0 || String(index) !== key || index >= value.length)
|
||||
throw new Error('Code output contains an invalid array');
|
||||
count++;
|
||||
} else {
|
||||
if (key.length > 1048576) throw new Error('Code output exceeds JSON limits');
|
||||
bytes += Buffer.byteLength(JSON.stringify(key)) + 1;
|
||||
}
|
||||
bytes++;
|
||||
visit(descriptor.value, depth + 1);
|
||||
}
|
||||
if (array && count !== value.length) throw new Error('Code output contains an array hole');
|
||||
ancestors.delete(value);
|
||||
} else throw new Error('Code output is not JSON');
|
||||
if (bytes > 1048576) throw new Error('Code output exceeds JSON limits');
|
||||
}
|
||||
try { visit(output, 0); }
|
||||
catch (error) { error.code = 'INVALID_JSON'; throw error; }
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const execute = new AsyncFunction('state', '"use strict";\\n' + workerData.source);
|
||||
const output = await execute(JSON.parse(workerData.state));
|
||||
parentPort.postMessage({status: 'success', output: serialize_output(output)});
|
||||
} catch (error) {
|
||||
parentPort.postMessage({status: 'failure',
|
||||
code: typeof error?.code === 'string' ? error.code.slice(0, 256) : 'CODE_EXECUTION_FAILED',
|
||||
message: (typeof error?.message === 'string' ? error.message : String(error)).slice(0, 4096)});
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal, deps) => {
|
||||
if (node.type !== 'code') throw new Error('Expected a code node');
|
||||
signal.throwIfAborted();
|
||||
assert_json(state);
|
||||
const serialized = JSON.stringify(state);
|
||||
let output: Json;
|
||||
if (deps.execute_code) {
|
||||
output = await deps.execute_code(node.config.source, JSON.parse(serialized) as BranchState, signal);
|
||||
} else {
|
||||
const worker = new Worker(worker_source, {eval: true, execArgv: [],
|
||||
workerData: {source: node.config.source, state: serialized},
|
||||
stdout: true, stderr: true,
|
||||
resourceLimits: {maxOldGenerationSizeMb: 32, maxYoungGenerationSizeMb: 8, stackSizeMb: 4}});
|
||||
// Capture and discard worker output instead of forwarding it to the bot process.
|
||||
worker.stdout.resume();
|
||||
worker.stderr.resume();
|
||||
let on_abort: () => void;
|
||||
try {
|
||||
output = await new Promise<Json>((resolve, reject) => {
|
||||
on_abort = () => reject(signal.reason);
|
||||
worker.once('message', message => {
|
||||
if (message?.status === 'success') {
|
||||
try {
|
||||
if (typeof message.output !== 'string' || Buffer.byteLength(message.output) > 1_048_576) {
|
||||
throw Object.assign(new Error('Invalid code worker output'), {code: 'INVALID_JSON'});
|
||||
}
|
||||
resolve(JSON.parse(message.output));
|
||||
} catch (error) { reject(error); }
|
||||
}
|
||||
else reject(Object.assign(new Error(typeof message?.message === 'string'
|
||||
? message.message : 'Code execution failed'),
|
||||
{code: typeof message?.code === 'string' ? message.code : 'CODE_EXECUTION_FAILED'}));
|
||||
});
|
||||
worker.once('error', reject);
|
||||
worker.once('exit', code => reject(Object.assign(
|
||||
new Error(`Code worker exited before returning a result (${code})`), {code: 'CODE_WORKER_EXITED'})));
|
||||
signal.addEventListener('abort', on_abort, {once: true});
|
||||
if (signal.aborted) on_abort();
|
||||
});
|
||||
} finally {
|
||||
signal.removeEventListener('abort', on_abort!);
|
||||
await worker.terminate();
|
||||
worker.removeAllListeners();
|
||||
}
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
assert_json(output);
|
||||
return {port: 'success', result: {status: 'success', output}};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, port} from '../metadata.ts';
|
||||
import {evaluate_condition} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'condition', version: '1.0.0',
|
||||
i18n: describe('条件判断', '计算条件表达式,根据布尔结果选择为真或为假分支。'),
|
||||
config: [
|
||||
{name: 'condition', type: 'condition', required: true,
|
||||
i18n: describe('判断条件', '支持多个条件全部满足(and/且)、任一满足(or/或)及嵌套分组、取反;每项可使用 eq、contains、gt、lt 比较字面量或状态引用。')},
|
||||
],
|
||||
output: {...output('boolean', '判断结果', '条件表达式的布尔结果,true 和 false 均为成功执行;计算失败时输出 null。'), nullable: true},
|
||||
ports: [port('true', '为真', '条件结果为 true 时执行。'),
|
||||
port('false', '为假', '条件结果为 false 时执行。'), failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal) => {
|
||||
if (node.type !== 'condition') throw new Error('Expected a condition node');
|
||||
signal.throwIfAborted();
|
||||
const output = evaluate_condition(node.config.condition, state);
|
||||
return {port: output ? 'true' : 'false', result: {status: 'success', output}};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type {NodeHandler, NodeResult} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, port} from '../metadata.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'error', version: '1.0.0',
|
||||
i18n: describe('异常分流', '检查上一节点的执行状态和错误代码,保留原执行结果并选择分支。'),
|
||||
config: [
|
||||
{name: 'codes', type: 'array', required: false,
|
||||
i18n: describe('错误代码', '省略此字段时匹配全部失败结果;提供列表时仅匹配其中的错误代码,显式空列表不匹配任何错误。'),
|
||||
item: {name: 'code', type: 'string', required: true,
|
||||
i18n: describe('错误代码', '与上一节点 error.code 精确匹配的字符串。')}},
|
||||
],
|
||||
output: output('json', '上一节点输出', '透传上一节点的 output,缺失时为 null;失败分支保留原失败状态及可选错误。'),
|
||||
ports: [port('normal', '正常', '上一节点不存在或状态不是 failure 时执行,节点状态为 success。'),
|
||||
port('error', '匹配异常', '上一节点失败且匹配错误代码规则时执行,保留 failure 状态。'),
|
||||
port('unmatched', '未匹配异常', '上一节点失败但不匹配错误代码规则时执行,保留 failure 状态。'), failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal) => {
|
||||
if (node.type !== 'error') throw new Error('Expected an error node');
|
||||
signal.throwIfAborted();
|
||||
const previous = state.last_result;
|
||||
if (!previous || previous.status !== 'failure') {
|
||||
return {port: 'normal', result: {status: 'success', output: previous?.output ?? null}};
|
||||
}
|
||||
const result: NodeResult = {status: previous.status, output: previous.output};
|
||||
if (previous.error) result.error = {...previous.error};
|
||||
const matches = node.config.codes === undefined
|
||||
|| (previous.error !== undefined && node.config.codes.includes(previous.error.code));
|
||||
return {port: matches ? 'error' : 'unmatched', result};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, success_port} from '../metadata.ts';
|
||||
import {resolve_value} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'function', version: '1.0.0',
|
||||
i18n: describe('调用函数', '解析参数值后调用机器人函数,返回 true 时成功,否则进入失败分支。'),
|
||||
provider: 'function',
|
||||
config: [
|
||||
{name: 'path', type: 'string', required: true,
|
||||
i18n: describe('函数路径', '函数所属目录,以 / 分隔;每段为 1 至 100 个字母、数字、下划线或连字符,禁止保留名称。')},
|
||||
{name: 'name', type: 'string', required: true, max_length: 100,
|
||||
i18n: describe('函数名称', '函数标识,由 1 至 100 个字母、数字、下划线或连字符组成,禁止保留名称。')},
|
||||
{name: 'args', type: 'array', required: true, default: [], max: 100,
|
||||
i18n: describe('函数参数', '按调用顺序提供参数,最多 100 项,每项可使用字面量或状态引用。'),
|
||||
item: {name: 'argument', type: 'value_source', required: true,
|
||||
i18n: describe('参数值', '解析字面量或状态引用,将得到的 JSON 值作为对应位置的函数参数。')}},
|
||||
],
|
||||
output: {...output('boolean', '函数返回值', '函数返回 true 时输出 true;返回其他值时输出 false,错误代码为 FUNCTION_RETURNED_FALSE;异常或超时输出 null。'), nullable: true},
|
||||
ports: [success_port, failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal, deps) => {
|
||||
if (node.type !== 'function') throw new Error('Expected a function node');
|
||||
signal.throwIfAborted();
|
||||
const output = await deps.run_function(node.config.path, node.config.name,
|
||||
node.config.args.map(source => resolve_value(source, state)), signal);
|
||||
signal.throwIfAborted();
|
||||
if (output !== true) {
|
||||
return {port: 'failure', result: {status: 'failure', output: false,
|
||||
error: {code: 'FUNCTION_RETURNED_FALSE', message: 'Function returned false', node_id: node.id}}};
|
||||
}
|
||||
return {port: 'success', result: {status: 'success', output}};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, port} from '../metadata.ts';
|
||||
import {assert_json} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'goto', version: '1.0.0',
|
||||
i18n: describe('重试跳转', '按当前节点的重试计数限制跳转次数,重试目标由 retry 出口连线指定。'),
|
||||
config: [
|
||||
{name: 'max_retries', type: 'number', required: true, default: 2, integer: true, min: 0, max: 10_000,
|
||||
i18n: describe('最大重试次数', '允许经过 retry 出口的次数;0 表示直接进入次数耗尽分支,默认允许重试 2 次。')},
|
||||
{name: 'delay_ms', type: 'number', required: true, default: 0, integer: true, min: 0, max: 2_147_483_647,
|
||||
i18n: describe('重试间隔', '每次重试跳转前等待的毫秒数,0 表示不增加等待时长。')},
|
||||
],
|
||||
output: output('json', '上一节点输出', '重试或次数耗尽时均透传上一节点的 output,缺失时为 null。'),
|
||||
ports: [port('retry', '重试', '未达到最大重试次数时增加计数、等待指定间隔,再以 success 状态跳转;必须连接重试目标。'),
|
||||
port('exhausted', '次数耗尽', '已达到最大重试次数时以 failure 状态执行,错误代码为 RETRY_EXHAUSTED。'), failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal, deps) => {
|
||||
if (node.type !== 'goto') throw new Error('Expected a goto node');
|
||||
signal.throwIfAborted();
|
||||
const count = Object.hasOwn(state.retry_counts, node.id) ? state.retry_counts[node.id] : 0;
|
||||
if (!Number.isSafeInteger(count) || count < 0 || !Number.isSafeInteger(node.config.max_retries)
|
||||
|| node.config.max_retries < 0 || !Number.isFinite(node.config.delay_ms) || node.config.delay_ms < 0) {
|
||||
throw Object.assign(new Error('Invalid retry configuration or count'), {code: 'INVALID_RETRY'});
|
||||
}
|
||||
if (count >= node.config.max_retries) {
|
||||
return {port: 'exhausted', result: {status: 'failure', output: state.last_result?.output ?? null,
|
||||
error: {code: 'RETRY_EXHAUSTED', message: 'Retry limit exhausted', node_id: node.id}}};
|
||||
}
|
||||
const retry_counts = {[node.id]: count + 1};
|
||||
assert_json(retry_counts);
|
||||
await deps.sleep('milliseconds', node.config.delay_ms, signal);
|
||||
signal.throwIfAborted();
|
||||
return {port: 'retry', result: {status: 'success', output: state.last_result?.output ?? null}, retry_counts};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, success_port} from '../metadata.ts';
|
||||
import {resolve_value} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'log', version: '1.0.0',
|
||||
i18n: describe('记录日志', '解析消息值并按指定级别记录日志,同时输出解析后的原始值。'),
|
||||
config: [
|
||||
{name: 'level', type: 'select', required: true, default: 'info',
|
||||
i18n: describe('日志级别', '选择日志的记录级别;错误级别本身不会使节点执行失败。'),
|
||||
options: [
|
||||
{value: 'info', i18n: describe('信息', '记录一般运行信息。')},
|
||||
{value: 'warning', i18n: describe('警告', '记录需要关注的情况。')},
|
||||
{value: 'error', i18n: describe('错误', '记录错误信息,仍按正常日志节点继续执行。')},
|
||||
]},
|
||||
{name: 'message', type: 'value_source', required: true,
|
||||
i18n: describe('日志消息', '使用字面量或状态引用;字符串直接记录,其他 JSON 值序列化后记录。')},
|
||||
],
|
||||
output: output('json', '消息原值', '解析 message 得到的原始 JSON 值,非字符串值不会因日志序列化而改变输出类型。'),
|
||||
ports: [success_port, failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal, deps) => {
|
||||
if (node.type !== 'log') throw new Error('Expected a log node');
|
||||
signal.throwIfAborted();
|
||||
const output = resolve_value(node.config.message, state);
|
||||
deps.log(node.config.level, typeof output === 'string' ? output : JSON.stringify(output));
|
||||
signal.throwIfAborted();
|
||||
return {port: 'success', result: {status: 'success', output}};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, edge_input_output, failure_port, output, success_port} from '../metadata.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'merge', version: '1.0.0',
|
||||
i18n: describe('分支合流', '按全部入线汇合或每次到达独立继续的方式处理分支,输出本次入线数据。'),
|
||||
config: [
|
||||
{name: 'mode', type: 'select', required: true, default: 'all',
|
||||
i18n: describe('合流方式', '选择是否等待每一条入线;全部入线模式至少需要两条入线且不能作为入口节点。'),
|
||||
options: [
|
||||
{value: 'all', i18n: describe('全部入线', '等待同一轮次的每一条入线均到达后合并状态并执行一次;互斥分支可能导致入线无法到齐。')},
|
||||
{value: 'any', i18n: describe('每次到达', '每条入线每次到达都独立执行,不等待其他入线,也不会只执行首次到达。')},
|
||||
]},
|
||||
],
|
||||
output: {...output('object', '入线数据', '以连线 ID 为键的 inputs 对象;all 包含本轮全部入线,any 包含本次到达的入线。'),
|
||||
nullable: true, additional_properties: edge_input_output},
|
||||
ports: [success_port, failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal) => {
|
||||
if (node.type !== 'merge') throw new Error('Expected a merge node');
|
||||
signal.throwIfAborted();
|
||||
return {port: 'success', result: {status: 'success', output: state.inputs}};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, success_port} from '../metadata.ts';
|
||||
import {assert_json, resolve_value} from '../runtime/value.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'query', version: '1.0.0',
|
||||
i18n: describe('查询数据', '解析命名参数后调用查询提供方,返回 JSON 数据,并可将结果保存到工作流变量。'),
|
||||
provider: 'query',
|
||||
config: [
|
||||
{name: 'source', type: 'string', required: true, provider: 'query', max_length: 100,
|
||||
i18n: describe('查询来源', '选择查询提供方,标识由 1 至 100 个字母、数字、下划线或连字符组成,禁止保留名称。')},
|
||||
{name: 'args', type: 'map', required: true, default: {},
|
||||
i18n: describe('查询参数', '按名称提供参数;键为 1 至 100 个字母、数字、下划线或连字符,禁止保留名称。'),
|
||||
item: {name: 'argument', type: 'value_source', required: true,
|
||||
i18n: describe('参数值', '解析字面量或状态引用,将得到的 JSON 值传给对应名称的查询参数。')}},
|
||||
{name: 'save_as', type: 'string', required: false, max_length: 100,
|
||||
i18n: describe('保存变量', '可选,将查询输出保存到 vars 中的此名称;名称为 1 至 100 个字母、数字、下划线或连字符,禁止保留名称。')},
|
||||
],
|
||||
output: output('json', '查询结果', '查询提供方返回的原始 JSON 值,与可选保存到 vars 的值相同。'),
|
||||
ports: [success_port, failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, state, signal, deps) => {
|
||||
if (node.type !== 'query') throw new Error('Expected a query node');
|
||||
signal.throwIfAborted();
|
||||
const args = Object.fromEntries(Object.entries(node.config.args)
|
||||
.map(([key, source]) => [key, resolve_value(source, state)]));
|
||||
assert_json(args);
|
||||
const output = await deps.query(node.config.source, args, signal);
|
||||
signal.throwIfAborted();
|
||||
assert_json(output);
|
||||
const result = {status: 'success' as const, output};
|
||||
if (node.config.save_as !== undefined) {
|
||||
const vars = {[node.config.save_as]: output};
|
||||
assert_json(vars);
|
||||
return {port: 'success', result, vars};
|
||||
}
|
||||
return {port: 'success', result};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {NodeHandler} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {WorkflowNodeTemplate} from '../../../types/bot/workflow/metadata.ts';
|
||||
import {describe, failure_port, output, success_port} from '../metadata.ts';
|
||||
|
||||
export const config_template: WorkflowNodeTemplate = {
|
||||
name: 'sleep', version: '1.0.0',
|
||||
i18n: describe('等待', '按毫秒或游戏刻等待指定时长,再继续执行。'),
|
||||
config: [
|
||||
{name: 'mode', type: 'select', required: true, default: 'milliseconds',
|
||||
i18n: describe('计时方式', '选择等待时长的单位。'),
|
||||
options: [
|
||||
{value: 'milliseconds', i18n: describe('毫秒', '按毫秒等待,1000 毫秒为 1 秒。')},
|
||||
{value: 'ticks', i18n: describe('游戏刻', '按游戏刻等待,实际时长取决于游戏刻推进速度。')},
|
||||
]},
|
||||
{name: 'value', type: 'number', required: true, default: 1000, integer: true, min: 0, max: 2_147_483_647,
|
||||
i18n: describe('等待时长', '使用所选单位的非负整数时长,0 表示不增加等待时长。')},
|
||||
],
|
||||
output: output('null', '等待结果', '等待完成后输出 null。'),
|
||||
ports: [success_port, failure_port],
|
||||
};
|
||||
|
||||
export const execute: NodeHandler = async (node, _state, signal, deps) => {
|
||||
if (node.type !== 'sleep') throw new Error('Expected a sleep node');
|
||||
signal.throwIfAborted();
|
||||
await deps.sleep(node.config.mode, node.config.value, signal);
|
||||
signal.throwIfAborted();
|
||||
return {port: 'success', result: {status: 'success', output: null}};
|
||||
};
|
||||
Reference in New Issue
Block a user