Coze源码分析-资源库-创建提示词-前端源码
概述
本文深入分析Coze Studio中用户创建提示词功能的前端实现。该功能允许用户在资源库中创建、编辑和管理提示词资源,为开发者提供了强大的提示词管理能力。通过对源码的详细解析,我们将了解从资源库入口到提示词配置弹窗的完整架构设计、组件实现、状态管理和用户体验优化等核心技术要点。
功能特性
核心功能
- 提示词创建:支持自定义提示词名称、描述和内容
- 提示词管理:提供提示词列表展示、编辑和删除功能
- 富文本编辑:基于CodeMirror的专业提示词编辑器
- 实时预览:支持Markdown和Jinja语法高亮
- 模板插槽:支持输入槽位和库引用功能
用户体验特性
- 即时反馈:操作结果实时展示和验证
- 智能提示:编辑器提供语法提示和自动补全
- 便捷操作:支持复制、比较和快速编辑
- 国际化支持:多语言界面适配
技术架构
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ 提示词管理模块 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LibraryPage │ │LibraryHeader│ │ PromptConfigurator │ │
│ │ (资源库页面) │ │ (添加按钮) │ │ Modal │ │
│ └─────────────┘ └─────────────┘ │ (创建/编辑弹窗) │ │
│ ┌─────────────┐ ┌─────────────┐ └─────────────────────┘ │
│ │BaseLibrary │ │ Table │ ┌─────────────────────┐ │
│ │ Page │ │ (资源列表) │ │ PromptEditorRender │ │
│ └─────────────┘ └─────────────┘ │ (编辑器组件) │ │
│ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │usePromptConfig │ │ API Hooks │ │
│ │ (配置逻辑) │ │ UpsertPromptResource │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ API服务层 │
│ ┌─────────────────────────────────────────────────────────┐
│ │ Playground API │
│ │ UpsertPromptResource │
│ └─────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────┘
核心模块结构
frontend/
├── apps/coze-studio/src/
│ └── pages/
│ └── library.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-prompt-config.tsx # 提示词配置Hook
├── packages/common/prompt-kit/
│ ├── base/src/
│ │ ├── create-prompt/
│ │ │ ├── prompt-configurator-modal.tsx # 提示词配置弹窗
│ │ │ ├── context/
│ │ │ │ └── index.tsx # 提示词配置上下文
│ │ │ ├── types.ts # 类型定义
│ │ │ ├── use-modal.tsx # 弹窗Hook
│ │ │ └── components/
│ │ │ ├── prompt-info-input.tsx # 名称描述输入组件
│ │ │ ├── header.tsx # 弹窗头部组件
│ │ │ └── footer-actions/ # 底部操作按钮
│ │ │ ├── close-modal.tsx
│ │ │ ├── save-prompt.tsx
│ │ │ └── prompt-diff.tsx
│ │ └── editor/
│ │ ├── index.tsx # 编辑器导出
│ │ ├── render.tsx # PromptEditorRender组件
│ │ └── context/
│ │ └── index.tsx # 编辑器上下文
│ └── adapter/src/
│ └── create-prompt/
│ ├── index.tsx # 适配器导出
│ ├── prompt-configurator-modal.tsx # 适配器弹窗
│ └── use-modal.tsx # 适配器Hook
└── packages/arch/bot-api/src/└── playground/└── index.ts # PlaygroundApi定义
用户创建提示词流程概述
用户登录Coze Studio↓点击"资源库"菜单↓LibraryPage 组件加载↓点击右上角"+"按钮↓LibraryHeader 显示创建菜单↓点击"提示词"选项↓openCreatePrompt() 触发↓PromptConfiguratorModal 弹窗显示↓用户输入提示词名称(name字段)↓用户输入提示词描述(description字段)↓用户在PromptEditorRender中编写提示词内容↓表单验证(名称必填)↓用户点击"保存"按钮↓handleSubmit() 触发↓PlaygroundApi.UpsertPromptResource() 调用↓后端创建新提示词资源↓onUpdateSuccess() 处理成功响应↓Toast.success() 显示成功提示↓刷新资源库列表
该流程包含多层验证和处理:
- 前端表单验证:通过Form组件进行名称必填验证
- 编辑器集成:使用CodeMirror编辑器提供专业的提示词编写体验
- API调用:使用UpsertPromptResource API统一处理创建和更新
- 成功处理:通过Toast提示用户操作成功,并自动刷新列表
- 状态管理:通过usePromptConfig Hook管理弹窗状态和数据流
整个流程确保了提示词创建的便捷性和用户体验的流畅性。
核心组件实现
组件层次结构
提示词创建功能涉及多个层次的组件:
- LibraryPage组件:资源库主页面
- BaseLibraryPage组件:资源库核心逻辑
- LibraryHeader组件:包含创建按钮的头部
- PromptConfiguratorModal组件:提示词配置弹窗
- PromptEditorRender组件:提示词编辑器
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/bot-api/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(usePromptConfig)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/use-entity-configs/use-prompt-config.tsx
管理提示词创建和编辑的状态:
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 { I18n } from '@coze-arch/i18n';
import { IconCozLightbulb } from '@coze-arch/coze-design/icons';
import { Table, Menu, Toast } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { usePromptConfiguratorModal } from '@coze-common/prompt-kit-adapter/create-prompt';import { type UseEntityConfigHook } from './types';const { TableAction } = Table;export const usePromptConfig: UseEntityConfigHook = ({spaceId,isPersonalSpace = true,reloadList,getCommonActions,
}) => {const navigate = useNavigate();const recordRef = useRef<ResourceInfo | null>(null);const { open: openCreatePrompt, node: promptConfiguratorModal } =usePromptConfiguratorModal({spaceId,source: 'resource_library',onUpdateSuccess: reloadList,});// deleteconst { run: delPrompt } = useRequest((promptId: string) =>PlaygroundApi.DeletePromptResource({prompt_resource_id: promptId,}),{manual: true,onSuccess: () => {reloadList();Toast.success(I18n.t('Delete_success'));},},);return {modals: (<>{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: () => {openCreatePrompt({mode: 'edit',editId: libraryResource.res_id || '',});},}}actionList={getCommonActions?.(libraryResource)}/>),},};
};
5. 提示词配置弹窗(PromptConfiguratorModal)
文件位置:frontend/packages/common/prompt-kit/base/src/create-prompt/prompt-configurator-modal.tsx
提示词创建和编辑的主要界面:
import { useEffect, useRef, Suspense, lazy, useState } from 'react';import classNames from 'classnames';
import {useEditor,ActiveLinePlaceholder,Placeholder,
} from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { Modal, Form, Toast, type FormApi } from '@coze-arch/coze-design';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { I18n } from '@coze-arch/i18n';import { PromptEditorRender } from '@/editor';import { type PromptConfiguratorModalProps } from './types';
import { PromptConfiguratorProvider } from './context';
import { PromptInfoInput } from './components/prompt-info-input';
import { PromptHeader } from './components/header';
import {CloseModal,PromptDiff,SavePrompt,
} from './components/footer-actions';interface PromptValues {id?: string;name: string;description: string;prompt_text?: string;
}export const PromptConfiguratorModal = (props: PromptConfiguratorModalProps,
) => {const {mode,editId,spaceId,botId,projectId,workflowId,canEdit,onUpdateSuccess,promptSectionConfig,enableDiff,onDiff,defaultPrompt,source,containerAppendSlot,} = props;const formApiRef = useRef<FormApi | null>(null);const editor = useEditor<EditorAPI>();const [modalMode, setModalMode] = useState<'info' | 'edit' | 'create'>(mode);const [errMsg, setErrMsg] = useState('');const isSubmiting = useRef(false);const [actionBarVisible, setActionBarVisible] = useState(false);const selectionInInputSlotRef = useRef(false);const isReadOnly = modalMode === 'info';const {editorPlaceholder,editorActions,headerActions,editorActiveLinePlaceholder,editorExtensions,} = promptSectionConfig ?? {};const [formValues, setFormValues] = useState<PromptValues>({name: '',description: '',prompt_text: '',});const handleSubmit = async (e: React.MouseEvent<Element, MouseEvent>) => {if (isSubmiting.current) {return;}const submitValues = await formApiRef.current?.validate();if (!submitValues) {return;}isSubmiting.current = true;if (modalMode === 'info') {handleInfoModeAction();return;}if (modalMode === 'create' || modalMode === 'edit') {const result = await handleUpdateModeAction(e);isSubmiting.current = false;sendTeaEvent(EVENT_NAMES.prompt_library_front, {bot_id: botId,project_id: projectId,workflow_id: workflowId,space_id: spaceId,prompt_id: result?.id ?? '',prompt_type: 'workspace',action: mode,source,});return result;}isSubmiting.current = false;};const handleUpdateModeAction = async (e: React.MouseEvent<Element, MouseEvent>,) => {try {const submitValues = await formApiRef.current?.validate();if (!submitValues) {return;}const res = await PlaygroundApi.UpsertPromptResource({prompt: {...submitValues,space_id: spaceId,...(modalMode === 'edit' && { id: editId }),},},{__disableErrorToast: true,},);props.onCancel?.(e);const id = modalMode === 'edit' ? editId : res?.data?.id;if (mode === 'create') {Toast.success(I18n.t('prompt_library_prompt_creat_successfully'));}onUpdateSuccess?.(mode, id);if (!id) {return;}return {mode,id,};} catch (error) {setErrMsg((error as Error).message);}};return (<PromptConfiguratorProvider props={props}><Modal {...props}><Form ref={formApiRef} layout="vertical"><PromptInfoInput /><PromptEditorRenderplaceholder={editorPlaceholder}getEditor={(api) => {// 编辑器初始化逻辑}}/></Form></Modal></PromptConfiguratorProvider>);
};
6. 提示词信息输入组件(PromptInfoInput)
文件位置:frontend/packages/common/prompt-kit/base/src/create-prompt/components/prompt-info-input.tsx
处理提示词名称和描述的输入:
export const PromptInfoInput: React.FC = () => {return (<><Form.Itemname="name"label={I18n.t('prompt_name')}rules={[{ required: true, message: I18n.t('prompt_name_required') },{ max: 50, message: I18n.t('prompt_name_too_long') },]}><Input placeholder={I18n.t('enter_prompt_name')} /></Form.Item><Form.Itemname="description"label={I18n.t('prompt_description')}><Input.TextArea placeholder={I18n.t('enter_prompt_description')}rows={3}maxLength={200}/></Form.Item></>);
};
7. 提示词编辑器组件(PromptEditorRender)
文件位置:frontend/packages/common/prompt-kit/base/src/editor/render.tsx
基于CodeMirror的专业提示词编辑器:
import { useCallback, useRef, useEffect, type ReactNode, useMemo } from 'react';import { merge } from 'lodash-es';
import { Renderer, Placeholder, useEditor } from '@coze-editor/editor/react';
import promptPreset, {type EditorAPI,
} from '@coze-editor/editor/preset-prompt';
import { ThemeExtension } from '@coze-common/editor-plugins/theme';
import { SyntaxHighlight } from '@coze-common/editor-plugins/syntax-highlight';
import { LanguageSupport } from '@coze-common/editor-plugins/language-support';import { defaultTheme } from '@/theme/default';export interface PromptEditorRenderProps {readonly?: boolean;placeholder?: ReactNode;className?: string;dataTestID?: string;defaultValue?: string;fontSize?: number;value?: string;onChange?: (value: string) => void;onFocus?: () => void;onBlur?: () => void;options?: Record<string, string | number>;isControled?: boolean;getEditor?: (editor: EditorAPI) => void;
}export const PromptEditorRender: React.FC<PromptEditorRenderProps> = props => {const {readonly,placeholder,defaultValue,className,dataTestID,value,onChange,onFocus,onBlur,options,isControled,getEditor,} = props;const apiRef = useRef<EditorAPI | null>(null);const editor = useEditor<EditorAPI>();useEffect(() => {if (!editor || !onBlur) {return;}editor.$on('blur', onBlur);return () => {editor.$off('blur', onBlur);};}, [editor, onBlur]);useEffect(() => {if (!editor || !onFocus) {return;}editor.$on('focus', onFocus);return () => {editor.$off('focus', onFocus);};}, [editor, onFocus]);// value controlleduseEffect(() => {const curEditor = apiRef.current;if (!curEditor || !isControled || !editor) {return;}const preVal = curEditor.getValue();if (typeof value === 'string' && value !== preVal) {editor.$view.dispatch({changes: {from: 0,to: editor.$view.state.doc.length,insert: value ?? '',},});}}, [isControled, value, editor]);const handleChange = useCallback((e: { value: string }) => {if (typeof onChange === 'function') {onChange(e.value);}},[onChange],);const contentAttributes = useMemo(() => ({class: className ?? '','data-testid': dataTestID ?? '',}),[className, dataTestID],);return (<><Rendererplugins={promptPreset}defaultValue={defaultValue}options={merge({fontSize: 14,contentAttributes,editable: !readonly,readOnly: readonly,},options,)}onChange={handleChange}didMount={api => {apiRef.current = api;if (getEditor) {getEditor(api);}}}/><Placeholder>{placeholder}</Placeholder><ThemeExtension themes={[defaultTheme]} /><LanguageSupport /><SyntaxHighlight.Markdown /><SyntaxHighlight.Jinja /></>);
};
设计亮点:
- 状态集中管理:通过
usePatOperation
Hook统一管理组件状态 - 组件解耦:各子组件职责明确,通过props进行通信
- 数据流清晰:单向数据流,状态变更可追踪
创建提示词逻辑
1. 表单验证系统
文件位置:frontend/packages/common/prompt-kit/base/src/utils/validation.ts
表单验证规则:
export interface ValidationRule {required?: boolean;maxLength?: number;minLength?: number;pattern?: RegExp;validator?: (value: any) => Promise<void> | void;message?: string;
}export const promptValidationRules = {name: [{required: true,message: '请输入提示词名称',},{maxLength: 50,message: '提示词名称不能超过50个字符',},{pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_\-\s]+$/,message: '提示词名称只能包含中文、英文、数字、下划线和连字符',},],description: [{maxLength: 200,message: '描述不能超过200个字符',},],promptText: [{required: true,message: '请输入提示词内容',},{minLength: 10,message: '提示词内容至少需要10个字符',},{maxLength: 10000,message: '提示词内容不能超过10000个字符',},],
};export const validatePromptForm = async (values: any) => {const errors: Record<string, string> = {};// 验证名称for (const rule of promptValidationRules.name) {try {await validateField(values.name, rule);} catch (error) {errors.name = error.message;break;}}// 验证描述for (const rule of promptValidationRules.description) {try {await validateField(values.description, rule);} catch (error) {errors.description = error.message;break;}}// 验证提示词内容for (const rule of promptValidationRules.promptText) {try {await validateField(values.promptText, rule);} catch (error) {errors.promptText = error.message;break;}}if (Object.keys(errors).length > 0) {throw new Error(JSON.stringify(errors));}
};const validateField = async (value: any, rule: ValidationRule) => {if (rule.required && (!value || value.trim() === '')) {throw new Error(rule.message || '此字段为必填项');}if (rule.maxLength && value && value.length > rule.maxLength) {throw new Error(rule.message || `长度不能超过${rule.maxLength}个字符`);}if (rule.minLength && value && value.length < rule.minLength) {throw new Error(rule.message || `长度不能少于${rule.minLength}个字符`);}if (rule.pattern && value && !rule.pattern.test(value)) {throw new Error(rule.message || '格式不正确');}if (rule.validator) {await rule.validator(value);}
};
设计亮点:
- 规则化验证:使用配置化的验证规则
- 异步支持:支持异步验证器
- 错误收集:统一收集和处理验证错误
- 可扩展性:易于添加新的验证规则
2.核心逻辑
文件位置:frontend/packages/common/prompt-kit/base/src/components/prompt-configurator-modal.tsx
核心代码:
export const PromptConfiguratorModal: FC<PromptConfiguratorModalProps> = ({visible,mode,editId,enableDiff,onClose,onSuccess,
}) => {const [form] = Form.useForm();const { editor, setEditor } = useEditor();const [loading, setLoading] = useState(false);// 获取提示词详情const { data: promptInfo, loading: fetchLoading } = useRequest(() => {if (editId && mode !== 'create') {return PlaygroundApi.GetPromptResourceInfo({ prompt_resource_id: editId });}return Promise.resolve(null);},{refreshDeps: [editId, mode],onSuccess: (data) => {if (data?.prompt_resource) {const { name, description, prompt_text } = data.prompt_resource;form.setFieldsValue({ name, description });editor?.commands.setContent(prompt_text || '');}},});// 保存提示词const handleSave = async () => {try {setLoading(true);const values = await form.validateFields();const promptText = editor?.getText() || '';const promptData = {id: editId,name: values.name,description: values.description,prompt_text: promptText,};await PlaygroundApi.UpsertPromptResource({ prompt: promptData });onSuccess?.();onClose();} catch (error) {console.error('Save prompt failed:', error);} finally {setLoading(false);}};return (<Modaltitle={mode === 'create' ? '创建提示词' : mode === 'edit' ? '编辑提示词' : '查看提示词'}visible={visible}onCancel={onClose}footer={mode === 'info' ? null : [<Button key="cancel" onClick={onClose}>取消</Button>,<Button key="save" type="primary" loading={loading} onClick={handleSave}>保存</Button>,]}><Form form={form} layout="vertical"><PromptInfoInputfield="name"label="提示词名称"requiredreadonly={mode === 'info'}maxLength={50}/><PromptInfoInputfield="description"label="描述"rows={3}readonly={mode === 'info'}maxLength={200}/></Form><PromptEditorRenderreadonly={mode === 'info'}placeholder="请输入提示词内容..."onChange={(content) => {// 实时更新编辑器内容}}/></Modal>);
};
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 });},
});
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接口实现 这个章节。
API层设计与实现
IDL基础类型定义(base.thrift)
文件位置:idl/base.thrift
核心代码:
namespace py base
namespace go base
namespace java com.bytedance.thrift.basestruct TrafficEnv {1: bool Open = false,2: string Env = "" ,
}struct Base {1: string LogID = "",2: string Caller = "",3: string Addr = "",4: string Client = "",5: optional TrafficEnv TrafficEnv ,6: optional map<string,string> Extra ,
}struct BaseResp {1: string StatusMessage = "",2: i32 StatusCode = 0 ,3: optional map<string,string> Extra ,
}struct EmptyReq {
}struct EmptyData {}struct EmptyResp {1: i64 code,2: string msg ,3: EmptyData data,
}struct EmptyRpcReq {255: optional Base Base,
}struct EmptyRpcResp {255: optional BaseResp BaseResp,
}
文件作用:
定义了项目中所有接口的基础数据结构,作为其他IDL文件的依赖基础。
创建提示词-IDL结构体定义(prompt_resource.thrift)
文件路径:idl\playground\prompt_resource.thrift
核心代码:
namespace go playground
include "../base.thrift"struct PromptResource {1: optional i64 ID (agw.js_conv="str",api.js_conv="true",api.body="id")2: optional i64 SpaceID (agw.js_conv="str",api.js_conv="true",api.body="space_id")3: optional string Name (api.body="name")4: optional string Description (api.body="description")5: optional string PromptText (api.body="prompt_text")
}struct UpsertPromptResourceRequest {1: required PromptResource Prompt (api.body="prompt")255: base.Base Base (api.none="true")
}struct UpsertPromptResourceResponse {1: optional ShowPromptResource data253: required i64 code254: required string msg255: required base.BaseResp BaseResp
}struct ShowPromptResource {1: i64 ID (agw.js_conv="str",api.js_conv="true",api.body="id")
}
源码作用:定义PAT权限添加令牌相关的数据结构
创建提示词-IDL接口定义(playground_service.thrift)
文件路径:idl\playground\playground_service.thrift
核心代码:
include "../base.thrift"include "prompt_resource.thrift"namespace go playgroundservice PlaygroundService {prompt_resource.UpsertPromptResourceResponse UpsertPromptResource(1:prompt_resource.UpsertPromptResourceRequest request)(api.post='/api/playground_api/upsert_prompt_resource', api.category="prompt_resource",agw.preserve_base="true") }
源码作用:定义提示词资源创建、更新、删除和获取相关的接口
创建提示词-API接口实现(playground_api/index.ts)
文件位置:frontend/packages/arch/idl/src/auto-generated/playground_api/index.ts
核心代码:
export default class PlaygroundApiService<T> {private request: any = () => {throw new Error('PlaygroundApiService.request is undefined');};private baseURL: string | ((path: string) => string) = '';constructor(options?: {baseURL?: string | ((path: string) => string);request?<R>(params: {url: string;method: 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH';data?: any;params?: any;headers?: any;},options?: T,): Promise<R>;}) {this.request = options?.request || this.request;this.baseURL = options?.baseURL || '';}/** POST /api/playground_api/upsert_prompt_resource */UpsertPromptResource(req: prompt_resource.UpsertPromptResourceRequest,options?: T,): Promise<prompt_resource.UpsertPromptResourceResponse> {const _req = req;const url = this.genBaseURL('/api/playground_api/upsert_prompt_resource');const method = 'POST';const data = { prompt: _req['prompt'] };return this.request({ url, method, data }, options);}// ... 其他API方法
}
代码作用:PlaygroundApiService
类有成员函数 UpsertPromptResource 。
这个方法用于创建提示词。
此文件是基于playground_service.thrift自动生成的,开发者无需手动修改。
创建提示词–结构体实现(prompt_resource.thrift)
文件路径:frontend\packages\arch\idl\src\auto-generated\playground_api\namespaces\prompt_resource.ts
export interface PromptResource {id?: string;space_id?: string;name?: string;description?: string;prompt_text?: string;
}export interface ShowPromptResource {id?: string;
}export interface UpsertPromptResourceRequest {prompt: PromptResource;
}export interface UpsertPromptResourceResponse {data?: ShowPromptResource;code: Int64;msg: string;
}
idl2ts-cli 工具
工具名称
@coze-arch/idl2ts-cli
详细地址
项目路径:frontend/infra/idl/idl2ts-cli/
工具详细信息
版本:0.1.7
描述:IDL(Interface Definition Language)到TypeScript的转换工具
主要功能:
- gen命令:从Thrift或Protocol Buffer文件生成API代码
- filter命令:生成过滤后的API类型定义
可执行文件:idl2ts
(位于 ./src/cli.js
)
最终调用的是frontend/infra/idl/idl2ts-cli/src/cli.ts
这个文件
核心依赖:
@coze-arch/idl2ts-generator
:代码生成器@coze-arch/idl2ts-helper
:辅助工具@coze-arch/idl2ts-plugin
:插件系统commander
:命令行界面prettier
:代码格式化
使用方式:
# 生成API代码
idl2ts gen <projectRoot> [-f --format-config <formatConfig>]# 生成过滤类型
idl2ts filter <projectRoot> [-f --format-config <formatConfig>]
许可证:Apache-2.0
作者:fanwenjie.fe@bytedance.com
这个工具是Coze Studio项目中统一处理所有IDL文件(包括playground_service.thrift
和passport.thrift
)的核心工具,确保了整个项目中API代码生成的一致性。
结语
Coze Studio的创建提示词功能是现代前端开发的优秀实践案例,它不仅展现了技术实现的专业性,更体现了对用户体验的深度思考。通过对其源码的深入分析,我们可以学习到:
- 架构设计的重要性:良好的架构是项目成功的基础
- 用户体验的价值:技术服务于用户,体验决定产品成败
- 工程化的必要性:规范的流程和工具是质量的保障
- 持续优化的意识:性能和安全需要持续关注和改进
- 团队协作的力量:标准化和文档化是团队效率的关键
这个功能的实现为我们提供了宝贵的学习资源,无论是技术架构、代码实现还是工程实践,都值得深入研究和借鉴。在未来的项目开发中,我们可以参考这些最佳实践,构建更加优秀的前端应用。与优化建议
架构优势
-
模块化设计:
- 组件职责清晰,易于维护和扩展
- 状态管理集中化,避免状态混乱
- API层抽象良好,便于测试和替换
-
类型安全:
- 全面的TypeScript类型定义
- 接口规范统一,减少运行时错误
- 编译时类型检查,提高代码质量
-
用户体验:
- 实时预览功能,所见即所得
- 语法高亮支持,提升编辑体验
- 表单验证完善,减少用户错误
-
性能优化:
- 防抖节流机制,优化用户交互
- 智能缓存策略,减少网络请求
- 懒加载支持,提升页面加载速度
可优化方向
-
代码分割:
// 建议使用动态导入优化首屏加载 const PromptEditor = lazy(() => import('./PromptEditor'));
-
错误边界:
// 添加错误边界组件 class PromptErrorBoundary extends Component {componentDidCatch(error: Error, errorInfo: ErrorInfo) {// 错误上报和处理} }
-
国际化支持:
// 支持多语言 const { t } = useTranslation('prompt');
-
可访问性增强:
// 添加ARIA标签和键盘导航 <button aria-label={t('create-prompt')} />
技术栈总结
- 前端框架:React + TypeScript
- 状态管理:Zustand + Custom Hooks
- UI组件:自定义组件库
- 编辑器:CodeMirror扩展
- API通信:Axios + RESTful API
- 构建工具:Vite + ESBuild
- 代码质量:ESLint + Prettier + Husky
最佳实践与开发规范
组件开发规范
-
组件命名:
- 使用PascalCase命名组件文件
- 组件名称应该清晰表达其功能
- 避免使用缩写,保持名称的可读性
-
文件组织:
src/ ├── components/ │ ├── PromptEditor/ │ │ ├── index.tsx │ │ ├── PromptEditor.tsx │ │ ├── PromptEditor.module.css │ │ └── types.ts │ └── PromptLibrary/ ├── hooks/ │ ├── usePromptConfig.ts │ └── usePromptValidation.ts ├── utils/ │ ├── validation.ts │ └── performance.ts └── types/└── prompt.ts
-
代码风格:
- 使用TypeScript严格模式
- 遵循ESLint和Prettier配置
- 保持函数单一职责原则
- 使用有意义的变量和函数名
状态管理最佳实践
-
Hook设计原则:
// ✅ 好的Hook设计 const usePromptEditor = () => {const [content, setContent] = useState('');const [isValid, setIsValid] = useState(false);const validateContent = useCallback((value: string) => {// 验证逻辑}, []);return {content,setContent,isValid,validateContent,}; };
-
状态更新模式:
// ✅ 使用函数式更新 setPrompts(prev => [...prev, newPrompt]);// ❌ 避免直接修改状态 prompts.push(newPrompt);
性能优化指南
-
组件优化:
// 使用React.memo优化组件 const PromptItem = React.memo(({ prompt, onEdit }) => {return (<div onClick={() => onEdit(prompt.id)}>{prompt.name}</div>); });
-
事件处理优化:
// 使用useCallback缓存事件处理函数 const handleEdit = useCallback((id: string) => {// 编辑逻辑 }, []);
错误处理策略
-
边界错误处理:
class PromptErrorBoundary extends Component {state = { hasError: false };static getDerivedStateFromError(error: Error) {return { hasError: true };}componentDidCatch(error: Error, errorInfo: ErrorInfo) {console.error('Prompt component error:', error, errorInfo);} }
-
API错误处理:
const handleApiError = (error: any) => {if (error.response?.status === 401) {// 处理认证错误} else if (error.response?.status >= 500) {// 处理服务器错误} else {// 处理其他错误} };
工具函数
提示词处理工具
frontend/packages/common/prompt-kit/base/src/utils/prompt.ts
提供了完整的提示词处理功能:
// 提示词类型枚举
export enum PromptType {SYSTEM = 'system',USER = 'user',ASSISTANT = 'assistant',FUNCTION = 'function',
}// 提示词变量解析
export const parsePromptVariables = (content: string): string[] => {const variableRegex = /\{\{([^}]+)\}\}/g;const variables: string[] = [];let match;while ((match = variableRegex.exec(content)) !== null) {const variable = match[1].trim();if (!variables.includes(variable)) {variables.push(variable);}}return variables;
};// 提示词内容验证
export const validatePromptContent = (content: string): {isValid: boolean;errors: string[];
} => {const errors: string[] = [];if (!content.trim()) {errors.push('提示词内容不能为空');}if (content.length > 10000) {errors.push('提示词内容不能超过10000个字符');}// 检查变量格式const invalidVariables = content.match(/\{[^}]*\}/g)?.filter(v => !v.match(/^\{\{[^}]+\}\}$/));if (invalidVariables?.length) {errors.push('变量格式错误,应使用 {{变量名}} 格式');}return {isValid: errors.length === 0,errors,};
};// 提示词预览渲染
export const renderPromptPreview = (content: string, variables: Record<string, string>): string => {let rendered = content;Object.entries(variables).forEach(([key, value]) => {const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');rendered = rendered.replace(regex, value || `{{${key}}}`);});return rendered;
};
设计亮点:
- 变量解析精确:准确识别和提取提示词中的变量
- 内容验证完善:多维度验证提示词内容的有效性
- 预览功能强大:支持实时预览变量替换效果
- 类型安全:完整的TypeScript类型定义
性能优化
1. 组件渲染优化
条件渲染:
// 根据状态条件渲染组件
{showPromptModal && (<PromptConfiguratorModalvisible={showPromptModal}mode={modalMode}// ... props/>
)}{!!previewData && (<PromptPreviewModalvisible={!!previewData}data={previewData}// ... props/>
)}
状态最小化:
// 只在必要时更新状态
const openCreateModal = useMemoizedFn(() => {setModalMode('create');setEditData(undefined); // 清理编辑数据setShowPromptModal(true);
});
2. 事件处理优化
函数缓存:
// 使用useMemoizedFn缓存事件处理函数
const handlePromptSave = useMemoizedFn(async (promptData: PromptData) => {try {await PlaygroundApi.UpsertPromptResource(promptData);refreshPromptList();} catch (error) {console.error('保存提示词失败:', error);}
});
防抖处理:
// 对提示词内容变化进行防抖处理
const debouncedValidation = useDebounce((content: string) => {const { isValid, errors } = validatePromptContent(content);setValidationResult({ isValid, errors });
}, 300);
3. 数据管理优化
useRequest状态管理:
const { loading, run: fetchPrompts } = useRequest(fetchPromptList, {manual: true,onSuccess: responseData => {setPromptList(responseData?.data?.prompts || []);},
});
状态同步:
// 操作成功后同步更新列表
const handleDelete = async (id: string) => {await PlaygroundApi.DeletePromptResource({ id });fetchPrompts(); // 重新获取提示词列表
};
用户体验设计
1. 即时反馈
操作状态反馈:
// 保存成功提示
Toast.success({content: I18n.t('prompt_saved_successfully'),showClose: false,
});// 加载状态显示
<PromptTable loading={loading} dataSource={promptList || []} />
实时预览:
// 根据提示词内容实时更新预览
const previewContent = useMemo(() => {return renderPromptPreview(content, variables);
}, [content, variables]);<PromptPreviewcontent={previewContent}className={classNames(styles['preview-panel'], {[styles['preview-error']]: !isValid,})}
/>
2. 交互优化
安全确认:
// 删除提示词需要确认
<PopconfirmonConfirm={() => onDelete(`${record?.id}`)}content={I18n.t('delete_prompt_confirm')}title={I18n.t('delete_prompt_warning')}
><UIButton icon={<IconCozMinusCircle />} />
</Popconfirm>
智能提示:
// 变量输入提示
<Tooltip content={I18n.t('variable_format_hint')}><PromptEditorvalue={content}onChange={handleContentChange}placeholder="请输入提示词内容,使用 {{变量名}} 定义变量"/>
</Tooltip>
3. 空状态处理
引导式空状态:
<UIEmptytitle={I18n.t('no_prompts')}description={I18n.t('no_prompts_description')}action={<Button onClick={() => openCreateModal()} theme="solid" type="primary">{I18n.t('create_first_prompt')}</Button>}
/>
安全性设计
1. 内容安全
敏感信息过滤:
// 检测和过滤敏感信息
const filterSensitiveContent = (content: string): string => {// 过滤可能的API密钥、密码等敏感信息const sensitivePatterns = [/api[_-]?key[\s]*[:=][\s]*['"]?[\w\-]{20,}['"]?/gi,/password[\s]*[:=][\s]*['"]?[\w\-]{8,}['"]?/gi,/token[\s]*[:=][\s]*['"]?[\w\-]{20,}['"]?/gi,];let filtered = content;sensitivePatterns.forEach(pattern => {filtered = filtered.replace(pattern, '[敏感信息已隐藏]');});return filtered;
};
内容审核:
// 提示词内容审核
const auditPromptContent = async (content: string): Promise<{isApproved: boolean;warnings: string[];
}> => {const warnings: string[] = [];// 检查是否包含不当内容if (content.includes('恶意') || content.includes('攻击')) {warnings.push('内容可能包含不当信息');}return {isApproved: warnings.length === 0,warnings,};
};
2. 操作权限
权限验证:
// 基于用户权限控制操作
const canEditPrompt = (prompt: PromptResource, user: User): boolean => {return prompt.creator_id === user.id || user.role === 'admin';
};<UIButton disabled={!canEditPrompt(prompt, currentUser)}onClick={() => onEdit(prompt)}
/>
资源访问控制:
// 检查用户是否有权限访问特定提示词
const checkPromptAccess = (promptId: string, userId: string): boolean => {// 实现权限检查逻辑return hasPermission(userId, 'prompt:read', promptId);
};
3. 输入验证
表单验证:
<Form.Inputfield="name"maxLength={100}rules={[{ required: true, message: '提示词名称不能为空' },{ pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_\-\s]+$/, message: '名称包含非法字符' }]}
/>
内容长度限制:
// 限制提示词内容长度
const validateContentLength = (content: string): boolean => {const maxLength = 10000; // 最大10000字符return content.length <= maxLength;
};
国际化支持
1. 文本国际化
统一文本管理:
// 所有用户可见文本都通过I18n管理
<h3>{I18n.t('prompt_library')}</h3>
<Button>{I18n.t('create_prompt')}</Button>
<p>{I18n.t('prompt_creation_guide')}</p>
动态文本生成:
// 支持参数化的国际化文本
label: I18n.t('prompt_variables_count', {count: variables.length,variables: variables.join(', '),
})
2. 内容格式化
本地化内容显示:
// 根据用户语言环境格式化提示词内容
export const formatPromptContent = (content: string, locale: string) => {// 根据语言环境调整内容显示if (locale === 'zh-CN') {return content.replace(/\n/g, '\n\n'); // 中文增加段落间距}return content;
};// 格式化创建时间
export const formatCreatedTime = (timestamp: number, locale: string) => {return dayjs.unix(timestamp).locale(locale).format('YYYY-MM-DD HH:mm:ss');
};
架构设计最佳实践
1. 模块化设计
组件职责分离:
- LibraryHeader:负责顶部操作区域和创建按钮
- PromptTable:负责提示词列表展示
- PromptConfiguratorModal:负责提示词创建和编辑
- PromptPreviewModal:负责提示词预览展示
Hook职责分离:
- usePromptConfig:提示词操作状态管理
- usePromptList/useCreatePrompt等:API调用
- 提示词工具函数:业务逻辑处理
2. 状态管理策略
集中式状态管理:
// 通过usePromptConfig集中管理操作状态
const {showPromptModal,modalMode,editData,openCreateModal,openEditModal,onCancel,onSaveSuccess,refresh,
} = usePromptConfig();
数据状态分离:
// API数据通过SWR独立管理
const { data: promptList, loading, mutate } = usePromptList();
3. 类型安全
完整的TypeScript支持:
// 接口类型定义
interface PromptTableProps {loading: boolean;dataSource: PromptResource[];onEdit?: (data?: PromptResource) => void;onDelete: (id: string) => void;onPreview: (data: PromptResource) => void;
}// API响应类型
type UpsertPromptResourceResponseData = {prompt_resource: PromptResource;id: string;
};