Coze源码分析-资源库-编辑工作流-前端源码-核心组件

概述

本文深入分析Coze Studio中用户编辑插件功能的前端实现。该功能允许用户在资源库中找到并编辑已有的插件资源,特别是代码类插件和表单类插件。通过对源码的详细解析,我们将了解从资源库入口到插件编辑流程的完整架构设计、组件实现、状态管理、锁定机制和用户体验优化等核心技术要点。

功能特性

核心功能

  • 插件编辑:支持代码类插件和表单类插件的编辑功能
  • 插件类型管理:区分处理HTTP插件、App插件和本地插件
  • 编辑锁定机制:实现插件编辑锁定,防止多用户同时编辑造成冲突
  • 权限控制:基于用户权限的访问控制机制,确保插件资源的安全性
  • 状态管理:支持插件编辑状态、锁定状态的有效管理

用户体验特性

  • 即时反馈:操作结果实时展示和验证
  • 表单验证:完善的插件信息验证机制
  • 便捷操作:支持一键编辑、快速导航和删除功能
  • 冲突提示:当插件被其他用户占用时显示友好提示
  • 国际化支持:多语言界面适配

技术架构

整体架构设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      插件编辑管理模块                        │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ LibraryPage │  │LibraryHeader│  │CreateFormPlugin     │  │
│  │ (资源库页面) │  │ (添加按钮)  │  │     Modal           │  │
│  └─────────────┘  └─────────────┘  │  (表单插件创建弹窗)  │  │
│  ┌─────────────┐  ┌─────────────┐  └─────────────────────┘  │
│  │BaseLibrary  │  │   Table     │  ┌─────────────────────┐  │
│  │    Page     │  │ (资源列表)  │  │CreateCodePlugin     │  │
│  └─────────────┘  └─────────────┘  │    Modal            │  │
│                                    │ (代码插件编辑弹窗)  │  │
│                                    └─────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                      状态管理层                             │
│  ┌─────────────────┐  ┌─────────────────────────────────┐  │
│  │  usePluginConfig│  │ useBotCodeEditOutPlugin         │  │
│  │  (配置逻辑)      │  │      (代码插件编辑逻辑)         │  │
│  └─────────────────┘  └─────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                       API服务层                            │
│  ┌─────────────────────────────────────────────────────────┐
│  │           PluginDevelopApi                              │
│  │         插件获取/锁定/保存/删除相关API接口                 │
│  └─────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────┘

核心模块结构

复制代码
frontend/
├── apps/coze-studio/src/
│   └── pages/
│       ├── library.tsx            # 资源库入口页面
│       ├── plugin/                # 插件相关页面
│       └── develop.tsx            # 开发页面
├── 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/plugin-edit/
│   │   └── index.tsx              # 代码插件编辑组件和Hook
│   ├── component/
│   │   └── CreateFormPluginModal/ # 表单插件创建弹窗
│   └── hook/
│       └── use-bot-code-edit-out-plugin.tsx # 代码插件编辑Hook
├── packages/studio/stores/bot-plugin/src/utils/
│   └── api.ts                     # 插件锁定相关API
├── packages/arch/idl/src/auto-generated/
│   ├── plugin_develop/            # 插件开发相关类型
│   └── plugin_develop_api/        # 插件API相关类型
└── packages/arch/bot-api/src/
    └── plugin-develop-api.ts      # PluginDevelopApi实例定义

插件编辑流程

复制代码
用户登录Coze Studio
        ↓
  点击"资源库"菜单
        ↓
  LibraryPage 组件加载并显示插件列表
        ↓
  点击目标插件行
        ↓
  判断插件类型(代码插件/表单插件)
        ↓
  对于代码插件,触发onItemClick事件
        ↓
  检查插件权限,确定是否可编辑
        ↓
  调用open()函数打开编辑弹窗
        ↓
  CreateCodePluginModal 弹窗显示
        ↓
  用户点击"编辑"按钮
        ↓
  checkOutPluginContext()检查插件锁定状态
        ↓
  如果锁定成功,设置editable为true
        ↓
  用户编辑插件信息(描述、配置等)
        ↓
  用户点击"保存"按钮
        ↓
  调用PluginDevelopApi.SavePlugin()保存更改
        ↓
  后端更新插件资源
        ↓
  onSuccess()处理成功响应
        ↓
  调用unlockOutPluginContext()解锁插件
        ↓
  刷新资源库列表
        ↓
  编辑弹窗关闭

该流程包含多层安全机制和验证:

  1. 权限控制:通过检查操作权限确定用户是否可以编辑插件
  2. 锁定机制:防止多用户同时编辑导致的冲突
  3. API调用:通过PluginDevelopApi处理插件编辑和保存
  4. 状态管理:通过useBotCodeEditOutPlugin和usePluginConfig Hook管理状态
  5. 错误处理:当插件被占用时显示友好提示
    整个流程确保了插件编辑的安全性和用户体验的流畅性。

核心组件实现

组件层次结构

插件编辑功能涉及多个层次的组件:

  1. LibraryPage组件:资源库主页面
  2. BaseLibraryPage组件:资源库核心逻辑
  3. LibraryHeader组件:包含创建按钮的头部
  4. usePluginConfig Hook:插件配置和状态管理
  5. useBotCodeEditOutPlugin Hook:代码插件编辑弹窗管理
  6. CreateCodePluginModal组件:代码插件编辑弹窗
  7. CreateFormPluginModal组件:表单插件编辑弹窗

1. 资源库入口组件(LibraryPage)

文件位置:frontend/packages/studio/workspace/entry-adapter/src/pages/library/index.tsx

作为资源库的适配器组件,整合各种资源配置,包括插件配置:

typescript 复制代码
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}
    </>
  );
};

设计亮点

  • 状态集中管理 :通过 useWorkflowConfig Hook统一管理组件状态
  • 组件解耦:各子组件职责明确,通过props进行通信
  • 数据流清晰:单向数据流,状态变更可追踪

2. 插件配置Hook(usePluginConfig)

文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/use-entity-configs/use-plugin-config.tsx

管理插件资源的配置和编辑状态,是插件编辑功能的核心Hook:

typescript 复制代码
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';

export const usePluginConfig: UseEntityConfigHook = ({
  spaceId,
  reloadList,
  getCommonActions,
}) => {
  const [showFormPluginModel, setShowFormPluginModel] = useState(false);
  const [loading, setLoading] = useState(false);
  const navigate = useNavigate();
  const { modal: editPluginCodeModal, open } = useBotCodeEditOutPlugin({
    modalProps: {
      onSuccess: reloadList,
    },
  });

  // 检查编辑权限
  const checkEditPermission = useCallback((item: ResourceInfo): boolean => {
    return item.actions?.some(
      action => action.key === ActionKey.Edit && action.enable
    ) || false;
  }, []);

  // 检查删除权限
  const checkDeletePermission = useCallback((item: ResourceInfo): boolean => {
    return item.actions?.some(
      action => action.key === ActionKey.Delete && action.enable
    ) || false;
  }, []);

  // 处理插件删除
  const handlePluginDelete = useCallback(async (item: ResourceInfo) => {
    try {
      // 只有本地插件支持删除
      if (item.res_sub_type !== PluginType.LOCAL) {
        Toast.error(I18n.t('plugin_delete_only_local'));
        return;
      }
      
      // 确认删除
      const confirmResult = await Modal.confirm({
        title: I18n.t('plugin_delete_confirm_title'),
        content: I18n.t('plugin_delete_confirm_content', { name: item.name }),
        okText: I18n.t('common_confirm'),
        cancelText: I18n.t('common_cancel'),
        okType: 'danger',
      });
      
      if (confirmResult) {
        setLoading(true);
        await PluginDevelopApi.DelPlugin({ plugin_id: item.res_id });
        reloadList();
        Toast.success(I18n.t('Delete_success'));
      }
    } catch (error) {
      console.error('删除插件失败:', error);
      Toast.error(I18n.t('Delete_failed'));
    } finally {
      setLoading(false);
    }
  }, [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 === PluginType.APP || item.res_sub_type === PluginType.HTTP || item.res_sub_type === PluginType.LOCAL)
        ) {
          const disable = !checkEditPermission(item);
          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.local"
                  color="cyan"
                  size="mini"
                  className="flex-shrink-0 flex-grow-0"
                >
                  {I18n.t('local_plugin_label')}
                </Tag>
              ) : item.res_sub_type === PluginType.HTTP ? (
                <Tag
                  data-testid="workspace.library.item.tag.http"
                  color="green"
                  size="mini"
                  className="flex-shrink-0 flex-grow-0"
                >
                  {I18n.t('http_plugin_label')}
                </Tag>
              ) : item.res_sub_type === PluginType.APP ? (
                <Tag
                  data-testid="workspace.library.item.tag.app"
                  color="purple"
                  size="mini"
                  className="flex-shrink-0 flex-grow-0"
                >
                  {I18n.t('app_plugin_label')}
                </Tag>
              ) : null
            ) : null
          }
        />
      ),
      renderActions: (item: ResourceInfo) => {
        const hasDeletePermission = checkDeletePermission(item);
        
        // 构建自定义操作列表
        const actionList = [];
        
        if (getCommonActions) {
          actionList.push(...getCommonActions(item));
        }
        
        // 只有本地插件才显示删除按钮
        const deleteProps = {
          disabled: !hasDeletePermission || item.res_sub_type !== PluginType.LOCAL,
          deleteDesc: I18n.t('library_delete_desc'),
          handler: () => handlePluginDelete(item),
        };

        return (
          <TableAction
            deleteProps={deleteProps}
            actionList={actionList}
          />
        );
      },
    },
  };
};

核心功能

  • 插件类型管理:支持不同类型插件的处理逻辑
  • 编辑弹窗集成:集成代码插件编辑弹窗
  • 菜单渲染:在创建菜单中显示插件选项
  • 本地插件标识:为本地开发的插件显示特殊标签
  • 资源操作:集成插件的点击、编辑、删除等操作
  • 权限控制:根据操作权限控制删除功能的可用性

3. 代码插件编辑Hook(useBotCodeEditOutPlugin)

文件位置:frontend/packages/agent-ide/bot-plugin/export/src/component/bot_edit/plugin-edit/index.tsx

管理代码插件编辑弹窗的状态和编辑逻辑:

typescript 复制代码
export const useBotCodeEditOutPlugin = ({
  modalProps,
}: {
  modalProps: Pick<CreatePluginProps, 'onSuccess'>;
}) => {
  const [pluginInfo, setPluginInfo] = useState<PluginInfoProps>({});
  const [modalVisible, setModalVisible] = useState(false);
  const [editable, setEditable] = useState(false);
  const [disableEdit, setDisableEdit] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const pluginId = pluginInfo?.plugin_id || '';

  const action = useMemo(() => {
    if (disableEdit) {
      return null;
    }

    return (
      <div className={styles.actions}>
        {editable ? (
          <UIButton
            onClick={() => {
              setEditable(false);
              unlockOutPluginContext(pluginId);
            }}
          >
            {I18n.t('Cancel')}
          </UIButton>
        ) : (
          <UIButton
            theme="solid"
            loading={loading}
            onClick={async () => {
              setLoading(true);
              try {
                const userId = getUserInfo()?.user_id || '';
                const isLocked = await checkOutPluginContext(pluginId, userId);
                
                if (isLocked) {
                  return;
                }
                
                setEditable(true);
                setError(null);
              } catch (err) {
                console.error('检查插件锁定状态失败:', err);
                setError('无法开始编辑插件');
              } finally {
                setLoading(false);
              }
            }}
          >
            {I18n.t('Edit')}
          </UIButton>
        )}
      </div>
    );
  }, [editable, pluginId, disableEdit, loading]);

  useEffect(() => {
    if (modalVisible) {
      setEditable(false);
    }
  }, [modalVisible]);

  const modal = (
    <CreateCodePluginModal
      {...modalProps}
      editInfo={pluginInfo}
      isCreate={false}
      visible={modalVisible}
      loading={loading}
      error={error}
      onCancel={() => {
        setModalVisible(false);
        setError(null);
        if (!disableEdit) {
          unlockOutPluginContext(pluginId).catch(err => {
            console.warn('解锁插件失败,但不影响关闭操作:', err);
          });
        }
      }}
      disabled={!editable}
      actions={action}
    />
  );

  const open = useCallback(async (id: string, disable: boolean) => {
    setLoading(true);
    setError(null);
    try {
      const res = await PluginDevelopApi.GetPluginInfo({
        plugin_id: id || '',
      });
      setPluginInfo({
        plugin_id: id,
        code_info: {
          plugin_desc: res.code_info?.plugin_desc || '',
          openapi_desc: res.code_info?.openapi_desc || '',
          client_id: res.code_info?.client_id || '',
          client_secret: res.code_info?.client_secret || '',
          service_token: res.code_info?.service_token || '',
        },
      });
      setDisableEdit(disable);
      setModalVisible(true);
    } catch (err) {
      console.error('获取插件信息失败:', err);
      setError('加载插件信息失败');
      message.error('加载插件信息失败');
    } finally {
      setLoading(false);
    }
  }, []);

  return { modal, open };
};

核心功能

  • 状态管理:管理插件信息、编辑状态和弹窗可见性
  • 插件锁定机制:编辑前自动检查和锁定插件,防止多人同时编辑
  • 权限控制:根据disableEdit参数控制编辑权限
  • 插件信息加载:通过API获取插件详细信息
  • 弹窗渲染:渲染代码插件编辑弹窗组件
  • 解锁机制:编辑完成或取消时自动解锁插件

4. 插件锁定检查(checkOutPluginContext)

文件位置:frontend/packages/studio/stores/bot-plugin/src/utils/api.ts

负责检查插件的锁定状态,确保编辑安全性:

typescript 复制代码
export const checkOutPluginContext = async (pluginId: string, userId?: string): Promise<boolean> => {
  try {
    const resp = await PluginDevelopApi.CheckAndLockPluginEdit({
      plugin_id: pluginId,
      user_id: userId || '',
    });

    if (resp.code !== 0) {
      // 其他锁定失败情况
      UIModal.error({
        okText: I18n.t('guidance_got_it'),
        title: I18n.t('plugin_team_edit_tip_lock_fail'),
        content: resp.message || I18n.t('plugin_team_edit_tip_lock_fail_desc'),
        hasCancel: false,
      });
      return true;
    }

    // 插件被其他用户占用
    if (!resp.can_edit && resp.lock_user_name && !resp.is_self_locked) {
      UIModal.info({
        okText: I18n.t('guidance_got_it'),
        title: I18n.t('plugin_team_edit_tip_unable_to_edit'),
        content: `${resp.lock_user_name}(${resp.lock_user_role || I18n.t('role_unknown')}) ${I18n.t('plugin_team_edit_tip_another_user_is_editing')}`,
        hasCancel: false,
      });

      return true;
    }

    return !resp.can_edit;
  } catch (error) {
    // 处理API调用异常
    console.error('检查插件锁定状态失败:', error);
    UIModal.error({
      okText: I18n.t('guidance_got_it'),
      title: I18n.t('plugin_team_edit_tip_lock_fail'),
      content: I18n.t('plugin_team_edit_tip_lock_fail_desc'),
      hasCancel: false,
    });
    return true;
  }
};

核心功能

  • 锁定检查:检查插件是否被其他用户锁定
  • 用户提示:当插件被其他用户占用时,显示友好的提示信息
  • 权限验证:确保只有合法用户可以编辑插件
  • 冲突避免:通过锁定机制避免编辑冲突
相关推荐
有梦想的攻城狮2 小时前
从0开始学vue:vue和react的比较
前端·vue.js·react.js
FIN66682 小时前
昂瑞微,凭啥?
前端·科技·产品运营·创业创新·制造·射频工程
kura_tsuki3 小时前
[Web网页] Web 基础
前端
鱼樱前端4 小时前
uni-app快速入门章法(二)
前端·uni-app
silent_missile4 小时前
vue3父组件和子组件之间传递数据
前端·javascript·vue.js
IT_陈寒5 小时前
Vue 3.4 实战:这7个Composition API技巧让我的开发效率飙升50%
前端·人工智能·后端
少年阿闯~~6 小时前
HTML——1px问题
前端·html
Mike_jia6 小时前
SafeLine:自托管WAF颠覆者!一键部署守护Web安全的雷池防线
前端
brzhang7 小时前
把网页的“好句子”都装进侧边栏:我做了个叫 Markbox 的收藏器,开源!
前端·后端·架构