概述
本文深入分析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()解锁插件
↓
刷新资源库列表
↓
编辑弹窗关闭
该流程包含多层安全机制和验证:
- 权限控制:通过检查操作权限确定用户是否可以编辑插件
- 锁定机制:防止多用户同时编辑导致的冲突
- API调用:通过PluginDevelopApi处理插件编辑和保存
- 状态管理:通过useBotCodeEditOutPlugin和usePluginConfig Hook管理状态
- 错误处理:当插件被占用时显示友好提示
整个流程确保了插件编辑的安全性和用户体验的流畅性。
核心组件实现
组件层次结构
插件编辑功能涉及多个层次的组件:
- LibraryPage组件:资源库主页面
- BaseLibraryPage组件:资源库核心逻辑
- LibraryHeader组件:包含创建按钮的头部
- usePluginConfig Hook:插件配置和状态管理
- useBotCodeEditOutPlugin Hook:代码插件编辑弹窗管理
- CreateCodePluginModal组件:代码插件编辑弹窗
- 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;
}
};
核心功能:
- 锁定检查:检查插件是否被其他用户锁定
- 用户提示:当插件被其他用户占用时,显示友好的提示信息
- 权限验证:确保只有合法用户可以编辑插件
- 冲突避免:通过锁定机制避免编辑冲突