feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
export const REAL_DATA_ID = '0';
export const REAL_DATA_MOCKSET = {
id: REAL_DATA_ID,
name: I18n.t('real_data'),
};
// 初始化仅有real_data
export const MOCK_OPTION_LIST = [REAL_DATA_MOCKSET];
export const POLLING_INTERVAL = 10000;
export const DELAY_TIME = 2000;
export const CONNECTOR_ID = '10000010';

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { create } from 'zustand';
import { produce } from 'immer';
import {
type BizCtx,
type MockSet,
type MockSetBinding,
} from '@coze-arch/bot-api/debugger_api';
import { type BasicMockSetInfo } from '@coze-studio/mockset-shared';
import { isCurrent } from '../../util';
export interface EnabledMockSetInfo {
mockSetBinding: MockSetBinding;
mockSetDetail?: MockSet;
}
interface MockInfoStoreState {
bizCtx: BizCtx;
enabledMockSetInfo: Array<EnabledMockSetInfo>;
isPolling: boolean;
isLoading: boolean;
currentMockComp: Array<BasicMockSetInfo>;
timer?: NodeJS.Timeout;
restartTimer?: NodeJS.Timeout;
}
interface MockInfoStoreAction {
setPolling: (polling: boolean) => void;
setLoading: (loading: boolean) => void;
setCurrentBizCtx: (bizCtx: BizCtx) => void;
setEnabledMockSetInfo: (mockSetList?: Array<EnabledMockSetInfo>) => void;
removeMockComp: (mockComp: BasicMockSetInfo) => number;
addMockComp: (mockComp: BasicMockSetInfo) => number;
setTimer: (timer?: NodeJS.Timeout) => void;
setRestartTimer: (timer?: NodeJS.Timeout) => void;
}
export const useMockInfoStore = create<
MockInfoStoreState & MockInfoStoreAction
>((set, get) => ({
bizCtx: {},
enabledMockSetInfo: [],
isPolling: false,
isLoading: false,
currentMockComp: [],
setPolling: polling => {
set({ isPolling: polling });
},
setLoading: loading => {
set({ isLoading: loading });
},
setCurrentBizCtx: bizCtx => {
set({ bizCtx });
},
setEnabledMockSetInfo: enabledMockSetInfo => {
set({ enabledMockSetInfo });
},
addMockComp: mockSetInfo => {
set(
produce<MockInfoStoreState>(s => {
const index = s.currentMockComp.findIndex(item =>
isCurrent(item, mockSetInfo),
);
index <= -1 && s.currentMockComp.push(mockSetInfo);
}),
);
return get().currentMockComp.length;
},
removeMockComp: mockSetInfo => {
set(
produce<MockInfoStoreState>(s => {
const index = s.currentMockComp.findIndex(item =>
isCurrent(item, mockSetInfo),
);
if (index > -1) {
s.currentMockComp.splice(index, 1);
}
}),
);
return get().currentMockComp.length;
},
setTimer: timer => {
set({ timer });
},
setRestartTimer: timer => {
set({ timer });
},
}));

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';
import { nanoid } from 'nanoid';
import axios, { type Canceler } from 'axios';
import { logger } from '@coze-arch/logger';
import {
type BizCtx,
type MockSet,
type MockSetBinding,
} from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import { MockTrafficEnabled } from '../../util/get-mock-set-options';
import { isSameScene } from '../../util';
import { type EnabledMockSetInfo, useMockInfoStore } from './store';
function combineBindMockSetInfo(
mockSetBindingList: Array<MockSetBinding>,
mockSetDetailSet: Record<string, MockSet>,
): Array<EnabledMockSetInfo> {
return mockSetBindingList.map(mockSetInfo => {
const { mockSetID } = mockSetInfo;
const detail = mockSetID ? mockSetDetailSet[mockSetID] : {};
return {
mockSetBinding: mockSetInfo,
mockSetDetail: detail,
};
});
}
// eslint-disable-next-line max-lines-per-function
export const useInitialGetEnabledMockSet = ({
bizCtx,
pollingInterval,
}: {
bizCtx: BizCtx;
pollingInterval?: number;
}) => {
const {
enabledMockSetInfo,
setPolling,
setEnabledMockSetInfo,
bizCtx: currentBizCtx,
setCurrentBizCtx,
addMockComp,
removeMockComp,
isPolling,
setTimer,
timer,
currentMockComp,
setLoading,
isLoading,
restartTimer,
setRestartTimer,
} = useMockInfoStore();
const status = useRef<boolean>(false);
const lastRequestId = useRef(0);
const pollingTurnRef = useRef<string>();
const cancelReq = useRef<Canceler>();
const requestFn = async (curBizCtx: BizCtx) => {
const currentRequestId = ++lastRequestId.current;
const currentPollingTurn = pollingTurnRef.current;
try {
const { mockSetBindings = [], mockSetDetails = {} } =
await debuggerApi.MGetMockSetBinding(
{
bizCtx: curBizCtx,
needMockSetDetail: true,
},
{
headers: {
'rpc-persist-mock-traffic-enable': MockTrafficEnabled.ENABLE,
},
cancelToken: new axios.CancelToken(function executor(c) {
cancelReq.current = c;
}),
},
);
if (
(currentRequestId > 1 && currentRequestId !== lastRequestId.current) ||
!pollingTurnRef.current ||
pollingTurnRef.current !== currentPollingTurn
) {
return;
}
setEnabledMockSetInfo?.(
combineBindMockSetInfo(mockSetBindings, mockSetDetails),
);
return { mockSetBindings, mockSetDetails };
} catch (e) {
if (axios.isCancel(e)) {
logger.info('poll_scene_mockset_canceled');
} else {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'poll_scene_mockset_fail' });
}
}
};
const request = async () => {
try {
const {
trafficCallerID,
connectorID,
connectorUID,
bizSpaceID,
trafficScene,
} = bizCtx;
!status.current && (status.current = true);
setLoading(true);
if (status.current && pollingInterval) {
setPolling(true);
const id = setTimeout(() => {
status.current && request();
}, pollingInterval);
setTimer(id);
}
await requestFn({
trafficCallerID,
connectorID,
connectorUID,
bizSpaceID,
trafficScene,
});
} finally {
setLoading(false);
}
};
// 取消
const cancel = () => {
pollingTurnRef.current = undefined;
cancelReq.current?.();
lastRequestId.current = 0;
cancelRestartTask();
if (timer) {
clearTimeout(timer);
setPolling(false);
setTimer(undefined);
status.current && (status.current = false);
}
};
const cancelRestartTask = () => {
if (restartTimer) {
clearTimeout(restartTimer);
setRestartTimer(undefined);
}
};
const start = async () => {
cancel();
pollingTurnRef.current = nanoid();
await request();
};
useEffect(() => {
if (currentBizCtx && isSameScene(bizCtx, currentBizCtx)) {
return;
}
setCurrentBizCtx(bizCtx);
}, [bizCtx]);
return {
start,
cancel,
isLoading,
data: enabledMockSetInfo,
addMockComp,
removeMockComp,
currentMockComp,
isPolling,
setRestartTimer,
restartTimer,
};
};

View File

@@ -0,0 +1,157 @@
.layout-header {
padding: 16px 24px 24px;
}
.content-title {
max-width: 1160px;
margin: 0 auto 16px;
font-size: 16px;
font-weight: 600;
line-height: 32px;
color: var(--semi-color-text-0);
}
.list-container_scroll {
width: 100%;
max-width: 1160px;
margin-right: auto;
margin-left: auto;
padding-bottom: 10px;
}
.list-container_flexible,
.list-container-no-header_flexible {
width: 100%;
max-width: 1160px;
margin-right: auto;
margin-left: auto;
}
.list-container_flexible {
height: calc(100% - 72px);
}
.list-container-no-header_flexible {
height: calc(100%);
}
/** mock-set-intro */
.mock-set-intro-title {
width: 100%;
margin-top: 2px;
margin-bottom: 4px;
&.mock-set-intro-title_full {
margin-top: 0;
margin-bottom: 0;
}
.mock-set-intro-name {
overflow: hidden;
font-size: 18px;
font-weight: 600;
line-height: 32px;
color: var(--semi-color-text-0);
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-set-intro-name_full {
font-size: 14px;
line-height: 20px;
}
.mock-set-intro-edit {
width: 16px;
height: 16px;
color: var(--semi-color-text-2)
}
.mock-set-intro-edit_full,
.mock-set-intro-edit_full svg {
width: 14px;
height: 14px;
}
}
.mock-set-intro-desc_priority.mock-set-intro-desc {
overflow: hidden;
width: 100%;
font-size: 12px;
line-height: 22px;
color: var(--semi-color-text-1);
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-set-intro-desc_priority.mock-set-intro-desc_full {
line-height: 16px;
}
/** 创建框 **/
.mock-creation-modal {
:global {
.semi-modal-content .semi-modal-body {
/** 保证内部 tooltip 不被遮盖 **/
overflow: unset
}
.semi-modal-footer .semi-button{
margin-left: 8px;
}
}
}
.mock-creation-card {
height: calc(100% - 24px);
}
div.mock-creation-modal-editor {
/** 兼容 modal 在小窗口下 body 高度不生效的问题 */
height: calc(100vh - 316px);
max-height: 500px;
}
.mock-creation-card-editor {
height: calc(100% - 40px - 32px - 48px);
margin-bottom: 40px;
}
.mock-creation-card-operation {
text-align: right;
}
/** mock-data-list **/
.skeleton {
:global {
.semi-skeleton-image {
width: 100%;
height: 100px;
margin-bottom: 12px;
}
}
}
.empty {
justify-content: center;
height: calc(100% - 68px);
padding-bottom: 10%;
:global {
.semi-empty-image svg {
width: 140px;
height: 140px;
}
.semi-empty-description {
font-weight: 600;
}
}
}

View File

@@ -0,0 +1,5 @@
.long-text.long-text-tooltip {
color: var(--semi-color-bg-0);
word-break: break-word;
overflow-wrap: break-word;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { type Ellipsis, type TextProps } from '@coze-arch/bot-semi/Typography';
import { Typography } from '@coze-arch/bot-semi';
import s from './index.module.less';
interface LongTextWithTooltip extends TextProps {
tooltipText?: string;
}
export function LongTextWithTooltip(props: LongTextWithTooltip) {
const { children, ellipsis, tooltipText, ...rest } = props;
const ellipsisConfig: boolean | Ellipsis | undefined =
ellipsis === false
? ellipsis
: {
showTooltip: {
opts: {
content: (
<Typography.Text
className={classNames(s['long-text-tooltip'], s['long-text'])}
onClick={e => e.stopPropagation()}
ellipsis={{ showTooltip: false, rows: 16 }}
>
{tooltipText || props.children}
</Typography.Text>
),
},
},
...(typeof ellipsis !== 'object' ? {} : ellipsis),
};
return (
<Typography.Text ellipsis={ellipsisConfig} {...rest}>
{props.children}
</Typography.Text>
);
}

View File

@@ -0,0 +1,175 @@
.mock-data-content {
overflow: auto;
min-height: 64px;
max-height: 500px;
word-wrap: break-word;
border: 1px var(--semi-color-border) solid;
border-radius: 8px;
:global {
.semi-tree-option-list-block .semi-tree-option-selected {
background-color: transparent;
}
.semi-tree-option-list-block .semi-tree-option:hover {
background-color: transparent;
}
}
}
.mock-data-card-operations {
position: absolute;
top: 16px;
right: 16px;
padding: 4px;
visibility: hidden;
background: #F7F7FA;
border-radius: 8px;
box-shadow: 0 4px 8px rgb(0 0 0 / 20%);
}
.mock-data-card {
position: relative;
margin-bottom: 16px;
&:hover .mock-data-card-operations {
visibility: visible;
}
}
.mock-data-card-edit,
.mock-data-card-delete {
width: 16px;
height: 16px;
}
.mock-data-content-code {
height: 300px;
}
.mock-data-banner {
position: absolute;
bottom: 16px;
left: 20%;
width: 60%;
:global {
.semi-banner-icon {
align-items: center;
}
}
}
.card-item {
position: relative;
display: inline-block;
}
.card-item_deleted {
text-decoration: line-through;
}
.card-item-text {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #1D1C23;
word-wrap: break-word;
}
.card-item-text_required,
.card-item-text_highlighted {
color: var(--semi-color-danger);
}
.card-item-text_primary {
font-weight: 600;
}
.card-item-text_invalid {
color: var(--semi-color-text-3);
}
.card-item-text_stretched {
overflow: hidden;
flex-grow: 1;
}
.card-item-text_wrap {
white-space: normal;
}
.card-item-tag {
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: #6B6B75;
word-wrap: break-word;
background: rgb(46 46 56 / 8%);
border-radius: 6px;
}
.card-branches {
pointer-events: none;
position: absolute;
top: 0;
left: 8px;
width: 100%;
height: 100%;
}
.card-branch-v {
display: inline-block;
width: 13px;
height: 100%;
margin-left: 6px;
vertical-align: top;
border-left: 1px solid transparent;
}
.card-branch-v_visible {
border-color: #C6C6CD;
}
.card-branch-v_half {
height: 10px;
}
.card-branch-h {
display: inline-block;
width: 6px;
height: 15px;
margin-left: -13px;
vertical-align: top;
border-color: #C6C6CD;
border-style: solid;
border-width: 0 0 1px 1px;
border-radius: 0 0 0 4px;
}
.card-branch-h_long {
width: 20px;
}
.card-non-tree-container {
padding: 8px 30px;
}

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
import { Space, Tree, UIIconButton } from '@coze-arch/bot-semi';
import { IconEditNew, IconDeleteOutline } from '@coze-arch/bot-icons';
import { type infra, type MockRule } from '@coze-arch/bot-api/debugger_api';
import { ROOT_KEY } from '@coze-studio/mockset-shared';
import { transUpperCase } from '../../util/utils';
import {
type MockDataInfo,
MockDataStatus,
MockDataValueType,
type MockDataWithStatus,
} from '../../util/typings';
import { useTransSchema } from '../../hook/use-trans-schema';
import { BranchType, useGenTreeBranch } from '../../hook/use-gen-tree-branch';
import s from './index.module.less';
interface MockDataCardProps {
mock?: MockRule;
schema?: string;
readOnly?: boolean;
className?: string;
onEdit?: (params: MockDataInfo) => void;
onRemove?: (params: MockDataInfo) => void;
bizCtx: infra.BizCtx;
}
/** mock data 展示卡片 */
export function MockDataCard({
mock,
readOnly,
schema,
onEdit,
onRemove,
}: MockDataCardProps) {
const { formattedResultExample, incompatible, mergedResult } = useTransSchema(
schema,
mock?.responseExpect?.responseExpectRule,
);
const { branchInfo, prunedData } = useGenTreeBranch(mergedResult);
const deleteHandler = () => {
onRemove?.({
schema,
mock,
});
};
const editHandler = () => {
onEdit?.({
schema,
mock,
mergedResultExample: formattedResultExample,
incompatible,
});
};
const renderBranches = (item: MockDataWithStatus, isLevel0Item: boolean) => {
const branchThisRow = item?.key ? branchInfo[item.key] : undefined;
return (
<span className={s['card-branches']}>
{branchThisRow?.v.map((type, index) => (
<span
key={index}
className={classNames(
s['card-branch-v'],
type !== BranchType.NONE ? s['card-branch-v_visible'] : '',
type === BranchType.HALF ? s['card-branch-v_half'] : '',
)}
/>
))}
{!isLevel0Item ? (
<span
className={classNames(
s['card-branch-h'],
item?.children ? '' : s['card-branch-h_long'],
)}
/>
) : (
''
)}
</span>
);
};
const renderFieldContent = (item: MockDataWithStatus) => {
const isRemoved = item?.status === MockDataStatus.REMOVED;
if (item?.status === MockDataStatus.ADDED) {
return (
<MockDataValueSpan
val={
item.isRequired
? I18n.t('mockset_field_is_required', { field: item?.label })
: undefined
}
className={classNames(
'ms-[8px]',
item.isRequired ? s['card-item-text_highlighted'] : '',
)}
/>
);
}
return (item?.type === MockDataValueType.ARRAY ||
item?.type === MockDataValueType.OBJECT) &&
item?.children ? (
''
) : (
<MockDataValueSpan
val={item?.displayValue}
className={classNames(
'ms-[8px]',
isRemoved ? s['card-item-text_highlighted'] : '',
)}
/>
);
};
// @ts-expect-error -- linter-disable-autofix
const renderLabel = (_, node?: TreeNodeData) => {
const item = node as MockDataWithStatus | undefined;
const isLevel0Item = `${ROOT_KEY}-${item?.label}` === item?.key;
const isRemoved = item?.status === MockDataStatus.REMOVED;
const isAdded = item?.status === MockDataStatus.ADDED && item.isRequired;
return item ? (
<>
{renderBranches(item, isLevel0Item)}
<span
className={classNames(
s['card-item'],
isRemoved || isAdded ? s['card-item-text_highlighted'] : '',
isRemoved ? s['card-item_deleted'] : '',
)}
>
<span
className={classNames(
s['card-item-text'],
isLevel0Item ? s['card-item-text_primary'] : '',
isRemoved || isAdded ? s['card-item-text_highlighted'] : '',
)}
>
{item?.label}
</span>
{item?.isRequired ? (
<span
className={classNames(
s['card-item-text'],
s['card-item-text_required'],
)}
>
*
</span>
) : null}
{!isRemoved && !isAdded ? (
<span className={classNames(s['card-item-tag'], 'ms-[8px]')}>
{transUpperCase(item?.type)}
{item?.type === MockDataValueType.ARRAY
? `<${transUpperCase(item?.childrenType)}>`
: ''}
</span>
) : null}
{renderFieldContent(item)}
</span>
</>
) : (
''
);
};
const renderData = () => {
if (
prunedData?.type === MockDataValueType.ARRAY ||
prunedData?.type === MockDataValueType.OBJECT
) {
if (prunedData.children?.length) {
return (
<Tree
defaultExpandAll
treeData={prunedData.children}
renderLabel={renderLabel}
/>
);
} else {
return (
<div className={s['card-non-tree-container']}>
<span
className={classNames(
s['card-item-text'],
s['card-item-text_invalid'],
)}
>
Empty
</span>
</div>
);
}
} else {
return (
<div className={s['card-non-tree-container']}>
<MockDataValueSpan val={prunedData?.displayValue} />
</div>
);
}
};
return mock?.responseExpect?.responseExpectRule ? (
<div className={s['mock-data-card']}>
<div className={s['mock-data-content']}>{renderData()}</div>
{!readOnly ? (
<Space className={s['mock-data-card-operations']} spacing={12}>
<UIIconButton
icon={<IconEditNew className={s['mock-data-card-edit']} />}
size="small"
theme="borderless"
onClick={editHandler}
/>
<UIIconButton
icon={<IconDeleteOutline className={s['mock-data-card-edit']} />}
size="small"
theme="borderless"
onClick={deleteHandler}
/>
</Space>
) : null}
</div>
) : null;
}
const MockDataValueSpan = (props: { val?: string; className?: string }) =>
props.val ? (
<span className={classNames(props.className, s['card-item-text'])}>
{props.val}
</span>
) : (
<span
className={classNames(
props.className,
s['card-item-text'],
s['card-item-text_invalid'],
)}
>
Undefined
</span>
);

View File

@@ -0,0 +1,279 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { useParams } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import {
type PluginMockSetCommonParams,
type PluginMockDataGenerateMode,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {
Space,
Toast,
UIButton,
UIModal,
Typography,
Divider,
} from '@coze-arch/bot-semi';
import { SpaceType } from '@coze-arch/bot-api/playground_api';
import { type mockset, type infra } from '@coze-arch/bot-api/debugger_api';
import {
calcStringSize,
type MockDataInfo,
MAX_SUBMIT_LENGTH,
getEnvironment,
} from '@coze-studio/mockset-shared';
import {
MocksetEditor,
type EditorAreaActions,
} from '@coze-studio/mockset-editor-adapter';
import {
PRE_DEFINED_NO_EMPTY_KEY,
useTransSchema,
} from '../hook/use-trans-schema';
import { useSaveMockData } from '../hook/use-save-mock-data';
import s from './index.module.less';
export enum CreationMode {
/** 弹窗形式 */
MODAL = 'modal',
/** 嵌入页面 */
CARD = 'card',
}
interface MockDataCreateCardProps {
mode: CreationMode;
mockInfo?: MockDataInfo;
// mode 为 modal 时生效
visible?: boolean;
// mode 为 modal 时生效
onCancel?: () => void;
onSuccess: (data?: mockset.MockRule[]) => void;
bizCtx: infra.BizCtx;
forceGenerate?: {
mode: PluginMockDataGenerateMode;
count: number;
};
}
/** 创建or编辑 mock data - */
export function MockDataCreateCard({
mode,
mockInfo,
visible,
onCancel,
onSuccess,
bizCtx,
forceGenerate,
}: MockDataCreateCardProps) {
const { schema } = mockInfo || {};
const editorsRef = useRef<EditorAreaActions>(null);
const [disableSubmit, setDisableSubmit] = useState(false);
const [disableSubmitWhenGenerating, setDisableSubmitWhenGenerating] =
useState(false);
const { testValueValid, formattedResultExample: initialExample } =
useTransSchema(schema);
const { mock_set_id, tool_id } = useParams<DynamicParams>();
// space信息
const spaceType = useSpaceStore(store => store.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const basicParams: PluginMockSetCommonParams = {
environment: getEnvironment(),
workspace_id: bizCtx.bizSpaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: tool_id || '',
mock_set_id: mock_set_id || '',
};
const { save, loading } = useSaveMockData({
mockSetId: mock_set_id,
basicParams,
bizCtx,
onSuccess,
});
const confirmHandler = () => {
const values = editorsRef.current?.getValue();
if (!values) {
return;
}
for (const value of values) {
if (!value) {
Toast.error('no data');
return;
}
if (calcStringSize(value) > MAX_SUBMIT_LENGTH) {
Toast.error({
content: I18n.t('mockset_toast_data_size_limit'),
showClose: false,
});
return;
}
if (!testValueValid(value)) {
Toast.error({
content: I18n.t('mockdata_field_empty', {
fieldName: PRE_DEFINED_NO_EMPTY_KEY,
}),
showClose: false,
});
return;
}
}
const mockDataId = String(mockInfo?.mock?.id || 0);
save(values, mockDataId);
};
const validateHandler = (isValid: boolean[]) => {
setDisableSubmit(isValid.some(v => !v));
};
useEffect(() => {
// @ts-expect-error -- linter-disable-autofix
const unloadHandler = e => {
const info = I18n.t('mockset_tip_data_will_lose');
e.preventDefault();
e.returnValue = info;
return info;
};
if (
(mode === CreationMode.MODAL && visible) ||
mode === CreationMode.CARD
) {
window.addEventListener('beforeunload', unloadHandler);
}
return () => {
window.removeEventListener('beforeunload', unloadHandler);
};
}, [mode, visible]);
useEffect(() => {
if (forceGenerate) {
editorsRef.current?.forceStartGenerate?.(
forceGenerate.mode,
forceGenerate.count,
);
}
}, []);
return mode === CreationMode.MODAL ? (
<UIModal
visible={visible}
title={
mockInfo?.mock ? I18n.t('edit_mock_data') : I18n.t('add_mock_data')
}
className={s['mock-creation-modal']}
keepDOM={false}
footer={
<>
<span className="mr-[8px]">{I18n.t('mockset_save_description')}</span>
<Divider layout="vertical" margin="0px" />
<UIButton type={'tertiary'} key="Cancel" onClick={onCancel}>
{I18n.t('cancel')}
</UIButton>
<UIButton
type={'primary'}
theme={'solid'}
key="Confirm"
onClick={confirmHandler}
loading={loading}
disabled={disableSubmit || disableSubmitWhenGenerating}
>
{I18n.t('confirm')}
</UIButton>
</>
}
width={1000}
maskClosable={false}
onCancel={onCancel}
>
<MocksetEditor
className={s['mock-creation-modal-editor']}
mockInfo={{
mergedResultExample: initialExample,
...mockInfo,
}}
readOnly={false}
ref={editorsRef}
onValidate={validateHandler}
environment={{
spaceId: bizCtx.bizSpaceID,
mockSetId: mock_set_id,
basicParams,
}}
isCreateScene={!mockInfo?.mock}
onGenerationStatusChange={isGenerating =>
setDisableSubmitWhenGenerating(isGenerating)
}
/>
</UIModal>
) : (
<div className={s['mock-creation-card']}>
<div className={s['mock-creation-card-editor']}>
<MocksetEditor
mockInfo={{
mergedResultExample: initialExample,
...mockInfo,
}}
ref={editorsRef}
onValidate={validateHandler}
environment={{
spaceId: bizCtx.bizSpaceID,
mockSetId: mock_set_id,
basicParams,
}}
isCreateScene={!mockInfo?.mock}
onGenerationStatusChange={isGenerating =>
setDisableSubmitWhenGenerating(isGenerating)
}
/>
</div>
<div className={s['mock-creation-card-operation']}>
<Space>
<Typography.Text>
{I18n.t('mockset_save_description')}
</Typography.Text>
<Divider layout="vertical" margin="0px" />
<UIButton
type={'primary'}
theme={'solid'}
onClick={confirmHandler}
loading={loading}
disabled={disableSubmit || disableSubmitWhenGenerating}
>
{I18n.t('mockset_save')}
</UIButton>
</Space>
</div>
</div>
);
}

View File

@@ -0,0 +1,370 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { useParams } from 'react-router-dom';
import {
useState,
useEffect,
forwardRef,
type ForwardedRef,
useImperativeHandle,
} from 'react';
import classNames from 'classnames';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import {
EVENT_NAMES,
sendTeaEvent,
type ParamsTypeDefine,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { Empty, Spin, UIModal } from '@coze-arch/bot-semi';
import { PageType, usePageJumpResponse } from '@coze-arch/bot-hooks';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import { infra, type MockRule } from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from '@douyinfe/semi-illustrations';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { getEnvironment } from '@coze-studio/mockset-shared';
import { type MockDataInfo } from '../util/typings';
import { SpaceHolder } from './space-holder';
import { CreationMode, MockDataCreateCard } from './mock-data-create-card';
import { MockDataCard } from './mock-data-card';
import s from './index.module.less';
enum RuleActions {
CREATE,
EDIT,
DELETE,
}
interface MockDataListProps {
mockSetID?: string;
perm: {
readOnly: boolean;
uninitialized: boolean;
};
toolSchema: string;
bizCtx: infra.BizCtx;
onListUpdate?: (length: number, needScrollToTop?: boolean) => void;
}
export interface MockDataListActions {
update: () => void;
create: () => void;
}
export const MockDataList = forwardRef(
(
{ mockSetID, perm, toolSchema, bizCtx, onListUpdate }: MockDataListProps,
ref: ForwardedRef<MockDataListActions>,
) => {
// loading
const [loading, setLoading] = useState(false);
// mock data list
const [mockDataList, setMockDataList] = useState<MockRule[]>([]);
// modal visible
const [createModalVisible, setCreateModalVisible] =
useState<boolean>(false);
// delete modal visible
const [deleteModalVisible, setDeleteModalVisible] =
useState<boolean>(false);
const [deleting, setDeleting] = useState<boolean>(false);
// 当前选中状态
const [currentSelect, setCurrentSelect] = useState<
MockDataInfo | undefined
>();
const routeResponse = usePageJumpResponse(PageType.PLUGIN_MOCK_DATA);
const { mock_set_id, space_id, tool_id } = useParams<DynamicParams>();
// space信息
const spaceType = useSpaceStore(store => store.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const clickItemUpdateEntryHandler = (params: MockDataInfo) => {
setCurrentSelect(params);
setCreateModalVisible(true);
};
const clickItemDeleteEntryHandler = (params: MockDataInfo) => {
setCurrentSelect(params);
setDeleteModalVisible(true);
};
// 获取当前 mock set 下的 mock data
const getMockData = async (needScrollToTop?: boolean) => {
try {
setLoading(true);
const data = await debuggerApi.MGetMockRule({
bizCtx,
mockSetID: mock_set_id,
orderBy: infra.OrderBy.UpdateTime,
desc: true,
});
setMockDataList(data.mockRules || []);
onListUpdate?.(data.mockRules?.length || 0, needScrollToTop);
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error, eventName: 'get_mock_data_fail' });
} finally {
setLoading(false);
}
};
const deleteConfirmHandler = async () => {
const { mock } = currentSelect || {};
if (!mock) {
return;
}
const basicParams: Omit<
ParamsTypeDefine[EVENT_NAMES.del_mock_front],
'status'
> = {
environment: getEnvironment(),
workspace_id: space_id || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: tool_id || '',
mock_set_id: mock_set_id || '',
mock_counts: 1,
};
try {
setDeleting(true);
await debuggerApi.DeleteMockRule({
bizCtx,
id: String(mock.id),
});
updateList(mock, RuleActions.DELETE);
setCurrentSelect(undefined);
setDeleteModalVisible(false);
sendTeaEvent(EVENT_NAMES.del_mock_front, {
...basicParams,
status: 0,
});
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error, eventName: 'delete_mock_fail' });
sendTeaEvent(EVENT_NAMES.del_mock_front, {
...basicParams,
status: 1,
// @ts-expect-error -- linter-disable-autofix
error,
});
} finally {
setDeleting(false);
}
};
// 前端更新
const updateList = (data: MockRule, action: RuleActions) => {
let len = 0;
if (action === RuleActions.CREATE) {
// 创建场景直接 force update
getMockData(true);
} else if (action === RuleActions.DELETE) {
len = mockDataList.length - 1;
setMockDataList(cur => {
const index = cur.findIndex(item => item.id === data?.id);
if (index !== -1) {
cur.splice(index, 1);
}
len = cur.length;
return [...cur];
});
onListUpdate?.(len);
} else {
len = mockDataList.length;
setMockDataList(cur => {
const index = cur.findIndex(item => item.id === data?.id);
if (index !== -1) {
cur.splice(index, 1);
cur.unshift(data);
}
len = cur.length;
return [...cur];
});
onListUpdate?.(len, true);
}
};
useImperativeHandle(ref, () => ({
update: getMockData,
create: () => {
setCurrentSelect(undefined);
setCreateModalVisible(true);
},
}));
useEffect(() => {
getMockData();
}, [mockSetID]);
useEffect(() => {
if (routeResponse?.generationMode) {
// 清除跳转参数
const state = {
...history.state,
usr: { ...(history.state.usr || {}), generationMode: undefined },
};
history.replaceState(state, '');
}
}, []);
const renderList = () => {
if (loading || perm.uninitialized) {
return (
<div className={s['list-container-no-header_flexible']}>
<Spin
size="large"
spinning
style={{ height: '80%', width: '100%' }}
/>
</div>
);
}
if (perm.readOnly && mockDataList.length === 0) {
return (
<>
<h1 className={classNames(s['content-title'])}>
{I18n.t('mockset_data')}
</h1>
<div className={s['list-container_flexible']}>
<Empty
className={s.empty}
image={<IllustrationNoContent />}
darkModeImage={<IllustrationNoContentDark />}
description={I18n.t('no_mock_yet')}
/>
</div>
</>
);
}
if (!perm.readOnly && mockDataList.length === 0) {
return (
<div className={s['list-container-no-header_flexible']}>
<MockDataCreateCard
mode={CreationMode.CARD}
mockInfo={{
schema: toolSchema,
}}
onSuccess={data => {
data && updateList(data[0], RuleActions.CREATE);
}}
bizCtx={bizCtx}
forceGenerate={
routeResponse?.generationMode
? {
mode: routeResponse.generationMode,
count: 1,
}
: undefined
}
/>
</div>
);
}
return (
<>
<h1 className={classNames(s['content-title'])}>
{I18n.t('mockset_data')}
</h1>
<div className={s['list-container_scroll']}>
{mockDataList.map(item => (
<MockDataCard
readOnly={perm.readOnly}
key={item.id}
mock={item}
schema={toolSchema}
onEdit={params => clickItemUpdateEntryHandler(params)}
onRemove={params => clickItemDeleteEntryHandler(params)}
bizCtx={bizCtx}
/>
))}
</div>
</>
);
};
return (
<>
<SpaceHolder height={24} />
{renderList()}
<MockDataCreateCard
mode={CreationMode.MODAL}
mockInfo={
currentSelect || {
schema: toolSchema,
}
}
visible={createModalVisible}
onCancel={() => {
setCurrentSelect(undefined);
setCreateModalVisible(false);
}}
onSuccess={data => {
setCurrentSelect(undefined);
setCreateModalVisible(false);
data?.[0] &&
updateList(
data[0],
currentSelect ? RuleActions.EDIT : RuleActions.CREATE,
);
}}
bizCtx={bizCtx}
/>
<UIModal
type="info"
icon={
<IconAlertCircle
size="extra-large"
className="inline-flex text-[#FF2710]"
/>
}
title={I18n.t('delete_mock_data')}
visible={deleteModalVisible}
onCancel={() => {
setCurrentSelect(undefined);
setDeleteModalVisible(false);
}}
okText={I18n.t('confirm')}
cancelText={I18n.t('cancel')}
confirmLoading={deleting}
onOk={() => deleteConfirmHandler()}
okType="danger"
>
{I18n.t('operation_cannot_be_reversed')}
</UIModal>
</>
);
},
);

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { UIBreadcrumb } from '@coze-studio/components';
import { logger } from '@coze-arch/logger';
import { UILayout } from '@coze-arch/bot-semi';
import { usePageJumpResponse, PageType } from '@coze-arch/bot-hooks';
import {
type PluginMetaInfo,
type PluginAPIInfo,
} from '@coze-arch/bot-api/developer_api';
import { type MockSet } from '@coze-arch/bot-api/debugger_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import s from './index.module.less';
interface MockSetPageBreadcrumbProps {
pluginId?: string;
apiInfo?: PluginAPIInfo;
mockSetInfo?: MockSet;
}
export function MockSetPageBreadcrumb({
pluginId,
apiInfo,
mockSetInfo,
}: MockSetPageBreadcrumbProps) {
const routeResponse = usePageJumpResponse(PageType.PLUGIN_MOCK_DATA);
// 插件详情
const [pluginInfo, setPluginInfo] = useState<PluginMetaInfo>({
name: routeResponse?.pluginName,
});
// 获取当前 plugin 信息
const getPluginInfo = async () => {
try {
const res = await DeveloperApi.GetPluginInfo(
{
plugin_id: pluginId || '',
},
{ __disableErrorToast: true },
);
if (res?.code === 0) {
setPluginInfo(res.meta_info || {});
}
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error, eventName: 'get_plugin_info_fail' });
}
};
useEffect(() => {
getPluginInfo();
}, [pluginId]);
return (
<UILayout.Header
className={s['layout-header']}
breadcrumb={
<UIBreadcrumb
showTooltip={{ width: '300px' }}
pluginInfo={pluginInfo}
pluginToolInfo={apiInfo}
mockSetInfo={mockSetInfo}
compact={false}
/>
}
/>
);
}

View File

@@ -0,0 +1,134 @@
.select-container {
display: inline-block;
max-width: 100%;
padding: 4px 6px;
background-color: transparent;
border: none;
border-radius: 6px;
&.switch-disabled {
* {
/* stylelint-disable-next-line declaration-no-important */
color: #1D1C23CC !important;
}
}
div {
span {
&[aria-label="small_triangle_down"] {
color: #1D1C23CC;
}
}
}
}
.option-list {
min-width: 234px;
max-width: 336px;
padding: 4px;
font-size: 12px;
:global {
.semi-typography {
color: #1D1C23;
}
.semi-select-option-list {
min-height: 40px;
}
.semi-select-loading-wrapper {
display: flex;
justify-content: center;
}
}
}
.item-selected {
overflow: hidden;
width: 100%;
height: 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
text-overflow: ellipsis;
}
.select-label {
svg {
>path {
/* stylelint-disable-next-line declaration-no-important */
fill: currentcolor !important;
fill-opacity: 1;
}
}
}
.custom-option-render-focused {
cursor: pointer;
background-color: rgb(46 46 56 / 8%);
}
.custom-option-render-selected {
font-weight: 600;
}
.custom-option-render-disabled {
cursor: not-allowed;
:global {
.semi-typography {
color: #1D1C2359;
}
}
}
.select-option-container {
display: flex;
align-items: center;
height: 32px;
padding: 8px 8px 8px 16px;
border-radius: 4px;
}
.divider {
height: 1px;
margin: 4px 0;
background: var(--semi-color-border);
}
.create-container {
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
padding: 0 16px;
color: #4D53E8;
}
.load-more {
display: flex;
align-items: center;
justify-content: center;
color: #4D53E8;
.spin-icon {
margin-right: 8px;
}
}

View File

@@ -0,0 +1,579 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-lines */
/* eslint-disable max-lines-per-function */
import {
useState,
useEffect,
useRef,
forwardRef,
type ForwardedRef,
useImperativeHandle,
} from 'react';
import classNames from 'classnames';
import {
useInViewport,
useInfiniteScroll,
useRequest,
useUnmount,
} from 'ahooks';
import { userStoreService } from '@coze-studio/user-store';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
type ParamsTypeDefine,
sendTeaEvent,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
// eslint-disable-next-line @coze-arch/no-pkg-dir-import
import { type SemiSelectActions } from '@coze-arch/bot-semi/src/components/ui-select';
import { Spin, Tooltip, UIButton, UISelect } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import {
TrafficScene,
infra,
type BizCtx,
type MockSet,
} from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import { IconTick, IconUploadError } from '@douyinfe/semi-icons';
import {
type MockSelectOptionProps,
type MockSelectRenderOptionProps,
type MockSetSelectProps,
MockSetStatus,
getEnvironment,
getMockSubjectInfo,
getPluginInfo,
} from '@coze-studio/mockset-shared';
import {
builtinSuccessCallback,
MockSetEditModal,
} from '@coze-studio/mockset-edit-modal-adapter';
import { MockSetDeleteModal } from '../mockset-delete-modal';
import { useInitialGetEnabledMockSet } from '../hooks/use-get-mockset';
import {
CONNECTOR_ID,
DELAY_TIME,
MOCK_OPTION_LIST,
POLLING_INTERVAL,
REAL_DATA_ID,
REAL_DATA_MOCKSET,
} from '../const';
import { getUsedScene, isCurrent, isRealData } from '../../util';
import { MockSetItem } from './option-item';
import styles from './index.module.less';
export function getMockSetOption(mockSet: MockSet): MockSelectOptionProps {
const isInValid =
!isRealData(mockSet) &&
(mockSet?.schemaIncompatible || !mockSet?.mockRuleQuantity);
return {
value: mockSet?.id || '',
label: (
<Tooltip
key={mockSet?.id}
content={I18n.t('mockset_invaild_tip', { MockSetName: mockSet.name })}
style={{ display: isInValid ? 'block' : 'none' }}
>
<span
className={classNames(
'flex items-center w-[100%] min-w-0',
styles['select-label'],
)}
>
{isInValid ? (
<IconUploadError
style={{
verticalAlign: 'middle',
marginRight: 2,
color: '#FF8500',
}}
/>
) : null}
<span
className={classNames(
'flex-1 min-w-0 overflow-hidden text-ellipsis',
isInValid ? 'text-[#1D1C2359]' : 'text-[#1D1C23CC]',
)}
>
{mockSet?.name || ''}
</span>
</span>
</Tooltip>
),
disabled: isInValid,
detail: mockSet,
};
}
export function getMockSetOptionList(
mockSets: MockSet[],
): Array<MockSelectOptionProps> {
return mockSets.map(mockSet => getMockSetOption(mockSet));
}
export interface MockSetSelectActions {
handleParentNodeDelete: () => void;
}
const MockSetSelectComp = (
{
bindSubjectInfo: mockSubjectInfo,
bizCtx: bizSceneCtx,
className,
style: baseStyle,
readonly,
}: MockSetSelectProps,
ref: ForwardedRef<MockSetSelectActions>,
) => {
const { detail: subjectDetail, ...bindSubjectInfo } = mockSubjectInfo;
const { spaceID, toolID, pluginID } = getPluginInfo(
bizSceneCtx,
bindSubjectInfo,
);
const uid = userStoreService.useUserInfo()?.user_id_str;
const spaceType = useSpaceStore(s => s.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const bizCtx: BizCtx = {
...bizSceneCtx,
connectorUID: uid,
connectorID: CONNECTOR_ID, // 业务线为Coze
};
const { jump } = usePageJumpService();
const [selectedMockSet, setSelectedMockSet] =
useState<MockSet>(REAL_DATA_MOCKSET);
const selectedValue = getMockSetOption(selectedMockSet);
const [optionList, setOptionList] = useState<Array<MockSelectOptionProps>>(
getMockSetOptionList(MOCK_OPTION_LIST),
);
const [showCreateModal, setShowCreateModal] = useState(false);
const [deleteMockSet, setDeleteMockSet] = useState<MockSet | undefined>();
const preSelectionRef = useRef<MockSet>(REAL_DATA_MOCKSET);
const selectionDomRef = useRef<HTMLDivElement>(null);
const selectionRef = useRef<SemiSelectActions>(null);
const [inViewPort] = useInViewport(selectionDomRef);
const {
data: enabledMockSetInfo,
addMockComp,
removeMockComp,
start,
cancel,
setRestartTimer,
} = useInitialGetEnabledMockSet({
bizCtx,
pollingInterval: POLLING_INTERVAL,
});
const { runAsync: changeMockSet, loading: changeMockSetLoading } = useRequest(
async (mockSet: MockSet, isBinding = true) => {
const basicParams: ParamsTypeDefine[EVENT_NAMES.use_mockset_front] = {
environment: getEnvironment(),
workspace_id: spaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: toolID || '',
status: 1,
mock_set_id: (mockSet.id as string) || '',
where: getUsedScene(bizCtx.trafficScene),
};
try {
await debuggerApi.BindMockSet({
mockSetID: isBinding ? mockSet.id : '0',
bizCtx,
mockSubject: bindSubjectInfo,
});
isBinding &&
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
...basicParams,
status: 0,
});
} catch (e) {
setSelectedMockSet(preSelectionRef.current);
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'change_mockset_fail' });
isBinding &&
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
...basicParams,
status: 1,
// @ts-expect-error -- linter-disable-autofix
error: e?.msg as string,
});
}
},
{
manual: true,
},
);
const handleChange = async (obj?: MockSelectOptionProps) => {
cancel();
preSelectionRef.current = selectedMockSet;
setSelectedMockSet((obj as MockSelectOptionProps)?.detail || {});
await changeMockSet((obj as MockSelectOptionProps)?.detail || {});
const restartTimerId = setTimeout(() => {
start();
}, DELAY_TIME);
setRestartTimer(restartTimerId);
};
const {
reload: fetchOptionList,
loadMore,
loading,
loadingMore,
data: optionListData,
} = useInfiniteScroll(
async d => {
try {
const res = await debuggerApi.MGetMockSet({
bizCtx,
// @ts-expect-error -- linter-disable-autofix
mockSubject: getMockSubjectInfo(bizCtx, mockSubjectInfo),
pageToken: d?.pageToken,
orderBy: infra.OrderBy.UpdateTime,
desc: true,
});
const mockSetList = getMockSetOptionList(res?.mockSets || []);
return {
list: mockSetList || [],
pageToken: res?.pageToken,
hasMore: res?.hasMore ?? true,
schema: res?.schema,
count: res?.count,
};
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'mockset_list_fetch_fail' });
return {
list: [],
pageToken: d?.pageToken,
hasMore: d?.hasMore,
};
}
},
{
manual: true,
},
);
useImperativeHandle(ref, () => ({
handleParentNodeDelete: () => {
changeMockSet(selectedMockSet, false);
},
}));
useEffect(() => {
const newOptionList = [
getMockSetOption(REAL_DATA_MOCKSET),
...(optionListData?.list || []),
];
setOptionList(newOptionList);
}, [optionListData]);
useEffect(() => {
const mockSetInfo = enabledMockSetInfo.find(mockInfo =>
isCurrent(
{
bizCtx: mockInfo?.mockSetBinding?.bizCtx || {},
bindSubjectInfo: mockInfo?.mockSetBinding?.mockSubject || {},
},
{
bizCtx,
bindSubjectInfo,
},
),
);
if (changeMockSetLoading) {
return;
}
if (mockSetInfo?.mockSetDetail) {
setSelectedMockSet(mockSetInfo?.mockSetDetail);
} else {
setSelectedMockSet(REAL_DATA_MOCKSET);
}
}, [enabledMockSetInfo]);
useUnmount(() => {
const length = removeMockComp({ bizCtx, bindSubjectInfo });
if (!length) {
cancel();
}
});
useEffect(() => {
if (inViewPort) {
const length = addMockComp({ bizCtx, bindSubjectInfo });
if (length === 1) {
start();
}
} else {
const length = removeMockComp({ bizCtx, bindSubjectInfo });
if (!length) {
cancel();
}
}
}, [inViewPort]);
const closePanel = () => {
selectionRef?.current?.close();
};
const handleView = (
record?: MockSet,
autoGenerateConfig?: { generateMode: number },
) => {
const { trafficScene } = bizCtx || {};
const { id } = record || {};
if (spaceID && pluginID && toolID && id) {
jump(
trafficScene === TrafficScene.CozeWorkflowDebug
? SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA
: SceneType.BOT__TO__PLUGIN_MOCK_DATA,
{
spaceId: spaceID,
pluginId: pluginID,
toolId: toolID,
toolName: subjectDetail?.name,
mockSetId: String(id),
mockSetName: record?.name,
bizCtx: JSON.stringify(bizCtx),
bindSubjectInfo: JSON.stringify(bindSubjectInfo),
generationMode: autoGenerateConfig?.generateMode,
},
);
}
};
const renderCreateMockSet = () => (
<>
<div className={styles.divider}></div>
<div
onClick={() => {
setShowCreateModal(true);
closePanel();
}}
className={styles['create-container']}
>
<IconAdd
className="mr-[10px]"
style={{ fontSize: 14, color: '#4D53E8' }}
/>
<span>{I18n.t('create_mockset')}</span>
</div>
</>
);
const renderLoadMore = () =>
loading ||
(optionListData?.list?.length || 0) >=
(optionListData?.count || 0) ? null : (
<div
className={classNames(
styles['select-option-container'],
styles['load-more'],
)}
>
{loadingMore ? (
<>
<Spin wrapperClassName={styles['spin-icon']} />
<span>{I18n.t('Loading')}</span>
</>
) : (
<UIButton
onClick={loadMore}
theme="borderless"
style={{ fontSize: 12 }}
>
{I18n.t('Load More' as any)}
</UIButton>
)}
</div>
);
const renderOptionItem = (renderProps: MockSelectRenderOptionProps) => {
const {
disabled,
selected,
value,
focused,
style,
onMouseEnter,
onClick,
detail,
} = renderProps;
const getTooltipInfo = () => {
if (detail?.schemaIncompatible) {
return I18n.t('tool_updated_check_mockset_compatibility');
} else if ((detail?.mockRuleQuantity || 0) <= 0) {
return I18n.t('mockset_is_empty_add_data_before_use');
}
return '';
};
return (
<Tooltip
zIndex={110}
content={getTooltipInfo()}
visible={disabled && focused}
position="left"
style={{ display: disabled ? 'block' : 'none' }} // visible disabled不生效
>
<div
style={style}
className={classNames(
styles['select-option-container'],
focused && styles['custom-option-render-focused'],
disabled && styles['custom-option-render-disabled'],
selected && styles['custom-option-render-selected'],
)}
onClick={onClick}
onMouseEnter={onMouseEnter}
>
<div className="w-[16px] h-[16px] mr-[8px]">
{selected ? (
<IconTick style={{ fontSize: 14 }} className="text-[#4D53E8]" />
) : (
<div className="w-[16px]"></div>
)}
</div>
{value === REAL_DATA_ID ? (
<span>{I18n.t('real_data')}</span>
) : (
<MockSetItem
status={
detail?.schemaIncompatible
? MockSetStatus.Incompatible
: MockSetStatus.Normal
}
name={detail?.name || ''}
onDelete={() => {
closePanel();
setDeleteMockSet(detail);
}}
onView={() => {
handleView(detail);
}}
disableCreator={isPersonal}
viewOnly={uid !== detail?.creator?.ID}
creatorName={detail?.creator?.name}
className="flex-1 min-w-0"
></MockSetItem>
)}
</div>
</Tooltip>
);
};
return (
<div ref={selectionDomRef} style={baseStyle} className={className}>
<UISelect
zIndex={100}
stopPropagation
disabled={readonly || changeMockSetLoading}
className={classNames(
styles['select-container'],
changeMockSetLoading && styles['switch-disabled'],
)}
ref={selectionRef}
selectedClassname={styles['item-selected']}
optionList={optionList}
dropdownClassName={styles['option-list']}
outerBottomSlot={renderCreateMockSet()}
innerBottomSlot={renderLoadMore()}
onDropdownVisibleChange={(visible: boolean) => {
if (visible) {
fetchOptionList();
} else {
setOptionList([getMockSetOption(REAL_DATA_MOCKSET)]);
}
}}
loading={loading}
renderOptionItem={renderOptionItem}
value={selectedValue}
onChangeWithObject
onChange={async obj => {
await handleChange(obj as unknown as MockSelectOptionProps);
}}
/>
{showCreateModal ? (
<MockSetEditModal
zIndex={9999}
visible={showCreateModal}
onCancel={() => setShowCreateModal(false)}
onSuccess={(info, config) => {
const { id } = info || {};
setShowCreateModal(false);
builtinSuccessCallback(config);
handleView({ id }, config);
}}
initialInfo={{
bizCtx,
bindSubjectInfo,
name: subjectDetail?.name,
}}
needResetPopoverContainer={
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
}
/>
) : null}
{deleteMockSet ? (
<MockSetDeleteModal
zIndex={9999}
visible={!!deleteMockSet}
mockSetInfo={{
detail: deleteMockSet,
ctx: { bizCtx, mockSubjectInfo: bindSubjectInfo },
}}
onSuccess={() => {
deleteMockSet.id === selectedMockSet.id &&
setSelectedMockSet(REAL_DATA_MOCKSET);
setDeleteMockSet(undefined);
cancel();
start();
}}
onCancel={() => setDeleteMockSet(undefined)}
needResetPopoverContainer={
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
}
/>
) : null}
</div>
);
};
export const MockSetSelect = forwardRef(MockSetSelectComp);

View File

@@ -0,0 +1,49 @@
.mock-select-item {
display: flex;
align-items: center;
justify-content: space-between;
width: '100%';
}
.mock-main-info {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
.status-icon {
margin-right: 4px;
color: #FF8500;
}
.mock-name {
font-size: 12px;
}
}
.mock-extra-info {
display: flex;
align-items: center;
justify-content: flex-end;
width: 64px;
margin-left: 16px;
.creator-name {
font-size: 12px;
color: #1D1C2359;
}
.operation-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-end;
width: 40px;
font-size: 14px;
color: #1D1C2399;
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type CSSProperties } from 'react';
import classNames from 'classnames';
import { Typography, UIIconButton } from '@coze-arch/bot-semi';
import { IconDeleteOutline, IconEdit } from '@coze-arch/bot-icons';
import { IconEyeOpened, IconUploadError } from '@douyinfe/semi-icons';
import { MockSetStatus } from '@coze-studio/mockset-shared';
import styles from './option-item.module.less';
export interface MockSetItemProps {
name: string;
onDelete?: () => void;
onView?: () => void;
status?: MockSetStatus;
creatorName?: string;
viewOnly?: boolean;
disableCreator?: boolean;
className?: string;
style?: CSSProperties;
}
export const MockSetItem = ({
name,
onDelete,
onView,
status = MockSetStatus.Normal,
creatorName,
viewOnly,
disableCreator,
className,
style,
}: MockSetItemProps) => {
const [isHover, setIsHover] = useState(false);
const renderExtraInfo = () => {
if (isHover) {
return (
<div
className={styles['operation-icon']}
onClick={e => e.stopPropagation()}
>
{viewOnly ? (
<UIIconButton onClick={onView} icon={<IconEyeOpened />} />
) : (
<>
<UIIconButton
onClick={onView}
icon={<IconEdit />}
wrapperClass="mr-[4px]"
/>
<UIIconButton onClick={onDelete} icon={<IconDeleteOutline />} />
</>
)}
</div>
);
}
return disableCreator ? null : (
<Typography.Text
ellipsis={{
showTooltip: {
opts: { content: creatorName },
},
}}
className={styles['creator-name']}
>
{creatorName}
</Typography.Text>
);
};
return (
<div
className={classNames(styles['mock-select-item'], className)}
style={style}
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => setIsHover(false)}
>
<span className={styles['mock-main-info']}>
{status !== MockSetStatus.Normal && (
<IconUploadError className={styles['status-icon']} />
)}
<Typography.Text ellipsis={{}} className={styles['mock-name']}>
{name}
</Typography.Text>
</span>
<div className={styles['mock-extra-info']}>{renderExtraInfo()}</div>
</div>
);
};

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import classNames from 'classnames';
import { Space, UIIconButton } from '@coze-arch/bot-semi';
import { IconEditNew } from '@coze-arch/bot-icons';
import { type infra, type MockSet } from '@coze-arch/bot-api/debugger_api';
import { MockSetEditModal } from '@coze-studio/mockset-edit-modal-adapter';
import { LongTextWithTooltip } from './long-text-with-tooltip';
import s from './index.module.less';
interface MockSetIntroProps {
isFullHeader: boolean;
readOnly?: boolean;
mockSetInfo: MockSet;
onUpdateMockSetInfo?: (mockSetInfo?: MockSet) => void;
bizCtx: infra.BizCtx;
}
const GAP_2 = 2;
const GAP_4 = 4;
export function MockSetIntro({
isFullHeader = true,
readOnly = true,
mockSetInfo,
onUpdateMockSetInfo,
bizCtx,
}: MockSetIntroProps) {
const [showEditModal, setShowEditModal] = useState<boolean>(false);
const editHandler = (info?: MockSet) => {
onUpdateMockSetInfo?.(info);
setShowEditModal(false);
};
return (
<>
<Space
spacing={isFullHeader ? GAP_2 : GAP_4}
className={classNames(
s['mock-set-intro-title'],
isFullHeader ? s['mock-set-intro-title_full'] : '',
)}
>
<LongTextWithTooltip
className={classNames(
s['mock-set-intro-name'],
isFullHeader ? s['mock-set-intro-name_full'] : '',
)}
>
{mockSetInfo.name}
</LongTextWithTooltip>
{!readOnly && mockSetInfo.name ? (
<UIIconButton
icon={
<IconEditNew
className={classNames(
s['mock-set-intro-edit'],
isFullHeader ? s['mock-set-intro-edit_full'] : '',
)}
/>
}
size="small"
theme="borderless"
onClick={() => setShowEditModal(true)}
/>
) : null}
<MockSetEditModal
visible={showEditModal}
initialInfo={{
bindSubjectInfo: mockSetInfo.mockSubject || {},
bizCtx,
id: String(mockSetInfo.id),
name: mockSetInfo.name,
desc: mockSetInfo.description,
}}
onSuccess={editHandler}
onCancel={() => setShowEditModal(false)}
></MockSetEditModal>
</Space>
{mockSetInfo.description ? (
<LongTextWithTooltip
className={classNames(
s['mock-set-intro-desc'],
s['mock-set-intro-desc_priority'],
isFullHeader ? s['mock-set-intro-desc_full'] : '',
)}
>
{mockSetInfo.description}
</LongTextWithTooltip>
) : null}
</>
);
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useRequest } from 'ahooks';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
type ParamsTypeDefine,
sendTeaEvent,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIModal } from '@coze-arch/bot-semi';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import {
type BizCtx,
type MockSet,
type ComponentSubject,
} from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { getEnvironment, getPluginInfo } from '@coze-studio/mockset-shared';
export interface MockSetInfo {
detail: MockSet;
ctx?: {
mockSubjectInfo?: ComponentSubject;
bizCtx?: BizCtx;
};
}
export interface MockSetEditModalProps {
visible: boolean;
zIndex?: number;
mockSetInfo: MockSetInfo;
onSuccess?: () => void;
onCancel?: () => void;
needResetPopoverContainer?: boolean;
}
function isValidRefCount(refCount: number) {
return refCount >= 0;
}
export const MockSetDeleteModal = ({
visible,
mockSetInfo,
onSuccess,
onCancel,
zIndex,
needResetPopoverContainer,
}: MockSetEditModalProps) => {
const {
detail: { id },
ctx,
} = mockSetInfo || {};
const [mockSetRefCount, setMockSetRefCount] = useState(-1);
// space信息
const spaceType = useSpaceStore(s => s.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const { run: fetchRefInfo } = useRequest(
async () => {
try {
const { spaceID } = getPluginInfo(
ctx?.bizCtx || {},
ctx?.mockSubjectInfo || {},
);
const { usersUsageCount } = await debuggerApi.GetMockSetUsageInfo({
mockSetID: id,
spaceID,
});
setMockSetRefCount(Number(usersUsageCount ?? 0));
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'fetch_mockset_ref_fail' });
setMockSetRefCount(0);
}
},
{
manual: true,
},
);
useEffect(() => {
fetchRefInfo();
}, [mockSetInfo]);
const renderTitle =
mockSetRefCount > 0
? I18n.t('people_using_mockset_delete', { num: mockSetRefCount })
: I18n.t('delete_the_mockset');
const handleOk = async () => {
const { toolID, spaceID } = getPluginInfo(
ctx?.bizCtx || {},
ctx?.mockSubjectInfo || {},
);
const basicParams: ParamsTypeDefine[EVENT_NAMES.del_mockset_front] = {
environment: getEnvironment(),
workspace_id: spaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: toolID || '',
mock_set_id: String(id) || '',
status: 1,
};
try {
id && (await debuggerApi.DeleteMockSet({ id, bizCtx: ctx?.bizCtx }));
onSuccess?.();
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
...basicParams,
status: 0,
});
} catch (e) {
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
...basicParams,
status: 1,
// @ts-expect-error -- linter-disable-autofix
error: e?.message as string,
});
}
};
return (
<UIModal
type="info"
zIndex={zIndex}
icon={
<IconAlertCircle
size="extra-large"
className="inline-flex text-[#FF2710]"
/>
}
title={renderTitle}
visible={isValidRefCount(mockSetRefCount) && visible}
onCancel={onCancel}
onOk={handleOk}
getPopupContainer={
needResetPopoverContainer ? () => document.body : undefined
}
okType="danger"
>
{I18n.t('operation_cannot_be_reversed')}
</UIModal>
);
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function SpaceHolder({
height,
width,
}: {
height?: number;
width?: number;
}) {
return (
<div style={{ width, height, display: width ? 'inline-block' : 'block' }} />
);
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />
declare module 'card-builder-sdk/*';

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect } from 'react';
import {
MockDataStatus,
MockDataValueType,
type MockDataWithStatus,
} from '../util/typings';
export enum BranchType {
NONE,
VISIBLE,
HALF,
}
type BranchInfo = Record<
string,
| {
// 纵向连接线
v: BranchType[];
isLast: boolean;
}
| undefined
>;
export function useGenTreeBranch(mockData?: MockDataWithStatus) {
const [branchInfo, setBranchInfo] = useState<BranchInfo>({});
const [pruned, setPruned] = useState<MockDataWithStatus>();
// 裁剪树枝
// @ts-expect-error -- linter-disable-autofix
const pruning = (data?: MockDataWithStatus) => {
if (!data?.children) {
return;
}
// @ts-expect-error -- linter-disable-autofix
const children = data.children.map(cur => {
if (
cur.type === MockDataValueType.ARRAY ||
cur.type === MockDataValueType.OBJECT
) {
if (cur.isRequired === false && cur.status === MockDataStatus.ADDED) {
return {
...cur,
children: undefined,
};
} else {
return pruning(cur);
}
} else {
return { ...cur };
}
});
return {
...data,
children,
};
};
const generate = (
data?: MockDataWithStatus,
branchPrefix: BranchType[] = [],
) => {
const branch: BranchInfo = {};
if (data?.children) {
const { length } = data.children;
data?.children.forEach((item, index) => {
const isLast = index === length - 1;
branch[item.key] = {
isLast,
v:
isLast && branchPrefix.length > 0
? [...branchPrefix.slice(0, -1), BranchType.HALF]
: branchPrefix,
};
const childBranchPrefix: BranchType[] =
isLast && branchPrefix.length > 0
? [
...branchPrefix.slice(0, -1),
BranchType.NONE,
BranchType.VISIBLE,
]
: [...branchPrefix, BranchType.VISIBLE];
Object.assign(branch, generate(item, childBranchPrefix));
});
}
return branch;
};
useEffect(() => {
const result = pruning(mockData);
const branch = generate(result);
setPruned(result);
setBranchInfo(branch);
}, [mockData]);
return {
branchInfo,
prunedData: pruned,
};
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable max-lines-per-function */
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemoizedFn, useRequest } from 'ahooks';
import { userStoreService } from '@coze-studio/user-store';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
type PluginMockDataGenerateMode,
sendTeaEvent,
type ParamsTypeDefine,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
import { SpaceType } from '@coze-arch/bot-api/playground_api';
import {
type MockSet,
type BizCtx,
infra,
TrafficScene,
type MockSetBinding,
} from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import {
getEnvironment,
getMockSubjectInfo,
getPluginInfo,
type MockSetSelectProps,
} from '@coze-studio/mockset-shared';
import { getUsedScene, isCurrent } from '../util/index';
import { MockTrafficEnabled } from '../util/get-mock-set-options';
import { type EnabledMockSetInfo } from '../component/hooks/store';
import { CONNECTOR_ID, REAL_DATA_MOCKSET } from '../component/const';
function combineBindMockSetInfo(
mockSetBindingList: Array<MockSetBinding>,
mockSetDetailSet: Record<string, MockSet>,
): Array<EnabledMockSetInfo> {
return mockSetBindingList.map(mockSetInfo => {
const { mockSetID } = mockSetInfo;
const detail = mockSetID ? mockSetDetailSet[mockSetID] : {};
return {
mockSetBinding: mockSetInfo,
mockSetDetail: detail,
};
});
}
const useMockSetInSettingModalController = ({
bindSubjectInfo: mockSubjectInfo,
bizCtx: bizSceneCtx,
readonly = false,
}: MockSetSelectProps) => {
const [isEnabled, setIsEnabled] = useState(!!0);
const [isInit, setIsInit] = useState(!0);
const { detail: subjectDetail, ...bindSubjectInfo } = mockSubjectInfo;
const { spaceID, toolID, pluginID } = getPluginInfo(
bizSceneCtx,
bindSubjectInfo,
);
const uid = userStoreService.useUserInfo()?.user_id_str;
const spaceType = useSpaceStore(s => s.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const { jump } = usePageJumpService();
const bizCtx: BizCtx = useMemo(
() => ({
...bizSceneCtx,
connectorUID: uid,
connectorID: CONNECTOR_ID, // 业务线为Coze
}),
[bizSceneCtx, uid, CONNECTOR_ID],
);
const [selectedMockSet, setSelectedMockSet] =
useState<MockSet>(REAL_DATA_MOCKSET);
const [showCreateModal, setShowCreateModal] = useState(false);
const preSelectionRef = useRef<MockSet>(REAL_DATA_MOCKSET);
const { runAsync: changeMockSet, loading: changeMockSetLoading } = useRequest(
async (mockSet: MockSet, isBinding = true) => {
const basicParams: ParamsTypeDefine[EVENT_NAMES.use_mockset_front] = {
environment: getEnvironment(),
workspace_id: spaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: toolID || '',
status: 1,
mock_set_id: (mockSet.id as string) || '',
where: getUsedScene(bizCtx.trafficScene),
};
try {
await debuggerApi.BindMockSet({
mockSetID: isBinding ? mockSet.id : '0',
bizCtx,
mockSubject: bindSubjectInfo,
});
isBinding &&
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
...basicParams,
status: 0,
});
} catch (e) {
setSelectedMockSet(preSelectionRef.current);
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'change_mockset_fail' });
isBinding &&
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
...basicParams,
status: 1,
// @ts-expect-error -- linter-disable-autofix
error: e?.msg as string,
});
}
},
{
manual: true,
},
);
const doChangeMock = (obj?: MockSet) => {
if (obj) {
preSelectionRef.current = selectedMockSet;
setSelectedMockSet(obj);
changeMockSet(obj);
}
};
const doEnabled = useMemoizedFn(() => {
if (isEnabled) {
doChangeMock(REAL_DATA_MOCKSET);
}
setIsEnabled(!isEnabled);
});
const initialInfo = useMemo(
() => ({
bizCtx,
bindSubjectInfo,
name: subjectDetail?.name,
}),
[bizCtx, bindSubjectInfo, subjectDetail],
);
const [deleteTargetId, setDeleteTargetId] = useState(undefined);
const { data: deleteUsingCountInfo } = useRequest(
async () => {
try {
const { usersUsageCount } = await debuggerApi.GetMockSetUsageInfo({
mockSetID: deleteTargetId,
spaceID,
});
return Number(usersUsageCount ?? 0);
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'fetch_mockset_ref_fail' });
return 0;
}
},
{
refreshDeps: [deleteTargetId],
ready: deleteTargetId !== undefined,
},
);
const deleteRenderTitle =
(deleteUsingCountInfo ?? 0) > 0
? I18n.t('people_using_mockset_delete', { num: deleteUsingCountInfo })
: I18n.t('delete_the_mockset');
const {
data: mockSetData,
loading: isListLoading,
refresh,
} = useRequest(
async d => {
try {
const res = await debuggerApi.MGetMockSet({
bizCtx,
// @ts-expect-error -- linter-disable-autofix
mockSubject: getMockSubjectInfo(bizCtx, mockSubjectInfo),
pageToken: d?.pageToken,
orderBy: infra.OrderBy.UpdateTime,
desc: true,
});
setIsInit(!!0);
return res?.mockSets ?? [];
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'mockset_list_fetch_fail' });
return [];
}
},
{ ready: !readonly },
);
const { data: enabledMockSetInfo, loading: isSettingLoading } = useRequest(
async () => {
try {
const { mockSetBindings = [], mockSetDetails = {} } =
await debuggerApi.MGetMockSetBinding(
{
bizCtx,
needMockSetDetail: true,
},
{
headers: {
'rpc-persist-mock-traffic-enable': MockTrafficEnabled.ENABLE,
},
},
);
return combineBindMockSetInfo(mockSetBindings, mockSetDetails);
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'poll_scene_mockset_fail' });
}
},
{ ready: !readonly, refreshDeps: [bizCtx] },
);
const doConfirmDelete = useMemoizedFn(async () => {
const basicParams: ParamsTypeDefine[EVENT_NAMES.del_mockset_front] = {
environment: getEnvironment(),
workspace_id: spaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: toolID || '',
mock_set_id: String(deleteTargetId) || '',
status: 1,
};
try {
deleteTargetId &&
(await debuggerApi.DeleteMockSet({
id: deleteTargetId,
bizCtx,
}));
refresh();
setDeleteTargetId(undefined);
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
...basicParams,
status: 0,
});
} catch (e) {
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
...basicParams,
status: 1,
// @ts-expect-error -- linter-disable-autofix
error: e?.message as string,
});
}
});
useEffect(() => {
if (!enabledMockSetInfo?.length) {
return;
}
const mockSetInfo = enabledMockSetInfo?.find(mockInfo =>
isCurrent(
{
bizCtx: mockInfo?.mockSetBinding?.bizCtx || {},
bindSubjectInfo: mockInfo?.mockSetBinding?.mockSubject || {},
},
{
bizCtx,
bindSubjectInfo,
},
),
);
if (changeMockSetLoading) {
return;
}
if (mockSetInfo?.mockSetDetail) {
setSelectedMockSet(mockSetInfo?.mockSetDetail);
setIsEnabled(!0);
} else {
setSelectedMockSet(REAL_DATA_MOCKSET);
}
}, [enabledMockSetInfo]);
const doHandleView = useMemoizedFn(
(
record?: MockSet,
autoGenerateConfig?: { generateMode: PluginMockDataGenerateMode },
) => {
const { trafficScene } = bizCtx || {};
const { id } = record || {};
if (spaceID && pluginID && toolID && id) {
jump(
trafficScene === TrafficScene.CozeWorkflowDebug
? SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA
: SceneType.BOT__TO__PLUGIN_MOCK_DATA,
{
spaceId: spaceID,
pluginId: pluginID,
toolId: toolID,
toolName: subjectDetail?.name,
mockSetId: String(id),
mockSetName: record?.name,
bizCtx: JSON.stringify(bizCtx),
bindSubjectInfo: JSON.stringify(bindSubjectInfo),
generationMode: autoGenerateConfig?.generateMode,
},
);
}
},
);
return {
// actions
doSetCreateModal: setShowCreateModal,
doHandleView,
doEnabled,
doSetDeleteId: setDeleteTargetId,
deleteRenderTitle,
doConfirmDelete,
doChangeMock,
// data-source
selectedMockSet,
mockSetData,
initialInfo,
// status
isListLoading: isListLoading && isInit,
isSettingLoading,
isEnabled,
showCreateModal,
};
};
export { useMockSetInSettingModalController };

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRequest } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
type PluginMockSetCommonParams,
sendTeaEvent,
} from '@coze-arch/bot-tea';
import { Toast } from '@coze-arch/bot-semi';
import {
type MockRule,
ResponseExpectType,
type BizCtx,
} from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
enum ResType {
SUCCESS = 'success',
FAIL = 'fail',
}
interface SuccessResType extends MockRule {
status: ResType;
}
interface FailResType {
status: ResType;
error: Error;
}
export function useSaveMockData({
mockSetId,
basicParams,
bizCtx,
onSuccess,
onError,
}: {
mockSetId?: string;
basicParams: PluginMockSetCommonParams;
bizCtx: BizCtx;
onSuccess?: (rules: MockRule[]) => void;
onError?: () => void;
}) {
const { runAsync: save, loading } = useRequest(
async (values: string[], mockDataId?: string) => {
const promises: Promise<SuccessResType | FailResType>[] = values.map(
async v => {
const rule = {
id: mockDataId,
mocksetID: mockSetId,
responseExpect: {
responseExpectType: ResponseExpectType.JSON,
responseExpectRule: v,
},
};
try {
const { id } = await debuggerApi.SaveMockRule(
{
bizCtx,
...rule,
},
{ __disableErrorToast: true },
);
return {
status: ResType.SUCCESS,
...rule,
id: id || mockDataId,
};
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error, eventName: 'save_mock_info_fail' });
return {
status: ResType.FAIL,
error,
};
}
},
);
const res = await Promise.all(promises);
const successRes = res.filter(
item => item.status === 'success',
) as SuccessResType[];
const failRes = res.filter(
item => item.status !== 'success',
) as FailResType[];
if (successRes.length) {
sendTeaEvent(EVENT_NAMES.create_mock_front, {
...basicParams,
mock_counts: successRes.length,
status: 0,
});
}
if (failRes.length) {
sendTeaEvent(EVENT_NAMES.create_mock_front, {
...basicParams,
mock_counts: failRes.length,
status: 1,
error: failRes[0].error?.message,
});
}
if (successRes.length === 0) {
// 仅全部失败时认为失败,此时需要 toast 提示
Toast.error({
content: withSlardarIdButton(
failRes[0]?.error?.message || I18n.t('error'),
),
showClose: false,
});
onError?.();
} else {
onSuccess?.(successRes);
}
},
{
manual: true,
},
);
return { save, loading };
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useMemo } from 'react';
import { safeJSONParse } from '@coze-arch/bot-utils';
import {
ROOT_KEY,
parseToolSchema,
stringifyEditorContent,
transDataWithStatus2Object,
transSchema2DataWithStatus,
MockDataValueType,
type MockDataWithStatus,
} from '@coze-studio/mockset-shared';
import { getMergedDataWithStatus } from '../util/utils';
/** 缓存最新一个解析结果 */
const cache: {
schema: string;
result: MockDataWithStatus | undefined;
} = {
schema: '',
result: undefined,
};
export const PRE_DEFINED_NO_EMPTY_KEY = 'response_for_model';
function useGetCachedSchemaData(schema?: string) {
const result = useMemo(() => {
if (schema && cache.schema === schema) {
return cache.result;
}
if (schema) {
cache.schema = schema;
const parsedSchema = parseToolSchema(schema);
const transData = transSchema2DataWithStatus(ROOT_KEY, parsedSchema);
cache.result = transData;
return transData;
}
return undefined;
}, [schema]);
return result;
}
export function useTransSchema(schema?: string, currentMock?: string) {
const result = useGetCachedSchemaData(schema);
const {
result: mergedResultExample,
merged: mergedResult,
incompatible,
formatted: formattedResultExample,
} = useMemo(() => {
const { result: merged, incompatible: mergedIncompatible } =
getMergedDataWithStatus(result, currentMock);
if (merged) {
const resultObj = transDataWithStatus2Object(
merged,
currentMock !== undefined,
)?.[ROOT_KEY];
return {
merged,
result: resultObj,
formatted: stringifyEditorContent(resultObj),
incompatible: mergedIncompatible,
};
} else {
return {
incompatible: mergedIncompatible,
};
}
}, [result, currentMock]);
// 特殊字段需要单独处理response_for_model 不能为空字符串
const testValueValid = useCallback(
(value: string) => {
if (
result?.children?.some(
item =>
item.label === PRE_DEFINED_NO_EMPTY_KEY &&
item.type === MockDataValueType.STRING,
)
) {
const parsedValue = safeJSONParse(value);
if (
typeof parsedValue === 'object' &&
(typeof parsedValue[PRE_DEFINED_NO_EMPTY_KEY] !== 'string' ||
parsedValue[PRE_DEFINED_NO_EMPTY_KEY].length === 0)
) {
return false;
}
}
return true;
},
[result],
);
return {
// 由 schema 生成的结构话数据,值为初始值
result,
// 合并模拟数据的结构话数据,全集
mergedResult,
// mergedResult 转换的 object
mergedResultExample,
// 格式化的数据
formattedResultExample,
// 是否兼容
incompatible,
// 是否合并模拟数据
isInit: currentMock === undefined,
testValueValid,
};
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// export { DemoComponent } from './demo';

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type BotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { getFlags } from '@coze-arch/bot-flags';
import { BotMode } from '@coze-arch/bot-api/developer_api';
import { TrafficScene } from '@coze-arch/bot-api/debugger_api';
export enum MockTrafficEnabled {
DISABLE = 0,
ENABLE = 1,
}
export function getMockSetReqOptions(baseBotInfo: BotInfoStore) {
const FLAGS = getFlags();
return FLAGS['bot.devops.plugin_mockset']
? {
headers: {
'rpc-persist-mock-traffic-scene':
baseBotInfo.mode === BotMode.MultiMode
? TrafficScene.CozeMultiAgentDebug
: TrafficScene.CozeSingleAgentDebug,
'rpc-persist-mock-traffic-caller-id': baseBotInfo.botId,
'rpc-persist-mock-space-id': baseBotInfo?.space_id,
'rpc-persist-mock-traffic-enable': MockTrafficEnabled.ENABLE,
},
}
: {};
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-expect-error -- linter-disable-autofix
import { isEqual } from 'lodash-es';
import { safeJSONParse } from '@coze-arch/bot-utils';
import {
type BizCtx,
TrafficScene,
type MockSet,
} from '@coze-arch/bot-api/debugger_api';
import { type BasicMockSetInfo } from '@coze-studio/mockset-shared';
import { REAL_DATA_MOCKSET } from '../component/const';
export { safeJSONParse } from './utils';
export function isRealData(mockSet: MockSet) {
return mockSet.id === REAL_DATA_MOCKSET.id;
}
export function isCurrent(sItem: BasicMockSetInfo, tItem: BasicMockSetInfo) {
const { bindSubjectInfo: mockSubject, bizCtx } = sItem;
const { bindSubjectInfo: compSubject, bizCtx: compBizCtx } = tItem;
const isCurrentComponent = isEqual(mockSubject, compSubject);
const { ext, ...baseBizCtx } = bizCtx || {};
const { ext: compExt, ...baseCompBizCxt } = compBizCtx || {};
const isCurrentScene = isSameScene(baseBizCtx, baseCompBizCxt);
const isWorkflowExt =
bizCtx?.trafficScene !== TrafficScene.CozeWorkflowDebug ||
isSameWorkflowTool(
ext?.mockSubjectInfo || '',
compExt?.mockSubjectInfo || '',
);
return isCurrentComponent && isCurrentScene && isWorkflowExt;
}
export function isSameWorkflowTool(
sMockSubjectInfo: string,
tMockSubjectInfo: string,
) {
const sMockInfo = safeJSONParse(sMockSubjectInfo || '{}');
const tMockInfo = safeJSONParse(tMockSubjectInfo || '{}');
return isEqual(sMockInfo, tMockInfo);
}
export function isSameScene(sBizCtx: BizCtx, tBizCtx: BizCtx) {
return (
sBizCtx.bizSpaceID === tBizCtx.bizSpaceID &&
sBizCtx.trafficScene === tBizCtx.trafficScene &&
sBizCtx.trafficCallerID === tBizCtx.trafficCallerID
);
}
export function getUsedScene(scene?: TrafficScene): 'bot' | 'agent' | 'flow' {
switch (scene) {
case TrafficScene.CozeSingleAgentDebug:
return 'bot';
case TrafficScene.CozeMultiAgentDebug:
return 'agent';
case TrafficScene.CozeWorkflowDebug:
return 'flow';
case TrafficScene.CozeToolDebug:
return 'bot';
default:
return 'bot';
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type MockRule } from '@coze-arch/bot-api/debugger_api';
export enum MockDataValueType {
STRING = 'string',
INTEGER = 'integer',
NUMBER = 'number',
OBJECT = 'object',
ARRAY = 'array',
BOOLEAN = 'boolean',
}
export enum MockDataStatus {
DEFAULT = 'default',
REMOVED = 'removed',
ADDED = 'added',
}
export interface MockDataWithStatus {
/** key */
key: string;
/** 字段名称 */
label: string;
/** 字段值 */
realValue?: string | number | boolean;
/** 展示使用 */
displayValue?: string;
/** 描述 */
description?: string;
/** 是否必填 */
isRequired: boolean;
/** 字段数据类型 */
type: MockDataValueType;
/** for array */
childrenType?: MockDataValueType;
/** 字段状态 */
status: MockDataStatus;
/** 字段子节点 */
children?: MockDataWithStatus[];
}
export interface MockDataInfo {
schema?: string;
mock?: MockRule;
mergedResultExample?: string;
incompatible?: boolean;
}

View File

@@ -0,0 +1,313 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
getArrayItemKey,
getMockValue,
ROOT_KEY,
} from '@coze-studio/mockset-shared';
import {
MockDataStatus,
MockDataValueType,
type MockDataWithStatus,
} from './typings';
export function transUpperCase(str?: string) {
return str ? `${str.slice(0, 1).toUpperCase()}${str.slice(1)}` : '';
}
// 仅开发中使用
export function sleep(t = 1000) {
return new Promise(r => {
setTimeout(() => {
r(1);
}, t);
});
}
export function safeJSONParse<T>(str: string, errCb?: () => T | undefined) {
try {
return JSON.parse(str) as T;
} catch (error) {
return errCb?.();
}
}
// 根据 value 生成 displayValue
function getTargetValue(
type: MockDataValueType,
value: string | number | boolean | undefined,
): [string | number | boolean | undefined, string | undefined] {
return getMockValue(type, {
getBooleanValue: () => Boolean(value),
getNumberValue: () => Number(value),
getStringValue: () => String(value),
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin resp 的类型由用户定义,包含任何可能
type PluginRespType = any;
// 合并 schema 和 mock data
export function getMergedDataWithStatus(
schemaData?: MockDataWithStatus,
currentMock?: string,
): {
result?: MockDataWithStatus;
incompatible: boolean;
} {
const isInit = currentMock === undefined;
if (!schemaData || isInit) {
return {
result: schemaData,
incompatible: false,
};
}
// parse mock string
const mock =
typeof currentMock === 'string'
? safeJSONParse<PluginRespType>(currentMock) || currentMock
: currentMock;
// 将 mock 转换为 MockDataWithStatus 格式
const processedMock = transMockData2DataWithStatus(ROOT_KEY, mock, {
defaultStatus: MockDataStatus.REMOVED,
});
// 合并
const { merged, incompatible } = mergeDataWithStatus(
schemaData.children,
processedMock?.children,
schemaData.type === MockDataValueType.ARRAY,
);
return {
result: {
...schemaData,
children: merged,
},
incompatible,
};
}
// 解析当前 realValue 所属于的 MockDataValueType
function getMockDataType(currentMock: PluginRespType) {
let dataTypeName = typeof currentMock as MockDataValueType | undefined;
if (currentMock instanceof Array) {
dataTypeName = MockDataValueType.ARRAY;
}
return dataTypeName;
}
function compareMockDataType(
mockDataType?: MockDataValueType,
initDataType?: MockDataValueType,
) {
// mock data 的类型是根据值识别出的,存在 Integer 类型被识别为 Number 的情况
if (mockDataType === MockDataValueType.NUMBER) {
return (
initDataType === MockDataValueType.NUMBER ||
initDataType === MockDataValueType.INTEGER
);
} else {
return mockDataType === initDataType;
}
}
// 转换 Object 格式到 DataWithStatus 格式
export function transMockData2DataWithStatus(
label: string,
currentMock: PluginRespType,
params?: {
defaultStatus: MockDataStatus;
keyPrefix?: string;
},
): MockDataWithStatus | undefined {
const { defaultStatus = MockDataStatus.DEFAULT } = params || {};
const dataTypeName = getMockDataType(currentMock);
if (!dataTypeName) {
return undefined;
}
const [realValue, displayValue] = getTargetValue(dataTypeName, currentMock);
const itemKey = params?.keyPrefix ? `${params?.keyPrefix}-${label}` : label;
const item: MockDataWithStatus = {
label,
realValue,
displayValue,
isRequired: false,
type: dataTypeName,
status: defaultStatus,
key: itemKey,
};
if (dataTypeName === MockDataValueType.OBJECT) {
const children: MockDataWithStatus[] = [];
for (const property of Object.keys(currentMock)) {
if (property) {
const child = transMockData2DataWithStatus(
property,
currentMock[property],
{
defaultStatus,
keyPrefix: itemKey,
},
);
child && children.push(child);
}
}
item.children = children;
}
if (dataTypeName === MockDataValueType.ARRAY) {
item.childrenType = getMockDataType(currentMock[0]);
const children: MockDataWithStatus[] = [];
for (const index in currentMock) {
if (currentMock[index] !== undefined) {
const child = transMockData2DataWithStatus(
getArrayItemKey(index),
currentMock[index],
{
defaultStatus,
keyPrefix: itemKey,
},
);
child && children.push(child);
}
}
item.children = children;
}
return item;
}
function mergeDataItems(item1: MockDataWithStatus, item2: MockDataWithStatus) {
let incompatible = false;
const newItem = {
...item1,
key: item2.key,
label: item2.label,
realValue: item2.realValue,
displayValue: item2.displayValue,
status: MockDataStatus.DEFAULT,
};
if (
item2.type === MockDataValueType.ARRAY ||
item2.type === MockDataValueType.OBJECT
) {
const { merged, incompatible: childIncompatible } = mergeDataWithStatus(
item1.children,
item2.children,
item1.type === MockDataValueType.ARRAY,
);
newItem.children = merged;
incompatible = incompatible || childIncompatible;
}
return {
result: newItem,
incompatible,
};
}
function merge2DataList(
autoInitDataList: MockDataWithStatus[],
mockDataListWithStatus: MockDataWithStatus[],
isArrayType = false,
): {
merged: MockDataWithStatus[];
incompatible: boolean;
} {
let incompatible = false;
let appendData: MockDataWithStatus[] = [...mockDataListWithStatus];
const originData: MockDataWithStatus[] = [...autoInitDataList];
for (const i in originData) {
if (originData[i]) {
const item = originData[i];
const index = appendData.findIndex(
data =>
data.label === item.label &&
compareMockDataType(data.type, item.type) &&
compareMockDataType(data.childrenType, item.childrenType),
);
if (index !== -1) {
const data = appendData.splice(index, 1);
const { result: newItem, incompatible: childIncompatible } =
mergeDataItems(item, data[0]);
originData[i] = newItem;
incompatible = incompatible || childIncompatible;
} else if (item.isRequired) {
incompatible = true;
}
}
}
// 在合并 array 时,会出现 autoInitDataList(originData) 中只存在一个初始化数据,而 mockDataListWithStatus(appendData) 存在多个相同结构的数据
// 需要将 mockDataListWithStatus 中剩余的数据进行合入
if (appendData.length && isArrayType) {
const target = autoInitDataList[0];
appendData.forEach(item => {
const { result: newItem, incompatible: childIncompatible } =
mergeDataItems(target, item);
originData.push(newItem);
incompatible = incompatible || childIncompatible;
});
appendData = [];
}
if (appendData.length) {
incompatible = true;
}
return {
merged: [...originData, ...appendData],
incompatible,
};
}
// 合并两个 MockDataWithStatus 数组,以 autoInitDataList 顺序优先
export function mergeDataWithStatus(
autoInitDataList?: MockDataWithStatus[],
mockDataListWithStatus?: MockDataWithStatus[],
isArrayType = false,
): {
merged: MockDataWithStatus[];
incompatible: boolean;
} {
if (autoInitDataList === undefined || mockDataListWithStatus === undefined) {
return {
merged: [...(autoInitDataList || []), ...(mockDataListWithStatus || [])],
incompatible: autoInitDataList !== mockDataListWithStatus,
};
}
// 在合并 array 时,存在 mockDataListWithStatus 为空的情况,此时直接判断
if (mockDataListWithStatus.length === 0 && isArrayType) {
return {
merged: [],
incompatible: false,
};
}
return merge2DataList(autoInitDataList, mockDataListWithStatus, isArrayType);
}