feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
definePluginCreator,
type PluginCreator,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowOperationReportService } from './services/workflow-operation-report-service';
export const createOperationReportPlugin: PluginCreator<object> =
definePluginCreator<object>({
onBind: ({ bind }) => {
bind(WorkflowOperationReportService).toSelf().inSingletonScope();
},
onInit(ctx): void {
ctx
.get<WorkflowOperationReportService>(WorkflowOperationReportService)
.init();
},
onDispose(ctx) {
ctx
.get<WorkflowOperationReportService>(WorkflowOperationReportService)
.dispose();
},
});

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { HistoryService } from '@flowgram-adapter/common';
/**
* 清空undo redo历史栈
* @returns
*/
export function useClearHistory(): {
clearHistory: () => void;
} {
const historyService = useService<HistoryService>(HistoryService);
const clearHistory = useCallback(() => {
historyService.clear();
}, []);
return {
clearHistory,
};
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WorkflowHistoryContainerModule } from './workflow-history-container-module';
export { useClearHistory } from './hooks/use-clear-history';
export { WorkflowHistoryConfig } from './workflow-history-config';
export { createOperationReportPlugin } from './create-operation-report-plugin';
export {
HistoryService,
createFreeHistoryPlugin,
} from '@flowgram-adapter/free-layout-editor';

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type AddOrDeleteLineOperationValue,
FreeOperationType,
WorkflowDocument,
WorkflowLinesManager,
} from '@flowgram-adapter/free-layout-editor';
import { type PluginContext } from '@flowgram-adapter/free-layout-editor';
import { type OperationMeta } from '@flowgram-adapter/free-layout-editor';
import { shouldMerge } from '../utils/should-merge';
export const addLineOperationMeta: OperationMeta<
AddOrDeleteLineOperationValue,
PluginContext,
void
> = {
type: FreeOperationType.addLine,
inverse: op => ({
...op,
type: FreeOperationType.deleteLine,
}),
apply: (operation, ctx: PluginContext) => {
const linesManager = ctx.get<WorkflowLinesManager>(WorkflowLinesManager);
const document = ctx.get<WorkflowDocument>(WorkflowDocument);
if (!operation.value.to || !document.getNode(operation.value.to)) {
return;
}
linesManager.createLine({
...operation.value,
key: operation.value.id,
});
},
shouldMerge,
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash-es';
import {
type AddOrDeleteWorkflowNodeOperationValue,
FreeOperationType,
WorkflowDocument,
type WorkflowNodeJSON,
} from '@flowgram-adapter/free-layout-editor';
import { type PluginContext } from '@flowgram-adapter/free-layout-editor';
import { type OperationMeta } from '@flowgram-adapter/free-layout-editor';
import { shouldMerge } from '../utils/should-merge';
export const addNodeOperationMeta: OperationMeta<
AddOrDeleteWorkflowNodeOperationValue,
PluginContext,
void
> = {
type: FreeOperationType.addNode,
inverse: op => ({
...op,
type: FreeOperationType.deleteNode,
}),
apply: (operation, ctx: PluginContext) => {
const document = ctx.get<WorkflowDocument>(WorkflowDocument);
document.createWorkflowNode(
cloneDeep(operation.value.node) as WorkflowNodeJSON,
true,
operation.value.parentID,
);
},
shouldMerge,
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type OperationMeta } from '@flowgram-adapter/free-layout-editor';
import { addNodeOperationMeta } from './add-node';
import { addLineOperationMeta } from './add-line';
export const operationMetas: OperationMeta[] = [
addNodeOperationMeta,
addLineOperationMeta,
];

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { snakeCase } from 'lodash-es';
import { injectable, inject } from 'inversify';
import { reporter } from '@coze-workflow/base';
import {
type AddOrDeleteLineOperationValue,
type AddOrDeleteWorkflowNodeOperationValue,
type ChangeNodeDataValue,
FreeOperationType,
} from '@flowgram-adapter/free-layout-editor';
import { DisposableCollection } from '@flowgram-adapter/common';
import { type Operation, OperationService } from '@flowgram-adapter/common';
@injectable()
export class WorkflowOperationReportService {
@inject(OperationService)
private readonly operationService: OperationService;
private toDispose = new DisposableCollection();
private lastOperation: Operation | null = null;
init() {
this.toDispose.pushAll([
this.operationService.onApply((operation: Operation) => {
if (!operation.type) {
return;
}
try {
if (!this.shouldReport(operation)) {
return;
}
const message = this.getMessageByOperation(operation);
reporter.info({ message });
} catch (e) {
reporter.error({
error: e as Error,
message: 'workflow operation report error',
});
}
this.lastOperation = operation;
}),
]);
}
dispose() {
this.toDispose.dispose();
}
private getMessageByOperation(operation: Operation): string {
const { type, value } = operation;
const eventName = this.getEventName(type);
if (
type === FreeOperationType.addLine ||
type === FreeOperationType.deleteLine
) {
const {
from,
to = '',
fromPort = '',
toPort = '',
} = value as AddOrDeleteLineOperationValue;
return `${eventName} from ${from}${this.portToString(fromPort)} to ${to}${this.portToString(toPort)}`;
}
if (
type === FreeOperationType.addNode ||
type === FreeOperationType.deleteNode
) {
const {
node: { id },
} = value as AddOrDeleteWorkflowNodeOperationValue;
return `${eventName} ${id}`;
}
if (this.isChangeDataType(type)) {
const { id, path } = value as ChangeNodeDataValue;
const message = `${eventName} node:${id} path:${path}`;
return message;
}
return eventName;
}
private getEventName(type: string) {
return `workflow_${snakeCase(type)}`;
}
private isChangeDataType(type: string) {
return (
type === FreeOperationType.changeNodeData || type === 'changeFormValues'
);
}
private shouldReport(operation: Operation) {
const { value, type } = operation;
// 修改同一个节点,同一个属性只上报一次, 防止频繁编辑上报过多
if (
this.isChangeDataType(type) &&
this.lastOperation &&
type === this.lastOperation.type
) {
const { path, id } = value as ChangeNodeDataValue;
const { path: lastPath, id: lastId } = this.lastOperation
.value as ChangeNodeDataValue;
if (path === lastPath && id === lastId) {
return false;
}
}
return true;
}
private portToString(port: string | number) {
return port ? `:${port}` : '';
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type OperationMeta } from '@flowgram-adapter/free-layout-editor';
export const shouldMerge: OperationMeta['shouldMerge'] = (_op, prev, element) =>
!!(prev && Date.now() - element.getTimestamp() < 500);

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { injectable } from 'inversify';
@injectable()
export class WorkflowHistoryConfig {
disabled = false;
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContainerModule } from 'inversify';
import { OperationContribution } from '@flowgram-adapter/free-layout-editor';
import { bindContributions } from '@flowgram-adapter/common';
import { WorkflowShortcutsContribution } from '@coze-workflow/render';
import { WorkflowHistoryShortcutsContribution } from './workflow-history-shortcuts-contribution';
import { WorklfowHistoryOperationsContribution } from './workflow-history-operations-contribution';
import { WorkflowHistoryConfig } from './workflow-history-config';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const WorkflowHistoryContainerModule = new ContainerModule(bind => {
bindContributions(bind, WorkflowHistoryShortcutsContribution, [
WorkflowShortcutsContribution,
]);
bindContributions(bind, WorklfowHistoryOperationsContribution, [
OperationContribution,
]);
bind(WorkflowHistoryConfig).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { injectable } from 'inversify';
import {
type OperationContribution,
type OperationRegistry,
} from '@flowgram-adapter/free-layout-editor';
import { operationMetas } from './operation-metas';
@injectable()
export class WorklfowHistoryOperationsContribution
implements OperationContribution
{
registerOperationMeta(operationRegistry: OperationRegistry): void {
operationMetas.forEach(operationMeta => {
operationRegistry.registerOperationMeta(operationMeta);
});
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { inject, injectable } from 'inversify';
import { PlaygroundConfigEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowCommands } from '@flowgram-adapter/free-layout-editor';
import { HistoryService } from '@flowgram-adapter/common';
import {
type WorkflowShortcutsContribution,
type WorkflowShortcutsRegistry,
} from '@coze-workflow/render';
import { reporter } from '@coze-workflow/base';
import { WorkflowHistoryConfig } from './workflow-history-config';
/**
* history 快捷键
*/
@injectable()
export class WorkflowHistoryShortcutsContribution
implements WorkflowShortcutsContribution
{
@inject(HistoryService)
private _historyService: HistoryService;
@inject(WorkflowHistoryConfig)
private _config: WorkflowHistoryConfig;
@inject(PlaygroundConfigEntity)
private _playgroundConfig: PlaygroundConfigEntity;
registerShortcuts(registry: WorkflowShortcutsRegistry): void {
registry.addHandlers(
/**
* 撤销
*/
{
commandId: WorkflowCommands.UNDO,
shortcuts: ['meta z', 'ctrl z'],
isEnabled: () => !this._playgroundConfig.readonly,
execute: () => {
if (this._config.disabled) {
return;
}
this._historyService.undo();
reporter.info({
message: 'workflow_undo',
});
},
},
/**
* 重做
*/
{
commandId: WorkflowCommands.REDO,
shortcuts: ['meta shift z', 'ctrl shift z'],
isEnabled: () => !this._playgroundConfig.readonly,
execute: () => {
if (this._config.disabled) {
return;
}
this._historyService.redo();
reporter.info({
message: 'workflow_redo',
});
},
},
);
}
}