Coze源码分析-资源库-创建插件-前端源码-核心组件

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

概述

本文深入分析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() 处理成功响应
        ↓
  导航到插件详情页面
        ↓
  刷新资源库列表

该流程包含多层验证和处理:

  1. 前端表单验证:通过Form组件进行名称、URL等必填字段验证
  2. 插件类型选择:支持HTTP插件、App插件和本地插件等多种类型
  3. API调用:使用PluginDevelopApi.RegisterPluginMeta API处理插件创建
  4. 成功处理:创建成功后自动跳转到插件详情页面进行进一步配置
  5. 状态管理:通过usePluginConfig Hook管理弹窗状态和数据流
    整个流程确保了插件创建的便捷性和用户体验的流畅性。

核心组件实现

组件层次结构

插件创建功能涉及多个层次的组件:

  1. LibraryPage组件:资源库主页面
  2. BaseLibraryPage组件:资源库核心逻辑
  3. LibraryHeader组件:包含创建按钮的头部
  4. CreateFormPluginModal组件:插件配置弹窗
  5. 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 (
    <>
      <BaseLibraryPage
        spaceId={spaceId}
        ref={basePageRef}
        entityConfigs={[
          pluginConfig,
          workflowConfig,
          knowledgeConfig,
          promptConfig,
          databaseConfig,
        ]}
      />
      {pluginModals}
      {workflowModals}
      {promptModals}
      {databaseModals}
      {knowledgeModals}
    </>
  );
};
      <ResultModal
        visible={!!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 (
      <Layout
        className={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>
    <Menu
      position="bottomRight"
      className="w-120px mt-4px mb-4px"
      render={
        <Menu.SubMenu mode="menu">
          {entityConfigs.map(config => config.renderCreateMenu?.() ?? null)}
        </Menu.SubMenu>
      }
    >
      <Button
        theme="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: (
      <>
        <CreateFormPluginModal
          isCreate={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.Item
          data-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 => (
        <BaseLibraryItem
          resourceInfo={item}
          defaultIcon={PluginDefaultIcon}
          tag={
            item.res_type === ResType.Plugin &&
            item.res_sub_type === PluginType.LOCAL ? (
              <Tag
                data-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 (
          <TableAction
            deleteProps={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>
            <CodeModal
              onCancel={onCancel}
              onSuccess={onSuccess}
              projectId={projectId}
            />
            <ImportModal
              onCancel={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 (
    <Modal
      title={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>
              <Button
                theme="solid"
                type="primary"
                onClick={confirmBtn}
                disabled={!isValidCheckResult}
                loading={loading}
              >
                {I18n.t('create_plugin_modal_button_confirm')}
              </Button>
            </div>
          </div>
        )
      }
    >
      <PluginForm
        pluginState={pluginState}
        visible={visible}
        isCreate={isCreate}
        disabled={disabled}
        editInfo={editInfo}
      />
    </Modal>
  );
};

设计亮点

  • 状态集中管理:通过 usePluginFormState Hook统一管理插件表单状态
  • 组件解耦:各子组件职责明确,通过props进行通信
  • 数据流清晰:单向数据流,状态变更可追踪
  • 错误处理完善:支持表单验证和安全检查
  • 用户体验优化:支持加载状态和实时反馈

网站公告

今日签到

点亮在社区的每一天
去签到