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,99 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { injectable, inject } from '@flowgram.ai/free-layout-editor';
import {
FreeLayoutPluginContext,
SelectionService,
Playground,
WorkflowDocument,
} from '@flowgram.ai/free-layout-editor';
import { Toast } from '@douyinfe/semi-ui';
import { I18n } from '@flowgram.ai/free-layout-editor';
import api, { type ApiResp } from '../../utils/axios';
import { stringifyFlowDoc } from '../utils/yaml';
interface RunResult { ok: boolean; ctx: any; logs: string[] }
// 兼容 BrowserRouter 与 HashRouter优先从 search 获取,若无则从 hash 的查询串中获取
function getFlowIdFromUrl(): string {
const searchId = new URLSearchParams(window.location.search).get('id');
if (searchId) return searchId;
const hash = window.location.hash || '';
const qIndex = hash.indexOf('?');
if (qIndex >= 0) {
const qs = hash.substring(qIndex + 1);
const hashId = new URLSearchParams(qs).get('id');
if (hashId) return hashId;
}
return '';
}
@injectable()
export class CustomService {
@inject(FreeLayoutPluginContext) ctx!: FreeLayoutPluginContext;
@inject(SelectionService) selectionService!: SelectionService;
@inject(Playground) playground!: Playground;
@inject(WorkflowDocument) document!: WorkflowDocument;
// 新增可选参数,用于静默保存时不弹出 Toast并返回是否保存成功
async save(opts?: { silent?: boolean }): Promise<boolean> {
const silent = !!opts?.silent;
try {
const id = getFlowIdFromUrl();
if (!id) {
if (!silent) Toast.error(I18n.t('Flow ID is missing, cannot save'));
return false;
}
const json = this.document.toJSON() as any;
const yaml = stringifyFlowDoc(json);
const design_json = JSON.stringify(json);
const { data } = await api.put<ApiResp<{ saved: boolean }>>(`/flows/${id}`, { yaml, design_json });
if (data?.code === 0) {
if (!silent) Toast.success(I18n.t('Saved'));
try {
const key = (() => {
const hash = window.location.hash || '';
if (hash.startsWith('#/')) {
return hash.slice(1);
}
return window.location.pathname + (window.location.search || '');
})();
window.dispatchEvent(new CustomEvent('flows:doc-dirty', { detail: { key, dirty: false } }));
} catch {}
return true;
} else {
const msg = data?.message || I18n.t('Save failed');
if (!silent) Toast.error(msg);
return false;
}
} catch (e: any) {
const msg = e?.message || I18n.t('Save failed');
if (!silent) Toast.error(msg);
return false;
}
}
async run(input: any = {}) {
try {
const id = getFlowIdFromUrl();
if (!id) {
Toast.error(I18n.t('Flow ID is missing, cannot run'));
return null;
}
const { data } = await api.post<ApiResp<RunResult>>(`/flows/${id}/run`, { input });
if (data?.code === 0) {
return data.data;
}
throw new Error(data?.message || I18n.t('Run failed'));
} catch (e: any) {
Toast.error(e?.message || I18n.t('Run failed'));
return null;
}
}
}