feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.long-text.long-text-tooltip {
|
||||
color: var(--semi-color-bg-0);
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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' }} />
|
||||
);
|
||||
}
|
||||
19
frontend/packages/agent-ide/bot-plugin/mock-set/src/edenx-app-env.d.ts
vendored
Normal file
19
frontend/packages/agent-ide/bot-plugin/mock-set/src/edenx-app-env.d.ts
vendored
Normal 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/*';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
17
frontend/packages/agent-ide/bot-plugin/mock-set/src/typings.d.ts
vendored
Normal file
17
frontend/packages/agent-ide/bot-plugin/mock-set/src/typings.d.ts
vendored
Normal 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' />
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user