coze-studio/frontend/packages/workflow/playground/src/services/workflow-dependency-service.ts

251 lines
9.4 KiB
TypeScript

/*
* 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 { get, debounce } from 'lodash-es';
import { inject, injectable } from 'inversify';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
import { Emitter, type Event } from '@flowgram-adapter/common';
import {
MessageBizType,
MessageOperateType,
StandardNodeType,
} from '@coze-workflow/base';
import type { WsMessageProps } from '@coze-project-ide/framework/src/types';
import { getFlags } from '@coze-arch/bot-flags';
import {
WorkflowGlobalStateEntity,
WorkflowDependencyStateEntity,
} from '@/entities';
import { DependencySourceType } from '@/constants';
import { WorkflowSaveService } from './workflow-save-service';
import { TestRunState, WorkflowRunService } from './workflow-run-service';
import { WorkflowOperationService } from './workflow-operation-service';
import { WorkflowModelsService } from './workflow-models-service';
export const bizTypeToDependencyTypeMap = {
[MessageBizType.Workflow]: DependencySourceType.Workflow,
[MessageBizType.Plugin]: DependencySourceType.Plugin,
[MessageBizType.Dataset]: DependencySourceType.DataSet,
[MessageBizType.Database]: DependencySourceType.DataBase,
};
const DEBOUNCE_TIME = 2000;
export interface DependencyStore {
refreshModalVisible: boolean;
setRefreshModalVisible: (visible: boolean) => void;
}
interface SubworkflowVersionChangeProps {
subWorkflowId: string;
}
@injectable()
export class WorkflowDependencyService {
@inject(WorkflowModelsService) modelsService: WorkflowModelsService;
@inject(WorkflowDocument) protected workflowDocument: WorkflowDocument;
@inject(WorkflowGlobalStateEntity) globalState: WorkflowGlobalStateEntity;
@inject(WorkflowSaveService) saveService: WorkflowSaveService;
@inject(WorkflowOperationService) operationService: WorkflowOperationService;
@inject(WorkflowRunService) testRunService: WorkflowRunService;
@inject(WorkflowDependencyStateEntity)
dependencyEntity: WorkflowDependencyStateEntity;
onDependencyChangeEmitter = new Emitter<WsMessageProps | undefined>();
onDependencyChange: Event<WsMessageProps | undefined> =
this.onDependencyChangeEmitter.event;
onSubWrokflowVersionChangeEmitter = new Emitter<
SubworkflowVersionChangeProps | undefined
>();
onSubWrokflowVersionChange: Event<SubworkflowVersionChangeProps | undefined> =
this.onSubWrokflowVersionChangeEmitter.event;
/**
* Possible badcases:
* - The save interface returns slowly, resulting in the refresh pop-up window of the long chain refresh before the version conflict error, but this scenario is relatively limited, and it is necessary to optimize it later.
* - Canvas interface returns slowly, resulting in a long chain push notification message consistent with the current version, resulting in an additional refresh.
* Avoid this problem by judging the version number again before refreshing.
*/
private workflowDocumentReload = debounce(
(callback, messageVersion?: bigint) => {
const isVersionNewer =
messageVersion && this.dependencyEntity.saveVersion > messageVersion;
if (this.dependencyEntity.refreshModalVisible || isVersionNewer) {
return;
}
callback?.();
},
DEBOUNCE_TIME,
);
updateDependencySources(props: WsMessageProps, callback?: () => void) {
const FLAGS = getFlags();
if (!FLAGS['bot.automation.project_multi_tab']) {
return;
}
const allNodes = this.workflowDocument.getAllNodes();
const llmNodes = allNodes.filter(
n => n.flowNodeType === StandardNodeType.LLM,
);
// LLM Node Skill Update
llmNodes?.forEach(node => {
const formData = node.getData(FlowNodeFormData);
const formValue = formData.toJSON();
const llmSkillIdFields = [
{ field: 'fcParam.pluginFCParam.pluginList', key: 'api_id' },
{ field: 'fcParam.knowledgeFCParam.knowledgeList', key: 'id' },
{ field: 'fcParam.workflowFCParam.workflowList', key: 'workflow_id' },
];
const skillNeedRefresh = llmSkillIdFields.some(subSkill => {
const subSkillList = get(formValue.inputs, subSkill.field) ?? [];
return subSkillList.find(
(item: unknown) => get(item, subSkill.key) === props?.resId,
);
});
if (skillNeedRefresh) {
const nextProps: WsMessageProps = {
...props,
extra: {
nodeIds: [node.id],
},
};
this.onDependencyChangeEmitter.fire(nextProps);
}
});
const { saveVersion } = this.dependencyEntity;
const dependencyHandlers: {
[key in DependencySourceType]: (
nodes: FlowNodeEntity[],
source?: WsMessageProps,
) => Promise<void> | void;
} = {
[DependencySourceType.Workflow]: (_nodes, resource) => {
// The current workflow is updated on other pages
if (resource?.resId === this.globalState.workflowId) {
// If you modify the workflow name or description, saveVersion will not be updated
const isMetaUpdate =
resource?.operateType === MessageOperateType.MetaUpdate;
// Switch workflow type scene needs to be refreshed
const isFlowModeChange =
resource?.extra?.flowMode !== undefined &&
this.globalState.flowMode.toString() !== resource.extra.flowMode;
const metaUpdateNeedRefresh = isMetaUpdate && isFlowModeChange;
// No refresh required during practice runs
const isTestRunning =
this.testRunService.testRunState === TestRunState.Executing ||
this.testRunService.testRunState === TestRunState.Paused;
// The current version is larger than the saved version of other pages and does not need to be refreshed.
const resourceVersion = BigInt(resource?.saveVersion ?? 0);
const isCurVersionNewer = saveVersion > resourceVersion;
if (
isCurVersionNewer ||
this.dependencyEntity.refreshModalVisible ||
isTestRunning
) {
if (metaUpdateNeedRefresh) {
this.workflowDocumentReload(callback);
}
return;
}
this.workflowDocumentReload(callback, resourceVersion);
return;
}
const subWorkflowNodes = _nodes.filter(
n => n.flowNodeType === StandardNodeType.SubWorkflow,
);
const needUpdateNodeIds: string[] = [];
subWorkflowNodes?.forEach(node => {
const formData = node.getData(FlowNodeFormData);
const formValue = formData.toJSON();
const { workflowId } = formValue.inputs;
// The child workflows within the current workflow are updated
if (resource?.resId === workflowId) {
needUpdateNodeIds.push(workflowId);
}
});
if (!needUpdateNodeIds.length) {
return;
}
const nextProps: WsMessageProps = {
...props,
extra: {
nodeIds: needUpdateNodeIds,
},
};
this.onDependencyChangeEmitter.fire(nextProps);
},
[DependencySourceType.Plugin]: (nodes, resource) => {
const apiNodes = nodes.filter(
n => n.flowNodeType === StandardNodeType.Api,
);
const needUpdateNodeIds: string[] = [];
apiNodes?.forEach(node => {
const formData = node.getData(FlowNodeFormData);
const formValue = formData.toJSON();
const apiParam = formValue.inputs.apiParam.find(
param => param.name === 'apiID',
);
const apiID = apiParam.input.value.content ?? '';
if (apiID === resource?.resId) {
needUpdateNodeIds.push(node.id);
}
});
if (!needUpdateNodeIds.length) {
return;
}
const nextProps: WsMessageProps = {
...props,
extra: {
nodeIds: needUpdateNodeIds,
},
};
this.onDependencyChangeEmitter.fire(nextProps);
},
[DependencySourceType.DataSet]: (_, resource) => {
// Clear the knowledge base cache
this.globalState.sharedDataSetStore.clearDataSetInfosMap();
this.onDependencyChangeEmitter.fire(resource);
},
[DependencySourceType.DataBase]: (_, resource) => {
this.onDependencyChangeEmitter.fire(resource);
},
[DependencySourceType.LLM]: (_, resource) => {
this.onDependencyChangeEmitter.fire(resource);
},
};
if (props?.bizType) {
// Set the refresh method in the refresh pop-up window
this.dependencyEntity.setRefreshFunc(() => {
callback?.();
});
const dependencyType = bizTypeToDependencyTypeMap[props.bizType];
dependencyHandlers[dependencyType](allNodes, props);
}
}
}