feat: 补全代码节点输入输出智能同步
- 为 JavaScript 和 Python 新节点生成 main 与显式调用脚手架 - 同步输入签名、静态返回字段和输出参数并推断受支持类型 - 增加安全改写边界与 Tinyflow 定向测试
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import type { Parameter } from '#types';
|
||||
|
||||
export type CodeNodeEngine = 'javascript' | 'python';
|
||||
|
||||
const PYTHON_RESERVED_WORDS = new Set([
|
||||
'False',
|
||||
'None',
|
||||
'True',
|
||||
'and',
|
||||
'as',
|
||||
'assert',
|
||||
'async',
|
||||
'await',
|
||||
'break',
|
||||
'class',
|
||||
'continue',
|
||||
'def',
|
||||
'del',
|
||||
'elif',
|
||||
'else',
|
||||
'except',
|
||||
'finally',
|
||||
'for',
|
||||
'from',
|
||||
'global',
|
||||
'if',
|
||||
'import',
|
||||
'in',
|
||||
'is',
|
||||
'lambda',
|
||||
'nonlocal',
|
||||
'not',
|
||||
'or',
|
||||
'pass',
|
||||
'raise',
|
||||
'return',
|
||||
'try',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
|
||||
const JAVASCRIPT_RESERVED_WORDS = new Set([
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 将代码节点执行引擎别名归一化为编辑器使用的语言名称。
|
||||
*/
|
||||
export function normalizeCodeNodeEngine(rawEngine?: string): CodeNodeEngine {
|
||||
const normalized = (rawEngine || 'js').trim().toLowerCase();
|
||||
return normalized === 'python' || normalized === 'py'
|
||||
? 'python'
|
||||
: 'javascript';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断参数名称能否安全写入目标语言的函数签名。
|
||||
*/
|
||||
export function isValidCodeNodeParameterName(name: string, rawEngine?: string) {
|
||||
const engine = normalizeCodeNodeEngine(rawEngine);
|
||||
if (engine === 'python') {
|
||||
return /^[A-Za-z_]\w*$/.test(name) && !PYTHON_RESERVED_WORDS.has(name);
|
||||
}
|
||||
return (
|
||||
/^[A-Za-z_$][\w$]*$/.test(name) && !JAVASCRIPT_RESERVED_WORDS.has(name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为新代码节点生成与当前运行时兼容的 main 脚手架。
|
||||
*/
|
||||
export function createCodeNodeScaffold(
|
||||
rawEngine?: string,
|
||||
parameters: Parameter[] = [],
|
||||
outputDefs: Parameter[] = [],
|
||||
) {
|
||||
const engine = normalizeCodeNodeEngine(rawEngine);
|
||||
const parameterNames = [
|
||||
...new Set(
|
||||
parameters
|
||||
.map((parameter) => String(parameter.name || '').trim())
|
||||
.filter((name) => isValidCodeNodeParameterName(name, engine)),
|
||||
),
|
||||
];
|
||||
const outputNames = [
|
||||
...new Set(
|
||||
outputDefs
|
||||
.map((parameter) => String(parameter.name || '').trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
const argumentsText = parameterNames.join(', ');
|
||||
|
||||
if (engine === 'python') {
|
||||
const outputLines = outputNames.map(
|
||||
(name) => ` ${JSON.stringify(name)}: None,`,
|
||||
);
|
||||
return [
|
||||
`def main(${argumentsText}):`,
|
||||
' return {',
|
||||
...outputLines,
|
||||
' }',
|
||||
'',
|
||||
`_result = main(${argumentsText})`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const outputLines = outputNames.map(
|
||||
(name) => ` ${JSON.stringify(name)}: null,`,
|
||||
);
|
||||
return [
|
||||
`function main(${argumentsText}) {`,
|
||||
' return {',
|
||||
...outputLines,
|
||||
' };',
|
||||
'}',
|
||||
'',
|
||||
`_result = main(${argumentsText});`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export const DEFAULT_CODE_NODE_JAVASCRIPT = createCodeNodeScaffold('js');
|
||||
@@ -0,0 +1,544 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Parameter } from '#types';
|
||||
|
||||
import { createCodeNodeScaffold } from './codeNodeScaffold';
|
||||
import { getAvailableNodes } from './nodePalette';
|
||||
import {
|
||||
analyzeCodeNodeOutputs,
|
||||
reconcileInferredCodeNodeOutputs,
|
||||
syncCodeNodeInputParameters,
|
||||
syncCodeNodeOutputDefinitions,
|
||||
} from './codeNodeSync';
|
||||
|
||||
describe('code node scaffold', () => {
|
||||
it('is attached to newly created code nodes', () => {
|
||||
const codeNode = getAvailableNodes().find(
|
||||
(node) => node.type === 'codeNode',
|
||||
);
|
||||
|
||||
expect(codeNode?.extra).toMatchObject({
|
||||
code: createCodeNodeScaffold('js'),
|
||||
codeScaffoldManaged: true,
|
||||
codeScaffoldVersion: 1,
|
||||
engine: 'js',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an explicit javascript main call', () => {
|
||||
expect(
|
||||
createCodeNodeScaffold('js', [{ name: 'data' }], [{ name: 'result' }]),
|
||||
).toBe(
|
||||
[
|
||||
'function main(data) {',
|
||||
' return {',
|
||||
' "result": null,',
|
||||
' };',
|
||||
'}',
|
||||
'',
|
||||
'_result = main(data);',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates an explicit python main call', () => {
|
||||
expect(
|
||||
createCodeNodeScaffold(
|
||||
'python',
|
||||
[{ name: 'data' }],
|
||||
[{ name: 'result' }],
|
||||
),
|
||||
).toBe(
|
||||
[
|
||||
'def main(data):',
|
||||
' return {',
|
||||
' "result": None,',
|
||||
' }',
|
||||
'',
|
||||
'_result = main(data)',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not generate invalid or duplicate function parameters', () => {
|
||||
const code = createCodeNodeScaffold('python', [
|
||||
{ name: 'data' },
|
||||
{ name: 'class' },
|
||||
{ name: 'data' },
|
||||
]);
|
||||
|
||||
expect(code).toContain('def main(data):');
|
||||
expect(code).toContain('_result = main(data)');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
engine: 'js',
|
||||
emptyCode: createCodeNodeScaffold('js'),
|
||||
mainWithData: 'function main(data)',
|
||||
callWithData: '_result = main(data);',
|
||||
mainWithQuery: 'function main(query)',
|
||||
callWithQuery: '_result = main(query);',
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
emptyCode: createCodeNodeScaffold('python'),
|
||||
mainWithData: 'def main(data):',
|
||||
callWithData: '_result = main(data)',
|
||||
mainWithQuery: 'def main(query):',
|
||||
callWithQuery: '_result = main(query)',
|
||||
},
|
||||
])('code node input sync for $engine', (fixture) => {
|
||||
it('syncs a newly configured input name to main and its call', () => {
|
||||
const blankParameter: Parameter = {
|
||||
id: 'input-1',
|
||||
name: '',
|
||||
dataType: 'String',
|
||||
};
|
||||
const blankResult = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[blankParameter],
|
||||
);
|
||||
|
||||
expect(blankResult.synced).toBe(false);
|
||||
expect(blankResult.code).toBe(fixture.emptyCode);
|
||||
|
||||
const namedResult = syncCodeNodeInputParameters(
|
||||
blankResult.code,
|
||||
fixture.engine,
|
||||
blankResult.parameters,
|
||||
[{ ...blankResult.parameters[0], name: 'data' }],
|
||||
);
|
||||
|
||||
expect(namedResult.synced).toBe(true);
|
||||
expect(namedResult.code).toContain(fixture.mainWithData);
|
||||
expect(namedResult.code).toContain(fixture.callWithData);
|
||||
expect(namedResult.parameters[0].codeSyncName).toBe('data');
|
||||
});
|
||||
|
||||
it('keeps the previous binding while the user clears and renames it', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[{ id: 'input-1', name: 'data', dataType: 'String' }],
|
||||
);
|
||||
const cleared = syncCodeNodeInputParameters(
|
||||
initial.code,
|
||||
fixture.engine,
|
||||
initial.parameters,
|
||||
[{ ...initial.parameters[0], name: '' }],
|
||||
);
|
||||
const renamed = syncCodeNodeInputParameters(
|
||||
cleared.code,
|
||||
fixture.engine,
|
||||
cleared.parameters,
|
||||
[{ ...cleared.parameters[0], name: 'query' }],
|
||||
);
|
||||
|
||||
expect(cleared.synced).toBe(false);
|
||||
expect(cleared.parameters[0].codeSyncName).toBe('data');
|
||||
expect(renamed.synced).toBe(true);
|
||||
expect(renamed.code).toContain(fixture.mainWithQuery);
|
||||
expect(renamed.code).toContain(fixture.callWithQuery);
|
||||
});
|
||||
|
||||
it('does not overwrite a user-managed main signature', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[{ id: 'input-1', name: 'data' }],
|
||||
);
|
||||
const manualCode = initial.code.replace(
|
||||
fixture.mainWithData,
|
||||
fixture.engine === 'python'
|
||||
? 'def main(custom):'
|
||||
: 'function main(custom)',
|
||||
);
|
||||
const result = syncCodeNodeInputParameters(
|
||||
manualCode,
|
||||
fixture.engine,
|
||||
initial.parameters,
|
||||
[{ ...initial.parameters[0], name: 'query' }],
|
||||
);
|
||||
|
||||
expect(result.synced).toBe(false);
|
||||
expect(result.code).toBe(manualCode);
|
||||
});
|
||||
|
||||
it('does not partially update multiple explicit main calls', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[{ id: 'input-1', name: 'data' }],
|
||||
);
|
||||
const duplicatedCall = [
|
||||
initial.code,
|
||||
fixture.engine === 'python'
|
||||
? '_result = main(data)'
|
||||
: '_result = main(data);',
|
||||
].join('\n');
|
||||
const result = syncCodeNodeInputParameters(
|
||||
duplicatedCall,
|
||||
fixture.engine,
|
||||
initial.parameters,
|
||||
[{ ...initial.parameters[0], name: 'query' }],
|
||||
);
|
||||
|
||||
expect(result.synced).toBe(false);
|
||||
expect(result.code).toBe(duplicatedCall);
|
||||
});
|
||||
|
||||
it('syncs input ordering and deletion', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[
|
||||
{ id: 'input-1', name: 'data' },
|
||||
{ id: 'input-2', name: 'question' },
|
||||
],
|
||||
);
|
||||
const reordered = syncCodeNodeInputParameters(
|
||||
initial.code,
|
||||
fixture.engine,
|
||||
initial.parameters,
|
||||
[initial.parameters[1], initial.parameters[0]],
|
||||
);
|
||||
const deleted = syncCodeNodeInputParameters(
|
||||
reordered.code,
|
||||
fixture.engine,
|
||||
reordered.parameters,
|
||||
[reordered.parameters[0]],
|
||||
);
|
||||
|
||||
expect(reordered.code).toContain('main(question, data)');
|
||||
expect(deleted.code).toContain('main(question)');
|
||||
expect(deleted.code).not.toContain('main(question, data)');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
engine: 'js',
|
||||
emptyCode: createCodeNodeScaffold('js'),
|
||||
expectedEntry: '"answer": null',
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
emptyCode: createCodeNodeScaffold('python'),
|
||||
expectedEntry: '"answer": None',
|
||||
},
|
||||
])('code node output sync for $engine', (fixture) => {
|
||||
it('adds a configured output name to the return object', () => {
|
||||
const blankOutput: Parameter = {
|
||||
id: 'output-1',
|
||||
name: '',
|
||||
dataType: 'String',
|
||||
};
|
||||
const blankResult = syncCodeNodeOutputDefinitions(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[blankOutput],
|
||||
);
|
||||
const namedResult = syncCodeNodeOutputDefinitions(
|
||||
blankResult.code,
|
||||
fixture.engine,
|
||||
blankResult.outputDefs,
|
||||
[{ ...blankResult.outputDefs[0], name: 'answer' }],
|
||||
);
|
||||
|
||||
expect(namedResult.synced).toBe(true);
|
||||
expect(namedResult.code).toContain(fixture.expectedEntry);
|
||||
expect(namedResult.outputDefs[0].codeSyncName).toBe('answer');
|
||||
});
|
||||
|
||||
it('renames the matching return key', () => {
|
||||
const initial = syncCodeNodeOutputDefinitions(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[{ id: 'output-1', name: 'answer', dataType: 'String' }],
|
||||
);
|
||||
const renamed = syncCodeNodeOutputDefinitions(
|
||||
initial.code,
|
||||
fixture.engine,
|
||||
initial.outputDefs,
|
||||
[{ ...initial.outputDefs[0], name: 'message' }],
|
||||
);
|
||||
|
||||
expect(renamed.synced).toBe(true);
|
||||
expect(renamed.code).toContain(
|
||||
fixture.engine === 'python' ? '"message": None' : '"message": null',
|
||||
);
|
||||
expect(renamed.code).not.toContain(fixture.expectedEntry);
|
||||
});
|
||||
|
||||
it('adds the renamed key when the previous return key is absent', () => {
|
||||
const renamed = syncCodeNodeOutputDefinitions(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[
|
||||
{
|
||||
id: 'output-1',
|
||||
name: 'answer',
|
||||
dataType: 'String',
|
||||
codeSyncName: 'answer',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 'output-1',
|
||||
name: 'message',
|
||||
dataType: 'String',
|
||||
codeSyncName: 'answer',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(renamed.code).toContain(
|
||||
fixture.engine === 'python' ? '"message": None' : '"message": null',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code node output inference', () => {
|
||||
it.each([
|
||||
{
|
||||
engine: 'js',
|
||||
code: [
|
||||
'function main(data) {',
|
||||
' const answer = data;',
|
||||
' return { answer };',
|
||||
'}',
|
||||
'_result = main(data);',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
code: [
|
||||
'def main(data):',
|
||||
' answer = data',
|
||||
' return {"answer": answer}',
|
||||
'_result = main(data)',
|
||||
].join('\n'),
|
||||
},
|
||||
])(
|
||||
'preserves the return expression when renaming for $engine',
|
||||
({ code, engine }) => {
|
||||
const renamed = syncCodeNodeOutputDefinitions(
|
||||
code,
|
||||
engine,
|
||||
[
|
||||
{
|
||||
id: 'output-1',
|
||||
name: 'answer',
|
||||
dataType: 'String',
|
||||
codeSyncName: 'answer',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 'output-1',
|
||||
name: 'message',
|
||||
dataType: 'String',
|
||||
codeSyncName: 'answer',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(renamed.code).toContain('"message": answer');
|
||||
},
|
||||
);
|
||||
|
||||
it('infers the python return fields used by the executable example', () => {
|
||||
const code = [
|
||||
'import json',
|
||||
'def main(data):',
|
||||
' r1 = data + "111"',
|
||||
' r2 = "222"',
|
||||
' return {',
|
||||
' "r1": r1,',
|
||||
' "r2": r2,',
|
||||
' }',
|
||||
'_result = main(data)',
|
||||
].join('\n');
|
||||
|
||||
expect(
|
||||
analyzeCodeNodeOutputs(code, 'python', [
|
||||
{ name: 'data', dataType: 'String' },
|
||||
]),
|
||||
).toEqual([
|
||||
{ name: 'r1', dataType: 'String' },
|
||||
{ name: 'r2', dataType: 'String' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('infers supported python primitive and container types', () => {
|
||||
const code = [
|
||||
'def main(data, count, file):',
|
||||
' text = f"{data}-ok"',
|
||||
' total = count + 1',
|
||||
' return {',
|
||||
' "text": text,',
|
||||
' "total": total,',
|
||||
' "ok": True,',
|
||||
' "file": file,',
|
||||
' "objectValue": {},',
|
||||
' "arrayValue": [],',
|
||||
' }',
|
||||
'_result = main(data, count, file)',
|
||||
].join('\n');
|
||||
|
||||
expect(
|
||||
analyzeCodeNodeOutputs(code, 'python', [
|
||||
{ name: 'data', dataType: 'String' },
|
||||
{ name: 'count', dataType: 'Number' },
|
||||
{ name: 'file', dataType: 'File' },
|
||||
]),
|
||||
).toEqual([
|
||||
{ name: 'text', dataType: 'String' },
|
||||
{ name: 'total', dataType: 'Number' },
|
||||
{ name: 'ok', dataType: 'Boolean' },
|
||||
{ name: 'file', dataType: 'File' },
|
||||
{ name: 'objectValue', dataType: 'Object' },
|
||||
{ name: 'arrayValue', dataType: 'Array' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('infers supported javascript primitive and container types', () => {
|
||||
const code = [
|
||||
'function main(data, count, file) {',
|
||||
' const text = data + "x";',
|
||||
' const total = count + 1;',
|
||||
' return {',
|
||||
' text,',
|
||||
' total,',
|
||||
' ok: true,',
|
||||
' file,',
|
||||
' objectValue: {},',
|
||||
' arrayValue: [],',
|
||||
' };',
|
||||
'}',
|
||||
'_result = main(data, count, file);',
|
||||
].join('\n');
|
||||
|
||||
expect(
|
||||
analyzeCodeNodeOutputs(code, 'js', [
|
||||
{ name: 'data', dataType: 'String' },
|
||||
{ name: 'count', dataType: 'Number' },
|
||||
{ name: 'file', dataType: 'File' },
|
||||
]),
|
||||
).toEqual([
|
||||
{ name: 'text', dataType: 'String' },
|
||||
{ name: 'total', dataType: 'Number' },
|
||||
{ name: 'ok', dataType: 'Boolean' },
|
||||
{ name: 'file', dataType: 'File' },
|
||||
{ name: 'objectValue', dataType: 'Object' },
|
||||
{ name: 'arrayValue', dataType: 'Array' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to String for a static key with a dynamic value', () => {
|
||||
const code = [
|
||||
'function main(data) {',
|
||||
' return { answer: externalCall(data) };',
|
||||
'}',
|
||||
'_result = main(data);',
|
||||
].join('\n');
|
||||
|
||||
expect(analyzeCodeNodeOutputs(code, 'js', [])).toEqual([
|
||||
{ name: 'answer', dataType: 'String' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not infer assignment types across complex control flow', () => {
|
||||
const code = [
|
||||
'function main(data) {',
|
||||
' let value = 1;',
|
||||
' if (data) {',
|
||||
' value = true;',
|
||||
' }',
|
||||
' return { value };',
|
||||
'}',
|
||||
'_result = main(data);',
|
||||
].join('\n');
|
||||
|
||||
expect(analyzeCodeNodeOutputs(code, 'js', [])).toEqual([
|
||||
{ name: 'value', dataType: 'String' },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
engine: 'js',
|
||||
code: [
|
||||
'function main(data) {',
|
||||
' return { fixed: 1, ...data };',
|
||||
'}',
|
||||
'_result = main(data);',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
code: [
|
||||
'def main(data):',
|
||||
' return {"fixed": 1, **data}',
|
||||
'_result = main(data)',
|
||||
].join('\n'),
|
||||
},
|
||||
])(
|
||||
'does not infer fields from a spread return object for $engine',
|
||||
({ code, engine }) => {
|
||||
expect(analyzeCodeNodeOutputs(code, engine, [])).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('preserves manual output types and removes stale inferred fields', () => {
|
||||
const outputs = reconcileInferredCodeNodeOutputs(
|
||||
[
|
||||
{
|
||||
id: 'manualized',
|
||||
name: 'answer',
|
||||
dataType: 'Number',
|
||||
codeInferred: true,
|
||||
codeInferredDataType: 'String',
|
||||
},
|
||||
{
|
||||
id: 'stale',
|
||||
name: 'oldField',
|
||||
dataType: 'String',
|
||||
codeInferred: true,
|
||||
codeInferredDataType: 'String',
|
||||
},
|
||||
],
|
||||
[
|
||||
{ name: 'answer', dataType: 'String' },
|
||||
{ name: 'newField', dataType: 'Boolean' },
|
||||
],
|
||||
() => 'generated',
|
||||
);
|
||||
|
||||
expect(outputs).toEqual([
|
||||
{
|
||||
id: 'manualized',
|
||||
name: 'answer',
|
||||
dataType: 'Number',
|
||||
},
|
||||
{
|
||||
id: 'generated',
|
||||
name: 'newField',
|
||||
dataType: 'Boolean',
|
||||
codeInferred: true,
|
||||
codeInferredDataType: 'Boolean',
|
||||
codeSyncName: 'newField',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@ import type { Node } from '@xyflow/svelte';
|
||||
|
||||
import type { TinyflowOptions } from '#types';
|
||||
|
||||
import { DEFAULT_CODE_NODE_JAVASCRIPT } from './codeNodeScaffold';
|
||||
|
||||
export type NodePaletteItem = {
|
||||
icon?: string;
|
||||
title: string;
|
||||
@@ -76,6 +78,12 @@ const BUILT_IN_NODES: NodePaletteItem[] = [
|
||||
sortNo: 700,
|
||||
description: '动态执行代码',
|
||||
category: '逻辑',
|
||||
extra: {
|
||||
code: DEFAULT_CODE_NODE_JAVASCRIPT,
|
||||
codeScaffoldManaged: true,
|
||||
codeScaffoldVersion: 1,
|
||||
engine: 'js',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
|
||||
|
||||
Reference in New Issue
Block a user