feat(flows): 新增流程编辑器基础功能与相关组件

feat(backend): 添加流程模型与服务支持
feat(frontend): 实现流程编辑器UI与交互
feat(assets): 添加流程节点图标资源
feat(plugins): 实现上下文菜单和运行时插件
feat(components): 新增基础节点和侧边栏组件
feat(routes): 添加流程相关路由配置
feat(models): 创建流程和运行日志数据模型
feat(services): 实现流程服务层逻辑
feat(migration): 添加流程相关数据库迁移
feat(config): 更新前端配置支持流程编辑器
feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
2025-09-15 00:27:13 +08:00
parent 9da3978f91
commit b0963e5e37
291 changed files with 17947 additions and 86 deletions

View File

@ -0,0 +1,211 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FC, useContext, useEffect, useState } from 'react';
import classnames from 'classnames';
import { useService, I18n } from '@flowgram.ai/free-layout-editor';
import { Button, SideSheet, Switch, Tag } from '@douyinfe/semi-ui';
import { IconClose, IconPlay, IconSpin } from '@douyinfe/semi-icons';
import { TestRunJsonInput } from '../testrun-json-input';
import { TestRunForm } from '../testrun-form';
import { NodeStatusGroup } from '../node-status-bar/group';
// 改为使用后端运行服务
import { CustomService } from '../../../services';
import { SidebarContext } from '../../../context';
import { IconCancel } from '../../../assets/icon-cancel';
import styles from './index.module.less';
interface TestRunSidePanelProps {
visible: boolean;
onCancel: () => void;
}
export const TestRunSidePanel: FC<TestRunSidePanelProps> = ({ visible, onCancel }) => {
const customService = useService(CustomService);
const { nodeId: sidebarNodeId, setNodeId } = useContext(SidebarContext);
const [isRunning, setRunning] = useState(false);
const [values, setValues] = useState<Record<string, unknown>>({});
const [errors, setErrors] = useState<string[]>();
const [result, setResult] = useState<
| {
ok?: boolean;
ctx: any;
logs: string[];
}
| undefined
>();
// en - Use localStorage to persist the JSON mode state
const [inputJSONMode, _setInputJSONMode] = useState(() => {
const savedMode = localStorage.getItem('testrun-input-json-mode');
return savedMode ? JSON.parse(savedMode) : false;
});
const setInputJSONMode = (checked: boolean) => {
_setInputJSONMode(checked);
localStorage.setItem('testrun-input-json-mode', JSON.stringify(checked));
};
const extractErrorMsg = (logs: string[] | undefined): string | undefined => {
if (!logs || logs.length === 0) return undefined;
const patterns = [/failed/i, /error/i, /panic/i];
for (const line of logs) {
if (patterns.some((p) => p.test(line))) return line;
}
return logs[logs.length - 1];
};
const onTestRun = async () => {
if (isRunning) {
// 后端运行不可取消,这里直接忽略重复点击
return;
}
setResult(undefined);
setErrors(undefined);
setRunning(true);
try {
// 运行前保存(静默),确保后端 YAML 与编辑器一致;若保存失败则不继续运行
const saved = await customService.save({ silent: true });
if (!saved) {
setErrors([I18n.t('Save failed, cannot run')]);
return;
}
const runRes = await customService.run(values);
if (runRes) {
// 若后端返回 ok=false则视为失败并展示失败信息与日志
if ((runRes as any).ok === false) {
setResult(runRes as any);
const err = extractErrorMsg((runRes as any).logs) || I18n.t('Run failed');
setErrors([err]);
} else {
setResult(runRes as any);
}
} else {
setErrors([I18n.t('Run failed')]);
}
} catch (e: any) {
setErrors([e?.message || I18n.t('Run failed')]);
} finally {
setRunning(false);
}
};
const onClose = async () => {
setValues({});
setRunning(false);
onCancel();
};
// sidebar effect
useEffect(() => {
setNodeId(undefined);
}, []);
useEffect(() => {
if (sidebarNodeId) {
onCancel();
}
}, [sidebarNodeId]);
const renderRunning = (
<div className={styles['testrun-panel-running']}>
<IconSpin spin size="large" />
<div className={styles.text}>{I18n.t('Running...')}</div>
</div>
);
const renderStatus = (
<div style={{ marginBottom: 8 }}>
{result?.ok === true && <Tag color="green">{I18n.t('Success')}</Tag>}
{(errors?.length || result?.ok === false) && (
<Tag color="red">{I18n.t('Failed')}</Tag>
)}
</div>
);
const renderForm = (
<div className={styles['testrun-panel-form']}>
<div className={styles['testrun-panel-input']}>
<div className={styles.title}>{I18n.t('Input Form')}</div>
<div>{I18n.t('JSON Mode')}</div>
<Switch
checked={inputJSONMode}
onChange={(checked: boolean) => setInputJSONMode(checked)}
size="small"
/>
</div>
{renderStatus}
{errors?.map((e) => (
<div className={styles.error} key={e}>
{e}
</div>
))}
{inputJSONMode ? (
<TestRunJsonInput values={values} setValues={setValues} />
) : (
<TestRunForm values={values} setValues={setValues} />
)}
{/* 展示后端返回的执行信息 */}
<NodeStatusGroup title={I18n.t('Context')} data={result?.ctx} optional disableCollapse />
<NodeStatusGroup title={I18n.t('Logs')} data={result?.logs} optional disableCollapse />
</div>
);
const renderButton = (
<Button
onClick={onTestRun}
icon={isRunning ? <IconCancel /> : <IconPlay size="small" />}
className={classnames(styles.button, {
[styles.running]: isRunning,
[styles.default]: !isRunning,
})}
>
{isRunning ? I18n.t('Running...') : I18n.t('Test Run')}
</Button>
);
return (
<SideSheet
title={I18n.t('Test Run')}
visible={visible}
mask={false}
motion={false}
onCancel={onClose}
width={400}
headerStyle={{
display: 'none',
}}
bodyStyle={{
padding: 0,
}}
style={{
background: 'none',
boxShadow: 'none',
}}
>
<div className={styles['testrun-panel-container']}>
<div className={styles['testrun-panel-header']}>
<div className={styles['testrun-panel-title']}>{I18n.t('Test Run')}</div>
<Button
className={styles['testrun-panel-title']}
type="tertiary"
icon={<IconClose />}
size="small"
theme="borderless"
onClick={onClose}
/>
</div>
<div className={styles['testrun-panel-content']}>
{isRunning ? renderRunning : renderForm}
</div>
<div className={styles['testrun-panel-footer']}>{renderButton}</div>
</div>
</SideSheet>
);
};