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-agent-ide/bot-plugin-tools
> 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,734 @@
/*
* 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 { describe, expect, it, vi } from 'vitest';
import { cloneDeep } from 'lodash-es';
import {
resetStoreKey,
setStoreExampleValue,
setWorkflowExampleValue,
typesConfig,
} from '../../../src/hooks/example/utils.ts';
vi.mock('nanoid', () => ({
nanoid: vi.fn(() => 'id'),
}));
describe('resetStoreKey', () => {
it('resets correctly', () => {
const original = [
{
name: 'arr',
desc: 'arr',
required: false,
type: 'array',
sub_params: [],
},
{
name: 'arrobj',
desc: 'arrobj',
required: false,
type: 'array',
sub_params: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 'string',
sub_params: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 'string',
sub_params: [],
},
],
},
{
name: 'obj',
desc: 'obj',
required: false,
type: 'object',
sub_params: [
{
type: 'string',
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
},
{
name: 'key2',
required: false,
description: 'key2',
type: 'string',
sub_params: [],
},
],
},
];
const expected = [
{
name: 'arr',
desc: 'arr',
required: false,
type: 5,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [],
},
],
},
{
name: 'arrobj',
desc: 'arrobj',
required: false,
type: 5,
sub_params: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
},
],
},
{
name: 'obj',
desc: 'obj',
required: false,
type: 4,
sub_params: [
{
type: 1,
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'key2',
required: false,
description: 'key2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
type: 1,
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'key2',
required: false,
description: 'key2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
},
];
const resetTarget = cloneDeep(original);
resetStoreKey(resetTarget, typesConfig);
expect(resetTarget).toEqual(expected);
});
});
describe('setStoreExampleValue', () => {
it('set data', () => {
const original = [
{
name: 'arr',
desc: 'arr',
required: false,
type: 'array',
sub_params: [],
},
{
name: 'arrobj',
desc: 'arrobj',
required: false,
type: 'array',
sub_params: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 'string',
sub_params: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 'string',
sub_params: [],
},
],
},
{
name: 'obj',
desc: 'obj',
required: false,
type: 'object',
sub_params: [
{
type: 'string',
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
},
{
name: 'key2',
required: false,
description: 'key2',
type: 'string',
sub_params: [],
},
],
},
];
const originalExampleValue = {
arr: ['564567'],
arrobj: [
{
objkey1: '66',
objkey2: '777',
},
],
obj: {
key2: '456456',
},
};
const expected = [
{
name: 'arr',
desc: 'arr',
required: false,
type: 5,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [],
},
],
global_default: '["564567"]',
},
{
name: 'arrobj',
desc: 'arrobj',
required: false,
type: 5,
sub_params: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [
{
name: 'objkey1',
required: false,
description: 'objkey1',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'objkey2',
required: false,
description: 'objkey2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
],
},
],
global_default: '[{"objkey1":"66","objkey2":"777"}]',
},
{
name: 'obj',
desc: 'obj',
required: false,
type: 4,
sub_params: [
{
type: 1,
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'key2',
required: false,
description: 'key2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
global_default: '456456',
},
],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [
{
type: 1,
sub_params: [],
name: 'key1',
required: false,
description: 'key1',
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
},
{
name: 'key2',
required: false,
description: 'key2',
type: 1,
sub_params: [],
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
sub_parameters: [],
global_default: '456456',
},
],
},
];
const resetTarget = cloneDeep(original);
setStoreExampleValue(resetTarget, originalExampleValue);
expect(resetTarget).toEqual(expected);
});
});
describe('setWorkflowExampleValue', () => {
it('set data', () => {
const original = [
{
description: 'arr',
name: 'arr',
required: false,
schema: {
type: 'string',
},
type: 'list',
},
{
description: 'arrobj',
name: 'arrobj',
required: false,
schema: {
schema: [
{
description: 'objkey1',
name: 'objkey1',
required: false,
type: 'string',
},
{
description: 'objkey2',
name: 'objkey2',
required: false,
type: 'string',
},
],
type: 'object',
},
type: 'list',
},
{
description: 'obj',
name: 'obj',
required: false,
schema: [
{
description: 'key1',
name: 'key1',
required: false,
type: 'string',
},
{
description: 'key2',
name: 'key2',
required: false,
type: 'string',
},
],
type: 'object',
},
];
const originalExampleValue = {
arr: ['564567'],
arrobj: [
{
objkey1: '66',
objkey2: '777',
},
],
obj: {
key2: '456456',
},
};
const expected = [
{
description: 'arr',
name: 'arr',
required: false,
schema: {
type: 'string',
},
type: 5,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'arr',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
type: 1,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [],
},
],
global_default: '["564567"]',
},
{
description: 'arrobj',
name: 'arrobj',
required: false,
schema: {
schema: [
{
description: 'objkey1',
name: 'objkey1',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'objkey1',
sub_parameters: [],
},
{
description: 'objkey2',
name: 'objkey2',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'objkey2',
sub_parameters: [],
},
],
type: 'object',
},
type: 5,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'arrobj',
sub_parameters: [
{
name: '[Array Item]',
is_required: false,
type: 4,
global_disable: false,
local_disable: false,
sub_type: 0,
sub_parameters: [
{
description: 'objkey1',
name: 'objkey1',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'objkey1',
sub_parameters: [],
},
{
description: 'objkey2',
name: 'objkey2',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'objkey2',
sub_parameters: [],
},
],
},
],
global_default: '[{"objkey1":"66","objkey2":"777"}]',
},
{
description: 'obj',
name: 'obj',
required: false,
schema: [
{
description: 'key1',
name: 'key1',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'key1',
sub_parameters: [],
},
{
description: 'key2',
name: 'key2',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'key2',
sub_parameters: [],
global_default: '456456',
},
],
type: 4,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'obj',
sub_parameters: [
{
description: 'key1',
name: 'key1',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'key1',
sub_parameters: [],
},
{
description: 'key2',
name: 'key2',
required: false,
type: 1,
is_required: false,
global_disable: false,
local_disable: false,
id: 'id',
desc: 'key2',
sub_parameters: [],
global_default: '456456',
},
],
},
];
const resetTarget = cloneDeep(original);
setWorkflowExampleValue(resetTarget, originalExampleValue);
expect(resetTarget).toEqual(expected);
});
});

View File

@@ -0,0 +1,98 @@
/*
* 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 { describe, expect, it, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react-hooks';
import { useParametersInSettingModalController } from '../../../src/hooks/parameters/use-parameters-in-setting-modal-controller';
vi.mock('@coze-arch/bot-flags', () => ({
useFlags: vi.fn(),
}));
vi.mock('@coze-arch/bot-studio-store', () => ({
useSpaceStore: {
getState: vi.fn(() => ({
getSpaceId: vi.fn(() => 'test-space-id'),
})),
},
}));
vi.mock('@coze-arch/bot-api', () => ({
PluginDevelopApi: {
GetBotDefaultParams: vi.fn().mockResolvedValue({
request_params: [
{
id: '1',
name: 'Request Param 1',
local_default: 'Default Value 1',
},
],
response_params: [
{
id: '2',
name: 'Response Param 1',
local_default: 'Default Value 2',
},
],
}),
UpdateBotDefaultParams: vi.fn().mockResolvedValue({
code: 0,
}),
},
}));
describe('useParametersInSettingModalController', () => {
it('should initialize requestParams and responseParams correctly', async () => {
const { result, waitForValueToChange } = renderHook(() =>
useParametersInSettingModalController({
botId: 'test-bot-id',
devId: 'test-dev-id',
pluginId: 'test-plugin-id',
apiName: 'test-api-name',
}),
);
await waitForValueToChange(() => result.current.requestParams);
expect(result.current.requestParams.length).toBe(1);
expect(result.current.requestParams[0].local_default).toBe(
'Default Value 1',
);
expect(result.current.responseParams.length).toBe(1);
expect(result.current.responseParams[0].local_default).toBe(
'Default Value 2',
);
expect(result.current.loaded).toBe(true);
});
it('should update requestParams and responseParams on handleUpdate', async () => {
const { result } = renderHook(() =>
useParametersInSettingModalController({
botId: 'test-bot-id',
devId: 'test-dev-id',
pluginId: 'test-plugin-id',
apiName: 'test-api-name',
}),
);
await act(() => {
result.current.doUpdateParams();
});
expect(result.current.isUpdateLoading).toBe(false); // 假设更新完成后isUpdateLoading为false
});
});

View File

@@ -0,0 +1,594 @@
/*
* 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-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {
addDepthAndValue,
checkHasArray,
checkSameName,
cloneWithRandomKey,
defaultNode,
deleteAllChildNode,
deleteNode,
findPathById,
findTemplateNodeByPath,
handleDeepArr,
isShowDelete,
maxDeep,
// sleep,
transformTreeToObj,
updateNodeById,
} from '../../src/components/plugin_modal/utils';
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn() },
}));
vi.mock('@coze-arch/bot-api/plugin_develop', () => ({
APIParameter: {},
ParameterLocation: {},
ParameterType: {
String: 1,
Integer: 2,
Number: 3,
Object: 4,
Array: 5,
Bool: 6,
},
}));
describe('findPathById', () => {
it('should traverse all nodes and call the callback function at each node', () => {
const targetId = 2;
const data = [
{ id: 1, name: 'Node 1', children: [{ id: 2, name: 'Node 2' }] },
{ id: 3, name: 'Node 3', children: [{ id: 4, name: 'Node 4' }] },
];
const callback = (item: { id: number; name: string }, path) => {
if (item.id === targetId) {
expect(path).toEqual([0, 0]);
expect(item.name).toEqual('Node 2');
}
};
findPathById({
data,
callback,
childrenName: 'children',
});
});
});
describe('addDepthAndValue', () => {
// 测试 1验证函数是否正常工作
it('should add depth to each node in the tree', () => {
const tree = [{ id: 1, sub_parameters: [{ id: 2 }, { id: 3 }] }, { id: 4 }];
addDepthAndValue(tree);
expect(tree[0].deep).toEqual(1);
expect(tree[0].sub_parameters[0].deep).toEqual(2);
expect(tree[0].sub_parameters[1].deep).toEqual(2);
expect(tree[1].deep).toEqual(1);
});
// 测试 2验证函数在空树情况下是否正常工作
it('should not fail on empty trees', () => {
const tree: any[] = [];
addDepthAndValue(tree);
expect(tree).toEqual([]);
});
// 测试 3验证函数在只有一个节点的树情况下是否正常工作
it('should handle single-node trees', () => {
const tree = [{ id: 1 }];
addDepthAndValue(tree);
expect(tree[0].deep).toEqual(1);
});
});
describe('handleDeepArr', () => {
it('should handle deep array correctly', () => {
const tree = [
{ deep: 1 },
{ deep: 2 },
{
deep: 3,
sub_parameters: [{ deep: 4 }, { deep: 5 }],
},
];
const deepArr = [];
handleDeepArr(tree, deepArr);
// 断言
expect(deepArr).toEqual([1, 2, 3, 4, 5]);
});
});
describe('maxDeep', () => {
it('should return the correct max deep', () => {
const tree1 = [
{ deep: 1 },
{ deep: 2 },
{
deep: 3,
sub_parameters: [{ deep: 4 }, { deep: 5 }],
},
];
// 测试 tree1 的最大深度
expect(maxDeep(tree1)).toEqual(5);
});
it('should return 0 for an empty tree or a tree with a single node', () => {
const tree2 = [];
const tree3 = [{ deep: 1 }];
// 测试空树的最大深度
expect(maxDeep(tree2)).toEqual(0);
// 测试只有一个节点的树的最大深度
expect(maxDeep(tree3)).toEqual(1);
});
});
describe('defaultNode', () => {
it('get default node', () => {
const result = defaultNode(false, false, 1);
expect(result).toEqual({
id: expect.any(String),
name: '',
desc: '',
type: 1,
location: undefined,
is_required: true,
sub_parameters: [],
deep: 1,
});
});
});
describe('deleteNode', () => {
it('should delete the node with the target key', () => {
const data = [
{ id: 'key1', sub_parameters: [] },
{ id: 'key2', sub_parameters: [] },
{ id: 'key3', sub_parameters: [] },
];
const targetKey = 'key2';
const result = deleteNode(data, targetKey);
// 断言删除成功
expect(result).toBe(true);
// 断言目标节点已删除
expect(data.find(node => node.id === targetKey)).toBeUndefined();
});
it('should return false if the target node is not found', () => {
const data = [
{ id: 'key1', sub_parameters: [] },
{ id: 'key2', sub_parameters: [] },
{ id: 'key3', sub_parameters: [] },
];
const targetKey = 'key4';
const result = deleteNode(data, targetKey);
// 断言删除失败
expect(result).toBe(false);
});
it('should delete a node with children', () => {
const data = [
{ id: 'key1', sub_parameters: [] },
{
id: 'key2',
sub_parameters: [
{ id: 'key3', sub_parameters: [] },
{ id: 'key4', sub_parameters: [] },
],
},
{ id: 'key5', sub_parameters: [] },
];
const targetKey = 'key2';
const result = deleteNode(data, targetKey);
// 断言删除成功
expect(result).toBe(true);
// 断言目标节点已删除
expect(data.find(node => node.id === targetKey)).toBeUndefined();
// 断言子节点已删除
expect(data[1].sub_parameters.length).toBe(0);
});
});
describe('deleteAllChildNode', () => {
const mockData = [
{
id: 'key1',
sub_parameters: [
{
id: 'key2',
sub_parameters: [],
},
],
},
{
id: 'key3',
sub_parameters: [
{
id: 'key4',
sub_parameters: [
{
id: 'key5',
sub_parameters: [],
},
],
},
],
},
{
id: 'key6',
sub_parameters: [],
},
];
it('should delete all child nodes of the target key', () => {
const targetKey = 'key1';
const result = deleteAllChildNode(mockData, targetKey);
expect(result).toBe(true);
expect(mockData[0].sub_parameters).toEqual([]);
});
it('should not delete any child nodes if target key is not found', () => {
const targetKey = 'key7';
const result = deleteAllChildNode(mockData, targetKey);
expect(result).toBe(false);
});
it('should delete all child nodes of the target key in nested structure', () => {
const targetKey = 'key4';
const result = deleteAllChildNode(mockData, targetKey);
expect(result).toBe(true);
expect(mockData[1].sub_parameters[0].sub_parameters).toEqual([]);
});
});
describe('updateNodeById', () => {
// 创建测试数据
const data = [
{
id: '1',
name: 'Node 1',
sub_parameters: [
{ id: '2', name: 'Node 2', sub_parameters: [] },
{ id: '3', name: 'Node 3', sub_parameters: [] },
],
},
{ id: '4', name: 'Node 4', sub_parameters: [] },
{ id: '5', name: 'Node 5', sub_parameters: [] },
];
it('should update the node data', () => {
const targetKey = '4';
const field = 'name';
const value = 'Updated Node';
// 调用被测试的函数
updateNodeById({ data, targetKey, field, value });
// 验证节点数据是否已更新
expect(data[1].name).toEqual(value);
});
it('should update the node data in nested structure', () => {
const targetKey = '3';
const field = 'name';
const value = 'Updated Node';
// 调用被测试的函数
updateNodeById({ data, targetKey, field, value });
expect(data[0].sub_parameters[1].name).toEqual(value);
});
});
describe('findTemplateNodeByPath', () => {
it('should return the correct path', () => {
const dsl = {
a: {
b: {
c: 1,
},
},
};
const path = ['a', 'b', 'c'];
const result = findTemplateNodeByPath(dsl, path);
expect(result).toEqual(path);
});
it('should return the correct path when the path does not exist', () => {
const dsl = {
a: {
b: {
c: 1,
},
},
};
const path = ['a', 'b', 'd'];
const result = findTemplateNodeByPath(dsl, path);
expect(result).toEqual([path[0], path[1], 0]);
});
it('should return the correct path when the path is empty', () => {
const dsl = {
a: {
b: {
c: 1,
},
},
};
const result = findTemplateNodeByPath(dsl, []);
expect(result).toEqual([]);
});
});
describe('transformTreeToObj', () => {
it('should convert a tree to an object', () => {
// 创建一个树结构的参数数组
const tree = [
{
name: 'stringParam',
type: 1,
value: 'Hello, World!',
},
{
name: 'numberParam',
type: 2,
value: 42,
},
{
name: 'boolParam',
type: 6,
value: 'true',
},
{
name: 'objectParam',
type: 4,
sub_parameters: [
{
name: 'nestedString',
type: 1,
value: 'Nested Value',
},
],
},
{
name: 'arrayParam',
type: 5,
sub_parameters: [
{
name: 'stringItem',
type: 1,
value: 'String in Array',
},
{
name: 'numberItem',
type: 2,
value: 101,
},
],
},
];
// 调用函数并将结果存储在变量 obj 中
const obj = transformTreeToObj(tree);
// 检查转换后的对象是否具有正确的属性和值
expect(obj).toEqual({
stringParam: 'Hello, World!',
numberParam: 42,
boolParam: true,
objectParam: {
nestedString: 'Nested Value',
},
arrayParam: ['String in Array', 101],
});
});
it('Optional parameter', () => {
const tree = [
{
name: 'boolParam',
type: 6,
},
{
name: 'objectParam',
type: 4,
sub_parameters: [
{
name: 'nestedString',
type: 1,
},
],
},
];
const obj = transformTreeToObj(tree);
expect(obj).toEqual({});
});
});
describe('cloneWithRandomKey', () => {
it('should clone an object with random key', () => {
const obj = {
id: 'value1',
prop2: {
subProp1: 'subValue1',
},
value: [1, 2, 3],
};
const clone = cloneWithRandomKey(obj);
expect(clone.id).not.toEqual(obj.id);
expect(clone.value).toEqual(null);
expect(clone.prop2).toEqual(obj.prop2);
});
it('with sub_parameters', () => {
const obj = {
id: 'value1',
prop2: {
sub_parameters: [
{
id: 2,
prop2: 'props',
value: [23],
},
],
},
value: [1, 2, 3],
};
const clone = cloneWithRandomKey(obj);
expect(clone.id).not.toEqual(obj.id);
expect(clone.value).toEqual(null);
expect(clone.prop2.sub_parameters.id).not.toEqual(
obj.prop2.sub_parameters[0].id,
);
expect(clone.prop2.sub_parameters[0].value).toEqual(null);
expect(clone.prop2.sub_parameters[0].prop2).toEqual(
obj.prop2.sub_parameters[0].prop2,
);
});
});
describe('checkHasArray', () => {
it('should return false when the input is not an array', () => {
const result = checkHasArray({});
expect(result).toBe(false);
});
it('should return true when the input is an array with at least one item of type Array', () => {
const data = [
{ type: 5 },
{ type: 6, sub_parameters: [{ type: 1, value: 'string' }] },
];
const result = checkHasArray(data);
expect(result).toBe(true);
});
it('should return false when the input is an empty array', () => {
const data: unknown[] = [];
const result = checkHasArray(data);
expect(result).toBe(false);
});
it('should return false when the input is an array without any items of type Array', () => {
const data = [{ type: 6 }, { type: 1 }];
const result = checkHasArray(data);
expect(result).toBe(false);
});
it('should recusively check child arrays when an item with children is encountered', () => {
const data = [
{ type: 6 },
{
type: 5,
sub_parameters: [{ type: 6 }, { type: 5 }],
},
];
const result = checkHasArray(data);
expect(result).toBe(true);
});
});
describe('checkSameName', () => {
const data = [
{
id: '1',
name: 'Node 1',
sub_parameters: [
{ id: '2', name: 'Node 2', sub_parameters: [] },
{ id: '2', name: 'Node 2', sub_parameters: [] },
{ id: '3', name: 'Node 3', sub_parameters: [] },
],
},
{ id: '4', name: 'Node 4', sub_parameters: [] },
{ id: '4', name: 'Node 4', sub_parameters: [] },
{ id: '5', name: 'Node 5', sub_parameters: [] },
];
it('should return true if there are multiple items with the same name', () => {
const result = checkSameName(data, '4', 'Node 4');
expect(result).toBe(true);
});
it('should return false if there are no items with the same name', () => {
const result = checkSameName(data, '1', 'Node 1');
expect(result).toBe(false);
});
it('should return undefined if the target key is not found', () => {
const result = checkSameName(data, '6', '180');
expect(result).toBeUndefined();
});
it('should work with nested data', () => {
const result = checkSameName(data, '2', 'Node 2');
expect(result).toBe(true);
});
});
describe('isShowDelete', () => {
const data = [
{
id: 'key1',
sub_parameters: [
{ id: 'key2', sub_parameters: [] },
{ id: 'key4', sub_parameters: [] },
],
},
{ id: 'key3', sub_parameters: [{ id: 'key5', sub_parameters: [] }] },
];
it('should return true if targetKey is found in children records', () => {
const targetKey = 'key1';
const result = isShowDelete(data, targetKey);
expect(result).toBe(true);
});
it('should return true if targetKey is found in children records', () => {
const targetKey = 'key4';
const result = isShowDelete(data, targetKey);
expect(result).toBe(true);
});
it('should return false if targetKey is found and data has only 1 element', () => {
const targetKey = 'key5';
const result = isShowDelete(data, targetKey);
expect(result).toBe(false || undefined);
});
});
// describe('sleep', () => {
// it('should resolve with the correct value after the specified time', async () => {
// const timeStart = new Date().getTime();
// const result = await sleep(1000);
// const timeEnd = new Date().getTime();
// expect(result).toEqual(0);
// expect(timeEnd - timeStart >= 1000).toEqual(true);
// });
// });

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,129 @@
{
"name": "@coze-agent-ide/bot-plugin-tools",
"version": "0.0.1",
"description": "plugin tools",
"license": "Apache-2.0",
"author": "lihuiwen.123@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.tsx",
"./useViewExample": "./src/hooks/example/use-view-example.tsx",
"./defaultValueInput": "./src/components/plugin_modal/params-components/default-value-input.tsx",
"./pluginModal/config": "./src/components/plugin_modal/config.ts",
"./pluginModal/types": "./src/components/plugin_modal/types",
"./pluginModal/debug": "./src/components/plugin_modal/debug",
"./pluginModal/form-components": "./src/components/plugin_modal/params-components/form-components.tsx",
"./infoPopover": "./src/components/info_popover/index.ts",
"./pluginModal/utils": "./src/components/plugin_modal/utils.ts",
"./example/utils": "./src/hooks/example/utils.ts",
"./example/useDebugFooter": "./src/hooks/example/use-debug-footer.tsx",
"./useResponseParams": "./src/components/plugin_modal/response-params",
"./useRequestParams": "./src/components/plugin_modal/request-params",
"./useBaseInfo": "./src/components/plugin_modal/base-info",
"./useBaseMore": "./src/components/plugin_modal/base-more",
"./useParametersInSettingModalController": "./src/hooks/parameters/use-parameters-in-setting-modal-controller.ts"
},
"main": "src/index.tsx",
"typesVersions": {
"*": {
"useViewExample": [
"./src/hooks/example/use-view-example.tsx"
],
"defaultValueInput": [
"./src/components/plugin_modal/params-components/default-value-input.tsx"
],
"pluginModal/config": [
"./src/components/plugin_modal/config.ts"
],
"pluginModal/types": [
"./src/components/plugin_modal/types"
],
"pluginModal/debug": [
"./src/components/plugin_modal/debug"
],
"infoPopover": [
"./src/components/info_popover/index.ts"
],
"pluginModal/utils": [
"./src/components/plugin_modal/utils.ts"
],
"./pluginModal/form-components": [
"./src/components/plugin_modal/params-components/form-components.tsx"
],
"example/utils": [
"./src/hooks/example/utils.ts"
],
"example/useDebugFooter": [
"./src/hooks/example/use-debug-footer.tsx"
],
"useResponseParams": [
"./src/components/plugin_modal/response-params"
],
"useRequestParams": [
"./src/components/plugin_modal/request-params"
],
"useBaseInfo": [
"./src/components/plugin_modal/base-info"
],
"useBaseMore": [
"./src/components/plugin_modal/base-more"
],
"useParametersInSettingModalController": [
"./src/hooks/parameters/use-parameters-in-setting-modal-controller.ts"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-md-box-adapter": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-studio/bot-plugin-store": "workspace:*",
"@coze-studio/bot-utils": "workspace:*",
"@coze-studio/file-kit": "workspace:*",
"@coze-studio/plugin-shared": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@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",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View File

@@ -0,0 +1,37 @@
.checkbox {
display: flex;
align-items: center;
.content {
display: flex;
gap: 12px;
align-items: center;
}
.label {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: var(--Light-usage-text---color-text-0, #1D1C24);
}
.line {
align-items: center;
width: 1px;
height: 16px;
margin-left: 12px;
background-color: var(--Light-usage-border---color-border-1, rgba(29, 28, 37, 12%));
}
.tip {
position: absolute;
top: 2px;
left: 0;
width: 16px;
height: 16px;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 FC, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Checkbox, Tooltip } from '@coze-arch/bot-semi';
import styles from './index.module.less';
interface ExampleCheckboxProps {
value: boolean;
onValueChange: (v: boolean) => void;
}
export const ExampleCheckbox: FC<ExampleCheckboxProps> = ({
value,
onValueChange,
}) => {
const [showTip, setShowTip] = useState(false);
// @ts-expect-error -- linter-disable-autofix
const onChange = e => {
onValueChange(e.target.checked);
if (!e.target.checked) {
setShowTip(false);
}
};
return (
<div className={styles.checkbox}>
<div className={styles.content}>
<Tooltip
content={I18n.t('plugin_edit_tool_test_run_cancel_example')}
visible={showTip}
trigger="custom"
>
<Checkbox
onChange={onChange}
checked={value}
onMouseEnter={() => value && setShowTip(true)}
onMouseLeave={() => setShowTip(false)}
></Checkbox>
</Tooltip>
<div className={styles.label}>
{I18n.t('plugin_edit_tool_test_run_save_results_as_example')}
</div>
</div>
<div className={styles.line}></div>
</div>
);
};

View File

@@ -0,0 +1,91 @@
/*
* 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 FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { UIModal } from '@coze-arch/bot-semi';
import { type PluginAPIInfo } from '@coze-arch/bot-api/plugin_develop';
import { STATUS } from '../plugin_modal/types/modal';
import { Debug } from '../plugin_modal/debug';
import { useDebugFooter } from '../../hooks/example/use-debug-footer';
export enum ExampleScene {
ViewExample,
EditExample,
ReadonlyExample,
}
interface ExampleModalProps {
visible: boolean;
onCancel: () => void;
apiInfo: PluginAPIInfo;
pluginId: string;
pluginName: string;
onSave?: () => void;
}
export const ExampleModal: FC<ExampleModalProps> = ({
visible,
onCancel,
apiInfo,
pluginId,
pluginName,
onSave,
}) => {
const [dugStatus, setDebugStatus] = useState<STATUS | undefined>(STATUS.FAIL);
const onNextStep = () => {
onSave?.();
setDebugStatus(undefined);
};
const cancelHandle = () => {
onCancel();
setDebugStatus(undefined);
};
const { debugFooterNode, setDebugExample, debugExample } = useDebugFooter({
apiInfo,
loading: false,
dugStatus,
btnLoading: false,
nextStep: onNextStep,
});
return (
<UIModal
title={I18n.t('plugin_edit_tool_edit_example')}
visible={visible}
width={1280}
style={{ height: 'calc(100vh - 140px)', minWidth: '1040px' }}
centered
onCancel={cancelHandle}
footer={<div>{debugFooterNode}</div>}
>
{apiInfo ? (
<Debug
disabled={false}
isViewExample={true}
setDebugStatus={setDebugStatus}
pluginId={pluginId}
apiId={apiInfo?.api_id ?? ''}
apiInfo={apiInfo as PluginAPIInfo}
pluginName={pluginName}
setDebugExample={setDebugExample}
debugExample={debugExample}
/>
) : null}
</UIModal>
);
};

View File

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

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 { Fragment } from 'react';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip, Typography } from '@coze-arch/coze-design';
import { type ExtInfoText } from '@coze-studio/plugin-shared';
interface InfoPopoverProps {
data: ExtInfoText[];
}
export const InfoPopover: React.FC<InfoPopoverProps> = props => {
const { data } = props;
return (
<Tooltip
showArrow
theme="dark"
position="right"
arrowPointAtCenter
className="!max-w-[320px]"
content={data?.map((item, index) => (
<Fragment key={`${item.type}${index}`}>
{/* 加粗标题 */}
{item.type === 'title' ? (
<Typography.Text fontSize="14px" className="dark coz-fg-primary">
{item.text}
</Typography.Text>
) : null}
{/* 文本 */}
{item.type === 'text' ? (
<Typography.Paragraph
fontSize="12px"
className="dark coz-fg-secondary"
>
{item.text}
</Typography.Paragraph>
) : null}
{/* 换行 */}
{item.type === 'br' ? <div className="h-[8px]" /> : null}
{/* 示例,边框内展示 */}
{item.type === 'demo' ? (
<div className="dark mt-[4px] p-[10px] border border-solid coz-stroke-primary">
<Typography.Paragraph
fontSize="12px"
className="dark coz-fg-secondary"
>
{item.text}
</Typography.Paragraph>
</div>
) : null}
</Fragment>
))}
>
<IconCozInfoCircle className="coz-fg-secondary" />
</Tooltip>
);
};

View File

@@ -0,0 +1,296 @@
/*
* 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 {
type Dispatch,
type SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import { useMemoizedFn } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIFormTextArea, Toast, Form } from '@coze-arch/bot-semi';
import {
APIMethod,
PluginType,
type UpdateAPIResponse,
type CreateAPIRequest,
type CreateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { ERROR_CODE, type RenderEnhancedComponentProps } from './types';
import s from './index.module.less';
export interface UseBaseInfoRequest {
space_id: string;
pluginId: string;
apiId?: string;
baseInfo?: {
name?: string;
desc?: string;
};
setApiId?: (id: string) => void;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
showModal: boolean;
disabled: boolean;
editVersion?: number;
showFunctionName?: boolean;
pluginType?: PluginType;
onSuccess?: (params: UpdateAPIResponse | CreateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderDescComponent'];
}
export interface UseBaseInfoReturnValue {
submitBaseInfo: () => Promise<boolean>;
baseInfoNode: JSX.Element;
}
const ENTER_KEY_CODE = 13;
export const useBaseInfo = ({
space_id,
pluginId,
apiId = '',
baseInfo = {},
setApiId,
showModal,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
showFunctionName = false,
pluginType,
onSuccess,
renderEnhancedComponent,
}: UseBaseInfoRequest): UseBaseInfoReturnValue => {
const formRef = useRef<Form>(null);
const [originDesc, setOriginDesc] = useState<string | undefined>(undefined);
useEffect(() => {
setOriginDesc(baseInfo?.desc);
formRef.current?.formApi.setValues({
name: baseInfo.name,
desc: baseInfo.desc,
});
}, [baseInfo.name, baseInfo.desc, showModal, disabled]);
const doSetDesc = useMemoizedFn((desc: string) => {
formRef.current?.formApi.setValue('desc', desc);
});
// 提交基础信息
const submitBaseInfo = async () => {
const status = await formRef.current?.formApi
.validate()
.then(() => true)
.catch(() => false);
if (!status) {
return false;
}
let baseResData;
const formValues = formRef.current?.formApi.getValues();
const params: CreateAPIRequest = {
plugin_id: pluginId,
name: formValues.name,
desc: formValues.desc,
edit_version: editVersion,
function_name: formValues.function_name,
};
try {
if (apiId) {
baseResData = await PluginDevelopApi.UpdateAPI(
{
...params,
api_id: apiId,
},
{
__disableErrorToast: true,
},
);
} else {
baseResData = await PluginDevelopApi.CreateAPI(
{
...params,
method: APIMethod.POST,
path: `/${params.name}`,
},
{
__disableErrorToast: true,
},
);
setApiId?.((baseResData as CreateAPIResponse).api_id || '');
}
onSuccess?.(baseResData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const changeVal = () => {
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
};
return {
submitBaseInfo,
baseInfoNode: (
<>
<Form<Record<string, unknown>>
showValidateIcon={false}
ref={formRef}
disabled={disabled}
className={s['base-info-form']}
>
{() =>
disabled ? (
<>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_name'),
required: true,
}}
>
{baseInfo.name}
</Form.Slot>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_dercribe'),
required: true,
}}
>
{baseInfo.desc}
</Form.Slot>
</>
) : (
<>
<UIFormTextArea
data-testid="plugin-create-tool-base-info-name"
className={s['textarea-single-line']}
field="name"
label={I18n.t('Create_newtool_s1_name')}
placeholder={I18n.t('Create_newtool_s1_title_empty')}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rows={1}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onKeyDown={(ele: any) => {
const e = window.event || ele;
if (
e.key === 'Enter' ||
e.code === 'Enter' ||
e.keyCode === ENTER_KEY_CODE
) {
e.returnValue = false;
return false;
}
}}
onChange={changeVal}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_title_empty'),
},
{
pattern: /^[a-zA-Z0-9_]+$/,
message: I18n.t('Create_newtool_s1_title_error1'),
},
]}
/>
<div className="relative">
{renderEnhancedComponent?.({
disabled: !originDesc,
originDesc,
className: 'absolute right-[0] top-[12px]',
plugin_id: pluginId,
space_id,
onSetDescription: doSetDesc,
})}
<UIFormTextArea
data-testid="plugin-create-tool-base-info-desc"
field="desc"
label={I18n.t('Create_newtool_s1_dercribe')}
placeholder={I18n.t('Create_newtool_s1_dercribe_error')}
rows={2}
trigger={['blur', 'change']}
maxCount={600}
maxLength={600}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_dercribe_empty'),
},
IS_OVERSEA && {
// eslint-disable-next-line no-control-regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('create_plugin_modal_descrip_error'),
},
]}
// @ts-expect-error -- linter-disable-autofix
onChange={v => {
changeVal();
setOriginDesc(v);
}}
/>
{showFunctionName && pluginType === PluginType.LOCAL ? (
<UIFormTextArea
className={s['textarea-single-line']}
field="function_name"
label={I18n.t('create_local_plugin_basic_tool_function')}
placeholder={I18n.t(
'create_local_plugin_basic_tool_function_input_placeholder',
)}
rows={1}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rules={[
{
required: true,
message: I18n.t(
'create_local_plugin_basic_warning_no_tool_function_entered',
),
},
]}
onChange={changeVal}
/>
) : null}
</div>
</>
)
}
</Form>
</>
),
};
};

View File

@@ -0,0 +1,413 @@
/*
* 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 { type Dispatch, type SetStateAction, useEffect, useRef } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import {
UIFormInput,
UIFormSelect,
UIFormTextArea,
Typography,
Toast,
Form,
Tooltip,
} from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import {
APIMethod,
type PluginMetaInfo,
AuthorizationType,
PluginToolAuthType,
type APIExtend,
PluginType,
type UpdateAPIRequest,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { InfoPopover } from '../info_popover';
import { ERROR_CODE } from './types';
import { methodType } from './config';
import s from './index.module.less';
const { Option } = UIFormSelect;
export interface UseBaseInfoRequest {
pluginId: string;
pluginMeta: PluginMetaInfo;
apiId?: string;
step?: number;
baseInfo?: {
name?: string;
desc?: string;
path?: string;
method?: APIMethod;
api_extend?: APIExtend;
function_name?: string;
};
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
showModal: boolean;
disabled: boolean;
editVersion?: number;
pluginType?: PluginType;
spaceId?: string;
onSuccess?: (params: UpdateAPIResponse) => void;
}
export interface UseBaseInfoReturnValue {
submitBaseInfo: () => Promise<boolean>;
baseInfoNode: JSX.Element;
}
export const useBaseMore = ({
pluginId,
pluginMeta,
apiId = '',
baseInfo = {},
showModal,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
pluginType,
onSuccess,
}: UseBaseInfoRequest): UseBaseInfoReturnValue => {
const { url: pluginUrl } = pluginMeta;
const formRef = useRef<Form>(null);
useEffect(() => {
formRef.current?.formApi.setValues({
path: baseInfo.path,
method: baseInfo.method || APIMethod.GET,
function_name: baseInfo.function_name,
auth_mode: baseInfo.api_extend?.auth_mode || PluginToolAuthType.Required,
});
}, [
baseInfo.path,
showModal,
disabled,
pluginMeta,
baseInfo.method,
baseInfo.function_name,
baseInfo.api_extend?.auth_mode,
]);
// 提交基础信息
const submitBaseInfo = async () => {
const status = await formRef.current?.formApi
.validate()
.then(() => true)
.catch(() => false);
if (!status || !apiId) {
return false;
}
let baseResData;
const formValues = formRef.current?.formApi.getValues();
const params: UpdateAPIRequest = {
api_id: apiId,
plugin_id: pluginId,
path: formValues.path,
method: formValues.method,
api_extend: {
auth_mode: formValues.auth_mode,
},
edit_version: editVersion,
function_name: formValues.function_name,
};
try {
baseResData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(baseResData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const changeVal = () => {
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
};
return {
submitBaseInfo,
baseInfoNode: (
<>
<Form<Record<string, unknown>>
showValidateIcon={false}
ref={formRef}
disabled={disabled}
className={s['base-info-form']}
>
{() =>
disabled ? (
<>
{pluginType === PluginType.LOCAL && (
<Form.Slot
label={{
text: I18n.t('create_local_plugin_basic_tool_function'),
required: true,
}}
>
{baseInfo.function_name ?? '-'}
</Form.Slot>
)}
{pluginType === PluginType.PLUGIN && (
<>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_url'),
required: true,
}}
>
{String(pluginUrl) + baseInfo.path}
</Form.Slot>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_method'),
required: true,
extra: <InfoPopover data={methodType} />,
}}
>
{API_METHOD_LABEL_MAP[baseInfo?.method || APIMethod.GET]}
</Form.Slot>
</>
)}
{pluginMeta?.auth_type?.includes(AuthorizationType.OAuth) ? (
<Form.Slot
label={{
text: I18n.t('plugin_edit_tool_oauth_enabled_title'),
required: true,
extra: (
<Tooltip
content={I18n.t(
'plugin_edit_tool_oauth_enabled_title_hover_tip',
)}
>
<IconInfo
style={{ color: 'rgba(28, 29, 35, 0.35)' }}
/>
</Tooltip>
),
}}
>
{
API_MODE_LABEL_MAP[
baseInfo.api_extend?.auth_mode ||
PluginToolAuthType.Required
]
}
</Form.Slot>
) : null}
</>
) : (
<>
{pluginType === PluginType.LOCAL && (
<UIFormTextArea
className={s['textarea-single-line']}
field="function_name"
label={I18n.t('create_local_plugin_basic_tool_function')}
placeholder={I18n.t(
'create_local_plugin_basic_tool_function_input_placeholder',
)}
rows={1}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rules={[
{
required: true,
message: I18n.t(
'create_local_plugin_basic_warning_no_tool_function_entered',
),
},
]}
onChange={changeVal}
/>
)}
{pluginType === PluginType.PLUGIN && (
<>
<UIFormInput
field="path"
label={{
text: I18n.t('Create_newtool_s1_url'),
}}
trigger={['blur', 'change']}
addonBefore={
<div className={s['plugin-url-prefix']}>
<Typography.Text
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: {
content: pluginUrl,
style: {
wordBreak: 'break-word',
},
},
},
}}
>
{pluginUrl}
</Typography.Text>
</div>
}
style={{ width: '100%' }}
className={s['plugin-url-input']}
placeholder={I18n.t('Create_newtool_s1_url_empty')}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_url_error2'),
},
{
pattern: /^\//,
message: I18n.t('Create_newtool_s1_url_error1'),
},
{
// eslint-disable-next-line no-control-regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('tool_new_S1_URL_error'),
},
]}
></UIFormInput>
<UIFormSelect
field="method"
initValue={APIMethod.GET}
label={{
text: I18n.t('Create_newtool_s1_method'),
extra: <InfoPopover data={methodType} />,
}}
showClear
trigger={['blur', 'change']}
style={{ width: '100%', borderRadius: '8px' }}
placeholder={I18n.t(
'workflow_detail_condition_pleaseselect',
)}
rules={[
{
required: true,
message: I18n.t(
'workflow_detail_condition_pleaseselect',
),
},
]}
>
{[
APIMethod.GET,
APIMethod.POST,
APIMethod.PUT,
APIMethod.DELETE,
APIMethod.PATCH,
].map(method => (
<Option value={method} key={method}>
{API_METHOD_LABEL_MAP[method]}
</Option>
))}
</UIFormSelect>
{pluginMeta?.auth_type?.includes(
AuthorizationType.OAuth,
) ? (
<UIFormSelect
field="auth_mode"
initValue={PluginToolAuthType.Required}
label={{
text: I18n.t('plugin_edit_tool_oauth_enabled_title'),
extra: (
<Tooltip
content={I18n.t(
'plugin_edit_tool_oauth_enabled_title_hover_tip',
)}
>
<IconInfo
style={{ color: 'rgba(28, 29, 35, 0.35)' }}
/>
</Tooltip>
),
}}
showClear
trigger={['blur', 'change']}
style={{ width: '100%', borderRadius: '8px' }}
placeholder={I18n.t(
'workflow_detail_condition_pleaseselect',
)}
rules={[
{
required: true,
message: I18n.t(
'workflow_detail_condition_pleaseselect',
),
},
]}
>
{[
PluginToolAuthType.Required,
PluginToolAuthType.Supported,
PluginToolAuthType.Disable,
].map(mode => (
<Option value={mode} key={mode}>
{API_MODE_LABEL_MAP[mode]}
</Option>
))}
</UIFormSelect>
) : null}
</>
)}
</>
)
}
</Form>
</>
),
};
};
const API_METHOD_LABEL_MAP: Record<APIMethod, string> = {
[APIMethod.GET]: I18n.t('Create_newtool_s1_method_get'),
[APIMethod.POST]: I18n.t('Create_newtool_s1_method_post'),
[APIMethod.PUT]: I18n.t('Create_newtool_s1_method_put'),
[APIMethod.DELETE]: I18n.t('Create_newtool_s1_method_delete'),
[APIMethod.PATCH]: I18n.t('Create_tool_s1_method_patch_name'),
};
const API_MODE_LABEL_MAP: Record<PluginToolAuthType, string> = {
[PluginToolAuthType.Required]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_required',
),
[PluginToolAuthType.Supported]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_optional',
),
[PluginToolAuthType.Disable]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_disabled',
),
};

View File

@@ -0,0 +1,164 @@
/*
* 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 FC, useEffect, useState } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { type CascaderProps } from '@coze-arch/coze-design';
import { Typography, UICascader } from '@coze-arch/bot-semi';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import {
type APIParameterRecord,
type CascaderOnChangValueType,
type CascaderValueType,
type InputItemProps,
} from '../../types/params';
import s from '../../index.module.less';
import {
ARRAYTAG,
assistToExtend,
extendToAssist,
getParameterTypeLabel,
getPluginParameterTypeOptions,
ParameterTypeExtend,
} from '../../config';
const getCascaderValueTypeFrom = (
record?: APIParameterRecord,
): CascaderValueType => {
if (record?.assist_type) {
return [ParameterTypeExtend.DEFAULT, assistToExtend(record.assist_type)];
}
// @ts-expect-error -- linter-disable-autofix
return [record.type];
};
const { Text } = Typography;
interface CProps extends Omit<InputItemProps, 'selectCallback'> {
selectCallback: (types: CascaderOnChangValueType) => void;
enableFileType?: boolean;
}
export const CascaderItem: FC<CProps> = ({
check = 0,
useBlockWrap = false,
record,
disabled,
selectCallback,
enableFileType = false,
}) => {
const [value, setValue] = useState<CascaderValueType>(
getCascaderValueTypeFrom(record),
);
const [errorStatus, setErrorStatus] = useState<number>(0);
// @ts-expect-error -- linter-disable-autofix
const isArrayType = record.name === ARRAYTAG;
// @ts-expect-error -- linter-disable-autofix
const isObjectField = (record.deep ?? 0) > 1 && record.name !== ARRAYTAG;
// 通过check触发校验提交时
useEffect(() => {
if (check === 0) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (val?: CascaderValueType) => {
const status = !val?.[0] ? 1 : 0;
setErrorStatus(status);
};
const onChange = (types: CascaderValueType) => {
if (types[1]) {
selectCallback([types[0], extendToAssist(types[1])]);
} else {
selectCallback([types[0]]);
}
setValue(types);
handleCheck(types);
};
const displayRender: CascaderProps['displayRender'] = (items, idx) => {
// @ts-expect-error -- linter-disable-autofix
let inputValue: string = items[0];
if (value[1]) {
if (value[1] === ParameterTypeExtend.DEFAULT) {
// @ts-expect-error -- linter-disable-autofix
inputValue = getParameterTypeLabel(
ParameterTypeExtend.DEFAULT,
isArrayType,
);
} else {
// @ts-expect-error -- linter-disable-autofix
inputValue = items[1];
}
}
return <Text ellipsis={{ showTooltip: true }}>{inputValue}</Text>;
};
const parameterTypeOptionsWithCustom = getPluginParameterTypeOptions(
isArrayType,
enableFileType && !isObjectField,
);
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
<UICascader
treeData={parameterTypeOptionsWithCustom}
validateStatus={errorStatus ? 'error' : 'default'}
// @ts-expect-error -- linter-disable-autofix
value={value}
disabled={disabled}
onChange={onChange as CascaderProps['onChange']}
displayRender={displayRender}
dropdownClassName={s.cascaderDropdown}
style={{ width: '100%' }}
/>
<br />
{errorStatus !== 0 && (
<div style={{ position: 'relative' }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
<IconAlertCircle className={s['plugin-icon-error']} />
<Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{errorStatus === 1 && (
<span>{I18n.t('plugin_Parameter_type')}</span>
)}
</Text>
</span>
</div>
)}
</span>
);
};

View File

@@ -0,0 +1,6 @@
.icon {
> svg {
width: 16px;
height: 15px;
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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 React, { type FC, useEffect, useState } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Typography, UIIconButton } from '@coze-arch/bot-semi';
import { AssistParameterType } from '@coze-arch/bot-api/plugin_develop';
import { FileTypeEnum } from '@coze-studio/file-kit/logic';
import { ACCEPT_UPLOAD_TYPES } from '@coze-studio/file-kit/config';
import { IconDeleteOutline, IconUploadOutlined1 } from '@coze-arch/bot-icons';
import { ItemErrorTip } from '../item-error-tip';
import { getFileAccept, getFileTypeFromAssistType } from '../../file';
import { PluginFileUpload } from './upload';
import styles from './index.module.less';
const { Text } = Typography;
const fileUnknownIcon = ACCEPT_UPLOAD_TYPES[FileTypeEnum.DEFAULT_UNKNOWN].icon;
export const FileUploadItem: FC<{
assistParameterType: AssistParameterType;
onChange?: (uri: string) => void;
required?: boolean;
withDescription?: boolean;
defaultValue?: string;
check?: number;
disabled?: boolean;
}> = ({
onChange,
required = false,
withDescription = false,
check = 0,
defaultValue,
disabled = false,
assistParameterType,
}) => {
const [isErrorStatus, setIsErrorStatus] = useState(false);
const [value, setValue] = useState(defaultValue);
const defaultFileType = getFileTypeFromAssistType(assistParameterType);
const isImageString = assistParameterType === AssistParameterType.IMAGE;
const btnText = isImageString
? I18n.t('plugin_file_upload_image')
: I18n.t('plugin_file_upload');
const errorTip = isImageString
? I18n.t('plugin_file_upload_mention_image')
: I18n.t('plugin_file_upload_mention');
const accept = getFileAccept(assistParameterType);
useEffect(() => {
if (check === 0) {
return;
}
setIsErrorStatus(required && !value);
}, [check]);
const onChangeHandler = (uri: string) => {
setValue(uri);
onChange?.(uri);
setIsErrorStatus(required && !uri);
};
return (
<>
<PluginFileUpload
defaultUrl={value}
defaultFileType={defaultFileType}
onUploadSuccess={onChangeHandler}
uploadProps={{
accept,
disabled,
maxSize: 20480,
}}
render={({ fileState, clearFile }) => {
const { uploading, uri, url, name, type } = fileState;
/**
* 回显 只有一个url(string),需要兼容 => 不展示icon,url作为文件名
*/
const onlyUrlString = !!url && !uri;
const displayName = onlyUrlString ? value : name;
let icon: string | undefined = url;
const uploadButton = (
<UIButton
icon={<IconUploadOutlined1 className={styles.icon} />}
loading={uploading}
disabled={disabled}
className="w-full"
>
{uploading ? I18n.t('plugin_file_uploading') : btnText}
</UIButton>
);
if (uploading) {
return uploadButton;
} else if (onlyUrlString && type === FileTypeEnum.IMAGE) {
/** image不是即时上传的无法确认其为合法资源路径 */
icon = fileUnknownIcon;
} else if (!isImageString) {
// @ts-expect-error -- linter-disable-autofix
const typeIcon = ACCEPT_UPLOAD_TYPES[type]?.icon;
if (typeIcon) {
icon = typeIcon;
} else {
icon = undefined;
}
}
if (onlyUrlString || uri) {
return (
<div
className={classNames(
'flex items-center justify-between w-full h-[32px]',
disabled ? 'cursor-not-allowed' : '',
)}
>
<div className="flex items-center min-w-0">
{icon ? (
<img
src={icon}
className="w-[20px] h-[20px] mr-[5px] rounded-[0.5px]"
/>
) : null}
<Text ellipsis={{ showTooltip: true }} className="mr-[2px]">
{displayName}
</Text>
</div>
<UIIconButton
icon={<IconDeleteOutline />}
disabled={disabled}
onClick={e => {
e.stopPropagation();
clearFile();
onChangeHandler('');
}}
/>
</div>
);
}
return uploadButton;
}}
/>
{isErrorStatus ? (
<ItemErrorTip withDescription={withDescription} tip={errorTip} />
) : null}
</>
);
};

View File

@@ -0,0 +1,164 @@
/*
* 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 FC, type ReactNode, useReducer } from 'react';
import { merge } from 'lodash-es';
import { produce } from 'immer';
import { userStoreService } from '@coze-studio/user-store';
import { I18n } from '@coze-arch/i18n';
import { uploadFileV2 } from '@coze-arch/bot-utils';
import { FileTypeEnum, getFileInfo } from '@coze-studio/file-kit/logic';
import { Upload, Toast, type UploadProps } from '@coze-arch/coze-design';
interface PluginFileUploadProps {
render: (props: { fileState: FileState; clearFile: () => void }) => ReactNode;
onUploadSuccess?: (uri: string) => void;
uploadProps?: Partial<UploadProps>;
disabled?: boolean;
defaultUrl?: string;
defaultFileType: FileTypeEnum | null;
}
interface FileState {
uri: string;
url: string;
name: string;
type: FileTypeEnum | null;
uploading: boolean;
abortSignal: AbortSignal;
}
const getDefaultFileState = (states?: Partial<FileState>): FileState =>
merge(
{
uri: '',
url: '',
name: '',
type: null,
uploading: false,
abortSignal: new AbortController().signal,
} satisfies FileState,
states,
);
type Action = Partial<Omit<FileState, 'abortSignal'>>;
export const PluginFileUpload: FC<PluginFileUploadProps> = ({
disabled = false,
uploadProps,
render,
onUploadSuccess,
defaultUrl,
defaultFileType,
}) => {
// @ts-expect-error -- linter-disable-autofix
const userId = userStoreService.useUserInfo().user_id_str;
const [fileState, setFileState] = useReducer(
(states: FileState, payload: Action) =>
produce(states, draft => {
if (!payload) {
return;
}
Object.keys(payload).forEach(key => {
// @ts-expect-error -- linter-disable-autofix
draft[key] = payload[key] ?? draft[key];
});
}),
getDefaultFileState({
url: defaultUrl ?? '',
type: defaultFileType ?? null,
}),
);
const clearFile = () => setFileState(getDefaultFileState());
const customRequest: UploadProps['customRequest'] = async ({
file,
fileInstance,
}) => {
// @ts-expect-error -- linter-disable-autofix
const type = getFileInfo(fileInstance).fileType;
setFileState({
uploading: true,
url: file.url,
name: file.name,
});
await uploadFileV2({
userId,
fileItemList: [
{
file: fileInstance,
fileType: type === FileTypeEnum.IMAGE ? 'image' : 'object',
},
],
signal: fileState.abortSignal,
timeout: undefined,
onSuccess: info => {
const uri = info?.uploadResult?.Uri;
if (!uri) {
return;
}
setFileState({
uploading: false,
uri,
type,
});
onUploadSuccess?.(uri);
},
onUploadError: () => {
setFileState({
uploading: false,
});
},
});
};
if (typeof render !== 'function') {
return null;
}
return (
<Upload
className="w-full"
draggable
limit={1}
disabled={disabled}
onAcceptInvalid={() => {
Toast.error(I18n.t('shortcut_Illegal_file_format'));
}}
onSizeError={() => {
if (uploadProps?.maxSize) {
Toast.error(
I18n.t('file_too_large', {
max_size: `${uploadProps.maxSize / 1024}MB`,
}),
);
}
}}
customRequest={customRequest}
showUploadList={false}
{...uploadProps}
>
{render({ fileState, clearFile })}
</Upload>
);
};

View File

@@ -0,0 +1,18 @@
/*
* 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 { CascaderItem } from './cascader-item';
export { FileUploadItem } from './file-upload-item';

View File

@@ -0,0 +1,19 @@
/* stylelint-disable declaration-no-important */
.check-box {
position: absolute;
}
.form-check-tip {
position: absolute;
top: 4px;
right: 0;
left: 0;
transform-origin: left;
display: inline-block;
font-size: 12px !important;
line-height: 16px;
color: var(--semi-color-danger);
}

View File

@@ -0,0 +1,40 @@
/*
* 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 React, { type FC } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import s from './index.module.less';
export const ItemErrorTip: FC<{ withDescription?: boolean; tip?: string }> = ({
withDescription = false,
tip = I18n.t('plugin_empty'),
}) => (
<div className={s['check-box']}>
<span
className={cl(
'whitespace-nowrap',
s['form-check-tip'],
withDescription ? '!top-[16px]' : '!top-0',
'errorDebugClassTag',
)}
>
{tip}
</span>
</div>
);

View File

@@ -0,0 +1,431 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import {
type AssistParameterType,
ParameterLocation,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { type ExtInfoText } from '@coze-studio/plugin-shared';
import { FileTypeEnum } from '@coze-studio/file-kit/logic';
import { type APIParameterRecord } from './types/params';
export const childrenRecordName = 'sub_parameters'; // 子节点名称
export const ROWKEY = 'id'; // 唯一标识符
export const ARRAYTAG = '[Array Item]'; // 数组元素标识符
export const ROOTTAG = '[Root Item]'; // root为数组的标识符
export const STARTNODE = 0;
export const REQUESTNODE = 1;
export const RESPONSENODE = 2;
export const DEBUGNODE = 3;
export const ENDSTEP = 4;
// 传入方法options
export const parameterLocationOptions = [
{
label: 'Body',
value: ParameterLocation.Body,
},
{
label: 'Path',
value: ParameterLocation.Path,
},
{
label: 'Query',
value: ParameterLocation.Query,
},
{
label: 'Header',
value: ParameterLocation.Header,
},
];
export enum ParameterTypeExtend {
/**
* 扩展类型
* 与 AssistParameterType 一一对应
*/
DEFAULT = 10001,
IMAGE,
DOC,
CODE,
PPT,
TXT,
EXCEL,
AUDIO,
ZIP,
VIDEO,
}
const enumDomain = 10000;
export const assistToExtend = (
type: AssistParameterType,
): ParameterTypeExtend => type + enumDomain;
export const extendToAssist = (
type: ParameterTypeExtend,
): AssistParameterType => type - enumDomain;
export type PluginParameterType = ParameterType | ParameterTypeExtend;
interface ParameterTypeOption {
label: string;
value: ParameterType | ParameterTypeExtend;
children?: Array<{
label: string;
value: ParameterTypeExtend;
}>;
}
/**
* 未扩展File类型前的 基础类型,多处使用 需要保留 start
*/
export const parameterTypeOptions: Array<ParameterTypeOption> = [
{
label: 'String',
value: ParameterType.String,
},
{
label: 'Integer',
value: ParameterType.Integer,
},
{
label: 'Number',
value: ParameterType.Number,
},
{
label: 'Object',
value: ParameterType.Object,
},
{
label: 'Array',
value: ParameterType.Array,
},
{
label: 'Boolean',
value: ParameterType.Bool,
},
];
export const parameterTypeOptionsSub: Array<ParameterTypeOption> = [
{
label: 'Array<String>',
value: ParameterType.String,
},
{
label: 'Array<Integer>',
value: ParameterType.Integer,
},
{
label: 'Array<Number>',
value: ParameterType.Number,
},
{
label: 'Array<Object>',
value: ParameterType.Object,
},
{
label: 'Array<Boolean>',
value: ParameterType.Bool,
},
];
/**
* 未扩展File类型前的 基础类型,多处使用 需要保留 end
*/
export const parameterTypeExtendMap: Record<
ParameterTypeExtend,
{
label: string;
listLabel: string;
fileTypes: FileTypeEnum[];
}
> = {
[ParameterTypeExtend.DEFAULT]: {
label: 'File',
listLabel: 'Array<File>',
fileTypes: [FileTypeEnum.DEFAULT_UNKNOWN],
},
[ParameterTypeExtend.IMAGE]: {
label: 'Image',
listLabel: 'Array<Image>',
fileTypes: [FileTypeEnum.IMAGE],
},
[ParameterTypeExtend.DOC]: {
label: 'Doc',
listLabel: 'Array<Doc>',
fileTypes: [FileTypeEnum.DOCX, FileTypeEnum.PDF],
},
[ParameterTypeExtend.CODE]: {
label: 'Code',
listLabel: 'Array<Code>',
fileTypes: [FileTypeEnum.CODE],
},
[ParameterTypeExtend.PPT]: {
label: 'PPT',
listLabel: 'Array<PPT>',
fileTypes: [FileTypeEnum.PPT],
},
[ParameterTypeExtend.TXT]: {
label: 'TXT',
listLabel: 'Array<TXT>',
fileTypes: [FileTypeEnum.TXT],
},
[ParameterTypeExtend.EXCEL]: {
label: 'Excel',
listLabel: 'Array<Excel>',
fileTypes: [FileTypeEnum.EXCEL, FileTypeEnum.CSV],
},
[ParameterTypeExtend.AUDIO]: {
label: 'Audio',
listLabel: 'Array<Audio>',
fileTypes: [FileTypeEnum.AUDIO],
},
[ParameterTypeExtend.ZIP]: {
label: 'Zip',
listLabel: 'Array<Zip>',
fileTypes: [FileTypeEnum.ARCHIVE],
},
[ParameterTypeExtend.VIDEO]: {
label: 'Video',
listLabel: 'Array<Video>',
fileTypes: [FileTypeEnum.VIDEO],
},
};
const getParameterTypeOptionsWithCustom = (enableFileType = false) => {
if (!enableFileType) {
return parameterTypeOptions;
}
const parameterTypeOptionsWithCustom = cloneDeep(parameterTypeOptions);
parameterTypeOptionsWithCustom.splice(1, 0, {
label: 'File',
value: ParameterTypeExtend.DEFAULT,
children: Object.entries(parameterTypeExtendMap).map(
([type, { label }]) => ({
label,
value: Number(type) as ParameterTypeExtend,
}),
),
});
return parameterTypeOptionsWithCustom;
};
const getParameterTypeOptionsSubWithCustom = (enableFileType = false) => {
if (!enableFileType) {
return parameterTypeOptionsSub;
}
const parameterTypeOptionsSubWithCustom = cloneDeep(parameterTypeOptionsSub);
parameterTypeOptionsSubWithCustom.splice(1, 0, {
label: 'Array<File>',
value: ParameterTypeExtend.DEFAULT,
children: Object.entries(parameterTypeExtendMap).map(
([type, { listLabel }]) => ({
label: listLabel,
value: Number(type) as ParameterTypeExtend,
}),
),
});
return parameterTypeOptionsSubWithCustom;
};
export const getPluginParameterTypeOptions = (
isArrayType: boolean,
enableFileType: boolean,
) =>
isArrayType
? getParameterTypeOptionsSubWithCustom(enableFileType)
: getParameterTypeOptionsWithCustom(enableFileType);
const parameterTypeOptionsMap = parameterTypeOptions.reduce(
(prev: Partial<Record<PluginParameterType, string>>, curr) => {
prev[curr.value] = curr.label;
return prev;
},
{
...Object.entries(parameterTypeExtendMap).reduce(
(prev, [type, { label }]) => {
// @ts-expect-error -- linter-disable-autofix
prev[type] = label;
return prev;
},
{},
),
[ParameterTypeExtend.DEFAULT]: 'File',
},
);
const parameterTypeOptionsSubMap = parameterTypeOptionsSub.reduce(
(prev: Partial<Record<PluginParameterType, string>>, curr) => {
prev[curr.value] = curr.label;
return prev;
},
{
...Object.entries(parameterTypeExtendMap).reduce(
(prev, [type, { listLabel }]) => {
// @ts-expect-error -- linter-disable-autofix
prev[type] = listLabel;
return prev;
},
{},
),
[ParameterTypeExtend.DEFAULT]: 'Array<File>',
},
);
export const getParameterTypeLabel = (
type: PluginParameterType,
isArrayType = false,
) =>
isArrayType
? parameterTypeOptionsSubMap[type]
: parameterTypeOptionsMap[type];
export const getParameterTypeLabelFromRecord = (
record: APIParameterRecord,
isArrayType = false,
) => {
let type: PluginParameterType = record.type as PluginParameterType;
if (record?.assist_type) {
type = assistToExtend(record.assist_type);
}
return getParameterTypeLabel(type, isArrayType);
};
export const methodType: ExtInfoText[] = [
{
type: 'title',
text: 'Get',
},
{
type: 'text',
text: I18n.t('plugin_tooltip_url'),
},
{
type: 'demo',
text: 'GET /users?userId=123',
},
{
type: 'text',
text: I18n.t('used_to_obtain_user_information_with_id_123'),
},
{
type: 'br',
},
{
type: 'title',
text: 'Post',
},
{
type: 'text',
text: I18n.t(
'submit_data_to_a_specified_resource__often_used_to_submit_forms_or_upload_files_',
),
},
{
type: 'demo',
text: 'POST /users',
},
{
type: 'text',
text: I18n.t('attach_user_data_to_create_a_new_user_'),
},
{
type: 'title',
text: 'Put',
},
{
type: 'text',
text: I18n.t(
'upload_data_or_resources_to_a_specified_location__often_used_to_update_existing_',
),
},
{
type: 'demo',
text: 'PUT /users/123',
},
{
type: 'text',
text: I18n.t('used_to_update_user_information_with_id_123_'),
},
{
type: 'title',
text: 'Delete',
},
{
type: 'text',
text: I18n.t(
'requests_the_server_to_delete_the_specified_resource__example_',
),
},
{
type: 'demo',
text: 'DELETE /users/123',
},
{
type: 'text',
text: I18n.t('used_to_delete_the_user_with_id_123_'),
},
{
type: 'title',
text: I18n.t('Create_tool_s1_method_patch_tooltip_title'),
},
{
type: 'text',
text: I18n.t('Create_tool_s1_method_patch_tooltip_desp'),
},
{
type: 'demo',
text: I18n.t('Create_tool_s1_method_patch_tooltip_url'),
},
{
type: 'text',
text: I18n.t('Create_tool_s1_method_patch_tooltip_explain'),
},
];
export enum ParamsFormErrorStatus {
NO_ERROR = 0,
NAME_EMPTY = 1,
// 中文
CHINESE = 2,
// 重复
REPEAT = 3,
ASCII = 4,
// 未填写
DESC_EMPTY = 5,
}
export const paramsFormErrorStatusText = {
[ParamsFormErrorStatus.NO_ERROR]: '',
[ParamsFormErrorStatus.NAME_EMPTY]: I18n.t(
'Create_newtool_s2_table_name_error1',
),
[ParamsFormErrorStatus.CHINESE]: I18n.t(
'Create_newtool_s2_table_name_error2',
),
[ParamsFormErrorStatus.REPEAT]: I18n.t('plugin_Parameter_name_error'),
[ParamsFormErrorStatus.ASCII]: I18n.t('create_plugin_modal_descrip_error'),
[ParamsFormErrorStatus.DESC_EMPTY]: I18n.t(
'Create_newtool_s3_table_des_empty',
),
};

View File

@@ -0,0 +1,182 @@
/*
* 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 PropsWithChildren, type FC } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Banner, Space } from '@coze-arch/bot-semi';
import { IconPullDown } from '@coze-arch/bot-icons';
import { type CheckParamsProps } from '../types';
import { DiyMdBox, HeadingType } from './diy-mdbox';
import s from './index.module.less';
const Header = ({
// @ts-expect-error -- linter-disable-autofix
activeTab,
// @ts-expect-error -- linter-disable-autofix
setActiveTab,
// @ts-expect-error -- linter-disable-autofix
hideRawResponse,
// @ts-expect-error -- linter-disable-autofix
showRaw,
// @ts-expect-error -- linter-disable-autofix
setShowRaw,
}) => {
const handleOpenRawResponse = () => {
setShowRaw(!showRaw);
};
return (
<div className={s['debug-check-header']}>
<div className={s['debug-check-tab']}>
<div
className={classNames(s['debug-check-tab-item'], {
[s['debug-check-tab-item-active']]:
activeTab === HeadingType.Request,
})}
onClick={() => setActiveTab(HeadingType.Request)}
>
Request
</div>
<div className={s['debug-check-tab-line']}></div>
<div
className={classNames(s['debug-check-tab-item'], {
[s['debug-check-tab-item-active']]:
activeTab === HeadingType.Response,
})}
onClick={() => setActiveTab(HeadingType.Response)}
>
Response
</div>
</div>
{activeTab === HeadingType.Response && !hideRawResponse ? (
<Space spacing={8}>
<span>Raw Response</span>
<IconPullDown
className={classNames(s.icon, {
[s.open]: showRaw,
})}
onClick={handleOpenRawResponse}
></IconPullDown>
</Space>
) : null}
</div>
);
};
const ProcessContent: FC<PropsWithChildren> = ({ children }) => (
<div className={s['process-content']}>{children}</div>
);
/** stringify 缩进 */
const INDENTATION_SPACES = 2;
const LLMAndAPIContent: FC<{
toolMessageUnit: CheckParamsProps;
}> = ({ toolMessageUnit }) => {
const { request, response, failReason, rawResp } = toolMessageUnit;
const [activeTab, setActiveTab] = useState(1);
const [showRaw, setShowRaw] = useState(false);
return (
<>
{!request && !response ? (
<div className={s['llm-debug-empty']}>
<div className={s['llm-debug-empty-content']}>
{I18n.t('plugin_s4_debug_empty')}
</div>
</div>
) : (
<div className={s['debug-result-content']}>
<Header
activeTab={activeTab}
setActiveTab={setActiveTab}
hideRawResponse={!(!failReason && rawResp)}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
{activeTab === 1 ? (
<>
<div className={s['llm-api-content']}>
<DiyMdBox
markDown={
request
? JSON.stringify(
JSON.parse(request || '{}'),
null,
INDENTATION_SPACES,
)
: ''
}
headingType={activeTab}
showRaw={showRaw}
/>
</div>
</>
) : (
<>
<div className={s['llm-api-content']}>
{failReason ? (
<div className={s['error-reason-box']}>
<Banner
className={s['error-reason']}
fullMode={false}
icon={null}
closeIcon={null}
type="danger"
description={
<div>
<div>{I18n.t('plugin_s4_debug_detail')}</div>
<div style={{ wordBreak: 'break-word' }}>
{failReason}
</div>
</div>
}
/>
</div>
) : (
<DiyMdBox
headingType={activeTab}
markDown={JSON.stringify(
JSON.parse(response || '{}'),
null,
INDENTATION_SPACES,
)}
rawResponse={JSON.stringify(
JSON.parse(rawResp || '{}'),
null,
INDENTATION_SPACES,
)}
showRaw={showRaw}
/>
)}
</div>
</>
)}
</div>
)}
</>
);
};
export const DebugCheck: FC<{
checkParams: CheckParamsProps;
}> = ({ checkParams }) => (
<ProcessContent>
<LLMAndAPIContent toolMessageUnit={checkParams} />
</ProcessContent>
);

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo, useRef, useState } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Toast } from '@coze-arch/bot-semi';
import {
DebugExampleStatus,
PluginType,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
transformTreeToObj,
sleep,
scrollToErrorElement,
transformParamsToTree,
} from '../utils';
import { type CheckParamsProps, STATUS } from '../types';
import s from '../index.module.less';
import ParamsForm from './params-form';
/** stringify 缩进 */
const INDENTATION_SPACES = 2;
const SLEEP_NUM = 100;
export const DebugParams: React.FC<{
requestParams: APIParameter[] | undefined;
pluginId: string;
apiId: string;
operation?: number;
btnText?: string;
callback?: (val: CheckParamsProps) => void;
disabled: boolean;
debugExampleStatus?: DebugExampleStatus;
showExampleTag?: boolean;
pluginType?: PluginType;
}> = ({
requestParams = [],
pluginId,
apiId,
operation = 1,
btnText = I18n.t('Create_newtool_s4_run'),
callback,
disabled,
debugExampleStatus = DebugExampleStatus.Default,
showExampleTag = false,
pluginType,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const [check, setCheck] = useState<number>(0);
const paramsFormRef = useRef<{ data: Array<APIParameter> }>(null);
const handleAction = async () => {
// 校验是否必填
setCheck(check + 1);
await sleep(SLEEP_NUM);
const errorEle = document.getElementsByClassName('errorDebugClassTag');
if (!apiId || errorEle.length > 0) {
scrollToErrorElement('.errorDebugClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
let reqParams = {};
setLoading(true);
if (
Array.isArray(paramsFormRef.current?.data) &&
(paramsFormRef.current?.data || []).length > 0
) {
reqParams = transformTreeToObj(paramsFormRef.current?.data);
}
try {
const resData = await PluginDevelopApi.DebugAPI({
plugin_id: pluginId,
api_id: apiId,
parameters: JSON.stringify(reqParams),
operation,
});
callback?.({
status: resData.success ? STATUS.PASS : STATUS.FAIL,
request: resData.raw_req,
response: resData.resp,
failReason: resData.reason,
response_params: resData.response_params,
rawResp: resData.raw_resp,
});
} catch (e) {
callback?.({
status: STATUS.FAIL,
request: JSON.stringify(reqParams, null, INDENTATION_SPACES),
response: I18n.t('plugin_exception'),
failReason: I18n.t('plugin_exception'),
});
}
setLoading(false);
};
const requestParamsData = useMemo(
() => transformParamsToTree(requestParams),
[requestParams],
);
return (
<div className={s['debug-params-box']}>
<ParamsForm
height={443}
ref={paramsFormRef}
requestParams={requestParamsData}
defaultKey="global_default"
disabled={disabled}
check={check}
debugExampleStatus={debugExampleStatus}
showExampleTag={showExampleTag}
supportFileTypeUpload
/>
{!disabled && (
<div className={s.runbtn}>
<UIButton
disabled={disabled || pluginType === PluginType.LOCAL}
style={{ width: 98 }}
loading={loading}
// theme="solid"
type="tertiary"
onClick={handleAction}
>
{btnText === I18n.t('Create_newtool_s3_button_auto') &&
(loading
? I18n.t('plugin_s3_Parsing')
: I18n.t('Create_newtool_s3_button_auto'))}
{btnText === I18n.t('Create_newtool_s4_run') &&
(loading
? I18n.t('plugin_s3_running')
: I18n.t('Create_newtool_s4_run'))}
</UIButton>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,83 @@
/*
* 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 copy from 'copy-to-clipboard';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Space, Toast } from '@coze-arch/bot-semi';
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
import { IconCopy } from '@coze-arch/bot-icons';
import s from './index.module.less';
export enum HeadingType {
Request = 1,
Response = 2,
}
interface MdBoxProps {
markDown: string;
headingType: HeadingType;
rawResponse?: string;
showRaw: boolean;
}
const MAX_LENGTH = 30000;
export const DiyMdBox = ({
markDown,
headingType,
rawResponse,
showRaw,
}: MdBoxProps) => {
const getContent = () => {
if (!rawResponse) {
return '{}';
}
if (rawResponse.length < MAX_LENGTH) {
return rawResponse;
}
return `${rawResponse.slice(0, MAX_LENGTH)}...`;
};
return (
<div className={s['mb-content']}>
<div className={s['mb-header']}>
<Space spacing={8}>
<span>Json</span>
<IconCopy
className={s['icon-copy']}
onClick={() => {
copy(markDown);
Toast.success(I18n.t('copy_success'));
}}
></IconCopy>
</Space>
</div>
<div className={s['mb-main']}>
<div
className={classNames(s['mb-left'], {
[s['half-width']]: showRaw && headingType === HeadingType.Response,
})}
>
<MdBoxLazy markDown={`\`\`\`json\n${markDown}\n\`\`\``} />
</div>
{showRaw && headingType === HeadingType.Response ? (
<div className={s['mb-right']}>
<MdBoxLazy markDown={`\`\`\`json\n${getContent()}\n\`\`\``} />
</div>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,271 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
/* stylelint-disable declaration-no-important */
.process-content {
width: 100%;
height: 100%;
font-size: 12px;
font-weight: 400;
line-height: 15px;
/* 125% */
white-space: break-spaces;
background: var(--light-color-white-white, #fff);
.debug-result-content {
display: flex;
flex-direction: column;
height: 100%;
.llm-api-content {
height: calc(100% - 40px);
word-break: break-word;
}
}
}
.llm-debug-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 14px;
color: var(--light-usage-text-color-text-3,
var(--light-usage-disabled-color-disabled-text, rgb(28 31 35 / 35%)));
.llm-debug-empty-content {
padding: 16px;
}
}
.error-reason-box {
padding: 12px;
border-top: 1px solid rgba(29, 28, 36, 8%);
}
.mb-content {
display: flex;
flex-direction: column;
height: 100%;
:global {
.auto-hide-last-sibling-br>div {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.auto-hide-last-sibling-br>div>div:first-child {
display: none;
}
.flow-markdown-body {
flex: 1;
}
}
.mb-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
font-size: 12px;
line-height: 40px;
color: #f7f7fa;
background-color: #41414d;
.icon-copy {
cursor: pointer;
padding: 6px;
svg {
width: 16px;
height: 16px;
path {
fill: #fff;
fill-opacity: 1;
}
}
}
}
.mb-main {
overflow-y: auto;
display: flex;
height: 100%;
background-color: #12131b;
border-top-left-radius: 0;
border-top-right-radius: 0;
.mb-left {
width: 100%;
&.half-width {
width: 50%;
}
}
.mb-right {
position: relative;
flex: none;
width: 50%;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background-color: #565563;
}
}
}
}
.debug-params-table {
overflow: auto;
display: flex;
flex: 1;
width: 100%;
.empty {
margin-top: 90px;
}
:global {
.semi-spin-block.semi-spin,
.semi-spin-children,
.semi-table-fixed-header {
display: flex;
}
.semi-table-body {
max-height: calc(100% - 40px) !important;
padding-bottom: 12px;
}
.semi-table-row {
&:has(.disable) {
display: none;
}
}
.semi-table-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 16px;
width: calc(100% - 32px);
height: 1px;
background: var(--semi-color-border);
}
}
.semi-table-placeholder {
border-bottom: 0;
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell {
padding: 8px 16px;
vertical-align: top;
border-bottom: none;
}
.semi-table-thead>.semi-table-row>.semi-table-row-head {
padding-top: 9px;
padding-bottom: 9px;
border-bottom: none;
}
.semi-table-tbody>.semi-table-row:hover>.semi-table-row-cell {
background-color: transparent;
background-image: none;
}
}
}
.debug-check-header {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
padding: 0 16px;
.debug-check-tab {
display: flex;
gap: 12px;
align-items: center;
font-size: 14px;
font-weight: 600;
font-style: normal;
line-height: 20px;
.debug-check-tab-line {
width: 1px;
height: 16px;
background: var(--Light-usage-border---color-border, rgba(28, 29, 37, 12%));
}
.debug-check-tab-item {
cursor: pointer;
color: var(--Light-usage-text---color-text-2, rgba(29, 28, 36, 60%));
}
.debug-check-tab-item-active {
color: var(--Light-color-brand---brand-5, #4C54F0);
}
}
.icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 24px;
border: 0.75px solid rgba(29, 28, 36, 12%);
border-radius: 6px;
svg {
transform: rotate(-90deg);
width: 12px;
height: 12px;
}
&.open {
svg {
transform: rotate(90deg);
}
}
}
}

View File

@@ -0,0 +1,190 @@
/*
* 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 React, { type FC, useEffect, useState } from 'react';
import { Typography, UIInput } from '@coze-arch/bot-semi';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { updateNodeById } from '../../../utils';
import { type APIParameterRecord } from '../../../types/params';
import { ARRAYTAG, ROOTTAG, ROWKEY } from '../../../config';
import { ItemErrorTip } from '../../../components/item-error-tip';
import { FileUploadItem } from '../../../components/file-upload-item';
import { getColumnClass } from './utils';
interface InputItemProps {
val?: string;
width?: number | string;
height?: number;
check?: number;
callback: (val: string) => void;
useCheck?: boolean;
useBlockWrap?: boolean;
disabled: boolean;
desc: string;
}
const InputItem = ({
val = '',
callback,
check = 0,
width = '100%',
useCheck = false,
useBlockWrap = false,
disabled,
desc,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(val);
const [errorStatus, setErrorStatus] = useState(false);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0 || value === ARRAYTAG || value === ROOTTAG) {
return;
}
handleCheck(value);
}, [check]);
const handleCheck = (v: string) => {
if (!useCheck) {
return;
}
const filterVal = v ? String(v).replace(/\s+/g, '') : '';
setErrorStatus(filterVal === '');
};
return (
<span
style={{ width, ...(useBlockWrap ? { display: 'inline-block' } : {}) }}
>
<UIInput
disabled={disabled}
value={value}
validateStatus={errorStatus ? 'error' : 'default'}
onChange={(e: string) => {
setValue(e);
callback(e);
handleCheck(e);
}}
/>
<br />
{errorStatus ? <ItemErrorTip withDescription={!!desc} /> : null}
</span>
);
};
export const ValueColRender: FC<{
record: APIParameterRecord;
disabled?: boolean;
check: number;
needCheck: boolean;
defaultKey: string;
data: Array<APIParameter>;
supportFileTypeUpload: boolean;
}> = ({
record,
data,
disabled = false,
check,
needCheck,
defaultKey,
supportFileTypeUpload = false,
}) => {
const showInput = !(
record?.type === ParameterType.Object ||
record?.type === ParameterType.Array ||
(disabled && record.value === undefined)
);
const showFile =
record?.type === ParameterType.String && !!record?.assist_type;
let renderItem = <></>;
if (supportFileTypeUpload && showFile) {
renderItem = (
<FileUploadItem
// @ts-expect-error -- linter-disable-autofix
defaultValue={record.value || record?.[defaultKey]}
// @ts-expect-error -- linter-disable-autofix
assistParameterType={record.assist_type}
onChange={uri => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: 'value',
value: uri ? uri : null,
});
}}
withDescription={!!record?.desc}
required={needCheck || record?.is_required}
check={check}
disabled={disabled}
/>
);
} else if (showInput) {
renderItem = (
<div className={getColumnClass(record)}>
<InputItem
disabled={disabled}
useBlockWrap={true}
// @ts-expect-error -- linter-disable-autofix
val={record.value || record?.[defaultKey]}
check={check}
useCheck={needCheck || record?.is_required}
callback={(e: string) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: 'value',
value: e,
});
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: defaultKey,
value: e,
});
}}
// @ts-expect-error -- linter-disable-autofix
desc={record.desc}
/>
</div>
);
}
return (
<div className="mr-[3px]">
{renderItem}
{record.desc ? (
<Typography.Text
size="small"
ellipsis={{
showTooltip: {
opts: { content: record.desc },
},
}}
style={{ verticalAlign: showInput ? 'top' : 'middle' }}
>
{record.desc}
</Typography.Text>
) : null}
</div>
);
};

View File

@@ -0,0 +1,20 @@
/*
* 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 APIParameterRecord } from '../../../types/params';
export const getColumnClass = (record: APIParameterRecord) =>
record.global_disable ? 'disable' : 'normal';

View File

@@ -0,0 +1,294 @@
/*
* 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 React, {
type Ref,
forwardRef,
useImperativeHandle,
useMemo,
useState,
useEffect,
} from 'react';
import { set as ObjectSet, get as ObjectGet, cloneDeep } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Tag } from '@coze-arch/coze-design';
import {
UIButton,
Table,
Typography,
UITag,
Space,
} from '@coze-arch/bot-semi';
import { IconAddChildOutlined } from '@coze-arch/bot-icons';
import {
type APIParameter,
ParameterType,
DebugExampleStatus,
} from '@coze-arch/bot-api/plugin_develop';
import { IconDeleteStroked } from '@douyinfe/semi-icons';
import styles from '../index.module.less';
import {
findPathById,
deleteNode,
findTemplateNodeByPath,
cloneWithRandomKey,
handleIsShowDelete,
checkHasArray,
maxDeep,
} from '../../utils';
import { type APIParameterRecord } from '../../types/params';
import {
ARRAYTAG,
ROWKEY,
childrenRecordName,
getParameterTypeLabelFromRecord,
} from '../../config';
import { getColumnClass } from './columns/utils';
import { ValueColRender } from './columns/param-value-col';
const getName = (record: APIParameterRecord) => {
const paramType = getParameterTypeLabelFromRecord(record);
return (
<span className={getColumnClass(record)}>
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
style={{
maxWidth: `calc(100% - ${20 * (record.deep || 1) + 49}px)`,
}}
>
{record?.name}
</Typography.Text>
{record?.is_required ? (
<Typography.Text style={{ color: 'red' }}>{' * '}</Typography.Text>
) : null}
{paramType ? (
<Tag
size="mini"
prefixIcon={null}
className="!coz-fg-color-blue !coz-mg-color-blue shrink-0 font-normal px-6px rounded-[36px] ml-4px align-middle"
>
{paramType}
</Tag>
) : null}
</span>
);
};
export interface ParamsFormProps {
requestParams?: Array<APIParameter>;
disabled: boolean;
check: number;
needCheck?: boolean;
height?: number;
defaultKey?: 'global_default' | 'local_default';
debugExampleStatus?: DebugExampleStatus;
showExampleTag?: boolean;
supportFileTypeUpload?: boolean;
}
const getParamsTitle = (isShowExampleTag: boolean, disabled: boolean) =>
isShowExampleTag ? (
<Space>
<div>
{I18n.t(
disabled
? 'mkpl_plugin_tool_parameter_description'
: 'Create_newtool_s4_value',
)}
</div>
<UITag>{I18n.t('plugin_edit_tool_test_run_example_tip')}</UITag>
</Space>
) : (
I18n.t(
disabled
? 'mkpl_plugin_tool_parameter_description'
: 'Create_newtool_s4_value',
)
);
// eslint-disable-next-line @coze-arch/max-line-per-function -- 已经在拆了
const ParamsForm = (
props: ParamsFormProps,
ref: Ref<{ data: Array<APIParameter> } | null>,
) => {
const {
requestParams,
disabled,
check,
needCheck = false,
height = 236,
defaultKey = 'global_default',
debugExampleStatus = DebugExampleStatus.Default,
showExampleTag = false,
supportFileTypeUpload = false,
} = props;
const [data, setData] = useState(
cloneDeep(requestParams ? requestParams : []),
);
const [resourceData, setResourceData] = useState(
cloneDeep(requestParams ? requestParams : []),
);
useEffect(() => {
setData(requestParams ? cloneDeep(requestParams) : []);
setResourceData(requestParams ? cloneDeep(requestParams) : []);
}, [requestParams]);
useImperativeHandle(ref, () => ({
data,
}));
const [flag, setFlag] = useState<boolean>(false);
// 添加子节点
const addChildNode = (record: APIParameter) => {
if (!data) {
return;
}
let result: APIParameter & {
path?: Array<number>;
} = {};
// 1.查找路径
findPathById({
data,
callback: (item: APIParameter, path: Array<number>) => {
if (item[ROWKEY] === record[ROWKEY]) {
result = { ...item, path };
}
},
});
// 2.拼接路径
const path = (result?.path || [])
.map((v: number) => [v, childrenRecordName])
.flat();
// newPath是模版的路径下面添加节点newNode可以直接从该路径引用
const newPath = findTemplateNodeByPath(resourceData, path);
// 3.添加节点
const newData = cloneDeep(data);
if (Array.isArray(ObjectGet(newData, path))) {
// 这一步是为了根据newPath找到对应的根节点并且克隆一个新节点
const newNode = cloneWithRandomKey(ObjectGet(resourceData, newPath)[0]);
ObjectSet(newData, path, [...ObjectGet(newData, path), newNode]);
}
setData(newData);
};
const isShowExampleTag =
disabled &&
showExampleTag &&
debugExampleStatus === DebugExampleStatus.Enable;
const maxNum = maxDeep(data);
const columns = [
{
title: I18n.t('Create_newtool_s4_name'),
key: 'name',
className: styles['no-wrap'],
width: 180 + 20 * (maxNum - 1),
minWidth: 220,
render: (record: APIParameterRecord) => getName(record),
},
{
title: getParamsTitle(isShowExampleTag, disabled),
key: 'value',
className: styles['no-wrap'],
width: 200,
// @ts-expect-error -- linter-disable-autofix
render: record => (
<ValueColRender
record={record}
data={data}
disabled={disabled}
check={check}
needCheck={needCheck}
defaultKey={defaultKey}
supportFileTypeUpload={supportFileTypeUpload}
/>
),
},
{
title: I18n.t('dataset_detail_tableTitle_actions'),
key: 'operation',
width: 120,
render: (record: APIParameter) => (
<div className={getColumnClass(record)}>
{record?.type === ParameterType.Array && (
<UIButton
onClick={() => {
addChildNode(record);
setFlag(!flag);
}}
icon={<IconAddChildOutlined />}
type="secondary"
theme="borderless"
/>
)}
{record?.name === ARRAYTAG &&
handleIsShowDelete(data, record[ROWKEY]) && (
<UIButton
onClick={() => {
const clone = cloneDeep(data);
if (record?.id) {
deleteNode(clone, record?.id);
setData(clone);
}
}}
icon={<IconDeleteStroked />}
type="secondary"
theme="borderless"
/>
)}
</div>
),
},
];
const filterColumns =
disabled || !checkHasArray(requestParams)
? columns.filter(item => item.key !== 'operation')
: columns;
const scroll = useMemo(() => ({ y: height, x: '100%' }), []);
return (
<Table
className={styles['debug-params-table']}
pagination={false}
columns={filterColumns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
scroll={scroll}
empty={
!disabled && (
<div className={styles.empty}>
{I18n.t('plugin_form_no_result_desc')}
</div>
)
}
/>
);
};
export default forwardRef(ParamsForm);

View File

@@ -0,0 +1,186 @@
/*
* 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 { UITag, Typography, Space, Col, Row } from '@coze-arch/bot-semi';
import {
type DebugExample,
type PluginType,
type PluginAPIInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { type CheckParamsProps, STATUS } from './types';
import { DebugParams } from './debug-components/debug-params';
import { DebugCheck } from './debug-components/debug-check';
import s from './index.module.less';
const { Text } = Typography;
// @ts-expect-error -- linter-disable-autofix
const getApiTitle = (pluginName, name, labelKey) => (
<Text
className={s['card-title']}
ellipsis={{
showTooltip: {
opts: {
content: `${pluginName}.${name}`,
style: { wordBreak: 'break-word' },
},
},
}}
>
{pluginName}.{name} {I18n.t(labelKey)}
</Text>
);
export const Debug: React.FC<{
pluginType?: PluginType;
disabled: boolean;
apiInfo: PluginAPIInfo;
pluginId: string;
apiId: string;
pluginName: string;
debugExample?: DebugExample;
setDebugStatus?: (status: STATUS | undefined) => void;
setDebugExample?: (v: DebugExample) => void;
isViewExample?: boolean; // 查看 example 模式 标题不一样
onSuccessCallback?: () => void;
}> = ({
disabled,
apiInfo,
pluginId,
apiId,
pluginName,
setDebugStatus,
debugExample,
setDebugExample,
isViewExample = false,
pluginType,
onSuccessCallback,
}) => {
const [checkParams, setCheckParams] = useState<CheckParamsProps>({});
const [status, setStatus] = useState<STATUS | undefined>();
const handleAction = ({
status: innerStatus,
request,
response,
failReason,
rawResp,
}: CheckParamsProps) => {
setStatus(innerStatus);
setCheckParams({
status: innerStatus,
request,
response,
failReason,
rawResp,
});
setDebugStatus?.(innerStatus);
innerStatus === STATUS.PASS &&
setDebugExample?.({ req_example: request, resp_example: response });
// 调试成功后回调
innerStatus === STATUS.PASS && onSuccessCallback?.();
};
useEffect(() => {
if (debugExample) {
setCheckParams({
...checkParams,
request: debugExample?.req_example,
response: debugExample?.resp_example,
failReason: '',
});
} else {
setCheckParams({});
}
}, [debugExample]);
return (
<div
className={s['debug-check']}
data-testid="plugin.tool.debug-modal-content"
>
<Row gutter={16}>
<Col span={12}>
<div className={s['main-container']}>
<div className={s['card-header']}>
{isViewExample ? (
<Text className={s['card-title']}>
{I18n.t('Create_newtool_s4_title')}
</Text>
) : (
getApiTitle(pluginName, apiInfo.name, 'Create_newtool_s4_title')
)}
</div>
<div
style={{
maxHeight: isViewExample ? 'calc(100% - 55px)' : 542,
display: 'flex',
}}
>
<DebugParams
pluginType={pluginType}
disabled={disabled}
pluginId={pluginId}
apiId={apiId}
requestParams={apiInfo?.request_params}
callback={handleAction}
debugExampleStatus={apiInfo?.debug_example_status}
showExampleTag={!isViewExample}
/>
</div>
</div>
</Col>
<Col span={12}>
<div className={s['main-container']}>
<div className={s['card-header']}>
<Space style={{ width: '100%' }}>
{isViewExample ? (
<Text className={s['card-title']}>
{I18n.t('plugin_edit_tool_test_run_debugging_example')}
</Text>
) : (
getApiTitle(
pluginName,
apiInfo.name,
'Create_newtool_s4_result',
)
)}
{status === STATUS.PASS && (
<UITag color="green">{I18n.t('plugin_s4_debug_pass')}</UITag>
)}
{status === STATUS.FAIL && (
<UITag color="red">{I18n.t('plugin_s4_debug_failed')}</UITag>
)}
</Space>
</div>
<div
className={s['card-debug-check']}
style={{
height: isViewExample ? '100%' : 542,
}}
>
<DebugCheck checkParams={checkParams} />
</div>
</div>
</Col>
</Row>
</div>
);
};

View File

@@ -0,0 +1,74 @@
/*
* 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 AssistParameterType } from '@coze-arch/bot-api/plugin_develop';
import {
FILE_TYPE_CONFIG,
type FileTypeEnum,
} from '@coze-studio/file-kit/logic';
import { ACCEPT_UPLOAD_TYPES } from '@coze-studio/file-kit/config';
import { assistToExtend, parameterTypeExtendMap } from './config';
export const getFileAccept = (type: AssistParameterType) => {
const { fileTypes } = parameterTypeExtendMap[assistToExtend(type)];
const accept = fileTypes?.reduce((prev, curr) => {
const config = FILE_TYPE_CONFIG.find(c => c.fileType === curr);
if (!config) {
return prev;
}
prev = `${prev}${prev ? ',' : ''}${config.accept.join(',')}`;
return prev;
}, '');
if (!accept || accept === '*') {
return undefined;
}
return accept;
};
export const getFileTypeFromAssistType = (
type: AssistParameterType,
): FileTypeEnum | null => {
if (!type) {
return null;
}
const extendType = assistToExtend(type);
const config = Object.entries(parameterTypeExtendMap).find(
([key]) => Number(key) === extendType,
);
if (!config) {
return null;
}
for (const fileType of config[1].fileTypes) {
const iconConfig = ACCEPT_UPLOAD_TYPES[fileType];
if (iconConfig) {
return fileType;
}
}
return null;
};

View File

@@ -0,0 +1,372 @@
/* stylelint-disable declaration-no-important */
.create-modal {
:global {
.semi-modal {
max-width: 1800px;
}
.semi-table-row-cell {
overflow: hidden;
}
}
&.big-modal {
.modal-steps {
width: 810px;
margin: 0 auto;
margin-bottom: 24px;
}
}
}
.textarea-single-line {
:global {
.semi-input-textarea-counter {
position: absolute;
top: 3px;
right: 0;
}
}
}
.no-wrap {
white-space: nowrap;
}
.no-wrap-min-width {
white-space: nowrap;
}
.params-layout {
display: flex;
justify-content: space-between;
}
.params-tag {
margin-bottom: 18px;
padding-top: 22px;
font-size: 18px;
font-weight: 600;
}
.request-params,
.response-params {
:global {
.semi-table-placeholder {
padding: 1px 12px;
border-bottom: 0;
}
}
}
.request-params-edit,
.response-params-edit {
:global {
.semi-table-thead .semi-table-row-head:first-child {
padding-left: 32px !important;
}
.semi-table-placeholder {
padding: 1px 12px;
border-bottom: 0;
}
}
}
.check-box {
position: absolute;
}
.form-check-tip {
position: absolute;
top: 4px;
right: 0;
left: 0;
transform-origin: left;
display: inline-block;
font-size: 12px !important;
line-height: 16px;
color: var(--semi-color-danger);
}
.w110 {
width: 110%;
}
.plugin-icon-error {
position: relative;
top: 2px;
margin-right: 4px;
font-size: 13px;
}
.plugin-tooltip-error {
width: calc(100% - 20px);
font-size: 12px !important;
line-height: 16px !important;
color: var(--semi-color-danger) !important;
}
.add-params-btn-wrap {
margin: 0 24px;
padding-bottom: 12px;
border-top: 1px solid var(--semi-color-border);
}
.empty-content {
margin: 36px 0 54px;
font-size: 14px;
color: var(--light-usage-text-color-text-2, rgb(28 31 35 / 60%));
text-align: center;
}
// hover统一样式
.table-style-list {
:global {
.semi-table-body {
padding: 12px 0;
}
.semi-select {
border-radius: 8px;
}
.semi-table-row-cell {
padding: 12px 2px !important;
}
.semi-table-expand-icon {
margin-right: 8px;
}
.semi-table-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 24px;
width: calc(100% - 48px);
height: 1px;
background: var(--semi-color-border);
}
}
.semi-table-thead .semi-table-row-head:first-child {
padding-left: 32px !important;
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
border-bottom-color: transparent;
}
.semi-table-thead > .semi-table-row > .semi-table-row-head {
padding-right: 10px;
padding-left: 10px;
font-size: 12px;
font-weight: 600;
color: var(--light-usage-text-color-text-1, rgb(28 29 35 / 80%));
background: #f7f7fa;
border-bottom: 1px solid transparent;
}
.semi-table-row:hover > .semi-table-row-cell {
background: transparent !important;
border-bottom: 1px solid transparent !important;
}
.semi-table-tbody > .semi-table-row,
.semi-table-tbody > .semi-table-row > .semi-table-cell-fixed-left,
.semi-table-tbody > .semi-table-row > .semi-table-cell-fixed-right,
.semi-table-thead
> .semi-table-row
> .semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-thead
> .semi-table-row
> .semi-table-row-head.semi-table-cell-fixed-right::before {
cursor: pointer;
font-size: 12px;
font-weight: 400;
font-style: normal;
color: var(--light-usage-text-color-text-2, rgb(28 29 35 / 60%));
background: #f7f7fa;
}
.semi-spin-block.semi-spin {
height: 100%;
}
.semi-table-row:hover > .semi-table-row-cell:first-child {
border-top-left-radius: 8px !important;
border-bottom-left-radius: 8px !important;
}
.semi-table-row:hover > .semi-table-row-cell:last-child {
border-top-right-radius: 8px !important;
border-bottom-right-radius: 8px !important;
}
.semi-icon-chevron_down {
opacity: 0.6;
}
}
&.request-params,
&.response-params {
:global {
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
padding-left: 16px !important;
}
}
}
}
.input-modal {
.runbtn {
padding: 12px;
text-align: right;
}
:global {
.semi-modal-footer {
margin: 0 0 12px;
}
}
.debug-params-box {
:global {
.semi-table-thead > .semi-table-row > .semi-table-row-head {
border-bottom-width: 1px;
}
.semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell {
background: transparent !important;
border-bottom: 1px solid transparent !important;
}
}
}
}
.debug-check {
overflow: hidden;
height: 100%;
padding-bottom: 11px;
:global{
.semi-row, .semi-col{
height: 100%;
}
}
.main-container {
display: flex;
flex-direction: column;
max-width: 100vw;
height: 100%;
}
.card-header {
margin-bottom: 14px;
padding: 8px 0;
}
.card-title {
font-size: 18px;
font-weight: 600;
font-style: normal;
color: var(--light-usage-text-color-text-0, #1c1f23);
text-overflow: ellipsis;
}
.card-debug-check {
overflow: auto;
height: 100%;
max-height: 542px;
background: #fff;
border: 1px solid var(--Light-usage-border---color-border, rgba(29, 28, 37, 8%));
border-radius: 8px;
:global {
.markdown-body {
overflow: hidden;
}
}
}
.debug-params-box {
display: flex;
flex-direction: column;
width: 100%;
border: 1px solid rgb(29 28 35 / 8%);
border-radius: 8px;
.runbtn {
margin: 0 16px;
padding: 12px 0;
text-align: right;
border-top: 1px solid var(--semi-color-border);
:global {
.semi-button.semi-button-loading {
color: rgb(29 28 35 / 20%);
}
}
}
}
}
.safe-check-error {
color: #f93920;
a {
color: #4d53e8;
}
}
.base-info-form {
:global {
.semi-icon-chevron_down {
opacity: 0.6;
}
}
.plugin-url-input {
:global {
.semi-input-prepend {
border: none;
}
}
}
.plugin-url-prefix {
max-width: 480px;
}
}
.table-wrapper {
border: 1px solid rgb(29 28 35 / 8%);
border-radius: 8px;
}
.cascader-dropdown {
:global {
.semi-cascader-option-label {
font-weight: 400;
color: #1d1c23;
}
}
}

View File

@@ -0,0 +1,21 @@
.action-input-value-pre{
width: 115px;
border-right: none;
}
.action-input-value-content{
flex: 1;
}
.reference-option-item{
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
width: 200px;
.reference-option-subtext{
color:rgba(29, 28, 35, 35%)
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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 OptionProps } from '@coze-arch/bot-semi/Select';
import {
UIInput,
UISelect,
InputGroup,
Typography,
} from '@coze-arch/bot-semi';
import {
DefaultParamSource,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import styles from './index.module.less';
interface InputAndVariableItemProps {
record: APIParameter;
disabled?: boolean;
onSourceChange?: (val: number) => void;
onReferenceChange?: (val: string) => void;
onValueChange?: (val: string) => void;
referenceOption?: OptionProps[];
}
export const InputAndVariableItem = ({
record,
disabled,
onSourceChange,
onReferenceChange,
onValueChange,
referenceOption,
}: InputAndVariableItemProps) => (
<InputGroup style={{ width: '100%', flexWrap: 'nowrap' }}>
<UISelect
theme="light"
className={styles['action-input-value-pre']}
value={record.default_param_source || DefaultParamSource.Input}
disabled={disabled}
optionList={[
{
label: I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_reference',
),
value: DefaultParamSource.Variable,
},
{
label: I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_input',
),
value: DefaultParamSource.Input,
},
]}
onChange={val => {
onSourceChange?.(Number(val));
// 切换来源,清空默认值
onReferenceChange?.('');
onValueChange?.('');
}}
/>
{record.default_param_source === DefaultParamSource.Variable ? (
<UISelect
theme="light"
disabled={disabled}
style={{ width: '100%', overflow: 'hidden' }}
className={styles['action-input-value-content']}
placeholder={I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_reference_placeholder',
)}
value={record.variable_ref}
onChange={val => {
onReferenceChange?.(String(val));
}}
>
{referenceOption?.map(item => (
<UISelect.Option key={String(item.label)} value={String(item.label)}>
<div className={styles['reference-option-item']}>
<Typography.Text
className={styles['reference-option-text']}
ellipsis={{
showTooltip: {
opts: {
content: item.label,
style: { wordBreak: 'break-word' },
},
},
}}
>
{item.label}
</Typography.Text>
<Typography.Text
className={styles['reference-option-subtext']}
ellipsis={{
showTooltip: {
opts: {
content: item.value,
style: { wordBreak: 'break-word' },
},
},
}}
>
{item.value}
</Typography.Text>
</div>
</UISelect.Option>
))}
</UISelect>
) : (
<UIInput
disabled={disabled}
className={styles['action-input-value-content']}
placeholder={I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_input_placeholder',
)}
value={record.local_default}
onChange={val => {
onValueChange?.(String(val));
}}
/>
)}
</InputGroup>
);

View File

@@ -0,0 +1,138 @@
/*
* 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 FC } from 'react';
import { cloneDeep } from 'lodash-es';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { deleteAllChildNode } from '../../utils';
import {
type AddChildNodeFn,
type APIParameterRecord,
type UpdateNodeWithDataFn,
} from '../../types/params';
import {
ARRAYTAG,
getParameterTypeLabelFromRecord,
ParameterTypeExtend,
ROWKEY,
} from '../../config';
import { CascaderItem } from '../../components/cascader-item';
import { type ColumnsProps } from '..';
interface ParamTypeProps
extends Pick<
ColumnsProps,
'data' | 'setData' | 'disabled' | 'checkFlag' | 'isResponse'
> {
record: APIParameterRecord;
updateNodeWithData: UpdateNodeWithDataFn;
addChildNode: AddChildNodeFn;
enableFileType?: boolean;
}
const ParamTypeColRender: FC<ParamTypeProps> = ({
record,
disabled,
data,
setData,
checkFlag,
isResponse,
updateNodeWithData,
addChildNode,
enableFileType = false,
}) => {
// 删除全部子节点;
const handleDeleteAllChildNode = (r: APIParameter) => {
const cloneData = cloneDeep(data);
const delStatus = deleteAllChildNode(cloneData, r[ROWKEY] as string);
if (delStatus) {
setData(cloneData);
}
};
if (disabled) {
return (
<>{getParameterTypeLabelFromRecord(record, record.name === ARRAYTAG)}</>
);
}
return (
<CascaderItem
check={checkFlag}
record={record}
enableFileType={enableFileType}
selectCallback={([cascaderType, assistType]) => {
let type = cascaderType;
if (cascaderType === ParameterTypeExtend.DEFAULT) {
type = ParameterType.String;
}
if (!isResponse) {
// 切换类型重置default value
if (record.global_default) {
updateNodeWithData({
record,
key: ['global_default', 'global_disable'],
value: ['', false],
updateData: true,
});
}
}
const payload = {
record,
key: ['type', 'assist_type'],
value: [type, assistType ?? null],
};
// updateNodeWithData 会变更type类型保留原始的type
const recordType = record?.type;
if (type === ParameterType.Array) {
updateNodeWithData({
...payload,
updateData: true,
});
addChildNode({ record, isArray: true, type, recordType });
} else if (type === ParameterType.Object) {
updateNodeWithData({
...payload,
updateData: true,
});
addChildNode({ record, isArray: false, type, recordType });
} else if (
record?.type === ParameterType.Array ||
record?.type === ParameterType.Object
) {
updateNodeWithData(payload);
handleDeleteAllChildNode(record);
} else {
updateNodeWithData({
...payload,
updateData: true,
});
}
}}
/>
);
};
export default ParamTypeColRender;

View File

@@ -0,0 +1,224 @@
/*
* 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, useRef, useState } from 'react';
import { cloneDeep } from 'lodash-es';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { Toast, UIButton, UIModal } from '@coze-arch/bot-semi';
import { IconEdit } from '@coze-arch/bot-icons';
import {
ParameterType,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import {
scrollToErrorElement,
transformArrayToTree,
transformTreeToObj,
updateNodeById,
} from '../utils';
import { InputAndVariableItem } from '../input-and-variable';
import ParamsForm from '../debug-components/params-form';
import { ROWKEY } from '../config';
import { InputItem } from './form-components';
import styles from './index.module.less';
interface DefaultValueInputProps {
record: APIParameter;
data: Array<APIParameter>;
defaultKey?: 'global_default' | 'local_default';
disableKey?: 'global_disable' | 'local_disable';
setData: (val: Array<APIParameter>) => void;
canReference?: boolean;
referenceOption?: OptionProps[];
}
interface DefaultModalProps {
record: APIParameter;
defaultKey: 'global_default' | 'local_default';
disableKey: 'global_disable' | 'local_disable';
updateNodeAndData: (key: string, value: string) => void;
}
const DefaultValueModal = ({
record,
defaultKey,
disableKey,
updateNodeAndData,
}: DefaultModalProps) => {
const [check, setCheck] = useState(0);
const [visible, setVisible] = useState(false);
const paramsFormRef = useRef<{ data: Array<APIParameter> }>(null);
const [defRecord, setDefRecord] = useState<APIParameter>(
{} satisfies APIParameter,
);
const handleOpen = useCallback(() => {
setVisible(true);
const r = cloneDeep(record);
if (r[defaultKey]) {
const tree = transformArrayToTree(
JSON.parse(r[defaultKey] || '[]'),
r.sub_parameters || [],
);
r.sub_parameters = tree;
}
setDefRecord(r);
}, [record]);
const handleClose = () => {
setVisible(false);
setDefRecord({} satisfies APIParameter);
};
const handleSave = () => {
// 校验是否必填
setCheck(check + 1);
const errorEle = document.getElementsByClassName('errorDebugClassTag');
if (errorEle.length > 0) {
scrollToErrorElement('.errorDebugClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return;
}
const reqParams = Object.values(
transformTreeToObj(paramsFormRef.current?.data, false),
);
updateNodeAndData(defaultKey, JSON.stringify(reqParams[0]));
handleClose();
};
return (
<>
<UIButton
disabled={record.is_required && record[disableKey]}
icon={<IconEdit />}
className={styles['arr-edit-btn']}
style={{ width: '100%' }}
onClick={handleOpen}
>
{I18n.t('plugin_edit_tool_default_value_array_edit_button')}
</UIButton>
{visible ? (
<UIModal
title={I18n.t(
'plugin_edit_tool_default_value_array_edit_modal_title',
)}
width={792}
okText={I18n.t('Save')}
visible={visible}
onCancel={handleClose}
hasCancel={false}
onOk={handleSave}
zIndex={1050}
>
<ParamsForm
ref={paramsFormRef}
requestParams={[defRecord]}
defaultKey={defaultKey}
disabled={false}
check={check}
needCheck={false}
height={400}
/>
</UIModal>
) : null}
</>
);
};
export const DefaultValueInput = ({
record,
data,
setData,
canReference = false,
defaultKey = 'global_default', //输入框的key
disableKey = 'global_disable', //开启按钮key
referenceOption,
}: DefaultValueInputProps) => {
// @ts-expect-error -- linter-disable-autofix
const updateNodeAndData = (key, value) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: key,
value,
});
const cloneData = cloneDeep(data);
setData(cloneData);
};
if (record[defaultKey] === undefined) {
return <></>;
}
// 复杂类型暂不支持引用变量
if (record.type === ParameterType.Array) {
return (
<div className={styles['modal-wrapper']}>
<DefaultValueModal
record={record}
defaultKey={defaultKey}
disableKey={disableKey}
updateNodeAndData={updateNodeAndData}
/>
</div>
);
}
return (
<>
{canReference ? (
<InputAndVariableItem
record={record}
disabled={!!record[disableKey]}
referenceOption={referenceOption}
onSourceChange={val => {
updateNodeAndData('default_param_source', val);
}}
onReferenceChange={val => {
updateNodeAndData('variable_ref', val);
}}
onValueChange={val => {
updateNodeAndData(defaultKey, val);
}}
/>
) : (
<InputItem
width="100%"
placeholder={I18n.t(
'plugin_edit_tool_default_value_input_placeholder',
)}
max={2000}
val={record[defaultKey]}
useCheck={false}
filterSpace={false}
disabled={!!record[disableKey]}
callback={(e: string) => {
updateNodeAndData(defaultKey, e);
}}
/>
)}
</>
);
};

View File

@@ -0,0 +1,306 @@
/*
* 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 { useEffect, useState } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { UIInput, UISelect, Typography, Tooltip } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import { ParameterType } from '@coze-arch/bot-api/plugin_develop';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { checkSameName } from '../utils';
import { type InputItemProps } from '../types';
import s from '../index.module.less';
import {
ARRAYTAG,
ParamsFormErrorStatus,
paramsFormErrorStatusText,
ROOTTAG,
} from '../config';
const DEEP_INDENT_NUM = 20;
export const InputItem = ({
val = '',
max = 500,
check = 0,
width = 200,
useCheck = true,
filterSpace = true,
placeholder,
callback,
targetKey = '',
checkSame = false,
checkAscii = false,
isRequired = false,
data,
useBlockWrap = false,
disabled,
dynamicWidth = false,
deep = 1,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(val);
const [errorStatus, setErrorStatus] = useState<number>(0);
useEffect(() => {
setValue(val);
}, [val]);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0 || value === ARRAYTAG || value === ROOTTAG) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (checkVal: string) => {
let status =
checkVal === ''
? ParamsFormErrorStatus.NAME_EMPTY
: ParamsFormErrorStatus.NO_ERROR;
if (isRequired && checkVal === '') {
setErrorStatus(ParamsFormErrorStatus.DESC_EMPTY);
return;
}
if (checkAscii) {
if (!IS_OVERSEA) {
setErrorStatus(ParamsFormErrorStatus.NO_ERROR);
return;
}
// eslint-disable-next-line no-control-regex
status = /^[\x00-\x7F]+$/.test(checkVal)
? ParamsFormErrorStatus.NO_ERROR
: ParamsFormErrorStatus.ASCII;
status = checkVal === '' ? ParamsFormErrorStatus.NO_ERROR : status;
setErrorStatus(status);
}
if (!useCheck) {
return;
}
if (!status) {
status = !/^[\w-]+$/.test(checkVal)
? ParamsFormErrorStatus.CHINESE
: ParamsFormErrorStatus.NO_ERROR;
}
if (!status && data && checkSame) {
status = checkSameName(data, targetKey, checkVal)
? ParamsFormErrorStatus.REPEAT
: ParamsFormErrorStatus.NO_ERROR;
}
setErrorStatus(status);
};
// 过滤空格、限制输入长度
const handleFilter = (v: string) => {
if (filterSpace) {
v = v.replace(/\s+/g, '');
}
if (max > 0) {
v = v.slice(0, max);
}
return v;
};
const hasSub =
deep === 1
? data?.some(
item =>
item.type === ParameterType.Array ||
item.type === ParameterType.Object,
)
: true;
// 每增加一层因为有展开icon宽度减少20
const vWidth = dynamicWidth
? `calc(100% - ${DEEP_INDENT_NUM * deep}px)`
: width;
const tipWidth = dynamicWidth
? `calc(100% - ${DEEP_INDENT_NUM * deep}px - 8px)`
: width;
const errorStatusMsg = () => (
<>
{dynamicWidth && !hasSub ? (
<span style={{ display: 'inline-block', width: 22 }}></span>
) : null}
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{/* @ts-expect-error -- linter-disable-autofix */}
<span>{paramsFormErrorStatusText[errorStatus]}</span>
</Typography.Text>
</>
);
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
{dynamicWidth && !hasSub ? (
<span style={{ display: 'inline-block', width: 20 }}></span>
) : null}
<UIInput
placeholder={placeholder}
disabled={disabled || value === ARRAYTAG || value === ROOTTAG}
style={{ width: vWidth }}
value={value}
validateStatus={errorStatus ? 'error' : 'default'}
onChange={(e: string) => {
const newVal = handleFilter(e);
callback?.(newVal);
setValue(newVal);
handleCheck(newVal);
}}
onBlur={() => {
handleCheck(value);
}}
/>
<br />
{/* 参数名称设置动态列宽 */}
{errorStatus !== 0 && dynamicWidth ? (
<div className={s['check-box']} style={{ width: tipWidth }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
{errorStatusMsg()}
</span>
</div>
) : null}
{/* 非参数列表设置固定最大宽 */}
{errorStatus !== 0 && !dynamicWidth && (
<div className={s['check-box']} style={{ width: tipWidth }}>
<span
style={{
marginLeft: 4,
right: -15,
}}
className={cl(s['form-check-tip'], 'errorClassTag')}
>
{errorStatusMsg()}
</span>
</div>
)}
</span>
);
};
export const SelectItem = ({
check = 0,
useBlockWrap = false,
record,
disabled,
typeOptions,
selectCallback,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(!record?.type ? undefined : record?.type);
const [errorStatus, setErrorStatus] = useState<number>(0);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (val: string | ParameterType | undefined) => {
const status = val === undefined ? 1 : 0;
setErrorStatus(status);
};
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
<UISelect
theme="light"
validateStatus={errorStatus ? 'error' : 'default'}
value={value}
disabled={disabled}
onChange={e => {
selectCallback?.(e);
setValue(e as ParameterType);
handleCheck(e as ParameterType);
}}
style={{ width: '100%' }}
>
{typeOptions?.map(item => (
<UISelect.Option
key={(record?.id || '') + item.label}
value={item.value}
>
{item.label}
</UISelect.Option>
))}
</UISelect>
<br />
{errorStatus !== 0 && (
<div style={{ position: 'relative' }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
<IconAlertCircle className={s['plugin-icon-error']} />
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{errorStatus === 1 && (
<span>{I18n.t('plugin_Parameter_type')}</span>
)}
</Typography.Text>
</span>
</div>
)}
</span>
);
};
interface FormTitle {
name: string;
required?: boolean;
toolTipText?: string;
}
export const FormTitle = (titleInfo: FormTitle) => (
<div className="whitespace-nowrap">
{titleInfo.name}
{titleInfo.required ? (
<Typography.Text style={{ color: 'red', marginLeft: -3 }}>
{' * '}
</Typography.Text>
) : null}
{titleInfo.toolTipText ? (
<Tooltip content={titleInfo.toolTipText}>
<IconInfo
style={{
color: '#5f5f5f9e',
position: 'relative',
top: 3,
left: 2,
}}
/>
</Tooltip>
) : null}
</div>
);

View File

@@ -0,0 +1,36 @@
/* stylelint-disable declaration-no-important */
.modal-wrapper {
.arr-edit-btn {
background-color: #f7f7fa !important;
border-color: rgba(29, 28, 37, 12%) !important;
svg {
path:first-child {
stroke: #4D53E8;
stroke-opacity: 1;
}
path:last-child {
fill: #4D53E8;
fill-opacity: 1;
}
}
}
:global {
.semi-button-disabled {
color: rgba(56, 55, 67, 20%) !important;
background-color: rgba(75, 74, 88, 4%) !important;
svg {
path:first-child {
stroke: rgba(56, 55, 67, 20%);
}
path:last-child {
fill: rgba(56, 55, 67, 20%);
}
}
}
}
}

View File

@@ -0,0 +1,565 @@
/*
* 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 -- 历史逻辑 陆续在拆 */
import { cloneDeep, flow, get as ObjectGet, set as ObjectSet } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { ArrayUtil } from '@coze-arch/bot-utils';
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
import {
UIIconButton,
UISelect,
Typography,
Tooltip,
Switch,
Space,
Checkbox,
} from '@coze-arch/bot-semi';
import { IconAddChildOutlined, IconDeleteOutline } from '@coze-arch/bot-icons';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import {
defaultNode,
deleteAllChildNode,
deleteNode,
findPathById,
handleIsShowDelete,
updateNodeById,
} from '../utils';
import {
type UpdateNodeWithDataFn,
type APIParameterRecord,
type AddChildNodeFn,
} from '../types';
import s from '../index.module.less';
import {
childrenRecordName,
parameterLocationOptions,
ROWKEY,
} from '../config';
import { FormTitle, InputItem } from './form-components';
import { DefaultValueInput } from './default-value-input';
import ParamTypeColRender from './columns/param-type-col';
const DEEP_INDENT_NUM = 20;
const DISABLED_REQ_SLICE = -4;
const DISABLED_RES_SLICE = -3;
export interface ColumnsProps {
data: Array<APIParameter>;
flag: boolean;
checkFlag: number;
isResponse?: boolean;
disabled: boolean;
setCheckFlag: (val: number) => void;
setFlag: (val: boolean) => void;
setData: (val: Array<APIParameter>, checkDefault?: boolean) => void;
showSecurityCheckFailedMsg: boolean;
setShowSecurityCheckFailedMsg: (flag: boolean) => void;
/**
* 是否支持扩展的文件类型
*/
enableFileType?: boolean;
}
// eslint-disable-next-line max-lines-per-function
export const getColumns = ({
data,
checkFlag,
isResponse = false,
disabled,
setCheckFlag,
setData,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
enableFileType = false,
}: ColumnsProps) => {
// 添加子节点
const addChildNode: AddChildNodeFn = ({
record,
isArray = false,
type,
recordType,
}) => {
const newData = cloneDeep(data);
// @ts-expect-error -- linter-disable-autofix
const deleteArrayGlobalDefaultByPath = (obj: APIParameter[], path) => {
const index = path[0];
const child = obj[index];
if (child && child.type === ParameterType.Array) {
child.global_default = '';
child.global_disable = false;
} else {
if (child && child.sub_parameters) {
deleteArrayGlobalDefaultByPath(child.sub_parameters, path.slice(1));
}
}
};
setCheckFlag(0);
let result: APIParameter & {
path?: Array<number>;
} = {};
// 1.查找路径
findPathById({
data,
callback: (item: APIParameter, path: Array<number>) => {
if (item[ROWKEY] === record[ROWKEY]) {
result = { ...item, path };
// 修改复杂类型结构,需要重置数组的默认值
deleteArrayGlobalDefaultByPath(newData, path);
}
},
});
// 2.拼接路径
const path = (result?.path || [])
.map((v: number) => [v, childrenRecordName])
.flat();
// 如果是添加子节点,则更新父节点中的类型
if (recordType) {
const typePath = cloneDeep(path);
typePath.pop();
typePath.push('type');
// type 为4/5切换节点的时候需要先删除子节点
// recordType 原节点的类型
// newData 新节点数据
if (ObjectGet(newData, typePath) !== recordType) {
deleteAllChildNode(newData, record[ROWKEY] as string);
}
ObjectSet(newData, typePath, type);
}
// 3.添加节点
if (Array.isArray(ObjectGet(newData, path))) {
ObjectSet(newData, path, [
...ObjectGet(newData, path),
// @ts-expect-error -- linter-disable-autofix
defaultNode({ isArray, iscChildren: true, deep: record.deep + 1 }),
]);
} else {
ObjectSet(newData, path, [
defaultNode({
isArray,
iscChildren: true,
// @ts-expect-error -- linter-disable-autofix
deep: record.deep + 1,
}),
]);
}
setData(newData);
};
// 删除子节点
const deleteChildNode = (record: APIParameter) => {
const cloneData = cloneDeep(data);
const delStatsu = deleteNode(cloneData, record[ROWKEY] as string);
if (delStatsu) {
setData(cloneData);
}
};
const updateNodeWithData: UpdateNodeWithDataFn = ({
record,
key,
value,
updateData = false,
inherit = false,
}) => {
if (Array.isArray(key)) {
key.forEach((item, idx) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: item,
// @ts-expect-error -- linter-disable-autofix
value: value[idx],
});
});
} else {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: key,
value,
inherit,
});
}
if (updateData) {
const cloneData = cloneDeep(data);
setData(cloneData);
}
};
const columns: Array<ColumnProps<APIParameter>> = [
{
title: () => (
<FormTitle
name={I18n.t('Create_newtool_s3_table_name')}
required
toolTipText={
isResponse
? I18n.t('Create_newtool_s3_table_name_tooltip')
: I18n.t('Create_newtool_s2_table_name_tooltip')
}
/>
),
key: 'name',
className: s['no-wrap-min-width'],
render: (record: APIParameter & { deep?: number }) =>
disabled ? (
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
style={{
maxWidth: `calc(100% - ${
DEEP_INDENT_NUM * (record.deep || 1)
}px)`,
}}
>
{record.name}
</Typography.Text>
) : (
<InputItem
check={checkFlag}
val={record?.name}
data={data}
placeholder={I18n.t('Create_newtool_s2_table_name_empty')}
useBlockWrap={true}
checkSame={true}
targetKey={record[ROWKEY]}
dynamicWidth={true}
deep={record.deep}
callback={(e: string) => {
// record.name = e;
updateNodeWithData({
record,
key: 'name',
value: e,
updateData: true,
});
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
}}
/>
),
},
{
title: () => (
<FormTitle
name={I18n.t('Create_newtool_s2_table_des')}
required={!isResponse}
toolTipText={
isResponse
? I18n.t('Create_newtool_s3_table_des_tooltip')
: I18n.t('Create_newtool_s2_table_des_tooltip')
}
/>
),
key: 'desc',
render: (record: APIParameter) =>
// ,帮助用户/大模型更好地理解。
disabled ? (
<Typography.Text
component="div"
ellipsis={{
showTooltip: {
opts: {
style: { wordBreak: 'break-word' },
},
},
}}
style={{ maxWidth: '100%' }}
>
{record.desc}
</Typography.Text>
) : (
<InputItem
check={checkFlag}
width="100%"
placeholder={I18n.t('plugin_Parameter_des')}
val={record?.desc}
useCheck={false}
checkAscii={true}
filterSpace={false}
max={300}
isRequired={isResponse ? false : true}
callback={(e: string) => {
updateNodeWithData({
record,
key: 'desc',
value: e,
});
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
}}
/>
),
},
{
title: () => (
<FormTitle name={I18n.t('Create_newtool_s3_table_type')} required />
),
key: 'type',
width: 120,
render: (record: APIParameterRecord) => (
<ParamTypeColRender
record={record}
disabled={disabled}
data={data}
setData={setData}
checkFlag={checkFlag}
updateNodeWithData={updateNodeWithData}
addChildNode={addChildNode}
enableFileType={enableFileType}
/>
),
},
{
title: () => (
<FormTitle name={I18n.t('Create_newtool_s2_table_method')} required />
),
key: 'location',
width: 120,
render: (record: APIParameter) => {
if (record.location === undefined) {
return <></>;
}
const methodLabelMap = ArrayUtil.array2Map(
parameterLocationOptions,
'value',
'label',
);
return disabled ? (
methodLabelMap[record.location]
) : (
<UISelect
theme="light"
defaultValue={record.location}
onChange={e => {
updateNodeWithData({
record,
key: 'location',
value: e,
updateData: true,
inherit: true,
});
}}
style={{ width: '100%' }}
>
{parameterLocationOptions.map(item => (
<UISelect.Option key={record?.id + item.label} value={item.value}>
{item.label}
</UISelect.Option>
))}
</UISelect>
);
},
},
{
title: I18n.t('Create_newtool_s2_table_required'),
width: 80,
key: 'default',
render: (record: APIParameter) => (
<Checkbox
style={{ position: 'relative', left: 18 }}
disabled={disabled}
defaultChecked={record.is_required}
onChange={e => {
// 必填 + 没有默认值 = 可见
if (e.target.checked && !record.global_default) {
updateNodeWithData({
record,
key: 'global_disable',
value: false,
updateData: true,
inherit: true,
});
}
updateNodeWithData({
record,
key: 'is_required',
value: e.target.checked,
updateData: true,
inherit: true,
});
}}
></Checkbox>
),
},
{
title: I18n.t('plugin_api_list_table_action'),
key: 'addChild',
width: 107,
render: (record: APIParameter & { deep: number }) => (
<Space>
{record.type === ParameterType.Object && (
<Tooltip content={I18n.t('plugin_form_add_child_tooltip')}>
<UIIconButton
disabled={disabled}
style={{ marginLeft: '8px' }}
onClick={() => addChildNode({ record })}
icon={<IconAddChildOutlined />}
type="secondary"
/>
</Tooltip>
)}
{handleIsShowDelete(data, record[ROWKEY]) && (
<Tooltip content={I18n.t('Delete')}>
<UIIconButton
disabled={disabled}
style={{ marginLeft: '8px' }}
onClick={() => deleteChildNode(record)}
icon={<IconDeleteOutline />}
type="secondary"
/>
</Tooltip>
)}
</Space>
),
},
];
if (!isResponse) {
columns.splice(
-1,
0,
...[
{
title: () => (
<FormTitle
name={I18n.t(
'plugin_edit_tool_default_value_config_item_default_value',
)}
/>
),
key: 'global_default',
width: 120,
render: (record: APIParameter) => (
<DefaultValueInput record={record} data={data} setData={setData} />
),
},
{
title: (
<FormTitle
name={I18n.t('plugin_edit_tool_default_value_config_item_enable')}
toolTipText={I18n.t(
'plugin_edit_tool_default_value_config_item_enable_tip',
)}
/>
),
key: 'global_disable',
width: 78,
render: (record: APIParameter) => {
if (record.global_default === undefined) {
return <></>;
}
const switchNode = (
<Switch
style={{ position: 'relative', top: 3, left: 12 }}
defaultChecked={!record.global_disable}
disabled={record.is_required && !record.global_default}
onChange={e => {
updateNodeWithData({
record,
key: 'global_disable',
value: !e,
updateData: true,
inherit: true,
});
}}
/>
);
return record.is_required && !record.global_default ? (
<Tooltip
content={I18n.t(
'plugin_edit_tool_default_value_config_item_enable_disable_tip',
)}
>
{switchNode}
</Tooltip>
) : (
switchNode
);
},
},
],
);
}
//出参场景,移除 required增加 enabled 开关
if (isResponse) {
const targetIndex = columns.findIndex(c => c.key === 'default');
columns.splice(targetIndex, 1);
columns.splice(-1, 0, {
title: (
<FormTitle
name={I18n.t('plugin_edit_tool_default_value_config_item_enable')}
toolTipText={I18n.t('plugin_edit_tool_output_param_enable_tip')}
/>
),
key: 'global_disable',
width: 78,
render: (record: APIParameter) => {
if (record.global_default === undefined) {
return <></>;
}
const switchNode = (
<Switch
style={{ position: 'relative', top: 3, left: 12 }}
defaultChecked={!record.global_disable}
onChange={e => {
updateNodeWithData({
record,
key: 'global_disable',
value: !e,
updateData: true,
inherit: true,
});
}}
/>
);
return switchNode;
},
});
}
return flow(
// 将 columns 以函数参数形式传入,而非直接传给组合函数(`flow(...)(columns)`)是为了利于类型推导
() => columns,
// 只读状态不展示后四项操作列
newColumns => {
const len = isResponse ? DISABLED_RES_SLICE : DISABLED_REQ_SLICE;
return disabled ? newColumns.slice(0, len) : newColumns;
},
// response不需要location字段
newColumns =>
isResponse
? newColumns.filter(item => item.key !== 'location')
: newColumns,
)();
};

View File

@@ -0,0 +1,245 @@
/*
* 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 {
type Dispatch,
type ReactNode,
type SetStateAction,
useEffect,
useState,
} from 'react';
import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Toast, Table } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import {
type APIParameter,
type UpdateAPIRequest,
type PluginAPIInfo,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
initParamsDefault,
defaultNode,
maxDeep,
scrollToBottom,
scrollToErrorElement,
sleep,
} from './utils';
import { ERROR_CODE, type RenderEnhancedComponentProps } from './types';
import { getColumns } from './params-components';
import { ROWKEY, childrenRecordName } from './config';
import s from './index.module.less';
const STARTNUM = 4;
const CHANGENUM = 13;
const SMALLGAP = 19;
const MAXZGAP = 40;
const TIMER = 100;
export interface UseRequestParamsProps {
pluginId: string;
apiId?: string;
requestParams: Array<APIParameter> | undefined;
step?: number;
disabled: boolean;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
editVersion?: number;
functionName?: string;
apiInfo?: PluginAPIInfo;
spaceID: string;
onSuccess?: (params: UpdateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderParamsComponent'];
}
export interface UseRequestParamsReturnValue {
submitRequestParams: () => Promise<boolean>;
requestParamsNode: JSX.Element;
nlTool?: ReactNode;
}
export const useRequestParams = ({
apiInfo,
pluginId,
apiId,
requestParams,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
functionName,
spaceID,
onSuccess,
renderEnhancedComponent,
}: UseRequestParamsProps): UseRequestParamsReturnValue => {
const [data, setFormData] = useState<Array<APIParameter>>(
requestParams ? requestParams : [],
);
// @ts-expect-error -- linter-disable-autofix
const setData = (formData, checkDefault = true) => {
let fd = formData;
if (checkDefault) {
fd = initParamsDefault(formData, 'global_default');
}
setFormData(fd);
};
const [flag, setFlag] = useState<boolean>(false); // 为了更新视图
const [checkFlag, setCheckFlag] = useState<number>(0); // 全局校验用
const columns = getColumns({
data,
flag,
checkFlag,
setCheckFlag,
setFlag,
setData,
disabled,
// @ts-expect-error -- linter-disable-autofix
showSecurityCheckFailedMsg,
// @ts-expect-error -- linter-disable-autofix
setShowSecurityCheckFailedMsg,
enableFileType: true,
});
useEffect(() => {
if (
Array.isArray(requestParams) &&
requestParams.length === 0 &&
Array.isArray(data) &&
data.length === 0
) {
return;
}
setData(requestParams ? requestParams : []);
}, [disabled, requestParams]);
const submitRequestParams = async () => {
setCheckFlag(checkFlag + 1);
const sleepTime = 100;
await sleep(sleepTime);
if (!apiId || document.getElementsByClassName('errorClassTag').length > 0) {
scrollToErrorElement('.errorClassTag');
Toast.error({
content: I18n.t('tool_new_S2_feedback_failed'),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
try {
const params: UpdateAPIRequest = {
plugin_id: pluginId,
api_id: apiId,
request_params: data,
edit_version: editVersion,
function_name: functionName,
};
const resData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(resData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const addParams = () => {
setCheckFlag(0);
const cloneData = cloneDeep(data);
cloneData.push(defaultNode());
setData(cloneData);
setTimeout(() => {
scrollToBottom(document.getElementsByClassName('semi-table-body')[0]);
}, TIMER);
};
const maxNum = maxDeep(data);
return {
submitRequestParams,
requestParamsNode: (
<div>
<div
className={s['table-wrapper']}
style={{ minWidth: 1008, overflowY: 'auto' }}
>
<Table
// 最小宽度为了兼容多层级场景最大层级可支持超过50层
// 最小宽度 = 模块最小宽度 + (当前层级数 - 宽度变化起始层级) * (当前层级数 < 宽度变化起始层级 ? 小间隔数 : 大间隔数)
style={{
minWidth: `calc(1008px + ${
(maxNum - STARTNUM) * (maxNum < CHANGENUM ? SMALLGAP : MAXZGAP)
}px)`,
}} // 从第4层开始每多一层增加19px
pagination={false}
columns={columns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
className={classNames(
disabled ? s['request-params'] : s['request-params-edit'],
s['table-style-list'],
)}
empty={<div></div>}
/>
{!disabled && (
<div
style={
Array.isArray(data) && data.length === 0 ? { borderTop: 0 } : {}
}
className={s['add-params-btn-wrap']}
>
<UIButton
disabled={disabled}
icon={<IconAdd />}
style={{ marginTop: 12 }}
type="tertiary"
onClick={addParams}
>
{I18n.t('Create_newtool_s3_table_new')}
</UIButton>
</div>
)}
</div>
</div>
),
nlTool: renderEnhancedComponent?.({
disabled: !data?.length || disabled,
src: 'request',
originParams: data,
apiInfo,
onSetParams: p => setFormData(p),
spaceID,
pluginId,
}),
};
};

View File

@@ -0,0 +1,371 @@
/*
* 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 {
type Dispatch,
type ReactNode,
type SetStateAction,
useEffect,
useState,
} from 'react';
import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import { useMemoizedFn } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { UIButton, Toast, UIModal, Table } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import {
PluginType,
type APIParameter,
type UpdateAPIRequest,
type PluginAPIInfo,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
addDepthAndValue,
defaultNode,
sleep,
maxDeep,
scrollToErrorElement,
scrollToBottom,
initParamsDefault,
doRemoveDefaultFromResponseParams,
} from './utils';
import {
type RenderEnhancedComponentProps,
type CheckParamsProps,
ERROR_CODE,
STATUS,
} from './types';
import { getColumns } from './params-components';
import { DebugParams } from './debug-components/debug-params';
import { ROWKEY, childrenRecordName } from './config';
import s from './index.module.less';
export interface UseRequestParamsProps {
pluginId: string;
apiId: string;
requestParams: Array<APIParameter> | undefined;
responseParams: Array<APIParameter> | undefined;
step?: number;
disabled: boolean;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
editVersion?: number;
pluginType?: PluginType;
functionName?: string;
apiInfo?: PluginAPIInfo;
spaceID: string;
onSuccess?: (params: UpdateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderParamsComponent'];
}
export interface UseRequestParamsReturnValue {
submitResponseParams: () => Promise<boolean>;
responseParamsNode: JSX.Element;
extra?: ReactNode;
}
const SLEEP_TIME = 100;
const TIMER = 100;
export const useResponseParams = ({
apiInfo,
pluginId,
requestParams,
responseParams,
apiId,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
pluginType,
functionName,
spaceID,
onSuccess,
renderEnhancedComponent,
}: UseRequestParamsProps): UseRequestParamsReturnValue => {
const [data, setFormData] = useState<Array<APIParameter>>(
responseParams || [],
);
const [flag, setFlag] = useState<boolean>(false); // 为了更新视图
const [checkFlag, setCheckFlag] = useState<number>(0); // 全局校验用
const [inputModal, setInputModal] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const setData = useMemoizedFn((formData, checkDefault = true) => {
let fd = formData;
if (checkDefault) {
fd = initParamsDefault(formData, 'global_default');
}
setFormData(fd);
});
useEffect(() => {
if (
Array.isArray(responseParams) &&
responseParams.length === 0 &&
Array.isArray(data) &&
data.length === 0
) {
return;
}
setData(responseParams || []);
}, [disabled, responseParams]);
const columns = getColumns({
data,
flag,
checkFlag,
setCheckFlag,
setFlag,
setData,
isResponse: true,
disabled,
// @ts-expect-error -- linter-disable-autofix
showSecurityCheckFailedMsg,
// @ts-expect-error -- linter-disable-autofix
setShowSecurityCheckFailedMsg,
enableFileType: true,
});
const submitResponseParams = async () => {
setCheckFlag(checkFlag + 1);
await sleep(SLEEP_TIME);
if (!apiId || document.getElementsByClassName('errorClassTag').length > 0) {
scrollToErrorElement('.errorClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
if (!apiId) {
return false;
}
try {
const params: UpdateAPIRequest = {
plugin_id: pluginId,
api_id: apiId,
response_params: doRemoveDefaultFromResponseParams(data, false),
edit_version: editVersion,
function_name: functionName,
};
const resData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(resData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const handleAction = ({
response_params,
status,
failReason,
}: CheckParamsProps) => {
if (status === STATUS.PASS && response_params) {
addDepthAndValue(response_params);
setData(response_params);
Toast.success({
content: I18n.t('plugin_s3_success'),
duration: 3,
theme: 'light',
showClose: false,
});
} else {
Toast.error({
content: withSlardarIdButton(failReason ?? I18n.t('plugin_s3_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
}
setInputModal(false);
};
const handleActionNoParams = async () => {
try {
setLoading(true);
const resData = await PluginDevelopApi.DebugAPI({
plugin_id: pluginId,
api_id: apiId,
parameters: JSON.stringify({}),
operation: 2,
});
if (resData?.success && resData?.response_params) {
setData(resData.response_params);
Toast.success({
content: I18n.t('plugin_s3_success'),
duration: 3,
theme: 'light',
showClose: false,
});
} else {
Toast.error({
content: withSlardarIdButton(
resData?.reason ?? I18n.t('plugin_s3_failed'),
),
duration: 3,
theme: 'light',
showClose: false,
});
}
} catch (error) {
Toast.error({
content: withSlardarIdButton(I18n.t('plugin_s3_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
logger.persist.error({
message: 'Custom Error: debug api failed',
// @ts-expect-error -- linter-disable-autofix
error,
});
}
setLoading(false);
};
const addParams = () => {
setCheckFlag(0);
const cloneData = cloneDeep(data);
cloneData.push(defaultNode());
setData(cloneData);
setFlag(!flag);
setTimeout(() => {
scrollToBottom(document.getElementsByClassName('semi-table-body')[0]);
}, TIMER);
};
const maxNum = maxDeep(data);
return {
submitResponseParams,
responseParamsNode: (
<div>
<div
className={s['table-wrapper']}
style={{ minWidth: 1008, overflowY: 'auto' }}
>
<Table
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- ui
style={{ minWidth: `calc(1008px + ${(maxNum - 6) * 20}px)` }} // 从第6层开始每多一层增加20px
pagination={false}
columns={columns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
className={classNames(
disabled ? s['request-params'] : s['request-params-edit'],
s['table-style-list'],
)}
empty={<div></div>}
/>
{!disabled && (
<div
className={s['add-params-btn-wrap']}
style={
Array.isArray(data) && data.length === 0 ? { borderTop: 0 } : {}
}
>
<UIButton
disabled={disabled}
icon={<IconAdd />}
style={{ marginTop: 12 }}
type="tertiary"
onClick={addParams}
>
{I18n.t('Create_newtool_s3_table_new')}
</UIButton>
</div>
)}
</div>
<UIModal
visible={inputModal}
title={I18n.t('plugin_s3_Parse')}
className={s['input-modal']}
keepDOM={false}
footer={<></>}
width={800}
maskClosable={false}
onCancel={() => setInputModal(false)}
>
<DebugParams
disabled={disabled}
pluginId={pluginId}
apiId={apiId}
requestParams={requestParams}
operation={2}
btnText={I18n.t('Create_newtool_s3_button_auto')}
callback={handleAction}
/>
</UIModal>
</div>
),
extra: (
<>
{renderEnhancedComponent?.({
disabled: !data?.length || disabled,
src: 'response',
originParams: data,
apiInfo,
onSetParams: p => setData(p),
spaceID,
pluginId,
})}
<Button
disabled={disabled || pluginType === PluginType.LOCAL}
className="!mr-2"
color="primary"
loading={loading}
onClick={e => {
e.stopPropagation();
if (Array.isArray(requestParams) && requestParams.length > 0) {
setInputModal(true);
} else {
handleActionNoParams();
}
}}
>
{loading
? I18n.t('plugin_s3_Parsing')
: I18n.t('Create_newtool_s3_button_auto')}
</Button>
</>
),
};
};

View File

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

View File

@@ -0,0 +1,40 @@
/*
* 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 APIParameter } from '@coze-arch/bot-api/plugin_develop';
export enum STATUS {
PASS = 'PASS',
FAIL = 'FAIL',
WAIT = 'WAIT',
}
export interface CheckParamsProps {
status?: STATUS;
request?: string;
response?: string;
failReason?: string;
response_params?: Array<APIParameter>;
rawResp?: string;
}
export interface StepUpdateApiRes {
code: string | number;
msg: string;
}
export const ERROR_CODE = {
SAFE_CHECK: 720092020,
};

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 { type ReactNode } from 'react';
import {
type APIParameter,
type AssistParameterType,
type ParameterType,
type PluginAPIInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { type ParameterTypeExtend, type PluginParameterType } from '../config';
export interface APIParameterRecord extends APIParameter {
deep?: number;
value?: string;
}
export interface UpdateNodeWithDataFn {
(params: {
record: APIParameter;
key: string | Array<string>;
value: unknown;
updateData?: boolean;
checkDefault?: boolean;
inherit?: boolean;
}): void;
}
export interface AddChildNodeFn {
(params: {
record: APIParameterRecord;
isArray?: boolean;
isObj?: boolean;
type?: ParameterType;
recordType?: ParameterType;
}): void;
}
export interface InputItemProps {
val?: string;
max?: number;
check?: number;
width?: number | string;
useCheck?: boolean;
checkAscii?: boolean;
isRequired?: boolean;
placeholder?: string;
filterSpace?: boolean;
callback?: (val: string) => void;
selectCallback?: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
val: string | number | any[] | Record<string, any> | undefined,
) => void;
targetKey?: string;
data?: Array<APIParameter>;
checkSame?: boolean;
useBlockWrap?: boolean;
disabled?: boolean;
record?: APIParameterRecord;
dynamicWidth?: boolean;
deep?: number;
typeOptions?: Array<Record<string, string | number>>;
}
export type CascaderValueType = [PluginParameterType, ParameterTypeExtend?];
export type CascaderOnChangValueType = [
PluginParameterType,
AssistParameterType?,
];
export interface RenderEnhancedComponentProps {
renderDescComponent: (props: {
onSetDescription: (desc: string) => void;
originDesc: string | undefined;
className: string;
disabled?: boolean;
plugin_id: string;
space_id: string;
}) => ReactNode;
renderParamsComponent: (props: {
size?: 'small' | 'default';
src: 'request' | 'response';
apiInfo: PluginAPIInfo | undefined;
originParams: APIParameter[];
onSetParams: (params: APIParameter[]) => void;
disabled?: boolean;
spaceID: string;
pluginId: string;
}) => ReactNode;
}

View File

@@ -0,0 +1,662 @@
/*
* 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 complexity */
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-explicit-any -- 一些历史any 改不动 */
import { nanoid } from 'nanoid';
import { cloneDeep, has, isEmpty, isNumber, isObject } from 'lodash-es';
import {
type APIParameter,
ParameterLocation,
ParameterType,
DefaultParamSource,
} from '@coze-arch/bot-api/plugin_develop';
import { ARRAYTAG, ROWKEY, childrenRecordName } from './config';
// 遍历树返回目标id路径
export const findPathById = ({
data,
callback,
childrenName = childrenRecordName,
path = [],
}: {
data: any;
callback: (item: APIParameter, path: Array<number>) => void;
childrenName?: string;
path?: Array<number>;
}) => {
for (let i = 0; i < data.length; i++) {
const clonePath = JSON.parse(JSON.stringify(path));
clonePath.push(i);
callback(data[i], clonePath);
if (data[i][childrenName] && data[i][childrenName].length > 0) {
findPathById({
data: data[i][childrenName],
callback,
childrenName,
path: clonePath,
});
}
}
};
// 给每层对象增加层级深度标识
export const addDepthAndValue = (
tree: any,
valKey: 'global_default' | 'local_default' = 'global_default',
depth = 1,
) => {
if (!Array.isArray(tree)) {
return;
}
// 遍历树中的每个节点
for (const node of tree) {
// 为当前节点添加深度标识符
node.deep = depth;
if (node[valKey]) {
node.value = node[valKey];
}
// 如果当前节点有子节点,则递归地为子节点添加深度标识符
if (node[childrenRecordName]) {
addDepthAndValue(node[childrenRecordName], valKey, depth + 1);
}
}
};
// 将深度信息push到一个数组里最后取最大值
export const handleDeepArr = (tree: any, deepArr: Array<number> = []) => {
if (!Array.isArray(tree)) {
return;
}
// 遍历树中的每个节点
for (const node of tree) {
// 为当前节点添加深度标识符
if (isNumber(node.deep)) {
deepArr.push(node.deep);
}
if (node[childrenRecordName]) {
handleDeepArr(node[childrenRecordName], deepArr);
}
}
};
// 返回最大深度
export const maxDeep = (tree: any) => {
if (!Array.isArray(tree) || tree.length === 0) {
return 0;
}
const arr: Array<number> = [];
handleDeepArr(tree, arr);
return Math.max.apply(null, arr);
};
interface DefaultNode {
isArray?: boolean;
iscChildren?: boolean;
deep?: number;
}
// 默认子节点
export const defaultNode = ({
isArray = false,
iscChildren = false,
deep = 1,
}: DefaultNode = {}) => ({
[ROWKEY]: nanoid(),
name: isArray ? ARRAYTAG : '',
desc: '',
type: ParameterType.String,
location: iscChildren ? undefined : ParameterLocation.Query,
is_required: true,
sub_parameters: [],
deep,
});
// 删除当前节点
export const deleteNode = (data: any, targetKey: string) => {
for (let i = 0; i < data.length; i++) {
if (data[i][ROWKEY] === targetKey) {
data.splice(i, 1);
return true;
} else if (
data[i][childrenRecordName] &&
data[i][childrenRecordName].length > 0
) {
if (deleteNode(data[i][childrenRecordName], targetKey)) {
return true;
}
}
}
return false;
};
// 删除全部子节点
export const deleteAllChildNode = (data: any, targetKey: string) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
item[childrenRecordName] = [];
return true;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (deleteAllChildNode(item[childrenRecordName], targetKey)) {
return true;
}
}
}
return false;
};
interface UpdateNodeById {
data: APIParameter[];
targetKey: string;
field: string;
value: any;
/** 数组的子节点是否需要继承父节点的字段值,当前只有可见性开关需要继承 */
inherit?: boolean;
}
const updateNodeByVal = (data: any, field: any, val: any) => {
for (const item of data) {
item[field] = val;
if (Array.isArray(item[childrenRecordName])) {
updateNodeByVal(item[childrenRecordName], field, val);
}
}
};
// 更新节点信息
export const updateNodeById = ({
data,
targetKey,
field,
value,
inherit = false,
}: UpdateNodeById) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
// @ts-expect-error -- linter-disable-autofix
item[field] = value;
if (
inherit &&
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
updateNodeByVal(item[childrenRecordName], field, value);
}
return;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
updateNodeById({
data: item[childrenRecordName],
targetKey,
field,
value,
});
}
}
};
// 根据路径找对应模版值
export const findTemplateNodeByPath = (
dsl: any,
path: Array<string | number>,
) => {
let node = cloneDeep(dsl);
const newPath = [...path]; //创建新的路径,避免修改原路径
for (let i = 0; i < path.length; i++) {
// 如果存在节点,说明是源数据节点上增加子节点
if (node[path[i]]) {
node = node[path[i]];
} else {
// 如果不存在,说明是新增的节点增加子节点,这时需要将路径指向原始节点(第一个节点)
node = node[0];
newPath[i] = 0;
}
}
return newPath;
};
// 树转换成对象
export const transformTreeToObj = (tree: any, checkType = true): any =>
// 树的每一层级表示一个对象的属性集
tree.reduce((acc: any, item: any) => {
let arrTemp = [];
switch (item.type) {
case ParameterType.String:
if (item.value) {
acc[item.name] = item.value;
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Integer:
case ParameterType.Number:
if (item.value) {
acc[item.name] = Number(item.value);
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Bool:
if (item.value) {
acc[item.name] = item.value === 'true';
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Object:
if (item.sub_parameters) {
const obj = transformTreeToObj(item.sub_parameters, checkType);
if (!isEmpty(obj)) {
acc[item.name] = obj;
}
}
break;
case ParameterType.Array:
/**
* 如果是数组需要过滤掉空的项且数组的子项非object和array
* 这里用temp接收过滤后的子项避免直接修改原数组因为原数组和页面数据绑定不能直接删除空项
*/
arrTemp = item.sub_parameters;
if (
[
ParameterType.Bool,
ParameterType.Integer,
ParameterType.Number,
ParameterType.String,
].includes(item.sub_parameters[0].type)
) {
arrTemp = item.sub_parameters.filter((subItem: any) => subItem.value);
}
if (isEmpty(arrTemp)) {
break;
}
acc[item.name] = arrTemp.map((subItem: any) => {
// boolean类型匹配字符串true/false
if ([ParameterType.Bool].includes(subItem.type)) {
return checkType ? subItem.value === 'true' : subItem.value;
}
// 数字类型转为number
if (
[ParameterType.Integer, ParameterType.Number].includes(subItem.type)
) {
return checkType ? Number(subItem.value) : subItem.value;
}
// 字符串类型直接返回(进到这里的已经是过滤完空值的数组)
if ([ParameterType.String].includes(subItem.type)) {
return subItem.value;
}
// 如果是对象,递归遍历
if (subItem.type === ParameterType.Object) {
return transformTreeToObj(subItem.sub_parameters, checkType);
}
});
break;
default:
break;
}
return acc;
}, {});
// 克隆节点修改key及清空value
export const cloneWithRandomKey = (obj: any) => {
// 创建新对象储存值
const clone: any = {};
// 遍历原对象的所有属性
for (const prop in obj) {
// 如果原对象的这个属性是一个对象,递归调用 cloneWithRandomKey 函数
if (obj[prop]?.constructor === Object) {
clone[prop] = cloneWithRandomKey(obj[prop]);
} else {
// 否则,直接复制这个属性
clone[prop] = obj[prop];
}
}
// 如果这个对象有 sub_parameters 属性,需要遍历它
if ('sub_parameters' in clone) {
clone.sub_parameters = clone.sub_parameters?.map(cloneWithRandomKey);
}
// 生成一个新的随机 key
if (clone[ROWKEY]) {
clone[ROWKEY] = nanoid();
}
if (clone.value) {
clone.value = null;
}
// 返回克隆的对象
return clone;
};
// 判断参数是否显示删除按钮 先判断是否是根节点,根节点允许删除
export const handleIsShowDelete = (
data: any,
targetKey: string | undefined,
) => {
const rootIds = data.map((d: any) => d[ROWKEY]);
if (rootIds.includes(targetKey)) {
return true;
}
return isShowDelete(data, targetKey);
};
// 检查是否存在相同名称
export const checkSameName = (
data: Array<APIParameter>,
targetKey: string,
val: string,
): boolean | undefined => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
const items = data.filter(
(dataItem: APIParameter) => dataItem.name === val,
);
return items.length > 1;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (checkSameName(item[childrenRecordName], targetKey, val)) {
return true;
}
}
}
};
// 检查是否有array类型用来判断response是否需要操作列
export const checkHasArray = (data: unknown) => {
if (!Array.isArray(data)) {
return false;
}
for (const item of data) {
if (item.type === ParameterType.Array) {
return true;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
// 调整 循环退出时机
if (checkHasArray(item[childrenRecordName])) {
return true;
}
}
}
return false;
};
// 判断参数是否显示删除按钮object类型最后一个不允许删除
export const isShowDelete = (data: any, targetKey: string | undefined) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
return data.length > 1;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (isShowDelete(item[childrenRecordName], targetKey)) {
return true;
}
}
}
};
export const sleep = (time: number) =>
new Promise(resolve => {
setTimeout(() => {
resolve(0);
}, time);
});
// 该方法兼容chrome、Arch、Safari浏览器及iPad增加兼容firefox
export const scrollToErrorElement = (className: string) => {
const errorElement = document.querySelector(className);
if (errorElement) {
if (typeof (errorElement as any).scrollIntoViewIfNeeded === 'function') {
(errorElement as any).scrollIntoViewIfNeeded();
} else {
// 兼容性处理,如 Firefox
errorElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}
};
export const scrollToBottom = (ele: Element) => {
const scrollEle = ele;
scrollEle.scrollTo({
left: 0,
top: scrollEle.scrollHeight,
behavior: 'smooth',
});
};
export const initParamsDefault = (
data: Array<APIParameter>,
keyDefault: 'global_default' | 'local_default',
) => {
const result = cloneDeep(data);
const init = (obj: APIParameter) => {
if (keyDefault === 'local_default' && !has(obj, 'local_default')) {
obj[keyDefault] = obj.global_default;
}
if (!obj[keyDefault]) {
obj[keyDefault] = '';
}
// bot非引用+必填+local默认值为空+不可见,是异常场景,需手动拨正
const isUnusual =
obj.default_param_source === DefaultParamSource.Input &&
keyDefault === 'local_default' &&
obj.is_required &&
!obj.local_default &&
obj.local_disable;
if (isUnusual) {
obj.local_disable = false;
}
};
const addDefault = (res: Array<APIParameter>, isArray = false) => {
for (let i = 0, len = res.length; i < len; i++) {
const obj = res[i];
if (isArray) {
obj[keyDefault] = undefined;
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters, true);
}
} else if (obj.type === ParameterType.Array) {
init(obj);
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters, true);
}
} else {
if (obj.type === ParameterType.Object) {
obj[keyDefault] = undefined;
} else {
init(obj);
}
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters);
}
}
}
};
addDefault(result);
return result;
};
// @ts-expect-error -- linter-disable-autofix
export const transformArrayToTree = (array, template: Array<APIParameter>) => {
const arrObj = array;
const tree: Array<APIParameter> = [];
if (Array.isArray(arrObj)) {
arrObj.forEach(item => {
const subTree = createSubTree(item, template[0]);
tree.push(subTree);
});
}
return tree;
};
const createSubTree = (arrItem: any, tem: any) => {
let subTree: APIParameter & { value?: unknown } = {};
// 数组
if (Array.isArray(arrItem)) {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
arrItem.forEach(item => {
const arrItemSubTree = createSubTree(item, tem.sub_parameters[0]);
subTree.sub_parameters?.push(arrItemSubTree);
});
} else if (isObject(arrItem)) {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
let childTree: APIParameter & { value?: unknown } = {};
Object.keys(arrItem).map(key => {
if (Object.prototype.hasOwnProperty.call(arrItem, key)) {
// @ts-expect-error -- linter-disable-autofix
const value = arrItem[key];
if (Array.isArray(value) || typeof value === 'object') {
const nestedSubTree = createSubTree(
value,
// @ts-expect-error -- linter-disable-autofix
tem.sub_parameters.find(item => item.name === key),
);
subTree.sub_parameters?.push(nestedSubTree);
} else {
childTree = {
// @ts-expect-error -- linter-disable-autofix
...tem.sub_parameters.find(item => item.name === key),
id: nanoid(),
sub_parameters: [],
};
childTree.value = String(value);
subTree.sub_parameters?.push(childTree);
}
}
});
} else {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
subTree.value = String(arrItem);
}
return subTree;
};
export const transformParamsToTree = (params: Array<APIParameter>) => {
const result = cloneDeep(params);
for (let i = 0, len = result.length; i < len; i++) {
if (result[i].type === ParameterType.Array) {
const arr = JSON.parse(result[i].global_default || '[]');
if (arr.length > 0) {
const tree = transformArrayToTree(arr, result[i].sub_parameters || []);
result[i].sub_parameters = tree;
}
} else {
// 对象嵌数组有问题 被覆盖了 需要重置
result[i].sub_parameters = transformParamsToTree(
result[i].sub_parameters || [],
);
}
}
return result;
};
// data 额外加工 / 如果本身没有 global_default === undefined 就设置 global_disable 也是 undefined最后将所有的 global_default 设置成 undefined
export const doRemoveDefaultFromResponseParams = (
data: APIParameter[],
hasRequired = false,
) => {
if (!data.length) {
return [];
}
const returnData = cloneDeep(data);
for (let i = 0, len = returnData.length; i < len; i++) {
const current = returnData[i];
if (current.global_default === undefined) {
current.global_disable = undefined;
}
current.global_default = undefined;
if (!hasRequired) {
current.is_required = undefined;
}
current.sub_parameters = doRemoveDefaultFromResponseParams(
current.sub_parameters ?? [],
hasRequired,
);
}
return returnData;
};
// @ts-expect-error -- linter-disable-autofix
export const doValidParams = (
params: APIParameter[],
targetKey: keyof APIParameter,
) => {
if (!params?.length || !targetKey) {
return !!0;
}
for (let i = 0, j = params.length; i < j; i++) {
const target = params[i];
if (!target[targetKey]) {
return !!0;
}
// @ts-expect-error -- linter-disable-autofix
if (target.sub_parameters.length > 0) {
// @ts-expect-error -- linter-disable-autofix
const sub = doValidParams(target.sub_parameters, targetKey);
if (sub === !!0) {
return sub;
}
}
}
return !0;
};

View File

@@ -0,0 +1,113 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { Space, UIButton, UIToast } from '@coze-arch/bot-semi';
import {
type DebugExample,
type PluginAPIInfo,
DebugExampleStatus,
type UpdateAPIRequest,
PluginType,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { usePluginStore } from '@coze-studio/bot-plugin-store';
import { STATUS } from '../../components/plugin_modal/types';
import { ExampleCheckbox } from '../../components/example-checkbox';
interface DebugFooterProps {
btnLoading: boolean;
apiInfo: PluginAPIInfo | undefined;
dugStatus: STATUS | undefined;
loading: boolean;
nextStep: () => void;
previousStep?: () => void;
editVersion?: number;
isModal?: boolean;
}
export const useDebugFooter = ({
btnLoading,
apiInfo,
dugStatus,
loading,
nextStep,
editVersion,
}: DebugFooterProps) => {
const [saveExample, setSaveExample] = useState(
apiInfo?.debug_example_status === DebugExampleStatus.Enable,
);
const [debugExample, setDebugExample] = useState<DebugExample>();
const { loading: saveLoading, runAsync: runSaveExample } = useRequest(
(info: UpdateAPIRequest) => PluginDevelopApi.UpdateAPI(info),
{
manual: true,
},
);
const { pluginInfo } = usePluginStore(store => ({
pluginInfo: store.pluginInfo,
}));
const onSave = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await runSaveExample({
plugin_id: pluginInfo?.plugin_id ?? '',
api_id: apiInfo?.api_id ?? '',
edit_version: editVersion ?? pluginInfo?.edit_version,
save_example: saveExample,
debug_example: debugExample,
});
UIToast.success(I18n.t('Save_success'));
nextStep();
};
useEffect(() => {
setSaveExample(apiInfo?.debug_example_status === DebugExampleStatus.Enable);
setDebugExample(
apiInfo?.debug_example_status === DebugExampleStatus.Enable
? apiInfo?.debug_example
: undefined,
);
}, [apiInfo]);
return {
debugFooterNode: (
<Space spacing={12}>
<ExampleCheckbox value={saveExample} onValueChange={setSaveExample} />
<UIButton
disabled={
loading ||
(dugStatus !== STATUS.PASS &&
pluginInfo?.plugin_type !== PluginType.LOCAL)
}
style={{ minWidth: 98, margin: 0 }}
loading={btnLoading || saveLoading}
type="primary"
theme="solid"
onClick={onSave}
>
{I18n.t('Create_newtool_s4_done')}
</UIButton>
</Space>
),
debugExample,
setDebugExample,
};
};

View File

@@ -0,0 +1,84 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import {
type PluginAPIInfo,
DebugExampleStatus,
} from '@coze-arch/bot-api/plugin_develop';
import { usePluginStore } from '@coze-studio/bot-plugin-store';
import { addDepthAndValue } from '../../components/plugin_modal/utils';
import { ExampleModal } from '../../components/example-modal';
import { setEditToolExampleValue } from './utils';
// @ts-expect-error -- linter-disable-autofix
export const useEditExample = ({ onUpdate }) => {
const [visible, setVisible] = useState(false);
const [apiInfo, setApiInfo] = useState<PluginAPIInfo>();
const { pluginInfo } = usePluginStore(store => ({
pluginInfo: store.pluginInfo,
}));
const openExample = (info: PluginAPIInfo) => {
setVisible(true);
if (
info?.debug_example?.req_example &&
info?.debug_example_status === DebugExampleStatus.Enable
) {
const requestParams = cloneDeep(info?.request_params ?? []);
setEditToolExampleValue(
requestParams,
JSON.parse(info?.debug_example?.req_example),
);
addDepthAndValue(requestParams);
setApiInfo({ ...info, request_params: requestParams });
} else {
addDepthAndValue(info.request_params);
setApiInfo(info);
}
};
const closeExample = () => {
setVisible(false);
};
const onSave = () => {
onUpdate?.();
closeExample();
};
useEffect(() => {
if (!visible) {
setApiInfo(undefined);
}
}, [visible]);
return {
exampleNode: (
<ExampleModal
visible={visible}
onCancel={closeExample}
pluginId={pluginInfo?.plugin_id ?? ''}
apiInfo={apiInfo as PluginAPIInfo}
pluginName={pluginInfo?.meta_info?.name ?? ''}
onSave={onSave}
/>
),
openExample,
};
};

View File

@@ -0,0 +1,123 @@
/*
* 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, Suspense, lazy } from 'react';
import { cloneDeep } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { UIModal } from '@coze-arch/bot-semi';
import {
type PluginParameter,
type DebugExample,
type PluginAPIInfo,
DebugExampleStatus,
} from '@coze-arch/bot-api/plugin_develop';
import { addDepthAndValue } from '../../components/plugin_modal/utils';
import { setStoreExampleValue, setWorkflowExampleValue } from './utils';
const LazyDebug = lazy(async () => {
const { Debug } = await import('../../components/plugin_modal/debug');
return {
default: Debug,
};
});
interface ShowExampleParams {
scene: 'workflow' | 'bot';
requestParams: PluginParameter[];
debugExample: DebugExample;
}
interface ViewExampleProps {
getPopupContainer?: () => HTMLElement;
}
export const useViewExample = (props?: ViewExampleProps) => {
const [visible, setVisible] = useState(false);
const [apiInfo, setApiInfo] = useState<PluginAPIInfo>();
const doShowExample = ({
scene,
requestParams,
debugExample,
}: ShowExampleParams) => {
if (!requestParams || !debugExample?.req_example) {
return;
}
const requestParamsData = cloneDeep(requestParams);
if (scene === 'workflow') {
setWorkflowExampleValue(
requestParamsData,
JSON.parse(debugExample?.req_example),
);
} else if (scene === 'bot') {
setStoreExampleValue(
requestParamsData,
JSON.parse(debugExample?.req_example),
);
} else {
return;
}
addDepthAndValue(requestParamsData);
setApiInfo({
debug_example_status: DebugExampleStatus.Enable,
request_params:
requestParamsData as unknown as PluginAPIInfo['request_params'],
debug_example: debugExample,
});
setVisible(true);
};
const closeExample = () => {
setVisible(false);
};
useEffect(() => {
if (!visible) {
setApiInfo(undefined);
}
}, [visible]);
return {
exampleNode: (
<UIModal
title={I18n.t('plugin_edit_tool_test_run_example_tip')}
visible={visible}
width={1280}
style={{ height: 'calc(100vh - 140px)', minWidth: '1040px' }}
centered
onCancel={closeExample}
footer={null}
getPopupContainer={props?.getPopupContainer}
>
{apiInfo ? (
<Suspense fallback={null}>
<LazyDebug
disabled={true}
pluginId={''}
apiId={apiInfo?.api_id ?? ''}
apiInfo={apiInfo as PluginAPIInfo}
pluginName={''}
debugExample={apiInfo?.debug_example}
isViewExample={true}
/>
</Suspense>
) : null}
</UIModal>
),
doShowExample,
};
};

View File

@@ -0,0 +1,171 @@
/*
* 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 { nanoid } from 'nanoid';
import { get } from 'lodash-es';
import {
ParameterType,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
export const typesConfig = {
string: ParameterType.String,
integer: ParameterType.Integer,
number: ParameterType.Number,
object: ParameterType.Object,
array: ParameterType.Array,
bool: ParameterType.Bool,
list: ParameterType.Array,
float: ParameterType.Number,
boolean: ParameterType.Bool,
};
// tool 数据回显用
interface ExampleReqParamsType {
[key: string]: string | number | null | object | boolean;
}
export const setEditToolExampleValue = (
requestParams: APIParameter[],
exampleReqParams: ExampleReqParamsType[],
) => {
// @ts-expect-error -- linter-disable-autofix
const setDefault = (originData: APIParameter[], currentJsonData) => {
originData.forEach((paramItem: APIParameter) => {
const currentPathValue = get(currentJsonData, paramItem?.name ?? '');
if (currentPathValue !== undefined) {
if (paramItem.type === ParameterType.Object) {
setDefault(paramItem.sub_parameters ?? [], currentPathValue);
} else if (paramItem.type === ParameterType.Array) {
paramItem.global_default = JSON.stringify(currentPathValue);
} else {
paramItem.global_default = currentPathValue;
}
}
});
};
setDefault(requestParams, exampleReqParams);
};
// 重置 type is_required sub_parameters
// @ts-expect-error -- linter-disable-autofix
export const resetWorkflowKey = currentTarget => {
if (Array.isArray(currentTarget)) {
currentTarget.forEach(obj => {
resetWorkflowKey(obj);
});
} else {
// @ts-expect-error -- linter-disable-autofix
currentTarget.type = typesConfig[currentTarget.type];
// @ts-expect-error -- linter-disable-autofix
currentTarget.sub_type = typesConfig[currentTarget.sub_type];
currentTarget.is_required = currentTarget.required;
currentTarget.global_disable = false;
currentTarget.local_disable = false;
currentTarget.location = undefined;
currentTarget.id = nanoid();
currentTarget.desc = currentTarget.description;
if ('schema' in currentTarget) {
if (currentTarget.type === ParameterType.Array) {
currentTarget.sub_parameters = [
{
name: '[Array Item]',
is_required: currentTarget.required,
// @ts-expect-error -- linter-disable-autofix
type: typesConfig[currentTarget.schema?.type],
global_disable: false,
local_disable: false,
location: undefined,
sub_type: 0,
sub_parameters: currentTarget.schema?.schema ?? [],
},
];
} else if (currentTarget.type === ParameterType.Object) {
currentTarget.sub_parameters = [...currentTarget.schema];
} else {
currentTarget.sub_parameters = [];
}
} else {
currentTarget.sub_parameters = [];
}
resetWorkflowKey(
currentTarget.type === ParameterType.Array
? currentTarget.sub_parameters[0].sub_parameters
: currentTarget?.sub_parameters,
);
}
};
// @ts-expect-error -- linter-disable-autofix
export const resetStoreKey = currentTarget => {
if (Array.isArray(currentTarget)) {
currentTarget.forEach(obj => {
resetStoreKey(obj);
});
} else {
// @ts-expect-error -- linter-disable-autofix
currentTarget.type = typesConfig[currentTarget.type];
// @ts-expect-error -- linter-disable-autofix
currentTarget.sub_type = typesConfig[currentTarget.sub_type];
currentTarget.is_required = currentTarget.required;
currentTarget.global_disable = false;
currentTarget.local_disable = false;
currentTarget.location = undefined;
currentTarget.id = nanoid();
// store 那边是 sub_params 字段 个人 的是 sub_parameters
if (!('sub_parameters' in currentTarget)) {
currentTarget.sub_parameters = [];
}
if ('sub_params' in currentTarget) {
currentTarget.sub_parameters = currentTarget.sub_params;
}
if (currentTarget.type === ParameterType.Array) {
currentTarget.sub_parameters = [
{
name: '[Array Item]',
is_required: currentTarget.required,
type: currentTarget.sub_type,
global_disable: false,
local_disable: false,
location: undefined,
sub_type: 0,
sub_parameters: [...currentTarget.sub_parameters],
},
];
resetStoreKey(currentTarget.sub_parameters[0].sub_parameters);
} else {
resetStoreKey(currentTarget.sub_parameters);
}
}
};
export const setStoreExampleValue = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestParams: any,
exampleReqParams: ExampleReqParamsType[],
) => {
resetStoreKey(requestParams);
setEditToolExampleValue(requestParams, exampleReqParams);
};
export const setWorkflowExampleValue = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestParams: any,
exampleReqParams: ExampleReqParamsType[],
) => {
resetWorkflowKey(requestParams);
setEditToolExampleValue(requestParams, exampleReqParams);
};

View File

@@ -0,0 +1,134 @@
/*
* 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 @typescript-eslint/no-explicit-any */
import { useEffect, useMemo, useState } from 'react';
import { cloneDeep } from 'lodash-es';
import { useErrorHandler } from '@coze-arch/logger';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type APIParameter } from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
addDepthAndValue,
doRemoveDefaultFromResponseParams,
initParamsDefault,
updateNodeById,
} from '../../components/plugin_modal/utils';
import { ROWKEY } from '../../components/plugin_modal/config';
export interface SettingParamsProps {
botId?: string;
pluginId?: string;
devId?: string;
apiName?: string;
}
const useParametersInSettingModalController = (props: SettingParamsProps) => {
const capture = useErrorHandler();
const [isUpdateLoading, setIsUpdateLoading] = useState(!!0);
const [requestParams, setRequestParams] = useState<Array<APIParameter>>([]);
const [responseParams, setResponseParams] = useState<Array<APIParameter>>([]);
const [activeTab, setActiveTab] = useState(0);
const [loaded, setLoaded] = useState(!!0);
const commonParams = useMemo(
() => ({
bot_id: props.botId || '',
dev_id: props.devId || '',
plugin_id: props.pluginId || '',
api_name: props.apiName || '',
space_id: useSpaceStore.getState().getSpaceId(),
}),
[props],
);
const getColumnClass = (record: APIParameter) =>
record.global_disable ? 'disable' : 'normal';
const handleOpen = async () => {
try {
const { request_params, response_params } =
await PluginDevelopApi.GetBotDefaultParams({
...commonParams,
});
if (request_params && response_params) {
const reqParams = initParamsDefault(request_params, 'local_default');
addDepthAndValue(response_params);
addDepthAndValue(reqParams, 'local_default');
setRequestParams(reqParams);
const resParams = initParamsDefault(response_params, 'local_default');
setResponseParams(resParams);
setLoaded(true);
}
} catch (error) {
setLoaded(true);
capture(error);
}
};
const handleUpdate = async () => {
setIsUpdateLoading(!0);
await PluginDevelopApi.UpdateBotDefaultParams({
...commonParams,
request_params: requestParams,
response_params: doRemoveDefaultFromResponseParams(responseParams, false),
});
setIsUpdateLoading(!!0);
};
const updateNodeWithData = ({
record,
key,
value,
isForResponse = false,
}: {
record: APIParameter;
key: string;
value: any;
isForResponse?: boolean;
}) => {
updateNodeById({
data: isForResponse ? responseParams : requestParams,
targetKey: record[ROWKEY] as string,
field: key,
value,
});
const cloneData = cloneDeep(isForResponse ? responseParams : requestParams);
isForResponse ? setResponseParams(cloneData) : setRequestParams(cloneData);
};
useEffect(() => {
handleOpen();
}, []);
return {
doSetActive: setActiveTab,
doSetReqParams: setRequestParams,
doUpdateParams: handleUpdate,
doUpdateNodeWithData: updateNodeWithData,
getColumnClass,
loaded,
activeTab,
responseParams,
requestParams,
isUpdateLoading,
};
};
export { useParametersInSettingModalController };

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../../arch/bot-md-box-adapter/tsconfig.build.json"
},
{
"path": "../../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/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/bot-utils/tsconfig.build.json"
},
{
"path": "../../../studio/common/file-kit/tsconfig.build.json"
},
{
"path": "../../../studio/plugin-shared/tsconfig.build.json"
},
{
"path": "../../../studio/stores/bot-plugin/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,18 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true
}
}

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',
});