/* * 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 max-lines */ /* eslint-disable @coze-arch/max-line-per-function */ /* eslint-disable max-lines-per-function, complexity -- onApiToggle of PluginItem can be extracted and optimized later */ /* eslint-disable import/order */ import { type MutableRefObject, useEffect, useMemo, useRef, useState, } from 'react'; import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor'; import classNames from 'classnames'; import { useInViewport } from 'ahooks'; import groupBy from 'lodash-es/groupBy'; import { Collapse, Divider, Highlight, Image, Space, Toast, Typography, UIButton, } from '@coze-arch/bot-semi'; import { IconViewinchatOutlined } from '@coze-arch/bot-icons'; import { ConnectorList, OfficialLabel } from '@coze-community/components'; import { I18n } from '@coze-arch/i18n'; import { emitEvent, OpenBlockEvent, formatDate, formatNumber, } from '@coze-arch/bot-utils'; import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea'; import { PluginDevelopApi } from '@coze-arch/bot-api'; import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info'; import { useSpaceStore } from '@coze-arch/bot-studio-store'; import { OpenModeType } from '@coze-arch/bot-hooks'; import { From, type PluginModalModeProps, } from '../../types/plugin-modal-types'; import { getPluginApiKey } from '../../utils'; import { type PluginApi, type PluginInfoForPlayground, PluginType, } from '@coze-arch/bot-api/plugin_develop'; import { type Int64 } from '@coze-arch/bot-api/developer_api'; import s from './index.module.less'; import { type SimplifyProductInfo } from '../../service/fetch-plugin'; import { PluginItem } from './item'; import { extractApiParams } from './helper'; import { AvatarName } from '@coze-studio/components'; import { PluginPerfStatics } from './plugin-perf-statics'; import { withSlardarIdButton } from '@coze-studio/bot-utils'; import { Tag, Tooltip } from '@coze-arch/coze-design'; import { IconCozDesktop, IconCozInfoCircle, } from '@coze-arch/coze-design/icons'; import { isBoolean, isUndefined } from 'lodash-es'; import { type CommercialSetting, PluginType as ProductPluginType, } from '@coze-arch/bot-api/product_api'; import { PluginAuthMode } from '../../types/auth-mode'; export interface PluginPanelProps extends PluginModalModeProps { info: PluginInfoForPlayground & { listed_at?: Int64; version_name?: string; version_ts?: string; }; highlightWords?: string[]; showButton?: boolean; showCreator?: boolean; showCreateTime?: boolean; showMarketLink?: boolean; showProjectPluginLink?: boolean; showPublishTime?: boolean; className?: string; pluginApiList: PluginApi[]; onPluginApiListChange: (list: PluginApi[]) => void; productInfo?: SimplifyProductInfo; commercialSetting?: CommercialSetting; isFromMarket?: boolean; type?: string; scrollContainerRef?: MutableRefObject; activeKey?: string | string[] | undefined; agentId?: string; workflowNodes?: WorkflowNodeJSON[]; index?: number; addonBefore?: React.ReactNode; addonAfter?: React.ReactNode; } const CopyPlugin = ({ onApiToggle }: { onApiToggle: () => void }) => ( { e.stopPropagation(); onApiToggle?.(); }} > {I18n.t('add_resource_modal_copy_to_project')} ); export const PluginPanel: React.FC = ({ info, highlightWords, showCreator = false, showCreateTime = true, showPublishTime = false, showButton = true, showCopyPlugin = false, className = '', pluginApiList, productInfo, showMarketLink, showProjectPluginLink, clickProjectPluginCallback, isFromMarket, onPluginApiListChange, openMode, from, workflowNodes, openModeCallback, onCopyPluginCallback, scrollContainerRef, type, activeKey, agentId, index = 0, addonBefore, addonAfter, }) => { const { name, plugin_apis, id, plugin_icon, project_id, desc_for_human, create_time = 0, update_time = 0, statistic_data, listed_at, plugin_product_status, plugin_type, is_official, version_name, version_ts, } = info; const botId = useBotInfoStore(state => state.botId); const { id: productId, status: marketStatus, auth_mode } = productInfo || {}; const refTarget = useRef(null); const refHasReport = useRef(false); // Record the apiId of the currently clicked plugin const currentApiId = useRef(''); // Record whether the latest plug-in data of the store is currently being pulled const [isFetching, setIsFetching] = useState(false); // For example {'pluginID_apiID': [node1, node2, node3,...]} const pluginApiNodesMap = useMemo( () => groupBy(workflowNodes || [], item => { const apiParams = item.data?.inputs?.apiParam ?? []; const pluginID = extractApiParams('pluginID', apiParams); const apiID = extractApiParams('apiID', apiParams); return `${pluginID}_${apiID}`; }), [workflowNodes], ); const [isInView] = useInViewport(refTarget, { root: () => scrollContainerRef?.current, }); const isCheckNow = useMemo(() => { if (!id) { return false; } if (activeKey === id || activeKey?.includes(id)) { return true; } return false; }, [activeKey, id]); useEffect(() => { if (isCheckNow) { sendTeaEvent(EVENT_NAMES.product_click_front, { plugin_id: info.id, product_id: `${productInfo?.id}`, product_name: info?.name || '', entity_type: 'plugin', source: 'add_plugin_menu', from: 'add_plugin_menu', filter_tag: type || '', action: 'expand_tools', c_position: index, }); } }, [isCheckNow, productInfo?.id]); useEffect(() => { if (!isFromMarket || refHasReport.current) { return; } if (isInView) { refHasReport.current = true; sendTeaEvent(EVENT_NAMES.product_show_front, { plugin_id: info.id, product_id: `${productInfo?.id}`, product_name: info?.name || '', entity_type: 'plugin', source: 'add_plugin_menu', from: 'add_plugin_menu', filter_tag: type || '', c_position: index, }); } }, [isInView, productInfo, info, type]); const timePrefixText = showPublishTime ? I18n.t('mkl_plugin_publish') : showCreateTime ? I18n.t('mkl_plugin_created') : I18n.t('mkl_plugin_updated'); const timeToShow = (showPublishTime ? Number(listed_at) : showCreateTime ? Number(create_time) : Number(update_time)) || 0; const renderAuthStatus = () => { if (isUndefined(auth_mode) || auth_mode === PluginAuthMode.NoAuth) { return null; } if ( auth_mode === PluginAuthMode.Required || auth_mode === PluginAuthMode.Supported ) { return ( {I18n.t('plugin_tool_config_status_unauthorized')} ); } return ( {I18n.t('plugin_tool_config_status_authorized')} ); }; return (
{/* Plugin name up to 30 characters, no ellipsis required */} {renderAuthStatus()} {showProjectPluginLink && clickProjectPluginCallback ? ( { event.stopPropagation(); clickProjectPluginCallback?.({ ...info, }); }} /> ) : null} {showMarketLink && Number(productId) > 0 ? ( { event.stopPropagation(); sendTeaEvent(EVENT_NAMES.product_click_front, { plugin_id: info.id, product_id: `${productInfo?.id}`, product_name: info?.name || '', entity_type: 'plugin', source: 'add_plugin_menu', from: 'add_plugin_menu', filter_tag: type || '', action: 'enter_detailpage', c_position: index, }); window.open( `/store/plugin/${productId}?from=add_plugin_menu`, ); }} /> ) : null}
{plugin_type === PluginType.LOCAL ? ( <> {I18n.t('local_plugin_label')} ) : null} {desc_for_human}
{/* condition */} {addonBefore} {productInfo?.plugin_type === ProductPluginType.LocalPlugin ? ( } size="mini" > {I18n.t('store_service_plugin')} ) : null} {I18n.t('bot_edit_page_plugin_list_plugin_has_n_tools', { n: plugin_apis?.length, })} {!!statistic_data?.bot_quote && ( {I18n.t('bot_edit_page_plugin_list_plugin_n_bots_using', { n: formatNumber(statistic_data?.bot_quote), })} )} {productInfo?.connectors?.length ? ( <>
{I18n.t('store_service_plugin_connector')}
) : null}
{showCreator ? ( ) : null} {showCreator ? : null}
{`${timePrefixText} `} {formatDate(timeToShow, 'YYYY-MM-DD HH:mm')}
{addonAfter}
{showCopyPlugin ? (
{ onCopyPluginCallback?.({ pluginID: id, name, }); }} />
) : null} } itemKey={`${id}`} > {plugin_apis?.map(api => { const isAdded = pluginApiList.some( addedApi => (addedApi.api_id && addedApi.api_id === api.api_id) || (addedApi.plugin_id?.toString() ?? '0') + (addedApi.name ?? '') === (api.plugin_id?.toString() ?? '0') + (api.name ?? ''), ); return ( item.id === api.api_id) : undefined } isLocalPlugin={ productInfo?.plugin_type === ProductPluginType.LocalPlugin } connectors={productInfo?.connectors?.map(item => item.name ?? '')} marketStatus={isFromMarket ? marketStatus : undefined} onApiToggle={async () => { let isSuccess = true; emitEvent(OpenBlockEvent.PLUGIN_API_BLOCK_OPEN); if (isAdded) { onPluginApiListChange( pluginApiList.filter( item => getPluginApiKey(item) !== getPluginApiKey(api) && (!item?.api_id || item?.api_id !== api?.api_id), ), ); Toast.success({ content: I18n.t('bot_edit_tool_removed_toast', { api_name: api.name, }), showClose: false, }); sendTeaEvent(EVENT_NAMES.click_tool_select, { operation: 'remove', bot_id: botId, operation_type: 'single', tool_id: api?.api_id || '', tool_name: api?.name || '', product_id: `${productInfo?.id}`, product_name: info?.name || '', source: 'add_plugin_list', from: 'bot_develop', }); } else { let apiToSend: PluginApi | undefined; // The information of the plugin where the api is located is added and stored in the store to avoid GetPlaygroundPluginList multiple requests const pluginInfo = { plugin_icon, plugin_type, is_official, project_id, version_name, version_ts, }; // Check whether the name of the Plugins currently to be added has a duplicate name in the added list (the model does not support it, so this is added) if ( pluginApiList .map(i => (i.plugin_name ?? '') + i.name) .includes((api.plugin_name ?? '') + (api.name ?? '')) ) { Toast.error({ content: I18n.t('plugin_name_conflict_error'), showClose: false, }); isSuccess = false; } else { if (isFromMarket) { if (api.plugin_id && api?.api_id && api?.api_id !== '0') { setIsFetching(true); currentApiId.current = api.api_id; // Data from the market needs to be re-pulled to the latest data. const result = await PluginDevelopApi.GetPlaygroundPluginList({ page: 1, size: 1, plugin_ids: [api.plugin_id], plugin_types: [ PluginType.PLUGIN, PluginType.APP, PluginType.LOCAL, ], space_id: useSpaceStore.getState().getSpaceId(), }); setIsFetching(false); const targetApi = result?.data?.plugin_list?.[0]?.plugin_apis?.find( item => item?.api_id === api?.api_id, ); if (targetApi) { Object.assign(targetApi, { plugin_product_status: result?.data?.plugin_list?.[0] .plugin_product_status, ...pluginInfo, }); } apiToSend = targetApi; } } else { apiToSend = Object.assign( { ...api }, { plugin_product_status, ...pluginInfo }, ); } if (apiToSend) { // If it can only be added once, call callback after adding and close the pop-up window if ( openMode === OpenModeType.OnlyOnceAdd || (from && [ From.WorkflowAddNode, From.ProjectIde, From.ProjectWorkflow, ].includes(from)) ) { const cbResult = await openModeCallback?.({ ...apiToSend, ...pluginInfo, }); /** Allow to add failed scenarios */ if (isBoolean(cbResult)) { return cbResult; } return isSuccess; } if (!IS_OPEN_SOURCE) { // After successfully adding the plugin, quickly bind the preset card information await PluginDevelopApi.QuickBindPluginPresetCard({ plugin_id: apiToSend.plugin_id, api_name: apiToSend.name, bot_id: botId, agent_id: agentId, space_id: useSpaceStore.getState().getSpaceId(), }); } onPluginApiListChange([...pluginApiList, apiToSend]); Toast.success({ content: I18n.t('bot_edit_tool_added_toast', { api_name: api.name, }), showClose: false, }); } else { Toast.error({ content: withSlardarIdButton( I18n.t('bot_edit_tool_added_toast_error', { api_name: api.name, }), ), showClose: false, }); isSuccess = false; } } sendTeaEvent(EVENT_NAMES.click_tool_select, { operation: 'add', bot_id: botId, operation_type: 'single', tool_id: api?.api_id || '', tool_name: api?.name || '', product_id: `${productInfo?.id}`, product_name: info?.name || '', source: 'add_plugin_list', from: 'bot_develop', }); } return isSuccess; }} key={(api.plugin_id?.toString() ?? '') + (api.name ?? '')} showButton={showButton} loading={isFetching && api?.api_id === currentApiId.current} /> ); })}
); };