Coze源码分析-资源库-创建插件-前端源码-核心组件
概述
本文深入分析Coze Studio中用户创建插件功能的前端实现。该功能允许用户在资源库中创建、编辑和管理插件资源,为开发者提供了强大的插件开发和管理能力。通过对源码的详细解析,我们将了解从资源库入口到插件配置弹窗的完整架构设计、组件实现、状态管理和用户体验优化等核心技术要点。
功能特性
核心功能
- 插件创建:支持自定义插件名称、描述和配置信息
- 插件管理:提供插件列表展示、编辑和删除功能
- 多种插件类型:支持HTTP插件、App插件和本地插件
- 授权配置:支持OAuth、API Key等多种授权方式
- API接口管理:支持插件API接口的定义和配置
用户体验特性
- 即时反馈:操作结果实时展示和验证
- 表单验证:完善的插件信息验证机制
- 便捷操作:支持复制、导入和快速编辑
- 国际化支持:多语言界面适配
技术架构
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ 插件管理模块 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LibraryPage │ │LibraryHeader│ │CreateFormPlugin │ │
│ │ (资源库页面) │ │ (添加按钮) │ │ Modal │ │
│ └─────────────┘ └─────────────┘ │ (创建/编辑弹窗) │ │
│ ┌─────────────┐ ┌─────────────┐ └─────────────────────┘ │
│ │BaseLibrary │ │ Table │ ┌─────────────────────┐ │
│ │ Page │ │ (资源列表) │ │ PluginForm │ │
│ └─────────────┘ └─────────────┘ │ (插件表单组件) │ │
│ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │usePluginConfig │ │ API Hooks │ │
│ │ (配置逻辑) │ │ PluginDevelopApi │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ API服务层 │
│ ┌─────────────────────────────────────────────────────────┐
│ │ PluginDevelop API │
│ │ RegisterPluginMeta/DelPlugin │
│ └─────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────┘
核心模块结构
frontend/
├── apps/coze-studio/src/
│ └── pages/
│ ├── library.tsx # 资源库入口页面
│ └── plugin/
│ ├── layout.tsx # 插件页面布局
│ ├── page.tsx # 插件详情页面
│ └── tool/ # 插件工具相关
├── packages/studio/workspace/
│ ├── entry-adapter/src/pages/library/
│ │ └── index.tsx # LibraryPage适配器组件
│ └── entry-base/src/pages/library/
│ ├── index.tsx # BaseLibraryPage核心组件
│ ├── components/
│ │ └── library-header.tsx # LibraryHeader头部组件
│ └── hooks/use-entity-configs/
│ └── use-plugin-config.tsx # 插件配置Hook
├── packages/agent-ide/bot-plugin/
│ ├── export/src/component/
│ │ └── bot_edit/
│ │ └── bot-form-edit/
│ │ └── index.tsx # CreateFormPluginModal组件
│ ├── tools/src/components/
│ │ └── plugin_modal/
│ │ ├── index.tsx # 插件弹窗主组件
│ │ └── base-more.tsx # 插件配置表单
│ └── component/
│ └── index.tsx # 插件组件导出
├── packages/arch/idl/src/auto-generated/
│ └── plugin_develop/
│ └── namespaces/
│ └── plugin_develop_common.ts # 插件相关类型定义
└── packages/arch/bot-api/src/└── plugin-develop.ts # PluginDevelopApi定义
用户创建插件流程概述
用户登录Coze Studio↓点击"资源库"菜单↓LibraryPage 组件加载↓点击右上角"+"按钮↓LibraryHeader 显示创建菜单↓点击"插件"选项↓setShowFormPluginModel(true) 触发↓CreateFormPluginModal 弹窗显示↓用户输入插件名称(name字段)↓用户输入插件描述(desc字段)↓用户配置插件URL和授权信息↓表单验证(名称、URL必填)↓用户点击"确认"按钮↓confirmBtn() 触发↓PluginDevelopApi.RegisterPluginMeta() 调用↓后端创建新插件资源↓onSuccess() 处理成功响应↓导航到插件详情页面↓刷新资源库列表
该流程包含多层验证和处理:
- 前端表单验证:通过Form组件进行名称、URL等必填字段验证
- 插件类型选择:支持HTTP插件、App插件和本地插件等多种类型
- API调用:使用PluginDevelopApi.RegisterPluginMeta API处理插件创建
- 成功处理:创建成功后自动跳转到插件详情页面进行进一步配置
- 状态管理:通过usePluginConfig Hook管理弹窗状态和数据流
整个流程确保了插件创建的便捷性和用户体验的流畅性。
核心组件实现
组件层次结构
插件创建功能涉及多个层次的组件:
- LibraryPage组件:资源库主页面
- BaseLibraryPage组件:资源库核心逻辑
- LibraryHeader组件:包含创建按钮的头部
- CreateFormPluginModal组件:插件配置弹窗
- PluginForm组件:插件表单组件
1. 资源库入口组件(LibraryPage)
文件位置:frontend/packages/studio/workspace/entry-adapter/src/pages/library/index.tsx
作为资源库的适配器组件,整合各种资源配置:
import { type FC, useRef } from 'react';import {BaseLibraryPage,useDatabaseConfig,usePluginConfig,useWorkflowConfig,usePromptConfig,useKnowledgeConfig,
} from '@coze-studio/workspace-base/library';export const LibraryPage: FC<{ spaceId: string }> = ({ spaceId }) => {const basePageRef = useRef<{ reloadList: () => void }>(null);const configCommonParams = {spaceId,reloadList: () => {basePageRef.current?.reloadList();},};const { config: pluginConfig, modals: pluginModals } =usePluginConfig(configCommonParams);const { config: workflowConfig, modals: workflowModals } =useWorkflowConfig(configCommonParams);const { config: knowledgeConfig, modals: knowledgeModals } =useKnowledgeConfig(configCommonParams);const { config: promptConfig, modals: promptModals } =usePromptConfig(configCommonParams);const { config: databaseConfig, modals: databaseModals } =useDatabaseConfig(configCommonParams);return (<><BaseLibraryPagespaceId={spaceId}ref={basePageRef}entityConfigs={[pluginConfig,workflowConfig,knowledgeConfig,promptConfig,databaseConfig,]}/>{pluginModals}{workflowModals}{promptModals}{databaseModals}{knowledgeModals}</>);
};<ResultModalvisible={!!successData}data={successData}onOk={refresh}/></>);
};
设计亮点:
- 状态集中管理:通过
usePatOperation
Hook统一管理组件状态 - 组件解耦:各子组件职责明确,通过props进行通信
- 数据流清晰:单向数据流,状态变更可追踪
2. 资源库核心组件(BaseLibraryPage)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/index.tsx
负责资源库的核心展示逻辑:
import { forwardRef, useImperativeHandle } from 'react';import classNames from 'classnames';
import { useInfiniteScroll } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import {Table,Select,Search,Layout,Cascader,Space,
} from '@coze-arch/coze-design';
import { renderHtmlTitle } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {type ResType,type LibraryResourceListRequest,type ResourceInfo,
} from '@coze-arch/idl/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';import { type ListData, type BaseLibraryPageProps } from './types';
import { LibraryHeader } from './components/library-header';export const BaseLibraryPage = forwardRef<{ reloadList: () => void },BaseLibraryPageProps
>(({ spaceId, isPersonalSpace = true, entityConfigs }, ref) => {const { params, setParams, resetParams, hasFilter, ready } =useCachedQueryParams({spaceId,});const listResp = useInfiniteScroll<ListData>(async prev => {if (!ready) {return {list: [],nextCursorId: undefined,hasMore: false,};}const resp = await PluginDevelopApi.LibraryResourceList(entityConfigs.reduce<LibraryResourceListRequest>((res, config) => config.parseParams?.(res) ?? res,{...params,cursor: prev?.nextCursorId,space_id: spaceId,size: LIBRARY_PAGE_SIZE,},),);return {list: resp?.resource_list || [],nextCursorId: resp?.cursor,hasMore: !!resp?.has_more,};},{reloadDeps: [params, spaceId],},);useImperativeHandle(ref, () => ({reloadList: listResp.reload,}));return (<LayoutclassName={s['layout-content']}title={renderHtmlTitle(I18n.t('navigation_workspace_library'))}><Layout.Header className={classNames(s['layout-header'], 'pb-0')}><div className="w-full"><LibraryHeader entityConfigs={entityConfigs} />{/* 过滤器组件 */}</div></Layout.Header><Layout.Content>{/* 表格和列表内容 */}</Layout.Content></Layout>);}
);
3. 资源库头部组件(LibraryHeader)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/components/library-header.tsx
包含创建资源的入口按钮:
import React from 'react';import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { Button, Menu } from '@coze-arch/coze-design';import { type LibraryEntityConfig } from '../types';export const LibraryHeader: React.FC<{entityConfigs: LibraryEntityConfig[];
}> = ({ entityConfigs }) => (<div className="flex items-center justify-between mb-[16px]"><div className="font-[500] text-[20px]">{I18n.t('navigation_workspace_library')}</div><Menuposition="bottomRight"className="w-120px mt-4px mb-4px"render={<Menu.SubMenu mode="menu">{entityConfigs.map(config => config.renderCreateMenu?.() ?? null)}</Menu.SubMenu>}><Buttontheme="solid"type="primary"icon={<IconCozPlus />}data-testid="workspace.library.header.create">{I18n.t('library_resource')}</Button></Menu></div>
);
4. 插件配置Hook(usePluginConfig)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/use-entity-configs/use-plugin-config.tsx
管理插件创建和编辑的状态:
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';import {ActionKey,PluginType,ResType,type ResourceInfo,
} from '@coze-arch/idl/plugin_develop';
import { I18n } from '@coze-arch/i18n';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { useBotCodeEditOutPlugin } from '@coze-agent-ide/bot-plugin/hook';
import { CreateFormPluginModal } from '@coze-agent-ide/bot-plugin/component';
import { IconCozPlugin } from '@coze-arch/coze-design/icons';
import { Menu, Tag, Toast, Table } from '@coze-arch/coze-design';import { BaseLibraryItem } from '../../components/base-library-item';
import PluginDefaultIcon from '../../assets/plugin_default_icon.png';
import { type UseEntityConfigHook } from './types';const { TableAction } = Table;export const usePluginConfig: UseEntityConfigHook = ({spaceId,reloadList,getCommonActions,
}) => {const [showFormPluginModel, setShowFormPluginModel] = useState(false);const navigate = useNavigate();const { modal: editPluginCodeModal, open } = useBotCodeEditOutPlugin({modalProps: {onSuccess: reloadList,},});return {modals: (<><CreateFormPluginModalisCreate={true}visible={showFormPluginModel}onSuccess={pluginID => {navigate(`/space/${spaceId}/plugin/${pluginID}`);reloadList();}}onCancel={() => {setShowFormPluginModel(false);}}/>{editPluginCodeModal}</>),config: {typeFilter: {label: I18n.t('library_resource_type_plugin'),value: ResType.Plugin,},renderCreateMenu: () => (<Menu.Itemdata-testid="workspace.library.header.create.plugin"icon={<IconCozPlugin />}onClick={() => {setShowFormPluginModel(true);}}>{I18n.t('library_resource_type_plugin')}</Menu.Item>),target: [ResType.Plugin],onItemClick: (item: ResourceInfo) => {if (item.res_type === ResType.Plugin &&item.res_sub_type === 2 //Plugin:1-Http; 2-App; 6-Local;) {const disable = !item.actions?.find(action => action.key === ActionKey.Delete,)?.enable;open(item.res_id || '', disable);} else {navigate(`/space/${spaceId}/plugin/${item.res_id}`);}},renderItem: item => (<BaseLibraryItemresourceInfo={item}defaultIcon={PluginDefaultIcon}tag={item.res_type === ResType.Plugin &&item.res_sub_type === PluginType.LOCAL ? (<Tagdata-testid="workspace.library.item.tag"color="cyan"size="mini"className="flex-shrink-0 flex-grow-0">{I18n.t('local_plugin_label')}</Tag>) : null}/>),renderActions: (item: ResourceInfo) => {const deleteDisabled = !item.actions?.find(action => action.key === ActionKey.Delete,)?.enable;const deleteProps = {disabled: deleteDisabled,deleteDesc: I18n.t('library_delete_desc'),handler: async () => {await PluginDevelopApi.DelPlugin({ plugin_id: item.res_id });reloadList();Toast.success(I18n.t('Delete_success'));},};return (<TableActiondeleteProps={deleteProps}actionList={getCommonActions?.(item)}/>);},},};
};
5. 插件配置弹窗(CreateFormPluginModal)
文件位置:frontend/packages/agent-ide/bot-plugin/export/src/component/bot_edit/bot-form-edit/index.tsx
插件创建和编辑的主要界面:
import { type FC, useMemo, useState, useEffect } from 'react';import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import {PluginForm,usePluginFormState,convertPluginMetaParams,registerPluginMeta,updatePluginMeta,
} from '@coze-studio/plugin-form-adapter';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {type CreationMethod,type PluginType,
} from '@coze-arch/idl/plugin_develop';
import { ERROR_CODE } from '@coze-agent-ide/bot-plugin-tools/pluginModal/types';
import {Button,Divider,Modal,Space,Toast,
} from '@coze-arch/coze-design';import { PluginDocs } from '../../plugin-docs';
import { ImportModal } from './import-modal';
import { CodeModal } from './code-modal';export interface CreatePluginFormProps {visible: boolean;isCreate?: boolean;editInfo?: PluginInfoProps;disabled?: boolean;onCancel?: () => void;onSuccess?: (pluginID?: string) => Promise<void> | void;projectId?: string;
}export const CreateFormPluginModal: FC<CreatePluginFormProps> = props => {const {onCancel,editInfo,isCreate = true,visible,onSuccess,disabled = false,projectId,} = props;const { id } = useSpaceStore(store => store.space);const modalTitle = useMemo(() => {if (isCreate) {return (<div className="w-full flex justify-between items-center pr-[8px]"><div>{I18n.t('create_plugin_modal_title1')}</div><Space><CodeModalonCancel={onCancel}onSuccess={onSuccess}projectId={projectId}/><ImportModalonCancel={onCancel}onSuccess={onSuccess}projectId={projectId}/><Divider layout="vertical" className="h-5" /></Space></div>);}if (disabled) {return I18n.t('plugin_detail_view_modal_title');}return I18n.t('plugin_detail_edit_modal_title');}, [isCreate, disabled]);const [loading, setLoading] = useState(false);const pluginState = usePluginFormState();const {formApi,extItems,headerList,isValidCheckResult,setIsValidCheckResult,pluginTypeCreationMethod,defaultRuntime,} = pluginState;const confirmBtn = async () => {await formApi.current?.validate();const type = isCreate ? 'create' : 'edit';const val = formApi.current?.getValues();if (!val || !pluginTypeCreationMethod) {return;}const json: Record<string, string> = {};extItems?.forEach(item => {if (item.key in val) {json[item.key] = val[item.key];}});const [pluginType, creationMethod] = pluginTypeCreationMethod.split('-');const params = convertPluginMetaParams({val,spaceId: String(id),headerList,projectId,creationMethod: Number(creationMethod) as unknown as CreationMethod,defaultRuntime,pluginType: Number(pluginType) as unknown as PluginType,extItemsJSON: json,});const action = {create: () => registerPluginMeta({ params }),edit: () => updatePluginMeta({ params, editInfo }),};try {setLoading(true);const pluginID = await action[type]();Toast.success({content: isCreate? I18n.t('Plugin_new_toast_success'): I18n.t('Plugin_update_toast_success'),showClose: false,});onCancel?.();onSuccess?.(pluginID);} catch (error) {const { code, msg } = error as any;if (Number(code) === ERROR_CODE.SAFE_CHECK) {setIsValidCheckResult(false);} else {Toast.error({content: withSlardarIdButton(msg),});}} finally {setLoading(false);}};return (<Modaltitle={modalTitle}className="[&_.semi-modal-header]:items-center"visible={visible}keepDOM={isCreate}onCancel={() => onCancel?.()}modalContentClass="create-plugin-modal-content"footer={!disabled && (<div>{!isValidCheckResult && (<div className="text-red-500 mb-2">{I18n.t('plugin_validation_failed')}</div>)}<div className="flex justify-end gap-2"><Button onClick={() => onCancel?.()}>{I18n.t('create_plugin_modal_button_cancel')}</Button><Buttontheme="solid"type="primary"onClick={confirmBtn}disabled={!isValidCheckResult}loading={loading}>{I18n.t('create_plugin_modal_button_confirm')}</Button></div></div>)}><PluginFormpluginState={pluginState}visible={visible}isCreate={isCreate}disabled={disabled}editInfo={editInfo}/></Modal>);
};
设计亮点:
- 状态集中管理:通过
usePluginFormState
Hook统一管理插件表单状态 - 组件解耦:各子组件职责明确,通过props进行通信
- 数据流清晰:单向数据流,状态变更可追踪
- 错误处理完善:支持表单验证和安全检查
- 用户体验优化:支持加载状态和实时反馈