feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-devops/mockset-manage
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,130 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import {
type BizCtx,
ComponentType,
TrafficScene,
} from '@coze-arch/bot-api/debugger_api';
import { type BasicMockSetInfo } from '@/interface';
import { useInitialGetEnabledMockSet } from '@/hooks/use-get-mockset';
vi.mock('@/const', () => ({
IS_OVERSEA: true,
MockTrafficEnabled: {
DISABLE: 0,
ENABLE: 1,
},
}));
vi.mock('@coze-arch/bot-utils', () => ({
safeJSONParse: JSON.parse,
}));
vi.mock('@coze-arch/logger', () => ({
logger: {
createLoggerWith: vi.fn(),
error: vi.fn(),
info: vi.fn(),
},
reporter: {
createReporterWithPreset: vi.fn(),
},
}));
vi.mock('@coze-arch/bot-api', () => ({
debuggerApi: {
MGetMockSetBinding: vi.fn().mockResolvedValue({
code: 0,
msg: '',
mockSetBindings: [
{
mockSetID: '1',
mockSubject: {
componentID: 'tool1',
componentType: ComponentType.CozeTool,
parentComponentID: 'plugin1',
parentComponentType: ComponentType.CozePlugin,
},
bizCtx: {
bizSpaceID: '1',
trafficCallerID: '1',
trafficScene: TrafficScene.CozeSingleAgentDebug,
connectorID: '1',
connectorUID: '2',
},
},
],
mockSetDetails: {
'1': {
id: '1',
name: 'test-detail',
mockSubject: {
componentID: 'tool1',
componentType: ComponentType.CozeTool,
parentComponentID: 'plugin1',
parentComponentType: ComponentType.CozePlugin,
},
},
},
}),
},
}));
describe('mock-set-hooks', () => {
it('fetch-mock-list', async () => {
const TEST_COMMON_BIZ = {
connectorID: '1',
connectorUID: '2',
};
const TEST_BIZ_CTX1: BizCtx = {
bizSpaceID: '1',
trafficCallerID: '1',
trafficScene: TrafficScene.CozeSingleAgentDebug,
};
const TEST_MOCK_SUBJECT = {
componentID: 'tool1',
componentType: ComponentType.CozeTool,
parentComponentID: 'plugin1',
parentComponentType: ComponentType.CozePlugin,
};
const singleAgentToolItem: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_SUBJECT,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX1,
},
};
const { result } = renderHook(() =>
useInitialGetEnabledMockSet({
bizCtx: singleAgentToolItem.bizCtx,
pollingInterval: 0,
}),
);
const { start } = result.current;
await start();
const { data } = result.current;
expect(data[0]?.mockSetBinding.mockSetID).toEqual('1');
expect(data[0]?.mockSetDetail?.id).toEqual('1');
});
});

View File

@@ -0,0 +1,330 @@
/*
* 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 BizCtx,
ComponentType,
TrafficScene,
} from '@coze-arch/bot-api/debugger_api';
import {
isRealData,
isCurrent,
isSameWorkflowTool,
isSameScene,
getPluginInfo,
getMockSubjectInfo,
getUsedScene,
} from '@/utils';
import { type BasicMockSetInfo } from '@/interface';
vi.mock('@coze-arch/bot-utils', () => ({
safeJSONParse: JSON.parse,
}));
vi.mock('@/const', () => ({
IS_OVERSEA: true,
REAL_DATA_MOCKSET: {
id: '0',
},
}));
vi.mock('@coze-arch/logger', () => ({
logger: {
createLoggerWith: vi.fn(),
},
}));
const TEST_COMMON_BIZ = {
connectorID: '1',
connectorUID: '2',
};
const TEST_BIZ_CTX1: BizCtx = {
bizSpaceID: '1',
trafficCallerID: '1',
trafficScene: TrafficScene.CozeSingleAgentDebug,
};
const TEST_BIZ_CTX2: BizCtx = {
bizSpaceID: '1',
trafficCallerID: '2',
trafficScene: TrafficScene.CozeSingleAgentDebug,
};
const TEST_BIZ_CTX3: BizCtx = {
bizSpaceID: '1',
trafficCallerID: '1',
trafficScene: TrafficScene.CozeWorkflowDebug,
};
const TEST_BIZ_CTX4: BizCtx = {
bizSpaceID: '2',
trafficCallerID: '1',
trafficScene: TrafficScene.CozeWorkflowDebug,
};
const TEST_MOCK_SUBJECT1 = {
componentID: 'tool1',
componentType: ComponentType.CozeTool,
parentComponentID: 'plugin1',
parentComponentType: ComponentType.CozePlugin,
};
const TEST_MOCK_SUBJECT2 = {
componentID: 'tool2',
componentType: ComponentType.CozeTool,
parentComponentID: 'plugin1',
parentComponentType: ComponentType.CozePlugin,
};
const TEST_MOCK_WORKFLOW_NODE1 = {
componentID: 'node1',
componentType: ComponentType.CozeToolNode,
parentComponentID: 'workflow1',
parentComponentType: ComponentType.CozeWorkflow,
};
const TEST_MOCK_WORKFLOW_NODE2 = {
componentID: 'node2',
componentType: ComponentType.CozeToolNode,
parentComponentID: 'workflow1',
parentComponentType: ComponentType.CozeWorkflow,
};
const singleAgentToolItem1: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_SUBJECT1,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX1,
},
};
const singleAgentToolItem2: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_SUBJECT2,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX2,
},
};
const multiAgentToolItem: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_SUBJECT1,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX1,
trafficScene: TrafficScene.CozeMultiAgentDebug,
},
};
const workflowToolItem1: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_WORKFLOW_NODE1,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX3,
ext: { mockSubjectInfo: JSON.stringify(TEST_MOCK_SUBJECT1) },
},
};
const workflowToolItem2: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_WORKFLOW_NODE1,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX3,
ext: { mockSubjectInfo: JSON.stringify(TEST_MOCK_SUBJECT2) },
},
};
const workflowToolItem3: BasicMockSetInfo = {
bindSubjectInfo: TEST_MOCK_WORKFLOW_NODE2,
bizCtx: {
...TEST_COMMON_BIZ,
...TEST_BIZ_CTX3,
ext: { mockSubjectInfo: JSON.stringify(TEST_MOCK_SUBJECT1) },
},
};
describe('mock-set-utils-isRealData', () => {
it('real data', () => {
const res = isRealData({ id: '0' });
expect(res).toEqual(true);
});
it('not real data', () => {
const res = isRealData({ id: '123' });
expect(res).toEqual(false);
});
});
describe('mock-set-utils-isCurrent', () => {
it('same single agent mock tool', () => {
const res = isCurrent(singleAgentToolItem1, singleAgentToolItem1);
expect(res).toEqual(true);
});
it('same agent different mock tool', () => {
const res = isCurrent(singleAgentToolItem1, singleAgentToolItem2);
expect(res).toEqual(false);
});
it('different single multi agent mock tool', () => {
const res = isCurrent(singleAgentToolItem1, multiAgentToolItem);
expect(res).toEqual(false);
});
it('different single agent workflow mock tool', () => {
const res = isCurrent(singleAgentToolItem1, workflowToolItem1);
expect(res).toEqual(false);
});
it('same workflow mock tool', () => {
const res = isCurrent(workflowToolItem1, workflowToolItem1);
expect(res).toEqual(true);
});
it('different workflow mock tool', () => {
const res = isCurrent(workflowToolItem1, workflowToolItem2);
expect(res).toEqual(false);
});
it('different workflow mock tool', () => {
const res = isCurrent(workflowToolItem1, workflowToolItem2);
expect(res).toEqual(false);
});
it('different workflow mock node', () => {
const res = isCurrent(workflowToolItem1, workflowToolItem3);
expect(res).toEqual(false);
});
});
describe('mock-set-utils-isSameWorkflowTool', () => {
it('workflow same mock subject', () => {
const res = isSameWorkflowTool(
workflowToolItem1.bizCtx.ext?.mockSubjectInfo || '',
workflowToolItem1.bizCtx.ext?.mockSubjectInfo || '',
);
expect(res).toEqual(true);
});
it('different mock subject', () => {
const res = isSameWorkflowTool(
workflowToolItem1.bizCtx.ext?.mockSubjectInfo || '',
workflowToolItem2.bizCtx.ext?.mockSubjectInfo || '',
);
expect(res).toEqual(false);
});
});
describe('mock-set-utils-isSameScene', () => {
it('isSameScene', () => {
const res = isSameScene(TEST_BIZ_CTX1, TEST_BIZ_CTX1);
expect(res).toEqual(true);
});
it('different caller id', () => {
const res = isSameScene(TEST_BIZ_CTX1, TEST_BIZ_CTX2);
expect(res).toEqual(false);
});
it('different used scene', () => {
const res = isSameScene(TEST_BIZ_CTX1, TEST_BIZ_CTX3);
expect(res).toEqual(false);
});
it('different space id', () => {
const res = isSameScene(TEST_BIZ_CTX1, TEST_BIZ_CTX4);
expect(res).toEqual(false);
});
});
describe('mock-set-utils-getPluginInfo', () => {
it('get agent tool info', () => {
const res = getPluginInfo(TEST_BIZ_CTX1, TEST_MOCK_SUBJECT1);
expect(res.pluginID).toEqual(TEST_MOCK_SUBJECT1.parentComponentID);
expect(res.toolID).toEqual(TEST_MOCK_SUBJECT1.componentID);
expect(res.spaceID).toEqual(TEST_BIZ_CTX1.bizSpaceID);
});
it('get workflow tool info', () => {
const res = getPluginInfo(
workflowToolItem1.bizCtx,
workflowToolItem1.bindSubjectInfo,
);
expect(res.pluginID).toEqual(TEST_MOCK_SUBJECT1.parentComponentID);
expect(res.toolID).toEqual(TEST_MOCK_SUBJECT1.componentID);
expect(res.spaceID).toEqual(TEST_BIZ_CTX3.bizSpaceID);
});
});
describe('mock-set-utils-getMockSubjectInfo', () => {
it('get workflow mock subject info', () => {
const res = getMockSubjectInfo(
workflowToolItem1.bizCtx,
workflowToolItem1.bindSubjectInfo,
);
expect(res.componentID).toEqual(TEST_MOCK_SUBJECT1.componentID);
expect(res.componentType).toEqual(TEST_MOCK_SUBJECT1.componentType);
expect(res.parentComponentID).toEqual(TEST_MOCK_SUBJECT1.parentComponentID);
expect(res.parentComponentType).toEqual(
TEST_MOCK_SUBJECT1.parentComponentType,
);
});
it('get agent subject info', () => {
const res = getMockSubjectInfo(
singleAgentToolItem1.bizCtx,
singleAgentToolItem1.bindSubjectInfo,
);
expect(res.componentID).toEqual(TEST_MOCK_SUBJECT1.componentID);
expect(res.componentType).toEqual(TEST_MOCK_SUBJECT1.componentType);
expect(res.parentComponentID).toEqual(TEST_MOCK_SUBJECT1.parentComponentID);
expect(res.parentComponentType).toEqual(
TEST_MOCK_SUBJECT1.parentComponentType,
);
});
});
describe('mock-set-utils-getMockSubjectInfo', () => {
it('get single agent', () => {
const res = getUsedScene(TrafficScene.CozeSingleAgentDebug);
expect(res).toEqual('bot');
});
it('get multi agent', () => {
const res = getUsedScene(TrafficScene.CozeMultiAgentDebug);
expect(res).toEqual('agent');
});
it('get workflow', () => {
const res = getUsedScene(TrafficScene.CozeWorkflowDebug);
expect(res).toEqual('flow');
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,73 @@
{
"name": "@coze-devops/mockset-manage",
"version": "0.0.1",
"description": "plugin mockset management",
"license": "Apache-2.0",
"author": "linyueqiang@bytedance.com",
"maintainers": [],
"main": "src/index.tsx",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"devDependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-env": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-hooks": "workspace:*",
"@coze-arch/bot-http": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/bot-tea": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"@douyinfe/semi-ui": "~2.72.3",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"ahooks": "^3.7.8",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"debug": "^4.3.4",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5",
"zustand": "^4.4.7"
},
"peerDependencies": {
"@coze-studio/user-store": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"ahooks": "^3.7.8",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"zustand": "^4.4.7"
},
"// deps": "debug@^4.3.4 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1,36 @@
/* stylelint-disable selector-class-pattern */
.auto-generate-select {
:global {
.semi-radioGroup-horizontal {
width: 100%;
}
.semi-radio-cardRadioGroup {
flex-basis: 0;
flex-grow: 1;
border: 1px solid var(--semi-color-border);
}
.semi-radio-checked.semi-radio-cardRadioGroup {
border: 1px solid var(--semi-color-primary);
}
.semi-input-number {
width: 100%;
}
}
}
.auto-generate-radio {
:global {
.semi-radio-content {
min-width: 0;
overflow-wrap: break-word;
}
}
}
.auto-generate-label {
margin: 16px 0 8px;
color: var(--semi-color-text-1);
}

View File

@@ -0,0 +1,126 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { PluginMockDataGenerateMode } from '@coze-arch/bot-tea';
import { RadioGroup, Radio, InputNumber } from '@coze-arch/bot-semi';
// eslint-disable-next-line no-restricted-imports
import { type RadioChangeEvent } from '@douyinfe/semi-ui/lib/es/radio';
import {
getLatestAutoGenerationChoice,
setLatestAutoGenerationChoice,
} from '../../utils/auto-generate-storage';
import style from './index.module.less';
interface AutoGenerateSelectProps {
showLabel?: boolean;
enableMulti?: boolean;
defaultCount?: number;
onChange?: (mode: AutoGenerateConfig) => void;
onInit?: (mode: AutoGenerateConfig) => void;
}
export interface AutoGenerateConfig {
generateMode: PluginMockDataGenerateMode;
generateCount?: number;
}
export function AutoGenerateSelect({
showLabel,
enableMulti,
defaultCount,
onChange,
onInit,
}: AutoGenerateSelectProps) {
const [config, setConfig] = useState<AutoGenerateConfig>({
generateMode: PluginMockDataGenerateMode.MANUAL,
generateCount: defaultCount || 1,
});
const onSelectChange = (e: RadioChangeEvent) => {
const updatedConfig = { ...config, generateMode: e.target.value };
setLatestAutoGenerationChoice(e.target.value);
setConfig(updatedConfig);
onChange?.(updatedConfig);
};
const initMode = async () => {
const m = await getLatestAutoGenerationChoice();
const initConfig = { ...config, generateMode: m };
setConfig(initConfig);
onInit?.(initConfig);
};
useEffect(() => {
initMode();
}, []);
return (
<div className={style['auto-generate-select']}>
{showLabel ? (
<h4 className={style['auto-generate-label']}>
{I18n.t('plugin_creation_method')}
</h4>
) : null}
<RadioGroup
type="card"
value={config.generateMode}
onChange={onSelectChange}
>
<Radio
className={style['auto-generate-radio']}
value={PluginMockDataGenerateMode.RANDOM}
extra={I18n.t('generate_randomly_based_on_data_type_name')}
>
{I18n.t('randomlymode')}
</Radio>
<Radio
className={style['auto-generate-radio']}
value={PluginMockDataGenerateMode.LLM}
extra={I18n.t('intelligently_generated_by_large_language_model')}
>
{I18n.t('llm_mode')}
</Radio>
</RadioGroup>
{enableMulti && showLabel ? (
<h4 className={style['auto-generate-label']}>
{I18n.t('mock_data_quantity')}
</h4>
) : null}
{enableMulti ? (
<InputNumber
value={config.generateCount}
max={5}
min={1}
onChange={e => {
if (!Number.isNaN(Number(e))) {
const updatedConfig = { ...config, generateCount: Number(e) };
setConfig(updatedConfig);
onChange?.(updatedConfig);
}
}}
/>
) : null}
</div>
);
}

View File

@@ -0,0 +1,125 @@
.select-container {
max-width: 100%;
display: inline-block;
background-color: transparent;
border: none;
padding: 4px 6px;
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 {
max-width: 336px;
min-width: 234px;
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 {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
font-weight: 400;
height: 16px;
line-height: 16px;
}
.select-label {
svg {
>path {
/* stylelint-disable-next-line declaration-no-important */
fill: currentcolor !important;
fill-opacity: 1;
}
}
}
.custom-option-render-focused {
background-color: rgb(46 46 56 / 8%);
cursor: pointer;
}
.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;
background: var(--semi-color-border);
margin: 4px 0;
}
.create-container {
padding: 0 16px;
color: #4D53E8;
height: 32px;
display: flex;
align-items: center;
cursor: pointer;
}
.load-more {
color: #4D53E8;
display: flex;
align-items: center;
justify-content: center;
.spin-icon {
margin-right: 8px;
}
}

View File

@@ -0,0 +1,602 @@
/*
* 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 {
PluginMockDataGenerateMode,
sendTeaEvent,
type ParamsTypeDefine,
EVENT_NAMES,
} 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,
UIToast,
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 { MockSetEditModal } from '../mockset-edit-modal';
import { MockSetDeleteModal } from '../mockset-delete-modal';
import { type AutoGenerateConfig } from '../auto-generate-select';
import {
getEnvironment,
getMockSubjectInfo,
getPluginInfo,
getUsedScene,
isCurrent,
isRealData,
} from '../../utils';
import {
type MockSelectOptionProps,
type MockSelectRenderOptionProps,
type MockSetSelectProps,
MockSetStatus,
} from '../../interface';
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 { 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);
logger.error({ error: e as Error, eventName: 'change_mockset_fail' });
isBinding &&
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
...basicParams,
status: 1,
error: (e as Error | undefined)?.message 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,
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) {
logger.error({
error: e as Error,
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?: AutoGenerateConfig,
) => {
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
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, autoGenerateConfig) => {
const { id } = info || {};
setShowCreateModal(false);
const msgMap = {
[PluginMockDataGenerateMode.LLM]: I18n.t(
'created_mockset_please_add_mock_data_llm_generation',
),
[PluginMockDataGenerateMode.RANDOM]: I18n.t(
'created_mockset_please_add_mock_data_random_generation',
),
[PluginMockDataGenerateMode.MANUAL]: I18n.t(
'created_mockset_please_add_mock_data',
),
};
UIToast.success(
msgMap[
autoGenerateConfig?.generateMode ||
PluginMockDataGenerateMode.MANUAL
],
);
handleView({ id }, autoGenerateConfig);
}}
initialInfo={{
bizCtx,
bindSubjectInfo,
name: subjectDetail?.name,
}}
needResetPopoverContainer={
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
}
/>
) : null}
{deleteMockSet ? (
<MockSetDeleteModal
zIndex={9999}
visible={!!deleteMockSet}
mockSetInfo={{
detail: deleteMockSet,
ctx: { bizCtx, mockSubjectInfo: bindSubjectInfo },
}}
onSuccess={() => {
deleteMockSet.id === selectedMockSet.id &&
setSelectedMockSet(REAL_DATA_MOCKSET);
setDeleteMockSet(undefined);
cancel();
start();
}}
onCancel={() => setDeleteMockSet(undefined)}
needResetPopoverContainer={
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
}
/>
) : null}
</div>
);
};
export const MockSetSelect = forwardRef(MockSetSelectComp);

View File

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

View File

@@ -0,0 +1,107 @@
/*
* 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 '../../interface';
import styles from './option-item.module.less';
export interface MockSetItemProps {
name: string;
onDelete?: () => void;
onView?: () => void;
status?: MockSetStatus;
creatorName?: string;
viewOnly?: boolean;
disableCreator?: boolean;
className?: string;
style?: CSSProperties;
}
export const MockSetItem = ({
name,
onDelete,
onView,
status = MockSetStatus.Normal,
creatorName,
viewOnly,
disableCreator,
className,
style,
}: MockSetItemProps) => {
const [isHover, setIsHover] = useState(false);
const renderExtraInfo = () => {
if (isHover) {
return (
<div
className={styles['operation-icon']}
onClick={e => e.stopPropagation()}
>
{viewOnly ? (
<UIIconButton onClick={onView} icon={<IconEyeOpened />} />
) : (
<>
<UIIconButton
onClick={onView}
icon={<IconEdit />}
wrapperClass="mr-[4px]"
/>
<UIIconButton onClick={onDelete} icon={<IconDeleteOutline />} />
</>
)}
</div>
);
}
return disableCreator ? null : (
<Typography.Text
ellipsis={{
showTooltip: {
opts: { content: creatorName },
},
}}
className={styles['creator-name']}
>
{creatorName}
</Typography.Text>
);
};
return (
<div
className={classNames(styles['mock-select-item'], className)}
style={style}
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => setIsHover(false)}
>
<span className={styles['mock-main-info']}>
{status !== MockSetStatus.Normal && (
<IconUploadError className={styles['status-icon']} />
)}
<Typography.Text ellipsis={{}} className={styles['mock-name']}>
{name}
</Typography.Text>
</span>
<div className={styles['mock-extra-info']}>{renderExtraInfo()}</div>
</div>
);
};

View File

@@ -0,0 +1,162 @@
/*
* 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,
sendTeaEvent,
type ParamsTypeDefine,
} 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 '../../utils';
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 () => {
const { spaceID } = getPluginInfo(
ctx?.bizCtx || {},
ctx?.mockSubjectInfo || {},
);
try {
const { usersUsageCount } = await debuggerApi.GetMockSetUsageInfo({
mockSetID: id,
spaceID,
});
setMockSetRefCount(Number(usersUsageCount ?? 0));
} catch (e) {
logger.error({
error: e as Error,
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,
error: (e as Error | undefined)?.message as string,
});
}
};
return (
<UIModal
type="info"
zIndex={zIndex}
icon={
<IconAlertCircle
size="extra-large"
className="inline-flex text-[#FF2710]"
/>
}
title={renderTitle}
visible={isValidRefCount(mockSetRefCount) && visible}
onCancel={onCancel}
onOk={handleOk}
getPopupContainer={
needResetPopoverContainer ? () => document.body : undefined
}
okType="danger"
>
{I18n.t('operation_cannot_be_reversed')}
</UIModal>
);
};

View File

@@ -0,0 +1,15 @@
.mockset-create-form {
font-size: 14px;
:global {
.semi-checkbox-addon {
font-weight: 600;
color: var(--semi-color-text-0);
font-size: 14px;
}
.semi-form-field {
padding-top: 0;
}
}
}

View File

@@ -0,0 +1,301 @@
/*
* 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 { useRef } from 'react';
import { I18n } from '@coze-arch/i18n';
import { PluginMockDataGenerateMode } from '@coze-arch/bot-tea';
import {
EVENT_NAMES,
sendTeaEvent,
type ParamsTypeDefine,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type FormApi } from '@coze-arch/bot-semi/Form';
import { Form, UIFormTextArea, UIModal, UIToast } from '@coze-arch/bot-semi';
import { type ApiError, isApiError } from '@coze-arch/bot-http';
import { useFlags } from '@coze-arch/bot-flags';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import { type MockSet } from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import {
type AutoGenerateConfig,
AutoGenerateSelect,
} from '../auto-generate-select';
import { getEnvironment, getMockSubjectInfo, getPluginInfo } from '../../utils';
import { type BasicMockSetInfo } from '../../interface';
import { MOCK_SET_ERR_CODE, mockSetInfoRules } from '../../const';
import styles from './index.module.less';
export interface EditMockSetInfo
extends BasicMockSetInfo,
Partial<AutoGenerateConfig> {
id?: string;
name?: string;
desc?: string;
autoGenerate?: boolean;
}
export interface MockSetEditModalProps {
visible?: boolean;
zIndex?: number;
disabled?: boolean;
initialInfo: EditMockSetInfo;
onSuccess?: (
mockSetInfo?: MockSet,
autoGenerateConfig?: AutoGenerateConfig,
) => void;
onCancel?: () => void;
needResetPopoverContainer?: boolean;
}
function getRandomName(toolName?: string): string | undefined {
if (!toolName) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const num = Math.floor(Math.random() * 90 + 10);
return `${toolName} mockset${num}`;
}
export const MockSetEditModal = ({
visible,
zIndex,
disabled,
initialInfo,
onSuccess,
onCancel,
needResetPopoverContainer,
}: MockSetEditModalProps) => {
const formApiRef = useRef<FormApi<EditMockSetInfo>>();
const [FLAGS] = useFlags();
// 根据是否传入 id 判断是否为创建场景
const isCreate = !initialInfo.id;
// space信息
const spaceType = useSpaceStore(s => s.space.space_type);
const isPersonal = spaceType === SpaceType.Personal;
const handleAutoGenerateSelect = (config: AutoGenerateConfig) => {
formApiRef.current?.setValue?.('generateMode', String(config.generateMode));
formApiRef.current?.setValue?.(
'generateCount',
String(config.generateCount),
);
};
const handleSubmit = async (formValues: EditMockSetInfo) => {
const {
id: existingId,
name,
desc,
bindSubjectInfo,
bizCtx,
autoGenerate,
generateMode,
generateCount,
} = formValues;
const { toolID, spaceID } = getPluginInfo(bizCtx, bindSubjectInfo);
const basicParams: ParamsTypeDefine[EVENT_NAMES.create_mockset_front] = {
environment: getEnvironment(),
workspace_id: spaceID || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
tool_id: toolID || '',
status: 1,
mock_set_id: '',
auto_gen_mode: !autoGenerate
? PluginMockDataGenerateMode.MANUAL
: Number(generateMode) || PluginMockDataGenerateMode.RANDOM,
mock_counts: 1,
};
try {
const { id } = await debuggerApi.SaveMockSet(
{
name,
description: desc,
mockSubject: getMockSubjectInfo(bizCtx, bindSubjectInfo),
bizCtx,
id: existingId || '0',
},
{ __disableErrorToast: true },
);
onSuccess?.(
{ id, name, description: desc },
autoGenerate
? {
generateMode: Number(generateMode),
generateCount: Number(generateCount),
}
: undefined,
);
sendTeaEvent(EVENT_NAMES.create_mockset_front, {
...basicParams,
status: 0,
mock_set_id: String(id) || '',
});
} catch (e) {
const { message } = (e as Error | undefined) || {};
const reportParams: ParamsTypeDefine[EVENT_NAMES.create_mockset_front] = {
...basicParams,
status: 1,
error: message,
error_type: 'unknown',
};
if (isApiError(e)) {
const { code } = (e as ApiError) || {};
if (Number(code) === MOCK_SET_ERR_CODE.REPEAT_NAME) {
formApiRef.current?.setError('name', I18n.t('name_already_taken'));
sendTeaEvent(EVENT_NAMES.create_mockset_front, {
...reportParams,
error_type: 'repeat_name',
});
return;
}
}
if (message) {
UIToast.error(message);
}
sendTeaEvent(EVENT_NAMES.create_mockset_front, {
...reportParams,
error_type: 'unknown',
});
}
};
const handleOk = async () => {
await formApiRef.current?.submitForm();
};
return (
<UIModal
type="action-small"
zIndex={zIndex}
title={`${isCreate ? I18n.t('create_mockset') : I18n.t('edit_mockset')}`}
visible={visible}
getPopupContainer={
needResetPopoverContainer ? () => document.body : undefined
}
onCancel={onCancel}
okButtonProps={{
onClick: handleOk,
disabled,
}}
>
<Form<EditMockSetInfo>
getFormApi={api => (formApiRef.current = api)}
showValidateIcon={false}
initValues={
isCreate
? { ...initialInfo, name: getRandomName(initialInfo.name) }
: initialInfo
}
onSubmit={values => handleSubmit(values)}
className={styles['mockset-create-form']}
>
{({ formState }) => (
<>
{/* mockSet名称 */}
{disabled ? (
<Form.Slot
label={{
text: I18n.t('mockset_name'),
required: true,
}}
>
<div>{initialInfo?.name}</div>
</Form.Slot>
) : (
<UIFormTextArea
field="name"
label={I18n.t('mockset_name')}
placeholder={I18n.t('good_mockset_name_descriptive_concise')}
trigger={['blur', 'change']}
maxCount={50}
maxLength={50}
rows={1}
onBlur={() => {
formApiRef.current?.setValue(
'name',
formApiRef.current?.getValue('name')?.trim(),
);
}}
rules={mockSetInfoRules.name}
/>
)}
{/* mockSet描述 */}
{disabled ? (
<Form.Slot
label={{
text: I18n.t('mockset_description'),
}}
>
<div>{initialInfo?.desc}</div>
</Form.Slot>
) : (
<UIFormTextArea
field="desc"
label={{
text: I18n.t('mockset_description'),
}}
trigger={['blur', 'change']}
placeholder={I18n.t('describe_use_scenarios_of_mockset')}
rows={2}
maxCount={2000}
maxLength={2000}
rules={mockSetInfoRules.desc}
onBlur={() => {
formApiRef.current?.setValue(
'desc',
formApiRef.current?.getValue('desc')?.trim(),
);
}}
/>
)}
{/* 二期支持autoGenerate*/}
{/* 社区版暂不支持该功能 */}
{isCreate && FLAGS['bot.devops.mockset_auto_generate'] ? (
<>
<Form.Checkbox
field="autoGenerate"
noLabel
disabled={disabled}
className={styles['auto-generate-checkbox']}
>
{I18n.t('auto_generate')}
</Form.Checkbox>
{formState.values.autoGenerate ? (
<AutoGenerateSelect
onInit={handleAutoGenerateSelect}
onChange={handleAutoGenerateSelect}
/>
) : null}
</>
) : null}
</>
)}
</Form>
</UIModal>
);
};

View File

@@ -0,0 +1,73 @@
/*
* 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';
import { type RuleItem } from '@coze-arch/bot-semi/Form';
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';
export const mockSetInfoRules: {
name: Array<RuleItem>;
desc: Array<RuleItem>;
} = {
name: [
{
required: true,
message: I18n.t('please_enter_mockset_name'),
},
IS_OVERSEA
? {
pattern: /^[\w\s]+$/,
message: I18n.t('create_plugin_modal_nameerror'),
}
: {
pattern: /^[\w\s\u4e00-\u9fa5]+$/u, //
message: I18n.t('create_plugin_modal_nameerror_cn'),
},
],
desc: IS_OVERSEA
? [
{
// eslint-disable-next-line no-control-regex -- regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('create_plugin_modal_descrip_error'),
},
]
: [],
};
export const MOCK_SET_ERR_CODE = {
REPEAT_NAME: 600303100,
};
export enum MockTrafficEnabled {
DISABLE = 0,
ENABLE = 1,
}

View File

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

View File

@@ -0,0 +1,199 @@
/*
* 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 max-lines-per-function -- 代码迁移 */
/* eslint-disable @coze-arch/max-line-per-function -- 代码迁移 */
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 { isSameScene } from '../utils';
import { MockTrafficEnabled } from '../const';
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,
};
});
}
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 {
logger.error({
error: e as Error,
eventName: 'poll_scene_mockset_fail',
});
}
}
};
const request = async () => {
try {
const {
trafficCallerID,
connectorID,
connectorUID,
bizSpaceID,
trafficScene,
} = bizCtx;
!status.current && (status.current = true);
setLoading(true);
if (status.current && pollingInterval) {
setPolling(true);
const id = setTimeout(() => {
status.current && request();
}, pollingInterval);
setTimer(id);
}
await requestFn({
trafficCallerID,
connectorID,
connectorUID,
bizSpaceID,
trafficScene,
});
} finally {
setLoading(false);
}
};
// 取消
const cancel = () => {
pollingTurnRef.current = undefined;
cancelReq.current?.();
lastRequestId.current = 0;
cancelRestartTask();
if (timer) {
clearTimeout(timer);
setPolling(false);
setTimer(undefined);
status.current && (status.current = false);
}
};
const cancelRestartTask = () => {
if (restartTimer) {
clearTimeout(restartTimer);
setRestartTimer(undefined);
}
};
const start = async () => {
cancel();
pollingTurnRef.current = nanoid();
await request();
};
useEffect(() => {
if (currentBizCtx && isSameScene(bizCtx, currentBizCtx)) {
return;
}
setCurrentBizCtx(bizCtx);
}, [bizCtx]);
return {
start,
cancel,
isLoading,
data: enabledMockSetInfo,
addMockComp,
removeMockComp,
currentMockComp,
isPolling,
setRestartTimer,
restartTimer,
};
};

View File

@@ -0,0 +1,32 @@
/*
* 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 { MockTrafficEnabled, CONNECTOR_ID } from './const';
export {
MockSetSelect,
type MockSetSelectActions,
} from './components/mock-select';
export { MockSetDeleteModal } from './components/mockset-delete-modal';
export { MockSetEditModal } from './components/mockset-edit-modal';
export {
AutoGenerateSelect,
type AutoGenerateConfig,
} from './components/auto-generate-select';
export { getEnvironment, getUsedScene } from './utils';
export { type BindSubjectInfo, type BizCtxInfo } from './interface';

View File

@@ -0,0 +1,63 @@
/*
* 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 CSSProperties } from 'react';
import {
type OptionProps,
type optionRenderProps,
} from '@coze-arch/bot-semi/Select';
import {
type BizCtx,
type MockSet,
type ComponentSubject,
} from '@coze-arch/bot-api/debugger_api';
export interface BizCtxInfo
extends Omit<BizCtx, 'connectorID' | 'connectorUID' | 'ext'> {
ext?: { mockSubjectInfo?: string } & Record<string, string>;
}
export type BindSubjectInfo = ComponentSubject & { detail?: BindSubjectDetail };
export interface BasicMockSetInfo {
bindSubjectInfo: ComponentSubject;
bizCtx: BizCtx;
}
export interface BindSubjectDetail {
name?: string;
}
export interface MockSetSelectProps {
bindSubjectInfo: BindSubjectInfo;
bizCtx: BizCtxInfo;
className?: string;
style?: CSSProperties;
readonly?: boolean;
}
export enum MockSetStatus {
Incompatible = 'Incompatible',
Normal = 'Normal',
}
export interface MockSelectOptionProps extends OptionProps {
detail?: MockSet;
}
export interface MockSelectRenderOptionProps extends optionRenderProps {
detail?: MockSet;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />
declare module '*.svg' {
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement>
>;
/**
* The default export type depends on the svgDefaultExport config,
* it can be a string or a ReactComponent
* */
const content: any;
export default content;
}
declare module '*.less' {
const content: { [className: string]: string };
export default content;
}
declare const IS_OVERSEA: boolean;
declare const IS_PROD: boolean;
declare const IS_RELEASE_VERSION: boolean;
declare const IS_BOE: boolean;

View File

@@ -0,0 +1,53 @@
/*
* 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 { PluginMockDataGenerateMode } from '@coze-arch/bot-tea';
const LOCAL_STORAGE_KEY = 'mockset_auto_generate_latest_choice';
let latestAutoGenerationChoice = PluginMockDataGenerateMode.MANUAL;
async function getFromLocalStorage() {
const info = await localStorage.getItem(LOCAL_STORAGE_KEY);
if (Number(info) === PluginMockDataGenerateMode.LLM) {
latestAutoGenerationChoice = PluginMockDataGenerateMode.LLM;
} else {
latestAutoGenerationChoice = PluginMockDataGenerateMode.RANDOM;
}
if (!info || Number.isNaN(Number(info))) {
localStorage.setItem(
LOCAL_STORAGE_KEY,
String(PluginMockDataGenerateMode.RANDOM),
);
}
return latestAutoGenerationChoice;
}
export function getLatestAutoGenerationChoice() {
if (latestAutoGenerationChoice === PluginMockDataGenerateMode.MANUAL) {
return getFromLocalStorage();
} else {
return latestAutoGenerationChoice;
}
}
export function setLatestAutoGenerationChoice(
choice: PluginMockDataGenerateMode.RANDOM | PluginMockDataGenerateMode.LLM,
) {
latestAutoGenerationChoice = choice;
localStorage.setItem(LOCAL_STORAGE_KEY, String(choice));
}

View File

@@ -0,0 +1,131 @@
/*
* 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 { isEqual } from 'lodash-es';
import { safeJSONParse } from '@coze-arch/bot-utils';
import {
type BizCtx,
type ComponentSubject,
TrafficScene,
type MockSet,
} from '@coze-arch/bot-api/debugger_api';
import { type BasicMockSetInfo, type BizCtxInfo } from '../interface';
import { REAL_DATA_MOCKSET } from '../const';
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 getPluginInfo(
bizCtx: BizCtxInfo,
mockSubjectInfo: ComponentSubject,
): { spaceID?: string; pluginID?: string; toolID?: string } {
const { bizSpaceID, ext, trafficScene } = bizCtx || {};
const extMockSubjectInfo = safeJSONParse(ext?.mockSubjectInfo || '{}');
const { componentID, parentComponentID } = mockSubjectInfo;
switch (trafficScene) {
case TrafficScene.CozeWorkflowDebug:
return {
spaceID: bizSpaceID,
toolID: extMockSubjectInfo?.componentID,
pluginID: extMockSubjectInfo?.parentComponentID,
};
case TrafficScene.CozeSingleAgentDebug:
case TrafficScene.CozeMultiAgentDebug:
case TrafficScene.CozeToolDebug:
default:
return {
spaceID: bizSpaceID,
toolID: componentID,
pluginID: parentComponentID,
};
}
}
export function getMockSubjectInfo(
bizCtx: BizCtxInfo,
mockSubjectInfo: ComponentSubject,
) {
const { ext, trafficScene } = bizCtx || {};
const extMockSubjectInfo = safeJSONParse(ext?.mockSubjectInfo || '{}');
switch (trafficScene) {
case TrafficScene.CozeWorkflowDebug:
return extMockSubjectInfo;
case TrafficScene.CozeSingleAgentDebug:
case TrafficScene.CozeMultiAgentDebug:
case TrafficScene.CozeToolDebug:
default:
return mockSubjectInfo;
}
}
export function getEnvironment() {
if (!IS_PROD) {
return 'cn-boe';
}
const regionPart = IS_OVERSEA ? 'oversea' : 'cn';
const inhousePart = IS_RELEASE_VERSION ? 'release' : 'inhouse';
return [regionPart, inhousePart].join('-');
}
export function getUsedScene(scene?: TrafficScene): 'bot' | 'agent' | 'flow' {
switch (scene) {
case TrafficScene.CozeSingleAgentDebug:
return 'bot';
case TrafficScene.CozeMultiAgentDebug:
return 'agent';
case TrafficScene.CozeWorkflowDebug:
return 'flow';
case TrafficScene.CozeToolDebug:
return 'bot';
default:
return 'bot';
}
}

View File

@@ -0,0 +1,73 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"jsx": "react-jsx",
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"paths": {
"@/*": ["./src/*"]
},
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-env/tsconfig.build.json"
},
{
"path": "../../arch/bot-error/tsconfig.build.json"
},
{
"path": "../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../arch/bot-hooks/tsconfig.build.json"
},
{
"path": "../../arch/bot-http/tsconfig.build.json"
},
{
"path": "../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../arch/bot-tea/tsconfig.build.json"
},
{
"path": "../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../arch/logger/tsconfig.build.json"
},
{
"path": "../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../studio/user-store/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,22 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"jsx": "react-jsx",
"types": ["react", "react-dom", "vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
});