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}};
};