Coze源码分析-资源库-编辑插件-前端源码-核心逻辑
编辑插件逻辑
编辑插件功能是Coze平台资源库的核心功能之一,允许用户修改已创建的插件配置,特别是针对App类型插件提供了在线编辑能力。整个编辑流程从资源库表格的插件行点击开始,根据插件子类型选择直接编辑或导航到详情页,最终保存更新。
1. 编辑操作入口 - usePluginConfig Hook
编辑操作的入口位于 usePluginConfig
hook 中的 onItemClick
方法,该方法处理插件行的点击事件,根据插件子类型提供不同的编辑体验。
核心实现:
export const usePluginConfig: UseEntityConfigHook = ({spaceId,reloadList,getCommonActions,
}) => {const [showFormPluginModel, setShowFormPluginModel] = useState(false);const navigate = useNavigate();// 使用插件编辑弹窗Hookconst { 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) => {// 关键逻辑:只对App类型插件(res_sub_type=2)直接打开编辑弹窗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}`);}},
2. 插件编辑核心Hook - useBotCodeEditOutPlugin
文件位置:frontend/packages/agent-ide/bot-plugin/export/src/component/bot_edit/plugin-edit/index.tsx
这是插件编辑功能的核心Hook,负责管理编辑弹窗的显示和隐藏,处理插件锁定机制:
import { useCallback, useEffect, useMemo, useState } from 'react';import { I18n } from '@coze-arch/i18n';
import { UIButton } from '@coze-arch/bot-semi';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import {checkOutPluginContext,unlockOutPluginContext,
} from '@coze-studio/bot-plugin-store';import {CreateCodePluginModal,type CreatePluginProps,
} from '../bot-code-edit';import styles from './index.module.less';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 pluginId = pluginInfo?.plugin_id || '';// 生成操作按钮const action = useMemo(() => {if (disableEdit) {return null;}return (<div className={styles.actions}>{editable ? (// 取消编辑按钮<UIButtononClick={() => {setEditable(false);unlockOutPluginContext(pluginId);}}>{I18n.t('Cancel')}</UIButton>) : (// 开始编辑按钮<UIButtontheme="solid"onClick={async () => {const isLocked = await checkOutPluginContext(pluginId);if (isLocked) {return;}setEditable(true);}}>{I18n.t('Edit')}</UIButton>)}</div>);}, [editable, pluginId, disableEdit]);useEffect(() => {if (modalVisible) {setEditable(false);}}, [modalVisible]);const modal = (<CreateCodePluginModal{...modalProps}editInfo={pluginInfo}isCreate={false}visible={modalVisible}onCancel={() => {setModalVisible(false);if (!disableEdit) {unlockOutPluginContext(pluginId);}}}disabled={!editable}actions={action}/>);const open = useCallback(async (id: string, disable: boolean) => {const res = await PluginDevelopApi.GetPluginInfo({plugin_id: id || '',});setPluginInfo({plugin_id: id,code_info: {plugin_desc: res.code_info?.plugin_desc,/** yaml */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);}, []);return { modal, open };
};
3. 插件编辑弹窗 - CreateCodePluginModal
文件位置:frontend/packages/agent-ide/bot-plugin/export/src/component/bot_edit/bot-code-edit.tsx
这是插件编辑的核心界面组件,提供插件代码编辑功能:
import { useState, useEffect, type ReactNode } from 'react';import classNames from 'classnames';
import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import { I18n } from '@coze-arch/i18n';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { UIButton, UIModal, Toast, Space } from '@coze-arch/bot-semi';
import { PluginDevelopApi } from '@coze-arch/bot-api';import { Editor } from '../editor';import s from './index.module.less';export interface CreatePluginProps {visible: boolean;isCreate?: boolean;editInfo?: PluginInfoProps;disabled?: boolean;onCancel?: () => void;onSuccess?: (pluginId?: string) => void;actions?: ReactNode;projectId?: string;
}const INDENTATION_SPACES = 2;
const EDITOR_HEIGHT_MAX = 560;export const CreateCodePluginModal: React.FC<CreatePluginProps> = props => {const {isCreate = true,onCancel,editInfo,visible,onSuccess,disabled = false,actions,projectId,} = props;const [aiPlugin, setAiPlugin] = useState<string | undefined>();const [clientId, setClientId] = useState<string | undefined>();const [clientSecret, setClientSecret] = useState<string | undefined>();const [serviceToken, setServiceToken] = useState<string | undefined>();const [openApi, setOpenApi] = useState<string | undefined>();useEffect(() => {/** Reset pop-up data every time you open it */if (visible) {//Format jsonconst desc = JSON.stringify(safeJSONParse(editInfo?.code_info?.plugin_desc),null,INDENTATION_SPACES,);setAiPlugin(desc || '');setOpenApi(editInfo?.code_info?.openapi_desc || '');setClientId(editInfo?.code_info?.client_id);setClientSecret(editInfo?.code_info?.client_secret);setServiceToken(editInfo?.code_info?.service_token);}}, [visible]);const registerPlugin = async () => {const params = {ai_plugin: aiPlugin,client_id: clientId,client_secret: clientSecret,service_token: serviceToken,openapi: openApi,};let res;if (isCreate) {// 调用注册插件API,用于创建新插件// API路径: /api/developer/register// 必需参数: space_id, ai_plugin, openapires = await PluginDevelopApi.RegisterPlugin({...params,project_id: projectId,space_id: useSpaceStore.getState().getSpaceId(), // 空间ID参数});} else {// 调用更新插件API,用于修改现有插件// API路径: /api/developer/update// 必需参数: plugin_id, ai_plugin, openapiawait PluginDevelopApi.UpdatePlugin({...params,plugin_id: editInfo?.plugin_id,edit_version: editInfo?.edit_version,source_code: editInfo?.code_info?.source_code, // 源代码参数});}Toast.success({content: isCreate? I18n.t('register_success'): I18n.t('Plugin_update_success'),showClose: false,});onSuccess?.(res?.data?.plugin_id); // 创建成功时返回插件IDonCancel?.(); // 关闭弹窗};return (<UIModalfullScreenclassName="full-screen-modal"title={<div className={s['bot-code-edit-title-action']}><span>{isCreate ? I18n.t('plugin_create') : I18n.t('plugin_Update')}</span><div>{actions}</div></div>}visible={visible}onCancel={() => onCancel?.()}footer={!disabled ? (<Space><UIButton type="tertiary" onClick={() => onCancel?.()}>{I18n.t('Cancel')}</UIButton><UIButton type="primary" onClick={registerPlugin}>{I18n.t('Confirm')}</UIButton></Space>) : null}maskClosable={false}><div className={classNames(s.flex)}><div className={classNames(s['plugin-height'], s.flex5)}><div style={{ display: 'flex' }}><div style={{ flex: 1, borderRight: '1px solid rgb(215,218,221)' }}><div className={s.title}>{I18n.t('ai_plugin_(fill_in_json)_*')}</div><EditordataTestID="create-plugin-code-editor-json"disabled={disabled}theme="tomorrow"mode="json"height={EDITOR_HEIGHT_MAX}value={aiPlugin}useValidate={false}onChange={e => setAiPlugin(e)}/></div><div style={{ flex: 1 }}><div className={s.title}>{I18n.t('openapi_(fill_in_yaml)_*')}</div><EditordataTestID="create-plugin-code-editor-yaml"disabled={disabled}theme="tomorrow"mode="yaml"height={EDITOR_HEIGHT_MAX}value={openApi}useValidate={false}onChange={e => setOpenApi(e)}/></div></div></div></div></UIModal>);
};
4. 插件编辑流程的核心实现
插件编辑功能的核心实现包括以下几个关键步骤:
-
插件行点击事件处理:在
usePluginConfig
的onItemClick
方法中,根据插件类型决定打开编辑弹窗或导航到详情页 -
插件锁定机制:使用
CheckOutPluginContext
确保同一时间只有一个用户可以编辑插件,避免冲突 -
插件数据加载:通过
PluginDevelopApi.GetPluginInfo
获取插件详情,为编辑提供数据支持 -
编辑弹窗管理:使用
useBotCodeEditOutPlugin
和useBotCodeEditModal
管理编辑弹窗的显示、隐藏和数据传递 -
资源释放处理:编辑完成或组件卸载时,通过
UnlockOutPluginContext
释放插件锁定 -
成功回调处理:编辑成功后触发
onSuccess
回调,刷新资源列表,保持数据一致性
5. 插件编辑的API调用
插件编辑过程中主要使用的API:
- GetPluginInfo:获取插件详情信息,为编辑提供基础数据
- CheckOutPluginContext:检查并锁定插件,确保编辑时的资源独占性
- UnlockOutPluginContext:释放插件锁定,允许其他用户编辑
- UpdatePlugin:保存插件编辑内容(在编辑弹窗内部调用)
6. 错误处理和边界情况
插件编辑功能对各种边界情况进行了完善的处理:
- 插件锁定失败:当插件已被其他用户锁定时,提示用户稍后再试
- API调用失败:提供清晰的错误提示,帮助用户理解问题原因
- 组件卸载处理:组件卸载时确保释放插件锁定,避免资源泄漏
- 异步操作状态管理:通过loading状态提供清晰的用户反馈
7. 技术亮点总结
插件编辑功能的技术亮点包括:
- 统一的配置管理:通过
usePluginConfig
统一管理插件资源的配置和行为 - 灵活的类型处理:针对不同类型的插件提供差异化的编辑体验
- 安全的锁定机制:使用插件锁定确保编辑操作的安全性和一致性
- 清晰的组件结构:组件职责分离,便于维护和扩展
- 完善的错误处理:对各种边界情况进行了全面处理
- 响应式状态管理:通过React Hooks管理组件状态和副作用
这些设计确保了插件编辑功能的安全性、可靠性和用户体验的友好性,为开发者提供了便捷高效的插件管理工具。
/>
) : null}
{/* 删除按钮(带确认弹窗) */}
{!deleteProps.hide && (<Popconfirmtitle={i18n.t('delete_title')}content={i18n.t('delete_desc')}onConfirm={deleteProps?.handler} // 确认后执行删除disabled={deleteProps.disabled}><Tooltip content={i18n.t('Delete')} position="top"><UIIconButtonicon={<IconDeleteOutline />}disabled={deleteProps.disabled}/></Tooltip></Popconfirm>
)}
);
```
设计特点:
- 操作优先级:编辑操作位于中间位置,便于用户快速访问
- 交互差异:编辑直接触发,删除需要确认,符合用户习惯
- 权限集成:所有操作都支持基于权限的禁用控制
- 视觉反馈:禁用状态有明显的视觉区分
- 事件隔离:阻止操作按钮事件冒泡到表格行
4. 编辑功能集成 - usePromptConfig Hook
usePromptConfig
是资源库页面中提示词相关功能的核心Hook,负责集成编辑弹窗、权限控制、表格配置等功能。
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/use-entity-configs/use-prompt-config.tsx
4.1 Hook依赖与初始化
import { useNavigate } from 'react-router-dom';
import { useRef } from 'react';
import { useRequest } from 'ahooks';
import { ActionKey, ResType, type ResourceInfo } from '@coze-arch/idl/plugin_develop';
import { usePromptConfiguratorModal } from '@coze-common/prompt-kit/main';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { Table, Menu, Toast } from '@coze-arch/coze-design';const { TableAction } = Table;export const usePromptConfig: UseEntityConfigHook = ({spaceId, // 工作空间IDisPersonalSpace, // 是否个人空间reloadList, // 列表刷新函数getCommonActions, // 通用操作获取函数
}) => {const navigate = useNavigate();const [FLAGS] = useFlags();const recordRef = useRef<ResourceInfo | null>(null);const { open: openSelectIntelligenceModal, node: selectIntelligenceModal } =useSelectIntelligenceModal({spaceId,onSelect: (intelligence: IntelligenceData) => {const targetId = intelligence.basic_info?.id;const diffPromptResourceId = recordRef.current?.res_id;navigate(`/space/${spaceId}/bot/${targetId}`, {replace: true,state: {mode: 'diff',diffPromptResourceId,targetId,},});sendTeaEvent(EVENT_NAMES.compare_mode_front, {bot_id: targetId,compare_type: 'prompts',from: 'prompt_resource',source: 'bot_detail_page',action: 'start',});},});const { open: openCreatePrompt, node: promptConfiguratorModal } =usePromptConfiguratorModal({spaceId,source: 'resource_library',enableDiff: FLAGS['bot.studio.prompt_diff'],onUpdateSuccess: reloadList,onDiff: ({ libraryId }) => {recordRef.current = {res_id: libraryId,};openSelectIntelligenceModal();},});// 编辑提示词的核心逻辑通过 usePromptConfiguratorModal 实现const { open: openCreatePrompt, node: promptConfiguratorModal } =usePromptConfiguratorModal({spaceId,source: 'resource_library',enableDiff: FLAGS['bot.studio.prompt_diff'],onUpdateSuccess: reloadList, // 编辑成功后刷新列表onDiff: ({ libraryId }) => {recordRef.current = {res_id: libraryId,};openSelectIntelligenceModal();},});// 删除提示词的核心逻辑(保留用于删除操作)const { run: delPrompt } = useRequest((promptId: string) =>PlaygroundApi.DeletePromptResource({prompt_resource_id: promptId,}),{manual: true,onSuccess: () => {reloadList(); // 删除成功后刷新列表Toast.success(I18n.t('Delete_success')); // 显示成功提示},},);return {modals: (<>{selectIntelligenceModal}{promptConfiguratorModal}</>),config: {typeFilter: {label: I18n.t('library_resource_type_prompt'),value: ResType.Prompt,},renderCreateMenu: () => (<Menu.Itemdata-testid="workspace.library.header.create.prompt"icon={<IconCozLightbulb />}onClick={() => {sendTeaEvent(EVENT_NAMES.widget_create_click, {source: 'menu_bar',workspace_type: isPersonalSpace? 'personal_workspace': 'team_workspace',});openCreatePrompt({mode: 'create',});}}>{I18n.t('creat_new_prompt_prompt')}</Menu.Item>),target: [ResType.Prompt],onItemClick: (record: ResourceInfo) => {recordRef.current = record;const canEdit = record.actions?.find(action => action.key === ActionKey.Edit,)?.enable;openCreatePrompt({mode: 'info',canEdit,editId: record.res_id || '',});},// 渲染表格操作列,包含编辑和删除功能renderActions: (libraryResource: ResourceInfo) => (<TableActiondeleteProps={{// 根据权限控制删除按钮状态disabled: !libraryResource.actions?.find(action => action.key === ActionKey.Delete,)?.enable,// 删除确认描述deleteDesc: I18n.t('prompt_resource_delete_describ'),// 删除处理函数handler: () => {delPrompt(libraryResource.res_id || '');},}}// 编辑操作 - 核心功能editProps={{disabled: !libraryResource.actions?.find(action => action.key === ActionKey.Edit,)?.enable,handler: () => {// 打开编辑弹窗,传入编辑模式和资源IDopenCreatePrompt({mode: 'edit',editId: libraryResource.res_id || '',});},}}actionList={getCommonActions?.(libraryResource)}/>),},};
};
编辑提示词-API接口实现
bot-api/package.json
文件位置:frontend/packages/arch/bot-api/package.json
核心代码:
{"name": "@coze-arch/bot-api","version": "0.0.1","description": "RPC wrapper for bot studio application","author": "fanwenjie.fe@bytedance.com","exports": {".": "./src/index.ts",},
}
代码作用:
- 1.包定义 :定义了一个名为 @coze-arch/bot-api 的 npm 包,版本为 0.0.1,这是一个用于 bot studio 应用的 RPC 包装器。
- 2.通过主入口文件 :
在frontend\packages\arch\bot-api\src\index.ts
中, PlaygroundApi 被导出:
export { PlaygroundApi } from './playground-api';
这允许通过 @coze-arch/bot-api 直接导入 PlaygroundApi 。
3.PlaygroundApi 实现 :在 src/playground-api.ts 中, PlaygroundApi 是一个配置好的服务实例,它使用了 PlaygroundService 和 axios 请求配置。
src/playground-api.ts
文件位置:frontend\packages\arch\bot-api\src\playground-api.ts
核心代码:
import PlaygroundApiService from './idl/playground_api';
import { axiosInstance, type BotAPIRequestConfig } from './axios';// eslint-disable-next-line @typescript-eslint/naming-convention
export const PlaygroundApi = new PlaygroundApiService<BotAPIRequestConfig>({request: (params, config = {}) => {config.headers = Object.assign(config.headers || {}, {'Agw-Js-Conv': 'str',});return axiosInstance.request({ ...params, ...config });},
});
实现特点:
- 自动生成:基于IDL文件自动生成的API客户端
- 类型安全:完整的TypeScript类型支持
- 统一配置:使用共享的axios实例和请求配置
- 方法导出:直接导出常用的API方法便于调用
- 请求头设置:自动添加必要的请求头信息
axiosInstance说明
1.axiosInstance 在整个项目中是全局共享的
2.bot-api 包中的导入 ( frontend/packages/arch/bot-api/src/axios.ts )
是直接从 @coze-arch/bot-http 包导入了 axiosInstance 。
import {axiosInstance,isApiError,type AxiosRequestConfig,
} from '@coze-arch/bot-http';
3.bot-http 包中的定义 ( frontend/packages/arch/bot-http/src/axios.ts ):
export const axiosInstance = axios.create();
这里创建了一个全局的 axios 实例,与用户名修改保存请求的 axios 实例是同一个。
PlaygroundApiService说明
1.bot-api包中的导入路径:
import PlaygroundApiService from ‘./idl/playground_api’;
实际指向
frontend/packages/arch/bot-api/src/idl/playground_api.ts
文件内容重新导出了 @coze-arch/idl/playground_api 包的所有内容,包括默认导出
export * from '@coze-arch/idl/playground_api';
export { default as default } from '@coze-arch/idl/playground_api';
2.idl包的模块映射
文件位置:frontend/packages/arch/idl/package.json
核心代码:
"name": "@coze-arch/idl","version": "0.0.1","description": "IDL files for bot studio application","author": "fanwenjie.fe@bytedance.com","exports": {"./playground_api": "./src/auto-generated/playground_api/index.ts",
代码作用:将 @coze-arch/idl/playground_api 映射到实际文件路径frontend/packages/arch/idl/src/auto-generated/playground_api/index.ts
这个文件说明后续见 删除提示词-API接口实现 这个章节。
5. 编辑操作的完整流程
编辑插件的完整流程如下: