chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 0acef64f48
200 changed files with 35546 additions and 0 deletions
+132
View File
@@ -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}};
};
+23
View File
@@ -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}};
};
+32
View File
@@ -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};
};
+35
View File
@@ -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}};
};
+37
View File
@@ -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};
};
+31
View File
@@ -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}};
};
+25
View File
@@ -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}};
};
+40
View File
@@ -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};
};
+28
View File
@@ -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}};
};