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

263 lines
7.3 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 { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
import { inject, injectable } from 'inversify';
import {
type ApiNodeDetailDTO,
type ApiNodeIdentifier,
} from '@coze-workflow/nodes';
import { workflowApi, workflowQueryClient } from '@coze-workflow/base';
import {
PluginProductStatus,
ProductUnlistType,
} from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { WorkflowGlobalStateEntity } from '@/entities';
interface PluginNodeServiceState {
/**
* Is the plug-in node data loading?
*/
loading: boolean;
/**
* Plug-in node data, key is the unique identifier of the plug-in specific tool, and value is the plug-in node data.
*/
data: Record<string, ApiNodeDetailDTO>;
/**
* Plug-in node data loading error message, key is the unique identifier of the plug-in specific tool, and value is the error message
*/
error: Record<string, string | undefined>;
}
interface PluginNodeServiceAction {
getData: (identifier: ApiNodeIdentifier) => ApiNodeDetailDTO;
setData: (identifier: ApiNodeIdentifier, value: ApiNodeDetailDTO) => void;
getError: (identifier: ApiNodeIdentifier) => string | undefined;
setError: (identifier: ApiNodeIdentifier, value: string | undefined) => void;
clearError: (identifier: ApiNodeIdentifier) => void;
}
export type PluginNodeStore = PluginNodeServiceState & PluginNodeServiceAction;
const STALE_TIME = 5000;
function getCacheKey(identifier: ApiNodeIdentifier) {
return `${identifier.pluginID}_${identifier.plugin_version}_${identifier.api_id}`;
}
const createStore = () =>
createWithEqualityFn<PluginNodeServiceState & PluginNodeServiceAction>(
(set, get) => ({
loading: false,
data: {},
error: {},
getData(identifier) {
const key = getCacheKey(identifier);
return get().data[key];
},
setData(identifier, value) {
const key = getCacheKey(identifier);
set({
data: {
...get().data,
[key]: value,
},
});
},
getError(identifier) {
const key = getCacheKey(identifier);
return get().error[key];
},
setError(identifier, value) {
const key = getCacheKey(identifier);
set({
error: {
...get().error,
[key]: value,
},
});
},
clearError(identifier) {
const key = getCacheKey(identifier);
set({
error: {
...get().error,
[key]: undefined,
},
});
},
}),
shallow,
);
@injectable()
export class PluginNodeService {
@inject(WorkflowGlobalStateEntity) globalState: WorkflowGlobalStateEntity;
store = createStore();
set loading(v: boolean) {
this.store.setState({
loading: v,
});
}
get state() {
return this.store.getState();
}
getApiDetail(identifier: ApiNodeIdentifier) {
return this.state.getData(identifier);
}
getApiError(identifier: ApiNodeIdentifier) {
return this.state.getError(identifier);
}
clearApiError(identifier: ApiNodeIdentifier) {
this.state.clearError(identifier);
}
async fetchData(identifier: ApiNodeIdentifier) {
const { spaceId, projectId } = this.globalState;
return workflowQueryClient.fetchQuery({
queryKey: [
'loadApiNodeDetail',
spaceId,
identifier.pluginID,
identifier.plugin_version,
identifier.apiName,
identifier.api_id,
projectId,
],
// 1. Set up a 5s cache to ensure that the same request is only sent once in a process, and there will be no excessive performance degradation.
// 2. api detail contains the input and output, version information of the plug-in, the data has real-time sensitivity, and there is no data lag.
staleTime: STALE_TIME,
queryFn: async () =>
await workflowApi.GetApiDetail(
{
...identifier,
space_id: spaceId,
project_id: projectId,
},
{
__disableErrorToast: true,
},
),
});
}
/**
* Plugin status check, whether it is invalid, and determine whether ApiNode is available
* @param params
* Whether @returns is invalid
*/
isApiNodeDeprecated(params: {
currentSpaceID: string;
pluginSpaceID?: string;
pluginProductStatus?: PluginProductStatus;
pluginProductUnlistType?: ProductUnlistType;
}): boolean {
const {
currentSpaceID,
pluginSpaceID,
pluginProductStatus,
pluginProductUnlistType,
} = params;
// Not removed from the shelves
if (pluginProductStatus !== PluginProductStatus.Unlisted) {
return false;
}
// Removed by administrator
if (pluginProductUnlistType === ProductUnlistType.ByAdmin) {
return true;
}
// Removed by the user, but not the creation space of the current plugin
if (
pluginProductUnlistType === ProductUnlistType.ByUser &&
currentSpaceID !== pluginSpaceID
) {
return true;
}
// Removed by the user, but in the plugin creation space
return false;
}
async load(identifier: ApiNodeIdentifier) {
let apiDetail: ApiNodeDetailDTO | undefined;
let errorMessage = '';
try {
this.loading = true;
const response = await this.fetchData(identifier);
apiDetail = response.data as ApiNodeDetailDTO;
} catch (error) {
errorMessage = error.message;
if (error.code === '702095021') {
errorMessage = I18n.t('workflow_node_lose_efficacy', {
name: identifier.apiName,
});
}
} finally {
this.loading = false;
}
if (errorMessage) {
this.state.setError(identifier, errorMessage);
}
if (!apiDetail) {
this.state.setError(identifier, errorMessage || 'loadApiNode failed');
return;
} else {
this.state.setData(identifier, {
...apiDetail,
// If the plug-in name is changed (for example, getStock is changed to getStock_v1), apiName will not change or getStock, and name is the updated getStock_v1
// At this time, the name field needs to be taken first, otherwise testrun will use the old apiName, resulting in unsuccessful practice run
apiName: apiDetail.name || apiDetail.apiName,
});
}
const deprecated = this.isApiNodeDeprecated({
currentSpaceID: this.globalState.spaceId,
pluginSpaceID: apiDetail.spaceID,
pluginProductStatus: apiDetail.pluginProductStatus,
pluginProductUnlistType: apiDetail.pluginProductUnlistType,
});
if (deprecated) {
this.state.setError(
identifier,
I18n.t('workflow_node_lose_efficacy', { name: identifier.apiName }),
);
}
return apiDetail;
}
}