feat: 补全代码节点输入输出智能同步
- 为 JavaScript 和 Python 新节点生成 main 与显式调用脚手架 - 同步输入签名、静态返回字段和输出参数并推断受支持类型 - 增加安全改写边界与 Tinyflow 定向测试
This commit is contained in:
@@ -5,14 +5,21 @@
|
||||
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {parameterDataTypes} from '#consts';
|
||||
import {genShortId} from '../utils/IdGen';
|
||||
import type {Parameter} from '#types';
|
||||
import type {Parameter, ParameterChangeHandler} from '#types';
|
||||
import {deepClone} from '../utils/deepClone';
|
||||
|
||||
const { parameter, position, dataKeyName, placeholder = '请输入参数值' }: {
|
||||
const {
|
||||
parameter,
|
||||
position,
|
||||
dataKeyName,
|
||||
placeholder = '请输入参数值',
|
||||
onParametersChange,
|
||||
}: {
|
||||
parameter: Parameter,
|
||||
position: number[],
|
||||
dataKeyName: string,
|
||||
placeholder?: string,
|
||||
onParametersChange?: ParameterChangeHandler,
|
||||
} = $props();
|
||||
|
||||
|
||||
@@ -50,9 +57,12 @@
|
||||
|
||||
const updateAttribute = (key: string, value: any) => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
const parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||
if (parameters && position.length > 0) {
|
||||
let params = parameters as Parameter[];
|
||||
const previousParameters = deepClone(
|
||||
(node.data?.[dataKeyName] as Array<Parameter>) || []
|
||||
);
|
||||
const nextParameters = deepClone(previousParameters);
|
||||
if (nextParameters.length > 0 && position.length > 0) {
|
||||
let params = nextParameters;
|
||||
for (let i = 0; i < position.length; i++) {
|
||||
const pos = position[i];
|
||||
if (i == position.length - 1) {
|
||||
@@ -66,7 +76,12 @@
|
||||
}
|
||||
}
|
||||
return {
|
||||
[dataKeyName]: [...deepClone(parameters)]
|
||||
[dataKeyName]: nextParameters,
|
||||
...onParametersChange?.(
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
node.data
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -87,9 +102,12 @@
|
||||
let triggerObject: any;
|
||||
const handleDelete = () => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||
if (parameters && position.length > 0) {
|
||||
let params = parameters as Array<Parameter>;
|
||||
const previousParameters = deepClone(
|
||||
(node.data?.[dataKeyName] as Array<Parameter>) || []
|
||||
);
|
||||
const nextParameters = deepClone(previousParameters);
|
||||
if (nextParameters.length > 0 && position.length > 0) {
|
||||
let params = nextParameters;
|
||||
for (let i = 0; i < position.length; i++) {
|
||||
const pos = position[i];
|
||||
if (i == position.length - 1) {
|
||||
@@ -100,7 +118,12 @@
|
||||
}
|
||||
}
|
||||
return {
|
||||
[dataKeyName]: [...deepClone(parameters)]
|
||||
[dataKeyName]: nextParameters,
|
||||
...onParametersChange?.(
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
node.data
|
||||
)
|
||||
};
|
||||
});
|
||||
triggerObject?.hide();
|
||||
@@ -109,9 +132,12 @@
|
||||
|
||||
const handleAddChildParameter = () => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||
if (parameters && position.length > 0) {
|
||||
let params = parameters as Array<Parameter>;
|
||||
const previousParameters = deepClone(
|
||||
(node.data?.[dataKeyName] as Array<Parameter>) || []
|
||||
);
|
||||
const nextParameters = deepClone(previousParameters);
|
||||
if (nextParameters.length > 0 && position.length > 0) {
|
||||
let params = nextParameters;
|
||||
for (let i = 0; i < position.length; i++) {
|
||||
const pos = position[i];
|
||||
if (i == position.length - 1) {
|
||||
@@ -137,7 +163,12 @@
|
||||
}
|
||||
|
||||
return {
|
||||
[dataKeyName]: [...deepClone(parameters)]
|
||||
[dataKeyName]: nextParameters,
|
||||
...onParametersChange?.(
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
node.data
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {useNodesData} from '@xyflow/svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import type {Parameter} from '#types';
|
||||
import type {Parameter, ParameterChangeHandler} from '#types';
|
||||
import OutputDefItem from './OutputDefItem.svelte';
|
||||
|
||||
const {
|
||||
noneParameterText = '无输出参数',
|
||||
dataKeyName = 'outputDefs',
|
||||
placeholder = '请输入参数名称',
|
||||
onParametersChange,
|
||||
}: {
|
||||
noneParameterText?: string;
|
||||
dataKeyName?: string;
|
||||
placeholder?: string
|
||||
placeholder?: string;
|
||||
onParametersChange?: ParameterChangeHandler;
|
||||
} = $props();
|
||||
|
||||
let currentNodeId = getCurrentNodeId();
|
||||
@@ -24,7 +26,13 @@
|
||||
|
||||
{#snippet parameterList(params: Parameter[], position: number[])}
|
||||
{#each params as param, index (`${param.id}_${param.children ? param.children.length : 0}`)}
|
||||
<OutputDefItem parameter={param} position={[...position, index]} {dataKeyName} {placeholder} />
|
||||
<OutputDefItem
|
||||
parameter={param}
|
||||
position={[...position, index]}
|
||||
{dataKeyName}
|
||||
{placeholder}
|
||||
{onParametersChange}
|
||||
/>
|
||||
{#if param.children}
|
||||
{@render parameterList(param.children, [...position, index])}
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import {contentTypes, parameterRefTypes} from '#consts';
|
||||
import {useRefOptions} from '../utils/useRefOptions.svelte';
|
||||
import {onMount} from 'svelte';
|
||||
import type {Parameter} from '#types';
|
||||
import type {Parameter, ParameterChangeHandler} from '#types';
|
||||
import {deepClone} from '../utils/deepClone';
|
||||
import {
|
||||
isArrayDataType,
|
||||
resolveLoopOutputDataType,
|
||||
@@ -27,7 +28,8 @@
|
||||
fixedNumberMin,
|
||||
fixedNumberMax,
|
||||
acceptedContentTypes = [],
|
||||
loopOutputAggregation = false
|
||||
loopOutputAggregation = false,
|
||||
onParametersChange
|
||||
}: {
|
||||
parameter: Parameter,
|
||||
index: number,
|
||||
@@ -38,6 +40,7 @@
|
||||
fixedNumberMax?: number,
|
||||
acceptedContentTypes?: string[],
|
||||
loopOutputAggregation?: boolean,
|
||||
onParametersChange?: ParameterChangeHandler,
|
||||
} = $props();
|
||||
|
||||
|
||||
@@ -54,13 +57,21 @@
|
||||
|
||||
const updateParams = (patch: Partial<Parameter>) => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||
parameters[index] = {
|
||||
...parameters[index],
|
||||
const previousParameters = deepClone(
|
||||
(node.data?.[dataKeyName] as Array<Parameter>) || []
|
||||
);
|
||||
const nextParameters = deepClone(previousParameters);
|
||||
nextParameters[index] = {
|
||||
...nextParameters[index],
|
||||
...patch
|
||||
};
|
||||
return {
|
||||
[dataKeyName]: [...parameters]
|
||||
[dataKeyName]: nextParameters,
|
||||
...onParametersChange?.(
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
node.data
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -150,10 +161,18 @@
|
||||
let triggerObject: any;
|
||||
const handleDelete = () => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||
parameters.splice(index, 1);
|
||||
const previousParameters = deepClone(
|
||||
(node.data?.[dataKeyName] as Array<Parameter>) || []
|
||||
);
|
||||
const nextParameters = deepClone(previousParameters);
|
||||
nextParameters.splice(index, 1);
|
||||
return {
|
||||
[dataKeyName]: [...parameters]
|
||||
[dataKeyName]: nextParameters,
|
||||
...onParametersChange?.(
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
node.data
|
||||
)
|
||||
};
|
||||
});
|
||||
triggerObject?.hide();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {useNodesData} from '@xyflow/svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import type {ParameterChangeHandler} from '#types';
|
||||
import RefParameterItem from './RefParameterItem.svelte';
|
||||
|
||||
const {
|
||||
@@ -11,7 +12,8 @@
|
||||
fixedNumberMin,
|
||||
fixedNumberMax,
|
||||
acceptedContentTypes = [],
|
||||
loopOutputAggregation = false
|
||||
loopOutputAggregation = false,
|
||||
onParametersChange
|
||||
}: {
|
||||
noneParameterText?: string;
|
||||
dataKeyName?: string;
|
||||
@@ -21,6 +23,7 @@
|
||||
fixedNumberMax?: number,
|
||||
acceptedContentTypes?: string[],
|
||||
loopOutputAggregation?: boolean,
|
||||
onParametersChange?: ParameterChangeHandler,
|
||||
} = $props();
|
||||
|
||||
let currentNodeId = getCurrentNodeId();
|
||||
@@ -49,6 +52,7 @@
|
||||
{fixedNumberMax}
|
||||
{acceptedContentTypes}
|
||||
{loopOutputAggregation}
|
||||
{onParametersChange}
|
||||
/>
|
||||
{:else }
|
||||
<div class="none-params">{noneParameterText}</div>
|
||||
|
||||
@@ -8,9 +8,22 @@
|
||||
import {getCurrentNodeId, getOptions} from '#components/utils/NodeUtils';
|
||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||
import OutputDefList from '../core/OutputDefList.svelte';
|
||||
import {onMount} from 'svelte';
|
||||
import type {SelectItem, TinyflowNodeData} from '#types';
|
||||
import {onDestroy, onMount} from 'svelte';
|
||||
import type {
|
||||
Parameter,
|
||||
ParameterChangeHandler,
|
||||
SelectItem,
|
||||
TinyflowNodeData,
|
||||
} from '#types';
|
||||
import CodeScriptEditor from '../core/CodeScriptEditor.svelte';
|
||||
import {genShortId} from '../utils/IdGen';
|
||||
import {createCodeNodeScaffold} from '../utils/codeNodeScaffold';
|
||||
import {
|
||||
analyzeCodeNodeOutputs,
|
||||
reconcileInferredCodeNodeOutputs,
|
||||
syncCodeNodeInputParameters,
|
||||
syncCodeNodeOutputDefinitions,
|
||||
} from '../utils/codeNodeSync';
|
||||
|
||||
const { data, ...rest }: {
|
||||
data: TinyflowNodeData,
|
||||
@@ -22,32 +35,36 @@
|
||||
let currentNode = useNodesData(currentNodeId);
|
||||
const { addParameter } = useAddParameter();
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
let syncHint = $state('');
|
||||
const codeNodeHelp = `代码如何返回结果
|
||||
- JavaScript 定义 main 函数并返回对象;Python 定义 main 函数并返回 dict,系统会自动执行。
|
||||
- main 接收由输入参数组成的对象。
|
||||
- 输入参数会按名称传入 main,例如输入参数 data 对应 main(data)。
|
||||
- JavaScript 的 main 返回对象;Python 的 main 返回 dict。
|
||||
- 保留 _result = main(...),运行时会读取 _result 作为节点结果。
|
||||
|
||||
输出参数如何配置
|
||||
- 在“输出参数”中新增字段(如 answer、score)。
|
||||
- 字段名与 main 返回对象的 key 保持一致。
|
||||
- return 中的静态字段会自动补全到“输出参数”,并推断 String、Number、Boolean、File、Object、Array。
|
||||
- 在“输出参数”中新增或改名,也会同步 main 的 return 字段。
|
||||
- 下游节点可引用:代码节点ID.输出参数名。
|
||||
|
||||
JavaScript 示例
|
||||
function main({ input }) {
|
||||
return { answer: input, score: 95 };
|
||||
function main(data) {
|
||||
return { answer: data, score: 95 };
|
||||
}
|
||||
_result = main(data);
|
||||
|
||||
Python 示例
|
||||
def main(inputs):
|
||||
return {'answer': inputs.get('input'), 'score': 95}
|
||||
def main(data):
|
||||
return {'answer': data, 'score': 95}
|
||||
_result = main(data)
|
||||
|
||||
- 输出参数配置:answer(String)、score(Number)
|
||||
- 结束节点输出参数可引用:代码节点ID.answer、代码节点ID.score
|
||||
|
||||
兼容说明
|
||||
- 历史 JavaScript、Python 的 _result 写法仍然支持。`;
|
||||
- 结束节点输出参数可引用:代码节点ID.answer、代码节点ID.score`;
|
||||
|
||||
const nodeData = $derived.by(() => {
|
||||
return (currentNode?.current?.data || data) as TinyflowNodeData;
|
||||
});
|
||||
const editorParameters = $derived.by(() => {
|
||||
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||
return (nodeData.parameters as Parameter[]) || [];
|
||||
});
|
||||
|
||||
let engines = $state<SelectItem[]>([
|
||||
@@ -58,30 +75,132 @@ Python 示例
|
||||
return firstAvailable?.value || 'js';
|
||||
});
|
||||
const codePlaceholder = $derived.by(() => {
|
||||
const engine = String(data.engine || defaultEngine).trim().toLowerCase();
|
||||
const engine = String(nodeData.engine || defaultEngine).trim().toLowerCase();
|
||||
if (engine === 'python' || engine === 'py') {
|
||||
return "请输入代码,例如:def main(inputs):\n return {'output': inputs.get('input')}";
|
||||
return "请输入代码,例如:def main(data):\n return {'output': data}\n\n_result = main(data)";
|
||||
}
|
||||
return '请输入代码,例如:function main({ input }) { return { output: input }; }';
|
||||
return '请输入代码,例如:function main(data) { return { output: data }; }\n_result = main(data);';
|
||||
});
|
||||
|
||||
const reconcileOutputs = (
|
||||
code: string,
|
||||
engine: string,
|
||||
parameters: Parameter[],
|
||||
outputDefs: Parameter[],
|
||||
) => {
|
||||
const inferred = analyzeCodeNodeOutputs(code, engine, parameters);
|
||||
return inferred
|
||||
? reconcileInferredCodeNodeOutputs(
|
||||
outputDefs,
|
||||
inferred,
|
||||
genShortId
|
||||
)
|
||||
: outputDefs;
|
||||
};
|
||||
|
||||
const handleInputParametersChange: ParameterChangeHandler = (
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
currentData,
|
||||
) => {
|
||||
const engine = String(currentData.engine || defaultEngine);
|
||||
const syncResult = syncCodeNodeInputParameters(
|
||||
String(currentData.code || ''),
|
||||
engine,
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
);
|
||||
syncHint = syncResult.synced ? '' : (syncResult.reason || '');
|
||||
return {
|
||||
code: syncResult.code,
|
||||
parameters: syncResult.parameters,
|
||||
outputDefs: reconcileOutputs(
|
||||
syncResult.code,
|
||||
engine,
|
||||
syncResult.parameters,
|
||||
(currentData.outputDefs as Parameter[]) || [],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const handleOutputDefinitionsChange: ParameterChangeHandler = (
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
currentData,
|
||||
) => {
|
||||
const syncResult = syncCodeNodeOutputDefinitions(
|
||||
String(currentData.code || ''),
|
||||
String(currentData.engine || defaultEngine),
|
||||
previousParameters,
|
||||
nextParameters,
|
||||
);
|
||||
syncHint = syncResult.synced ? '' : (syncResult.reason || '');
|
||||
return {
|
||||
code: syncResult.code,
|
||||
outputDefs: syncResult.outputDefs,
|
||||
};
|
||||
};
|
||||
|
||||
let analysisTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const scheduleOutputAnalysis = (code: string) => {
|
||||
if (analysisTimer) {
|
||||
clearTimeout(analysisTimer);
|
||||
}
|
||||
analysisTimer = setTimeout(() => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
if (String(node.data.code || '') !== code) {
|
||||
return {};
|
||||
}
|
||||
const outputDefs = reconcileOutputs(
|
||||
code,
|
||||
String(node.data.engine || defaultEngine),
|
||||
(node.data.parameters as Parameter[]) || [],
|
||||
(node.data.outputDefs as Parameter[]) || [],
|
||||
);
|
||||
if (
|
||||
JSON.stringify(outputDefs) ===
|
||||
JSON.stringify(node.data.outputDefs || [])
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return { outputDefs };
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const updateEngine = (engine: string) => {
|
||||
syncHint = '';
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
const patch: TinyflowNodeData = { engine };
|
||||
if (node.data.codeScaffoldManaged === true) {
|
||||
patch.code = createCodeNodeScaffold(
|
||||
engine,
|
||||
(node.data.parameters as Parameter[]) || [],
|
||||
(node.data.outputDefs as Parameter[]) || [],
|
||||
);
|
||||
}
|
||||
return patch;
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const codeEngines = await options.provider?.codeEngine?.();
|
||||
if (codeEngines && codeEngines.length > 0) {
|
||||
engines = codeEngines;
|
||||
}
|
||||
|
||||
const currentEngine = data.engine;
|
||||
const currentEngine = nodeData.engine;
|
||||
const currentEngineSupported = engines.some((item) => item.value === currentEngine && item.selectable !== false);
|
||||
if (!currentEngine || !currentEngineSupported) {
|
||||
updateNodeData(currentNodeId, () => {
|
||||
return {
|
||||
engine: defaultEngine
|
||||
};
|
||||
});
|
||||
updateEngine(String(defaultEngine));
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (analysisTimer) {
|
||||
clearTimeout(analysisTimer);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -109,19 +228,14 @@ Python 示例
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<RefParameterList />
|
||||
<RefParameterList onParametersChange={handleInputParametersChange} />
|
||||
|
||||
<Heading level={3} mt="10px">代码</Heading>
|
||||
<div class="setting-title">执行引擎</div>
|
||||
<div class="setting-item">
|
||||
<Select items={engines} style="width: 100%" placeholder="请选择执行引擎" onSelect={(item)=>{
|
||||
const newValue = item.value;
|
||||
updateNodeData(currentNodeId, ()=>{
|
||||
return {
|
||||
engine: newValue
|
||||
}
|
||||
})
|
||||
}} value={data.engine ? [data.engine] : [defaultEngine]} />
|
||||
updateEngine(String(item.value));
|
||||
}} value={nodeData.engine ? [nodeData.engine] : [defaultEngine]} />
|
||||
</div>
|
||||
|
||||
<div class="setting-title">执行代码</div>
|
||||
@@ -131,18 +245,25 @@ Python 示例
|
||||
rows={10}
|
||||
placeholder={codePlaceholder}
|
||||
style="width: 100%"
|
||||
engine={(data.engine as string) || (defaultEngine as string)}
|
||||
engine={(nodeData.engine as string) || (defaultEngine as string)}
|
||||
parameters={editorParameters}
|
||||
oninput={(e:any)=>{
|
||||
const code = String(e.target.value || '');
|
||||
syncHint = '';
|
||||
updateNodeData(currentNodeId, ()=>{
|
||||
return {
|
||||
code: e.target.value
|
||||
code,
|
||||
codeScaffoldManaged: false
|
||||
}
|
||||
})
|
||||
});
|
||||
scheduleOutputAnalysis(code);
|
||||
}}
|
||||
value={data.code as string||""}
|
||||
value={nodeData.code as string||""}
|
||||
/>
|
||||
</div>
|
||||
{#if syncHint}
|
||||
<div class="sync-hint" role="status">{syncHint}</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="heading">
|
||||
@@ -155,7 +276,7 @@ Python 示例
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<OutputDefList />
|
||||
<OutputDefList onParametersChange={handleOutputDefinitionsChange} />
|
||||
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -180,4 +301,11 @@ Python 示例
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sync-hint {
|
||||
color: var(--tf-warning-soft-text);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
margin: -4px 0 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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>',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Snippet} from 'svelte';
|
||||
import type {Node, useSvelteFlow} from '@xyflow/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Node, useSvelteFlow } from '@xyflow/svelte';
|
||||
|
||||
export type TinyflowData = Partial<
|
||||
ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>
|
||||
@@ -135,10 +135,7 @@ export type TinyflowOptions = {
|
||||
onRunTest?: () => void | Promise<void>;
|
||||
hiddenNodes?: string[] | (() => string[]);
|
||||
onDataChange?: (data: TinyflowData) => void;
|
||||
onDataCommit?: (
|
||||
data: TinyflowData,
|
||||
reason: TinyflowDataCommitReason,
|
||||
) => void;
|
||||
onDataCommit?: (data: TinyflowData, reason: TinyflowDataCommitReason) => void;
|
||||
};
|
||||
|
||||
export type Parameter = {
|
||||
@@ -171,5 +168,14 @@ export type Parameter = {
|
||||
requiredDisabled?: boolean;
|
||||
systemReserved?: boolean;
|
||||
autoManaged?: boolean;
|
||||
codeInferred?: boolean;
|
||||
codeInferredDataType?: string;
|
||||
codeSyncName?: string;
|
||||
flattenAggregation?: boolean;
|
||||
};
|
||||
|
||||
export type ParameterChangeHandler = (
|
||||
previousParameters: Parameter[],
|
||||
nextParameters: Parameter[],
|
||||
nodeData: TinyflowNodeData,
|
||||
) => Partial<TinyflowNodeData> | void;
|
||||
|
||||
Reference in New Issue
Block a user