chore: import clean Agent Desk snapshot (1/7)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin';
|
||||
import {
|
||||
Layer,
|
||||
injectable,
|
||||
inject,
|
||||
FreeLayoutPluginContext,
|
||||
WorkflowHoverService,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowNodeJSON,
|
||||
WorkflowSelectService,
|
||||
WorkflowDocument,
|
||||
PositionSchema,
|
||||
WorkflowDragService,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { ContainerUtils } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
@injectable()
|
||||
export class ContextMenuLayer extends Layer {
|
||||
@inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext;
|
||||
|
||||
@inject(WorkflowNodePanelService) nodePanelService: WorkflowNodePanelService;
|
||||
|
||||
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
|
||||
|
||||
@inject(WorkflowSelectService) selectService: WorkflowSelectService;
|
||||
|
||||
@inject(WorkflowDocument) document: WorkflowDocument;
|
||||
|
||||
@inject(WorkflowDragService) dragService: WorkflowDragService;
|
||||
|
||||
onReady() {
|
||||
this.listenPlaygroundEvent('contextmenu', (e) => {
|
||||
if (this.config.readonlyOrDisabled) return;
|
||||
this.openNodePanel(e);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
openNodePanel(e: MouseEvent) {
|
||||
const mousePos = this.getPosFromMouseEvent(e);
|
||||
const containerNode = this.getContainerNode(mousePos);
|
||||
this.nodePanelService.callNodePanel({
|
||||
position: mousePos,
|
||||
containerNode,
|
||||
panelProps: {},
|
||||
// handle node selection from panel - 处理从面板中选择节点
|
||||
onSelect: async (panelParams?: NodePanelResult) => {
|
||||
if (!panelParams) {
|
||||
return;
|
||||
}
|
||||
const { nodeType, nodeJSON } = panelParams;
|
||||
const position = this.dragService.adjustSubNodePosition(nodeType, containerNode, mousePos);
|
||||
// create new workflow node based on selected type - 根据选择的类型创建新的工作流节点
|
||||
const node: WorkflowNodeEntity = this.ctx.document.createWorkflowNodeByType(
|
||||
nodeType,
|
||||
position,
|
||||
nodeJSON ?? ({} as WorkflowNodeJSON),
|
||||
containerNode?.id
|
||||
);
|
||||
// select the newly created node - 选择新创建的节点
|
||||
this.selectService.select(node);
|
||||
},
|
||||
// handle panel close - 处理面板关闭
|
||||
onClose: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
private getContainerNode(mousePos: PositionSchema): WorkflowNodeEntity | undefined {
|
||||
const allNodes = this.document.getAllNodes();
|
||||
const containerTransforms = ContainerUtils.getContainerTransforms(allNodes);
|
||||
const collisionTransform = ContainerUtils.getCollisionTransform({
|
||||
targetPoint: mousePos,
|
||||
transforms: containerTransforms,
|
||||
document: this.document,
|
||||
});
|
||||
return collisionTransform?.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
definePluginCreator,
|
||||
PluginCreator,
|
||||
FreeLayoutPluginContext,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { ContextMenuLayer } from './context-menu-layer';
|
||||
|
||||
export interface ContextMenuPluginOptions {}
|
||||
|
||||
/**
|
||||
* Creates a plugin of contextmenu
|
||||
* @param ctx - The plugin context, containing the document and other relevant information.
|
||||
* @param options - Plugin options, currently an empty object.
|
||||
*/
|
||||
export const createContextMenuPlugin: PluginCreator<ContextMenuPluginOptions> = definePluginCreator<
|
||||
ContextMenuPluginOptions,
|
||||
FreeLayoutPluginContext
|
||||
>({
|
||||
onInit(ctx, options) {
|
||||
ctx.playground.registerLayer(ContextMenuLayer);
|
||||
},
|
||||
});
|
||||
6
flowgram-editor/src/plugins/context-menu-plugin/index.ts
Normal file
6
flowgram-editor/src/plugins/context-menu-plugin/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { createContextMenuPlugin } from './context-menu-plugin';
|
||||
9
flowgram-editor/src/plugins/index.ts
Normal file
9
flowgram-editor/src/plugins/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { createContextMenuPlugin } from './context-menu-plugin';
|
||||
export { createRuntimePlugin } from './runtime-plugin';
|
||||
export { createVariablePanelPlugin } from './variable-panel-plugin';
|
||||
export { createPanelManagerPlugin } from './panel-manager-plugin';
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export enum PanelType {
|
||||
NodeFormPanel = 'nodeFormPanel',
|
||||
TestRunFormPanel = 'testRunFormPanel',
|
||||
ProblemPanel = 'problemPanel',
|
||||
}
|
||||
44
flowgram-editor/src/plugins/panel-manager-plugin/hooks.ts
Normal file
44
flowgram-editor/src/plugins/panel-manager-plugin/hooks.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { usePanelManager } from '@flowgram.ai/panel-manager-plugin';
|
||||
|
||||
import type { NodeFormPanelProps } from '../../components/sidebar/node-form-panel';
|
||||
import { PanelType } from './constants';
|
||||
|
||||
export const useNodeFormPanel = () => {
|
||||
const panelManager = usePanelManager();
|
||||
|
||||
const open = (props: NodeFormPanelProps) => {
|
||||
panelManager.open(PanelType.NodeFormPanel, 'right', {
|
||||
props: props,
|
||||
});
|
||||
};
|
||||
const close = () => panelManager.close(PanelType.NodeFormPanel);
|
||||
|
||||
return { open, close };
|
||||
};
|
||||
|
||||
export const useTestRunFormPanel = () => {
|
||||
const panelManager = usePanelManager();
|
||||
|
||||
const open = () => {
|
||||
panelManager.open(PanelType.TestRunFormPanel, 'docked-right');
|
||||
};
|
||||
const close = () => panelManager.close(PanelType.TestRunFormPanel);
|
||||
|
||||
return { open, close };
|
||||
};
|
||||
|
||||
export const useProblemPanel = () => {
|
||||
const panelManager = usePanelManager();
|
||||
|
||||
const open = () => {
|
||||
panelManager.open(PanelType.ProblemPanel, 'bottom');
|
||||
};
|
||||
const close = () => panelManager.close(PanelType.ProblemPanel);
|
||||
|
||||
return { open, close };
|
||||
};
|
||||
46
flowgram-editor/src/plugins/panel-manager-plugin/index.tsx
Normal file
46
flowgram-editor/src/plugins/panel-manager-plugin/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
createPanelManagerPlugin as create,
|
||||
PanelFactory,
|
||||
} from '@flowgram.ai/panel-manager-plugin';
|
||||
|
||||
import { DemoTools } from '../../components/tools';
|
||||
import {
|
||||
TestRunSidePanel,
|
||||
TestRunSidePanelProps,
|
||||
} from '../../components/testrun/testrun-panel/test-run-panel';
|
||||
import { NodeFormPanel, NodeFormPanelProps } from '../../components/sidebar/node-form-panel';
|
||||
import { ProblemPanel } from '../../components/problem-panel/problem-panel';
|
||||
import { PanelType } from './constants';
|
||||
|
||||
const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {
|
||||
key: PanelType.NodeFormPanel,
|
||||
defaultSize: 500,
|
||||
maxSize: 800,
|
||||
minSize: 300,
|
||||
render: (props: NodeFormPanelProps) => <NodeFormPanel {...props} />,
|
||||
};
|
||||
|
||||
const testRunPanelFactory: PanelFactory<TestRunSidePanelProps> = {
|
||||
key: PanelType.TestRunFormPanel,
|
||||
defaultSize: 400,
|
||||
render: () => <TestRunSidePanel />,
|
||||
};
|
||||
|
||||
const problemPanelFactory: PanelFactory<void> = {
|
||||
key: PanelType.ProblemPanel,
|
||||
defaultSize: 200,
|
||||
render: () => <ProblemPanel />,
|
||||
};
|
||||
|
||||
export const createPanelManagerPlugin = () =>
|
||||
create({
|
||||
factories: [nodeFormPanelFactory, testRunPanelFactory, problemPanelFactory],
|
||||
layerProps: {
|
||||
children: <DemoTools />,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowGramAPIName, IRuntimeClient } from '@flowgram.ai/runtime-interface';
|
||||
import { injectable } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowRuntimeClient implements IRuntimeClient {
|
||||
constructor() {}
|
||||
|
||||
public [FlowGramAPIName.TaskRun]: IRuntimeClient[FlowGramAPIName.TaskRun];
|
||||
|
||||
public [FlowGramAPIName.TaskReport]: IRuntimeClient[FlowGramAPIName.TaskReport];
|
||||
|
||||
public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult];
|
||||
|
||||
public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel];
|
||||
|
||||
public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate];
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
WorkflowStatus,
|
||||
type IReport,
|
||||
type TaskRunInput,
|
||||
type WorkflowInputs,
|
||||
type WorkflowOutputs,
|
||||
} from '@flowgram.ai/runtime-interface';
|
||||
|
||||
type DebugNode = {
|
||||
id: string;
|
||||
type: string;
|
||||
data?: {
|
||||
config?: Record<string, unknown>;
|
||||
inputsValues?: Record<string, DebugValue>;
|
||||
};
|
||||
};
|
||||
|
||||
type DebugEdge = {
|
||||
sourceNodeID: string;
|
||||
targetNodeID: string;
|
||||
};
|
||||
|
||||
type DebugSchema = {
|
||||
nodes?: DebugNode[];
|
||||
edges?: DebugEdge[];
|
||||
};
|
||||
|
||||
type DebugValue = {
|
||||
type?: string;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
const BUSINESS_NODE_TYPES = new Set([
|
||||
'conversation_understanding',
|
||||
'reply_policy',
|
||||
'knowledge_retrieve',
|
||||
'answerability_gate',
|
||||
'llm_reply',
|
||||
'analyze_conversation',
|
||||
'prepare_ticket_draft',
|
||||
'human_confirm',
|
||||
'create_ticket',
|
||||
'handoff_to_human',
|
||||
'send_reply',
|
||||
]);
|
||||
|
||||
export function isBusinessDebugSchema(input: TaskRunInput): boolean {
|
||||
const schema = parseSchema(input.schema);
|
||||
return (schema.nodes ?? []).some((node) => BUSINESS_NODE_TYPES.has(node.type));
|
||||
}
|
||||
|
||||
export function buildBusinessDebugReport(input: TaskRunInput, taskID: string): IReport {
|
||||
const schema = parseSchema(input.schema);
|
||||
const nodes = schema.nodes ?? [];
|
||||
const nodesByID = new Map(nodes.map((node) => [node.id, node]));
|
||||
const outgoing = new Map<string, DebugEdge[]>();
|
||||
for (const edge of schema.edges ?? []) {
|
||||
outgoing.set(edge.sourceNodeID, [...(outgoing.get(edge.sourceNodeID) ?? []), edge]);
|
||||
}
|
||||
|
||||
const values = new Map<string, WorkflowOutputs>();
|
||||
const reports: IReport['reports'] = {};
|
||||
const startedAt = Date.now();
|
||||
let current = nodes.find((node) => node.type === 'start');
|
||||
let workflowOutputs: WorkflowOutputs = { status: 'completed', debug: true };
|
||||
|
||||
for (let step = 0; current && step < 128; step += 1) {
|
||||
const nodeInputs = resolveNodeInputs(current, values);
|
||||
const nodeOutputs = simulateNode(current, nodeInputs, input.inputs);
|
||||
values.set(current.id, nodeOutputs);
|
||||
const now = Date.now();
|
||||
reports[current.id] = {
|
||||
id: current.id,
|
||||
status: WorkflowStatus.Succeeded,
|
||||
terminated: true,
|
||||
startTime: now,
|
||||
endTime: now,
|
||||
timeCost: 0,
|
||||
snapshots: [
|
||||
{
|
||||
id: `${taskID}-${current.id}`,
|
||||
nodeID: current.id,
|
||||
inputs: nodeInputs,
|
||||
outputs: nodeOutputs,
|
||||
data: { debug: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
if (current.type === 'end') {
|
||||
workflowOutputs = { ...nodeOutputs, status: 'completed', debug: true };
|
||||
break;
|
||||
}
|
||||
const nextEdge = (outgoing.get(current.id) ?? [])[0];
|
||||
current = nextEdge ? nodesByID.get(nextEdge.targetNodeID) : undefined;
|
||||
}
|
||||
|
||||
const endedAt = Date.now();
|
||||
return {
|
||||
id: taskID,
|
||||
inputs: input.inputs,
|
||||
outputs: workflowOutputs,
|
||||
workflowStatus: {
|
||||
status: WorkflowStatus.Succeeded,
|
||||
terminated: true,
|
||||
startTime: startedAt,
|
||||
endTime: endedAt,
|
||||
timeCost: endedAt - startedAt,
|
||||
},
|
||||
reports,
|
||||
messages: {
|
||||
log: [],
|
||||
info: [],
|
||||
debug: [],
|
||||
error: [],
|
||||
warning: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseSchema(raw: string): DebugSchema {
|
||||
try {
|
||||
return JSON.parse(raw) as DebugSchema;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNodeInputs(
|
||||
node: DebugNode,
|
||||
values: Map<string, WorkflowOutputs>
|
||||
): WorkflowInputs {
|
||||
return Object.fromEntries(
|
||||
Object.entries(node.data?.inputsValues ?? {}).map(([name, value]) => [
|
||||
name,
|
||||
resolveValue(value, values),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function resolveValue(value: DebugValue, values: Map<string, WorkflowOutputs>): unknown {
|
||||
if (value?.type === 'ref' && Array.isArray(value.content)) {
|
||||
const [nodeID, field] = value.content;
|
||||
if (typeof nodeID === 'string' && typeof field === 'string') {
|
||||
return values.get(nodeID)?.[field];
|
||||
}
|
||||
}
|
||||
return value?.content;
|
||||
}
|
||||
|
||||
function simulateNode(
|
||||
node: DebugNode,
|
||||
inputs: WorkflowInputs,
|
||||
workflowInputs: WorkflowInputs
|
||||
): WorkflowOutputs {
|
||||
const userMessage = String(workflowInputs.userMessage ?? workflowInputs.query ?? '测试消息');
|
||||
switch (node.type) {
|
||||
case 'start':
|
||||
return { ...workflowInputs, userMessage, query: userMessage };
|
||||
case 'conversation_understanding':
|
||||
return {
|
||||
normalizedMessage: userMessage,
|
||||
messageIntent: 'ticket_request',
|
||||
answerScope: 'needs_ticket',
|
||||
confidence: 1,
|
||||
riskSignals: [],
|
||||
reason: '调试模拟结果',
|
||||
};
|
||||
case 'reply_policy':
|
||||
return { action: 'prepare_ticket', requiresFlow: true, targetFlow: 'prepare_ticket' };
|
||||
case 'knowledge_retrieve':
|
||||
return { documents: [], count: 0, query: String(inputs.query ?? userMessage) };
|
||||
case 'answerability_gate':
|
||||
return { answerability: 'answerable', reason: '调试模拟结果' };
|
||||
case 'analyze_conversation':
|
||||
return { intent: 'ticket_request', riskLevel: 'low', needTicket: true };
|
||||
case 'prepare_ticket_draft': {
|
||||
const issue = String(inputs.issue ?? userMessage).trim() || '测试问题';
|
||||
const title = issue.length > 30 ? `${issue.slice(0, 30)}…` : issue;
|
||||
const ticketDraft = {
|
||||
ready: true,
|
||||
title,
|
||||
description: issue,
|
||||
missingFields: [],
|
||||
followUpQuestions: [],
|
||||
conversationFacts: [issue],
|
||||
};
|
||||
return { ticketDraft, ...ticketDraft };
|
||||
}
|
||||
case 'human_confirm':
|
||||
return { confirmed: true, responseText: '调试运行自动确认' };
|
||||
case 'create_ticket':
|
||||
return {
|
||||
ticketId: 0,
|
||||
ticketNo: '',
|
||||
created: false,
|
||||
skipped: true,
|
||||
message: '调试运行不会创建工单。',
|
||||
};
|
||||
case 'handoff_to_human':
|
||||
return { handoffId: 0, skipped: true, message: '调试运行不会转人工。' };
|
||||
case 'llm_reply': {
|
||||
const staticReply = String(node.data?.config?.staticReply ?? '').trim();
|
||||
return { replyText: staticReply || '调试回复' };
|
||||
}
|
||||
case 'send_reply':
|
||||
return { sent: Boolean(inputs.replyText), replyMessageId: 0 };
|
||||
case 'condition':
|
||||
return { matched: true };
|
||||
case 'end':
|
||||
return { ...inputs, status: 'completed' };
|
||||
default:
|
||||
return { ...inputs, debug: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import {
|
||||
FlowGramAPIName,
|
||||
IRuntimeClient,
|
||||
type TaskReportOutput,
|
||||
} from '@flowgram.ai/runtime-interface';
|
||||
import { injectable } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { buildBusinessDebugReport, isBusinessDebugSchema } from './business-debug-runtime';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowRuntimeBrowserClient implements IRuntimeClient {
|
||||
private businessDebugTasks = new Map<string, TaskReportOutput>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
public [FlowGramAPIName.TaskRun]: IRuntimeClient[FlowGramAPIName.TaskRun] = async (input) => {
|
||||
if (isBusinessDebugSchema(input)) {
|
||||
const taskID = `business-debug-${Date.now()}`;
|
||||
this.businessDebugTasks.set(taskID, buildBusinessDebugReport(input, taskID));
|
||||
return { taskID };
|
||||
}
|
||||
const { TaskRunAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
|
||||
return TaskRunAPI(input);
|
||||
};
|
||||
|
||||
public [FlowGramAPIName.TaskReport]: IRuntimeClient[FlowGramAPIName.TaskReport] = async (
|
||||
input
|
||||
) => {
|
||||
if (this.businessDebugTasks.has(input.taskID)) {
|
||||
return this.businessDebugTasks.get(input.taskID);
|
||||
}
|
||||
const { TaskReportAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
|
||||
return TaskReportAPI(input);
|
||||
};
|
||||
|
||||
public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult] = async (
|
||||
input
|
||||
) => {
|
||||
const report = this.businessDebugTasks.get(input.taskID);
|
||||
if (report) {
|
||||
return report.outputs;
|
||||
}
|
||||
const { TaskResultAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
|
||||
return TaskResultAPI(input);
|
||||
};
|
||||
|
||||
public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel] = async (
|
||||
input
|
||||
) => {
|
||||
if (this.businessDebugTasks.delete(input.taskID)) {
|
||||
return { success: true };
|
||||
}
|
||||
const { TaskCancelAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
|
||||
return TaskCancelAPI(input);
|
||||
};
|
||||
|
||||
public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate] = async (
|
||||
input
|
||||
) => {
|
||||
if (isBusinessDebugSchema(input)) {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
const { TaskValidateAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载
|
||||
return TaskValidateAPI(input);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { WorkflowRuntimeClient } from './base-client';
|
||||
export { WorkflowRuntimeBrowserClient } from './browser-client';
|
||||
export { WorkflowRuntimeServerClient } from './server-client';
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { ServerConfig } from '../../type';
|
||||
|
||||
export const DEFAULT_SERVER_CONFIG: ServerConfig = {
|
||||
domain: 'localhost',
|
||||
port: 4000,
|
||||
protocol: 'http',
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
FlowGramAPIName,
|
||||
IRuntimeClient,
|
||||
TaskCancelDefine,
|
||||
TaskCancelInput,
|
||||
TaskCancelOutput,
|
||||
TaskReportDefine,
|
||||
TaskReportInput,
|
||||
TaskReportOutput,
|
||||
TaskResultDefine,
|
||||
TaskResultInput,
|
||||
TaskResultOutput,
|
||||
TaskRunDefine,
|
||||
TaskRunInput,
|
||||
TaskRunOutput,
|
||||
TaskValidateDefine,
|
||||
TaskValidateInput,
|
||||
TaskValidateOutput,
|
||||
} from '@flowgram.ai/runtime-interface';
|
||||
import { injectable } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { ServerConfig } from '../../type';
|
||||
import type { ServerError } from './type';
|
||||
import { DEFAULT_SERVER_CONFIG } from './constant';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowRuntimeServerClient implements IRuntimeClient {
|
||||
private config: ServerConfig = DEFAULT_SERVER_CONFIG;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public init(config: ServerConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public async [FlowGramAPIName.TaskRun](input: TaskRunInput): Promise<TaskRunOutput | undefined> {
|
||||
return this.request<TaskRunOutput>(TaskRunDefine.path, TaskRunDefine.method, {
|
||||
body: input,
|
||||
errorMessage: 'TaskRun failed',
|
||||
});
|
||||
}
|
||||
|
||||
public async [FlowGramAPIName.TaskReport](
|
||||
input: TaskReportInput
|
||||
): Promise<TaskReportOutput | undefined> {
|
||||
return this.request<TaskReportOutput>(TaskReportDefine.path, TaskReportDefine.method, {
|
||||
queryParams: { taskID: input.taskID },
|
||||
errorMessage: 'TaskReport failed',
|
||||
});
|
||||
}
|
||||
|
||||
public async [FlowGramAPIName.TaskResult](
|
||||
input: TaskResultInput
|
||||
): Promise<TaskResultOutput | undefined> {
|
||||
return this.request<TaskResultOutput>(TaskResultDefine.path, TaskResultDefine.method, {
|
||||
queryParams: { taskID: input.taskID },
|
||||
errorMessage: 'TaskResult failed',
|
||||
fallbackValue: { success: false },
|
||||
});
|
||||
}
|
||||
|
||||
public async [FlowGramAPIName.TaskCancel](input: TaskCancelInput): Promise<TaskCancelOutput> {
|
||||
const result = await this.request<TaskCancelOutput>(
|
||||
TaskCancelDefine.path,
|
||||
TaskCancelDefine.method,
|
||||
{
|
||||
body: input,
|
||||
errorMessage: 'TaskCancel failed',
|
||||
fallbackValue: { success: false },
|
||||
}
|
||||
);
|
||||
return result ?? { success: false };
|
||||
}
|
||||
|
||||
public async [FlowGramAPIName.TaskValidate](
|
||||
input: TaskValidateInput
|
||||
): Promise<TaskValidateOutput | undefined> {
|
||||
return this.request<TaskValidateOutput>(TaskValidateDefine.path, TaskValidateDefine.method, {
|
||||
body: input,
|
||||
errorMessage: 'TaskValidate failed',
|
||||
});
|
||||
}
|
||||
|
||||
// Generic request method to reduce code duplication
|
||||
private async request<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
options: {
|
||||
body?: unknown;
|
||||
queryParams?: Record<string, string>;
|
||||
errorMessage: string;
|
||||
fallbackValue?: T;
|
||||
}
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
const url = this.url(path, options.queryParams);
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
redirect: 'follow',
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
requestOptions.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
requestOptions.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestOptions);
|
||||
const output: T | ServerError = await response.json();
|
||||
|
||||
if (this.isError(output)) {
|
||||
console.error(options.errorMessage, output);
|
||||
return options.fallbackValue;
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return options.fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Build URL with query parameters
|
||||
private url(path: string, queryParams?: Record<string, string>): string {
|
||||
const baseURL = this.getURL(`/api${path}`);
|
||||
if (!queryParams) {
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(queryParams);
|
||||
return `${baseURL}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
private isError(output: unknown | undefined): output is ServerError {
|
||||
return !!output && (output as ServerError).code !== undefined;
|
||||
}
|
||||
|
||||
private getURL(path: string): string {
|
||||
const protocol = this.config.protocol ?? window.location.protocol;
|
||||
const host = this.config.port
|
||||
? `${this.config.domain}:${this.config.port}`
|
||||
: this.config.domain;
|
||||
return `${protocol}://${host}${path}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface ServerError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { definePluginCreator, PluginContext } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { RuntimePluginOptions } from './type';
|
||||
import { WorkflowRuntimeService } from './runtime-service';
|
||||
import {
|
||||
WorkflowRuntimeBrowserClient,
|
||||
WorkflowRuntimeClient,
|
||||
WorkflowRuntimeServerClient,
|
||||
} from './client';
|
||||
|
||||
export const createRuntimePlugin = definePluginCreator<RuntimePluginOptions, PluginContext>({
|
||||
onBind({ bind, rebind }, options) {
|
||||
bind(WorkflowRuntimeClient).toSelf().inSingletonScope();
|
||||
bind(WorkflowRuntimeServerClient).toSelf().inSingletonScope();
|
||||
bind(WorkflowRuntimeBrowserClient).toSelf().inSingletonScope();
|
||||
if (options.mode === 'server') {
|
||||
rebind(WorkflowRuntimeClient).to(WorkflowRuntimeServerClient);
|
||||
} else {
|
||||
rebind(WorkflowRuntimeClient).to(WorkflowRuntimeBrowserClient);
|
||||
}
|
||||
bind(WorkflowRuntimeService).toSelf().inSingletonScope();
|
||||
},
|
||||
onInit(ctx, options) {
|
||||
if (options.mode === 'server') {
|
||||
const serverClient = ctx.get<WorkflowRuntimeServerClient>(WorkflowRuntimeClient);
|
||||
serverClient.init(options.serverConfig);
|
||||
}
|
||||
},
|
||||
});
|
||||
7
flowgram-editor/src/plugins/runtime-plugin/index.ts
Normal file
7
flowgram-editor/src/plugins/runtime-plugin/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { createRuntimePlugin } from './create-runtime-plugin';
|
||||
export { WorkflowRuntimeClient } from './client';
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
IReport,
|
||||
NodeReport,
|
||||
WorkflowInputs,
|
||||
WorkflowOutputs,
|
||||
WorkflowStatus,
|
||||
} from '@flowgram.ai/runtime-interface';
|
||||
import {
|
||||
injectable,
|
||||
inject,
|
||||
WorkflowDocument,
|
||||
Playground,
|
||||
WorkflowLineEntity,
|
||||
WorkflowNodeEntity,
|
||||
Emitter,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { WorkflowRuntimeClient } from '../client';
|
||||
import { GetGlobalVariableSchema } from '../../variable-panel-plugin';
|
||||
import { WorkflowNodeType } from '../../../nodes';
|
||||
|
||||
const SYNC_TASK_REPORT_INTERVAL = 500;
|
||||
|
||||
interface NodeRunningStatus {
|
||||
nodeID: string;
|
||||
status: WorkflowStatus;
|
||||
nodeResultLength: number;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class WorkflowRuntimeService {
|
||||
@inject(Playground) playground: Playground;
|
||||
|
||||
@inject(WorkflowDocument) document: WorkflowDocument;
|
||||
|
||||
@inject(WorkflowRuntimeClient) runtimeClient: WorkflowRuntimeClient;
|
||||
|
||||
@inject(GetGlobalVariableSchema) getGlobalVariableSchema: GetGlobalVariableSchema;
|
||||
|
||||
private runningNodes: WorkflowNodeEntity[] = [];
|
||||
|
||||
private taskID?: string;
|
||||
|
||||
private syncTaskReportIntervalID?: ReturnType<typeof setInterval>;
|
||||
|
||||
private reportEmitter = new Emitter<NodeReport>();
|
||||
|
||||
private resetEmitter = new Emitter<{}>();
|
||||
|
||||
private resultEmitter = new Emitter<{
|
||||
errors?: string[];
|
||||
result?: {
|
||||
inputs: WorkflowInputs;
|
||||
outputs: WorkflowOutputs;
|
||||
};
|
||||
}>();
|
||||
|
||||
private nodeRunningStatus: Map<string, NodeRunningStatus>;
|
||||
|
||||
public onNodeReportChange = this.reportEmitter.event;
|
||||
|
||||
public onReset = this.resetEmitter.event;
|
||||
|
||||
public onResultChanged = this.resultEmitter.event;
|
||||
|
||||
public isFlowingLine(line: WorkflowLineEntity) {
|
||||
return this.runningNodes.some((node) => node.lines.inputLines.includes(line));
|
||||
}
|
||||
|
||||
public async taskRun(inputs: WorkflowInputs): Promise<string | undefined> {
|
||||
if (this.taskID) {
|
||||
await this.taskCancel();
|
||||
}
|
||||
const isFormValid = await this.validateForm();
|
||||
if (!isFormValid) {
|
||||
this.resultEmitter.fire({
|
||||
errors: ['Form validation failed'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const schema = {
|
||||
...this.document.toJSON(),
|
||||
globalVariable: this.getGlobalVariableSchema(),
|
||||
};
|
||||
|
||||
const validateResult = await this.runtimeClient.TaskValidate({
|
||||
schema: JSON.stringify(schema),
|
||||
inputs,
|
||||
});
|
||||
if (!validateResult?.valid) {
|
||||
this.resultEmitter.fire({
|
||||
errors: validateResult?.errors ?? ['Internal Server Error'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
let taskID: string | undefined;
|
||||
try {
|
||||
const output = await this.runtimeClient.TaskRun({
|
||||
schema: JSON.stringify(schema),
|
||||
inputs,
|
||||
});
|
||||
taskID = output?.taskID;
|
||||
} catch (e) {
|
||||
this.resultEmitter.fire({
|
||||
errors: [(e as Error)?.message],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!taskID) {
|
||||
this.resultEmitter.fire({
|
||||
errors: ['Task run failed'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.taskID = taskID;
|
||||
this.syncTaskReportIntervalID = setInterval(() => {
|
||||
this.syncTaskReport();
|
||||
}, SYNC_TASK_REPORT_INTERVAL);
|
||||
return this.taskID;
|
||||
}
|
||||
|
||||
public async taskCancel(): Promise<void> {
|
||||
if (!this.taskID) {
|
||||
return;
|
||||
}
|
||||
await this.runtimeClient.TaskCancel({
|
||||
taskID: this.taskID,
|
||||
});
|
||||
}
|
||||
|
||||
private async validateForm(): Promise<boolean> {
|
||||
const allForms = this.document.getAllNodes().map((node) => node.form);
|
||||
const formValidations = await Promise.all(allForms.map(async (form) => form?.validate()));
|
||||
const validations = formValidations.filter((validation) => validation !== undefined);
|
||||
const isValid = validations.every((validation) => validation);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this.taskID = undefined;
|
||||
this.nodeRunningStatus = new Map();
|
||||
this.runningNodes = [];
|
||||
if (this.syncTaskReportIntervalID) {
|
||||
clearInterval(this.syncTaskReportIntervalID);
|
||||
}
|
||||
this.resetEmitter.fire({});
|
||||
}
|
||||
|
||||
private async syncTaskReport(): Promise<void> {
|
||||
if (!this.taskID) {
|
||||
return;
|
||||
}
|
||||
const report = await this.runtimeClient.TaskReport({
|
||||
taskID: this.taskID,
|
||||
});
|
||||
if (!report) {
|
||||
clearInterval(this.syncTaskReportIntervalID);
|
||||
console.error('Sync task report failed');
|
||||
return;
|
||||
}
|
||||
const { workflowStatus, inputs, outputs, messages } = report;
|
||||
if (workflowStatus.terminated) {
|
||||
clearInterval(this.syncTaskReportIntervalID);
|
||||
if (Object.keys(outputs).length > 0) {
|
||||
this.resultEmitter.fire({ result: { inputs, outputs } });
|
||||
} else {
|
||||
this.resultEmitter.fire({
|
||||
errors: messages?.error?.map((message) =>
|
||||
message.nodeID ? `${message.nodeID}: ${message.message}` : message.message
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
this.updateReport(report);
|
||||
}
|
||||
|
||||
private updateReport(report: IReport): void {
|
||||
const { reports } = report;
|
||||
this.runningNodes = [];
|
||||
this.document
|
||||
.getAllNodes()
|
||||
.filter(
|
||||
(node) =>
|
||||
![WorkflowNodeType.BlockStart, WorkflowNodeType.BlockEnd].includes(
|
||||
node.flowNodeType as WorkflowNodeType
|
||||
)
|
||||
)
|
||||
.forEach((node) => {
|
||||
const nodeID = node.id;
|
||||
const nodeReport = reports[nodeID];
|
||||
if (!nodeReport) {
|
||||
return;
|
||||
}
|
||||
if (nodeReport.status === WorkflowStatus.Processing) {
|
||||
this.runningNodes.push(node);
|
||||
}
|
||||
const runningStatus = this.nodeRunningStatus.get(nodeID);
|
||||
if (
|
||||
!runningStatus ||
|
||||
nodeReport.status !== runningStatus.status ||
|
||||
nodeReport.snapshots.length !== runningStatus.nodeResultLength
|
||||
) {
|
||||
this.nodeRunningStatus.set(nodeID, {
|
||||
nodeID,
|
||||
status: nodeReport.status,
|
||||
nodeResultLength: nodeReport.snapshots.length,
|
||||
});
|
||||
this.reportEmitter.fire(nodeReport);
|
||||
this.document.linesManager.forceUpdate();
|
||||
} else if (nodeReport.status === WorkflowStatus.Processing) {
|
||||
this.reportEmitter.fire(nodeReport);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
21
flowgram-editor/src/plugins/runtime-plugin/type.ts
Normal file
21
flowgram-editor/src/plugins/runtime-plugin/type.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface RuntimeBrowserOptions {
|
||||
mode?: 'browser';
|
||||
}
|
||||
|
||||
export interface RuntimeServerOptions {
|
||||
mode: 'server';
|
||||
serverConfig: ServerConfig;
|
||||
}
|
||||
|
||||
export type RuntimePluginOptions = RuntimeBrowserOptions | RuntimeServerOptions;
|
||||
|
||||
export interface ServerConfig {
|
||||
domain: string;
|
||||
port?: number;
|
||||
protocol?: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useVariableTree } from '@flowgram.ai/form-materials';
|
||||
import { Tree } from '@douyinfe/semi-ui';
|
||||
|
||||
export function FullVariableList() {
|
||||
const treeData = useVariableTree({});
|
||||
|
||||
return <Tree treeData={treeData} />;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
BaseVariableField,
|
||||
GlobalScope,
|
||||
useRefresh,
|
||||
useService,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { JsonSchemaEditor, JsonSchemaUtils } from '@flowgram.ai/form-materials';
|
||||
|
||||
export function GlobalVariableEditor() {
|
||||
const globalScope = useService(GlobalScope);
|
||||
|
||||
const refresh = useRefresh();
|
||||
|
||||
const globalVar = globalScope.getVar() as BaseVariableField;
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = globalScope.output.onVariableListChange(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!globalVar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = globalVar.type ? JsonSchemaUtils.astToSchema(globalVar.type) : { type: 'object' };
|
||||
|
||||
return (
|
||||
<JsonSchemaEditor
|
||||
value={value}
|
||||
onChange={(_schema) => globalVar.updateType(JsonSchemaUtils.schemaToAST(_schema))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
.panel-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.variable-panel-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 1;
|
||||
|
||||
&.close {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
width: 500px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);
|
||||
z-index: 30;
|
||||
|
||||
:global(.semi-tabs-bar) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
:global(.semi-tabs-content) {
|
||||
padding: 20px;
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Collapsible, Tabs, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { IconMinus } from '@douyinfe/semi-icons';
|
||||
|
||||
import iconVariable from '../../../assets/icon-variable.png';
|
||||
import { GlobalVariableEditor } from './global-variable-editor';
|
||||
import { FullVariableList } from './full-variable-list';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export function VariablePanel() {
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className={styles['panel-wrapper']}>
|
||||
<Tooltip content="Toggle Variable Panel">
|
||||
<Button
|
||||
className={`${styles['variable-panel-button']} ${isOpen ? styles.close : ''}`}
|
||||
theme={isOpen ? 'borderless' : 'light'}
|
||||
onClick={() => setOpen((_open) => !_open)}
|
||||
>
|
||||
{isOpen ? <IconMinus /> : <img src={iconVariable} width={20} height={20} />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Collapsible isOpen={isOpen}>
|
||||
<div className={styles['panel-container']}>
|
||||
<Tabs>
|
||||
<Tabs.TabPane itemKey="variables" tab="Variable List">
|
||||
<FullVariableList />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane itemKey="global" tab="Global Editor">
|
||||
<GlobalVariableEditor />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { createVariablePanelPlugin, GetGlobalVariableSchema } from './variable-panel-plugin';
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { domUtils, injectable, Layer } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { VariablePanel } from './components/variable-panel';
|
||||
|
||||
@injectable()
|
||||
export class VariablePanelLayer extends Layer {
|
||||
onReady(): void {
|
||||
// Fix variable panel in the right of canvas
|
||||
this.config.onDataChange(() => {
|
||||
const { scrollX, scrollY } = this.config.config;
|
||||
domUtils.setStyle(this.node, {
|
||||
position: 'absolute',
|
||||
right: 25 - scrollX,
|
||||
top: scrollY + 25,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return <VariablePanel />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
ASTFactory,
|
||||
definePluginCreator,
|
||||
GlobalScope,
|
||||
VariableDeclaration,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { IJsonSchema, JsonSchemaUtils } from '@flowgram.ai/form-materials';
|
||||
|
||||
import iconVariable from '../../assets/icon-variable.png';
|
||||
import { VariablePanelLayer } from './variable-panel-layer';
|
||||
|
||||
const fetchMockVariableFromRemote = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type GetGlobalVariableSchema = () => IJsonSchema;
|
||||
export const GetGlobalVariableSchema = Symbol('GlobalVariableSchemaGetter');
|
||||
|
||||
export const createVariablePanelPlugin = definePluginCreator<{ initialData?: IJsonSchema }>({
|
||||
onBind({ bind }) {
|
||||
bind(GetGlobalVariableSchema).toDynamicValue((ctx) => () => {
|
||||
const variable = ctx.container.get(GlobalScope).getVar() as VariableDeclaration;
|
||||
return JsonSchemaUtils.astToSchema(variable?.type);
|
||||
});
|
||||
},
|
||||
onInit(ctx, opts) {
|
||||
ctx.playground.registerLayer(VariablePanelLayer);
|
||||
|
||||
const globalScope = ctx.get(GlobalScope);
|
||||
|
||||
if (opts.initialData) {
|
||||
globalScope.setVar(
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: 'global',
|
||||
meta: {
|
||||
title: 'Global',
|
||||
icon: iconVariable,
|
||||
},
|
||||
type: JsonSchemaUtils.schemaToAST(opts.initialData),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// You can also fetch global variable from remote
|
||||
fetchMockVariableFromRemote().then((v) => {
|
||||
globalScope.setVar(
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: 'global',
|
||||
meta: {
|
||||
title: 'Global',
|
||||
icon: iconVariable,
|
||||
},
|
||||
type: JsonSchemaUtils.schemaToAST(v),
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user